diff --git a/.ci/Arch/Dockerfile b/.ci/Arch/Dockerfile new file mode 100644 index 000000000..36cf5c4ae --- /dev/null +++ b/.ci/Arch/Dockerfile @@ -0,0 +1,19 @@ +from archlinux:latest + +RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \ + base-devel \ + ccache \ + cmake \ + git \ + gtest \ + mariadb-libs \ + ninja \ + protobuf \ + qt6-base \ + qt6-imageformats \ + qt6-multimedia \ + qt6-svg \ + qt6-tools \ + qt6-translations \ + qt6-websockets \ + && pacman --sync --clean --clean --noconfirm diff --git a/.ci/Debian11/Dockerfile b/.ci/Debian11/Dockerfile new file mode 100644 index 000000000..b994863bf --- /dev/null +++ b/.ci/Debian11/Dockerfile @@ -0,0 +1,26 @@ +FROM debian:11 + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + ccache \ + clang-format \ + cmake \ + file \ + g++ \ + git \ + liblzma-dev \ + libmariadb-dev-compat \ + libprotobuf-dev \ + libqt5multimedia5-plugins \ + libqt5sql5-mysql \ + libqt5svg5-dev \ + libqt5websockets5-dev \ + ninja-build \ + protobuf-compiler \ + qt5-image-formats-plugins \ + qtmultimedia5-dev \ + qttools5-dev \ + qttools5-dev-tools \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* diff --git a/.ci/Debian12/Dockerfile b/.ci/Debian12/Dockerfile new file mode 100644 index 000000000..202405b84 --- /dev/null +++ b/.ci/Debian12/Dockerfile @@ -0,0 +1,28 @@ +FROM debian:12 + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + ccache \ + clang-format \ + cmake \ + file \ + g++ \ + git \ + libgl-dev \ + liblzma-dev \ + libmariadb-dev-compat \ + libprotobuf-dev \ + libqt6multimedia6 \ + libqt6sql6-mysql \ + ninja-build \ + protobuf-compiler \ + qt6-image-formats-plugins \ + qt6-l10n-tools \ + qt6-multimedia-dev \ + qt6-svg-dev \ + qt6-tools-dev \ + qt6-tools-dev-tools \ + qt6-websockets-dev \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* diff --git a/.ci/Debian13/Dockerfile b/.ci/Debian13/Dockerfile new file mode 100644 index 000000000..d7ab6ac86 --- /dev/null +++ b/.ci/Debian13/Dockerfile @@ -0,0 +1,29 @@ +FROM debian:13 + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + ccache \ + clang-format \ + cmake \ + file \ + g++ \ + git \ + libgl-dev \ + liblzma-dev \ + libmariadb-dev-compat \ + libprotobuf-dev \ + libqt6multimedia6 \ + libqt6sql6-mysql \ + ninja-build \ + protobuf-compiler \ + qt6-image-formats-plugins \ + qt6-l10n-tools \ + qt6-multimedia-dev \ + qt6-svg-dev \ + qt6-tools-dev \ + qt6-tools-dev-tools \ + qt6-websockets-dev \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* diff --git a/.ci/Fedora42/Dockerfile b/.ci/Fedora42/Dockerfile new file mode 100644 index 000000000..ee4a856f2 --- /dev/null +++ b/.ci/Fedora42/Dockerfile @@ -0,0 +1,16 @@ +FROM fedora:42 + +RUN dnf install -y \ + ccache \ + cmake \ + gcc-c++ \ + git \ + mariadb-devel \ + ninja-build \ + protobuf-devel \ + qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ + qt6-qtimageformats \ + rpm-build \ + xz-devel \ + zlib-devel \ + && dnf clean all diff --git a/.ci/Fedora43/Dockerfile b/.ci/Fedora43/Dockerfile new file mode 100644 index 000000000..27570cf99 --- /dev/null +++ b/.ci/Fedora43/Dockerfile @@ -0,0 +1,16 @@ +FROM fedora:43 + +RUN dnf install -y \ + ccache \ + cmake \ + gcc-c++ \ + git \ + mariadb-devel \ + ninja-build \ + protobuf-devel \ + qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ + qt6-qtimageformats \ + rpm-build \ + xz-devel \ + zlib-devel \ + && dnf clean all diff --git a/.ci/Servatrice_Debian11/Dockerfile b/.ci/Servatrice_Debian11/Dockerfile new file mode 100644 index 000000000..fadc9e0e7 --- /dev/null +++ b/.ci/Servatrice_Debian11/Dockerfile @@ -0,0 +1,21 @@ +FROM debian:11 + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + ccache \ + clang-format \ + cmake \ + file \ + g++ \ + git \ + libmariadb-dev-compat \ + libprotobuf-dev \ + libqt5sql5-mysql \ + libqt5websockets5-dev \ + ninja-build \ + protobuf-compiler \ + qttools5-dev \ + qttools5-dev-tools \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* diff --git a/.ci/Ubuntu22.04/Dockerfile b/.ci/Ubuntu22.04/Dockerfile new file mode 100644 index 000000000..93c8fdea9 --- /dev/null +++ b/.ci/Ubuntu22.04/Dockerfile @@ -0,0 +1,26 @@ +FROM ubuntu:22.04 + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + ccache \ + clang-format \ + cmake \ + file \ + g++ \ + git \ + liblzma-dev \ + libmariadb-dev-compat \ + libprotobuf-dev \ + libqt5multimedia5-plugins \ + libqt5sql5-mysql \ + libqt5svg5-dev \ + libqt5websockets5-dev \ + ninja-build \ + protobuf-compiler \ + qt5-image-formats-plugins \ + qtmultimedia5-dev \ + qttools5-dev \ + qttools5-dev-tools \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* diff --git a/.ci/Ubuntu24.04/Dockerfile b/.ci/Ubuntu24.04/Dockerfile new file mode 100644 index 000000000..809b2e43a --- /dev/null +++ b/.ci/Ubuntu24.04/Dockerfile @@ -0,0 +1,28 @@ +FROM ubuntu:24.04 + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + ccache \ + clang-format \ + cmake \ + file \ + g++ \ + git \ + libgl-dev \ + liblzma-dev \ + libmariadb-dev-compat \ + libprotobuf-dev \ + libqt6multimedia6 \ + libqt6sql6-mysql \ + ninja-build \ + protobuf-compiler \ + qt6-image-formats-plugins \ + qt6-l10n-tools \ + qt6-multimedia-dev \ + qt6-svg-dev \ + qt6-tools-dev \ + qt6-tools-dev-tools \ + qt6-websockets-dev \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* diff --git a/.ci/Ubuntu26.04/Dockerfile b/.ci/Ubuntu26.04/Dockerfile new file mode 100644 index 000000000..7b0cd389f --- /dev/null +++ b/.ci/Ubuntu26.04/Dockerfile @@ -0,0 +1,29 @@ +FROM ubuntu:26.04 + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + ccache \ + clang-format \ + cmake \ + file \ + g++ \ + git \ + libgl-dev \ + liblzma-dev \ + libmariadb-dev-compat \ + libprotobuf-dev \ + libqt6multimedia6 \ + libqt6sql6-mysql \ + ninja-build \ + protobuf-compiler \ + qt6-image-formats-plugins \ + qt6-l10n-tools \ + qt6-multimedia-dev \ + qt6-svg-dev \ + qt6-tools-dev \ + qt6-tools-dev-tools \ + qt6-websockets-dev \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* diff --git a/.ci/compile.sh b/.ci/compile.sh new file mode 100755 index 000000000..7ebdd6e4e --- /dev/null +++ b/.ci/compile.sh @@ -0,0 +1,322 @@ +#!/bin/bash + +# This script is to be used by the ci environment from the project root directory, do not use it from somewhere else. + +# Compiles cockatrice inside of a ci environment +# --install runs make install +# --package [] runs make package, optionally force the type +# --suffix renames package with this suffix, requires arg +# --server compiles servatrice +# --test runs tests +# --debug or --release sets the build type ie CMAKE_BUILD_TYPE +# --ccache [] uses ccache and shows stats, optionally provide size +# --evict-ccache runs ccache eviction based on given age after build +# --dir sets the name of the build dir, default is "build" +# --cmake-generator sets CMAKE_GENERATOR as used by cmake +# --target-macos-version sets the min os version - only used for macOS builds +# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE CCACHE_EVICTION_AGE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION +# (correspond to args: --debug/--release --install --package --suffix --server --test --ccache --dir ) +# exitcode: 1 for failure, 3 for invalid arguments + +# Read arguments +while [[ $# != 0 ]]; do + case "$1" in + '--') + shift + ;; + '--install') + MAKE_INSTALL=1 + shift + ;; + '--package') + MAKE_PACKAGE=1 + shift + if [[ $# != 0 && ${1:0:1} != - ]]; then + PACKAGE_TYPE="$1" + shift + fi + ;; + '--suffix') + shift + if [[ $# == 0 ]]; then + echo "::error file=$0::--suffix expects an argument" + exit 3 + fi + PACKAGE_SUFFIX="$1" + shift + ;; + '--server') + MAKE_SERVER=1 + shift + ;; + '--no-client') + MAKE_NO_CLIENT=1 + shift + ;; + '--test') + MAKE_TEST=1 + shift + ;; + '--debug') + BUILDTYPE="Debug" + shift + ;; + '--release') + BUILDTYPE="Release" + shift + ;; + '--ccache') + USE_CCACHE=1 + shift + if [[ $# != 0 && ${1:0:1} != - ]]; then + CCACHE_SIZE="$1" + shift + fi + ;; + '--evict-ccache') + shift + if [[ $# == 0 ]]; then + echo "::error file=$0::--evict-ccache expects an argument" + exit 3 + fi + CCACHE_EVICTION_AGE=$1 + shift + ;; + '--vcpkg') + USE_VCPKG=1 + shift + ;; + '--dir') + shift + if [[ $# == 0 ]]; then + echo "::error file=$0::--dir expects an argument" + exit 3 + fi + BUILD_DIR="$1" + shift + ;; + '--cmake-generator') + shift + if [[ $# == 0 ]]; then + echo "::error file=$0::--cmake-generator expects an argument" + exit 3 + fi + export CMAKE_GENERATOR=$1 + shift + ;; + '--target-macos-version') + shift + if [[ $# == 0 ]]; then + echo "::error file=$0::--target-macos-version expects an argument" + exit 3 + fi + TARGET_MACOS_VERSION="$1" + shift + ;; + *) + echo "::error file=$0::unrecognized option: $1" + exit 3 + ;; + esac +done + +set -e + +# Setup +./servatrice/check_schema_version.sh +if [[ ! $BUILDTYPE ]]; then + BUILDTYPE=Release +fi +if [[ ! $BUILD_DIR ]]; then + BUILD_DIR="build" +fi +mkdir -p "$BUILD_DIR" +cd "$BUILD_DIR" + +# Set minimum CMake Version +export CMAKE_POLICY_VERSION_MINIMUM=3.10 + +# Add cmake flags +flags=("-DCMAKE_BUILD_TYPE=$BUILDTYPE") +if [[ $MAKE_SERVER ]]; then + flags+=("-DWITH_SERVER=1") +fi +if [[ $MAKE_NO_CLIENT ]]; then + flags+=("-DWITH_CLIENT=0" "-DWITH_ORACLE=0") +fi +if [[ $MAKE_TEST ]]; then + flags+=("-DTEST=1") +fi +if [[ $USE_CCACHE ]]; then + flags+=("-DUSE_CCACHE=1") + if [[ $CCACHE_SIZE ]]; then + # note, this setting persists after running the script + ccache --max-size "$CCACHE_SIZE" + fi +fi +if [[ $PACKAGE_TYPE ]]; then + flags+=("-DCPACK_GENERATOR=$PACKAGE_TYPE") +fi +if [[ $USE_VCPKG ]]; then + flags+=("-DUSE_VCPKG=1") +fi + +# Add cmake --build flags +buildflags=(--config "$BUILDTYPE") + +function ccachestatsverbose() { + # note, verbose only works on newer ccache, discard the error + local got + if got="$(ccache --show-stats --verbose 2>/dev/null)"; then + echo "$got" + else + ccache --show-stats + fi +} + +# Compile +if [[ $RUNNER_OS == macOS ]]; then + # QTDIR is needed for macOS since we actually only use the cached thin Qt binaries instead of the install-qt-action, + # which sets a few environment variables + if QTDIR=$(find "$GITHUB_WORKSPACE/Qt" -depth -maxdepth 2 -name macos -type d -print -quit); then + echo "found QTDIR at $QTDIR" + else + echo "could not find QTDIR!" + exit 2 + fi + # the qtdir is located at Qt/[qtversion]/macos + # we use find to get the first subfolder with the name "macos" + # this works independent of the qt version as there should be only one version installed on the runner at a time + export QTDIR + + if [[ $TARGET_MACOS_VERSION ]]; then + # CMAKE_OSX_DEPLOYMENT_TARGET is a vanilla cmake flag needed to compile to target macOS version + flags+=("-DCMAKE_OSX_DEPLOYMENT_TARGET=$TARGET_MACOS_VERSION") + + # vcpkg dependencies need a vcpkg triplet file to compile to the target macOS version + # an easy way is to copy the x64-osx.cmake file and modify it + triplets_dir="/tmp/cmake/triplets" + triplet_version="custom-triplet" + triplet_file="$triplets_dir/$triplet_version.cmake" + arch=$(uname -m) + if [[ $arch == x86_64 ]]; then + arch="x64" + fi + mkdir -p "$triplets_dir" + cp "../vcpkg/triplets/$arch-osx.cmake" "$triplet_file" + echo "set(VCPKG_CMAKE_SYSTEM_VERSION $TARGET_MACOS_VERSION)" >>"$triplet_file" + echo "set(VCPKG_OSX_DEPLOYMENT_TARGET $TARGET_MACOS_VERSION)" >>"$triplet_file" + flags+=("-DVCPKG_OVERLAY_TRIPLETS=$triplets_dir") + flags+=("-DVCPKG_HOST_TRIPLET=$triplet_version") + flags+=("-DVCPKG_TARGET_TRIPLET=$triplet_version") + echo "::group::Generated triplet $triplet_file" + cat "$triplet_file" + echo "::endgroup::" + fi + + echo "::group::Signing Certificate" + if [[ -n "$MACOS_CERTIFICATE_NAME" ]]; then + echo "$MACOS_CERTIFICATE" | base64 --decode >"certificate.p12" + security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain + security default-keychain -s build.keychain + security set-keychain-settings -t 3600 -l build.keychain + security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain + security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain + echo "macOS signing certificate successfully imported and keychain configured." + else + echo "No signing certificate configured. Skipping set up of keychain in macOS environment." + fi + echo "::endgroup::" + + if [[ $MAKE_PACKAGE ]]; then + # Workaround https://github.com/actions/runner-images/issues/7522 + # have hdiutil repeat the command 10 times in hope of success + hdiutil_script="/tmp/hdiutil.sh" + # shellcheck disable=SC2016 + echo '#!/bin/bash + i=0 + while ! hdiutil "$@"; do + if (( ++i >= 10 )); then + echo "Error: hdiutil failed $i times!" >&2 + break + fi + sleep 1 + done' >"$hdiutil_script" + chmod +x "$hdiutil_script" + flags+=(-DCPACK_COMMAND_HDIUTIL="$hdiutil_script") + fi + +elif [[ $RUNNER_OS == Windows ]]; then + # Enable MTT, see https://devblogs.microsoft.com/cppblog/improved-parallelism-in-msbuild/ + # and https://devblogs.microsoft.com/cppblog/cpp-build-throughput-investigation-and-tune-up/#multitooltask-mtt + buildflags+=(-- -p:UseMultiToolTask=true -p:EnableClServerMode=true) +fi + +if [[ $USE_CCACHE ]]; then + echo "::group::Show ccache stats" + ccachestatsverbose + echo "::endgroup::" +fi + +echo "::group::Configure cmake" +cmake --version +echo "Running cmake with flags: ${flags[*]}" +cmake .. "${flags[@]}" +echo "::endgroup::" + +echo "::group::Build project" +echo "Running cmake --build with flags: ${buildflags[*]}" +cmake --build . "${buildflags[@]}" +echo "::endgroup::" + +if [[ $USE_CCACHE ]]; then + if [[ $CCACHE_EVICTION_AGE ]]; then + echo "::group::evict ccache files older than $CCACHE_EVICTION_AGE" + ccache --evict-older-than "$CCACHE_EVICTION_AGE" + echo "::endgroup::" + fi + echo "::group::Show ccache stats again" + ccachestatsverbose + echo "::endgroup::" +elif [[ $CCACHE_EVICTION_AGE ]]; then + echo "::error file=$0::ccache eviction is enabled while ccache is disabled!" +fi + +if [[ $RUNNER_OS == macOS ]]; then + echo "::group::Inspect Mach-O binaries" + for app in cockatrice oracle servatrice; do + binary="$GITHUB_WORKSPACE/build/$app/$app.app/Contents/MacOS/$app" + echo "Inspecting $app..." + vtool -show-build "$binary" + file "$binary" + lipo -info "$binary" + echo "" + done + echo "::endgroup::" +fi + +if [[ $MAKE_TEST ]]; then + echo "::group::Run tests" + ctest -C "$BUILDTYPE" --output-on-failure + echo "::endgroup::" +fi + +if [[ $MAKE_INSTALL ]]; then + echo "::group::Install" + cmake --build . --target install --config "$BUILDTYPE" + echo "::endgroup::" +fi + +if [[ $MAKE_PACKAGE ]]; then + echo "::group::Create package" + cmake --build . --target package --config "$BUILDTYPE" + echo "::endgroup::" + + if [[ $PACKAGE_SUFFIX ]]; then + echo "::group::Update package name" + cd .. + BUILD_DIR="$BUILD_DIR" .ci/name_build.sh "$PACKAGE_SUFFIX" + echo "::endgroup::" + fi +fi diff --git a/.ci/docker.sh b/.ci/docker.sh new file mode 100644 index 000000000..46112daaa --- /dev/null +++ b/.ci/docker.sh @@ -0,0 +1,188 @@ +#!/bin/bash + +# This script is to be used by the ci environment from the project root directory, do not use it from somewhere else. + +# Creates or loads docker images to use in compilation, creates RUN function to start compilation on the docker image. +# +# usage: source + + + + + + + + + + diff --git a/doc/doxygen/html/header.html b/doc/doxygen/html/header.html new file mode 100644 index 000000000..261581add --- /dev/null +++ b/doc/doxygen/html/header.html @@ -0,0 +1,81 @@ + + + + + + + + + $projectname: $title + $title + + + + + + + + + + + + + $treeview + $search + $mathjax + $darkmode + + $extrastylesheet + + + +
+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
$projectname $projectnumber + +
+ +
$projectbrief
+
+
$projectbrief
+
$searchbox
$searchbox
+
+ + diff --git a/doc/doxygen/images/classic_database_display.png b/doc/doxygen/images/classic_database_display.png new file mode 100644 index 000000000..fb762d9a3 Binary files /dev/null and b/doc/doxygen/images/classic_database_display.png differ diff --git a/doc/doxygen/images/classic_database_display_add_buttons.png b/doc/doxygen/images/classic_database_display_add_buttons.png new file mode 100644 index 000000000..b9cb7cd32 Binary files /dev/null and b/doc/doxygen/images/classic_database_display_add_buttons.png differ diff --git a/doc/doxygen/images/classic_deck_editor.png b/doc/doxygen/images/classic_deck_editor.png new file mode 100644 index 000000000..9371651e9 Binary files /dev/null and b/doc/doxygen/images/classic_deck_editor.png differ diff --git a/doc/doxygen/images/deck_dock_deck_list.png b/doc/doxygen/images/deck_dock_deck_list.png new file mode 100644 index 000000000..bf12d97c2 Binary files /dev/null and b/doc/doxygen/images/deck_dock_deck_list.png differ diff --git a/doc/doxygen/images/deck_dock_deck_list_buttons.png b/doc/doxygen/images/deck_dock_deck_list_buttons.png new file mode 100644 index 000000000..cb66b41da Binary files /dev/null and b/doc/doxygen/images/deck_dock_deck_list_buttons.png differ diff --git a/doc/doxygen/images/deck_dock_deck_list_group_by.png b/doc/doxygen/images/deck_dock_deck_list_group_by.png new file mode 100644 index 000000000..ba23b1fb5 Binary files /dev/null and b/doc/doxygen/images/deck_dock_deck_list_group_by.png differ diff --git a/doc/doxygen/images/deckeditordeckdockwidget.png b/doc/doxygen/images/deckeditordeckdockwidget.png new file mode 100644 index 000000000..33b9588d0 Binary files /dev/null and b/doc/doxygen/images/deckeditordeckdockwidget.png differ diff --git a/doc/doxygen/images/printing_selector.png b/doc/doxygen/images/printing_selector.png new file mode 100644 index 000000000..c06ac90bc Binary files /dev/null and b/doc/doxygen/images/printing_selector.png differ diff --git a/doc/doxygen/images/printing_selector_disable.png b/doc/doxygen/images/printing_selector_disable.png new file mode 100644 index 000000000..8bdd8f292 Binary files /dev/null and b/doc/doxygen/images/printing_selector_disable.png differ diff --git a/doc/doxygen/images/printing_selector_enable.png b/doc/doxygen/images/printing_selector_enable.png new file mode 100644 index 000000000..2c7d05afa Binary files /dev/null and b/doc/doxygen/images/printing_selector_enable.png differ diff --git a/doc/doxygen/images/printing_selector_navigation.png b/doc/doxygen/images/printing_selector_navigation.png new file mode 100644 index 000000000..a91ed4511 Binary files /dev/null and b/doc/doxygen/images/printing_selector_navigation.png differ diff --git a/doc/doxygen/images/printing_selector_options.png b/doc/doxygen/images/printing_selector_options.png new file mode 100644 index 000000000..2079b70ac Binary files /dev/null and b/doc/doxygen/images/printing_selector_options.png differ diff --git a/doc/doxygen/images/printing_selector_pre_providerid.png b/doc/doxygen/images/printing_selector_pre_providerid.png new file mode 100644 index 000000000..a1be993ee Binary files /dev/null and b/doc/doxygen/images/printing_selector_pre_providerid.png differ diff --git a/doc/doxygen/images/release_channel_beta.png b/doc/doxygen/images/release_channel_beta.png new file mode 100644 index 000000000..00869a9cf Binary files /dev/null and b/doc/doxygen/images/release_channel_beta.png differ diff --git a/doc/doxygen/images/vde_deck_analytics.png b/doc/doxygen/images/vde_deck_analytics.png new file mode 100644 index 000000000..0b4b11e19 Binary files /dev/null and b/doc/doxygen/images/vde_deck_analytics.png differ diff --git a/doc/doxygen/images/vde_flat_layout_color_grouped.png b/doc/doxygen/images/vde_flat_layout_color_grouped.png new file mode 100644 index 000000000..51f176668 Binary files /dev/null and b/doc/doxygen/images/vde_flat_layout_color_grouped.png differ diff --git a/doc/doxygen/images/vde_flat_layout_type_grouped.png b/doc/doxygen/images/vde_flat_layout_type_grouped.png new file mode 100644 index 000000000..4a33e0017 Binary files /dev/null and b/doc/doxygen/images/vde_flat_layout_type_grouped.png differ diff --git a/doc/doxygen/images/vde_overlap_layout_type_grouped.png b/doc/doxygen/images/vde_overlap_layout_type_grouped.png new file mode 100644 index 000000000..606144586 Binary files /dev/null and b/doc/doxygen/images/vde_overlap_layout_type_grouped.png differ diff --git a/doc/doxygen/images/vde_sample_hand.png b/doc/doxygen/images/vde_sample_hand.png new file mode 100644 index 000000000..95924a5d4 Binary files /dev/null and b/doc/doxygen/images/vde_sample_hand.png differ diff --git a/doc/doxygen/js/graph_toggle.js b/doc/doxygen/js/graph_toggle.js new file mode 100644 index 000000000..3d8fa83d0 --- /dev/null +++ b/doc/doxygen/js/graph_toggle.js @@ -0,0 +1,147 @@ +document.addEventListener("DOMContentLoaded", () => { + document.querySelectorAll(".memdoc").forEach(memdoc => { + let callContent = null, callHeader = null; + let callerContent = null, callerHeader = null; + + memdoc.querySelectorAll("div.dynheader").forEach(header => { + const text = (header.textContent || "").trim().toLowerCase(); + let content = header.nextElementSibling; + let tries = 0; + while (content && !content.classList.contains("dyncontent") && tries < 8) { + content = content.nextElementSibling; + tries++; + } + if (!content) return; + + if (text.includes("caller")) { + callerContent = content; + callerHeader = header; + } else if (text.includes("call graph") || text.includes("callgraph") || text.includes("call graph for")) { + callContent = content; + callHeader = header; + } else if (text.includes("call")) { + if (!callContent) { + callContent = content; + callHeader = header; + } + } + }); + + if (!callContent && !callerContent) return; + if (memdoc.querySelector(".graph-toggle")) return; + + const toggle = document.createElement("div"); + toggle.className = "graph-toggle"; + + const callerBtn = document.createElement("button"); + callerBtn.type = "button"; + callerBtn.textContent = "Caller Graph"; + const callBtn = document.createElement("button"); + callBtn.type = "button"; + callBtn.textContent = "Call Graph"; + + toggle.appendChild(callerBtn); + toggle.appendChild(callBtn); + + const firstHeader = memdoc.querySelector("div.dynheader"); + memdoc.insertBefore(toggle, firstHeader || memdoc.firstChild); + + // hide everything initially + if (callerContent) { + callerContent.style.display = "none"; + callerHeader.style.display = "none"; + } + if (callContent) { + callContent.style.display = "none"; + callHeader.style.display = "none"; + } + + // disable missing buttons + if (!callerContent) { + callerBtn.disabled = true; + callerBtn.classList.add("disabled"); + } + if (!callContent) { + callBtn.disabled = true; + callBtn.classList.add("disabled"); + } + + // track current state + let current = null; // "caller", "call", "both", null=hidden + + function setActive(type) { + if (type === "caller") { + if (current === "caller") { // hide it + if (callerContent) { + callerContent.style.display = "none"; + callerHeader.style.display = "none"; + } + current = null; + } else if (current === "call") { // show both + if (callerContent) { + callerContent.style.display = "block"; + callerHeader.style.display = "block"; + } + current = "both"; + } else if (current === "both") { // hide caller only → call only + if (callerContent) { + callerContent.style.display = "none"; + callerHeader.style.display = "none"; + } + current = "call"; + } else { // nothing visible → show caller + if (callerContent) { + callerContent.style.display = "block"; + callerHeader.style.display = "block"; + } + if (callContent) { + callContent.style.display = "none"; + callHeader.style.display = "none"; + } + current = "caller"; + } + } else if (type === "call") { + if (current === "call") { // hide it + if (callContent) { + callContent.style.display = "none"; + callHeader.style.display = "none"; + } + current = null; + } else if (current === "caller") { // show both + if (callContent) { + callContent.style.display = "block"; + callHeader.style.display = "block"; + } + current = "both"; + } else if (current === "both") { // hide call only → caller only + if (callContent) { + callContent.style.display = "none"; + callHeader.style.display = "none"; + } + current = "caller"; + } else { // nothing visible → show call only + if (callContent) { + callContent.style.display = "block"; + callHeader.style.display = "block"; + } + if (callerContent) { + callerContent.style.display = "none"; + callerHeader.style.display = "none"; + } + current = "call"; + } + } + + // update button styles + callerBtn.classList.toggle("active", current === "caller" || current === "both"); + callBtn.classList.toggle("active", current === "call" || current === "both"); + } + + callerBtn.addEventListener("click", () => { + if (!callerBtn.disabled) setActive("caller"); + }); + callBtn.addEventListener("click", () => { + if (!callBtn.disabled) setActive("call"); + }); + }); +}); diff --git a/doc/doxygen/theme b/doc/doxygen/theme new file mode 160000 index 000000000..d52eafe3e --- /dev/null +++ b/doc/doxygen/theme @@ -0,0 +1 @@ +Subproject commit d52eafe3e9303399fda15661f3d7bb8fe3d7eabc diff --git a/doc/faq.md b/doc/faq.md deleted file mode 100644 index 32703d20e..000000000 --- a/doc/faq.md +++ /dev/null @@ -1,138 +0,0 @@ -FAQ -=== - -How to update the card database ---- - -When a new set comes out, you need to update your card database to be able to -see the new cards in the deck editor and use them in the game. To do this, -open the Oracle tool in the Cockatrice folder. It's a stand-alone program and -is run directly out of the folder without running Cockatrice. - -When the Oracle tool is running, open the menu and select 'Download sets -information'. Use the suggested address and click OK. After at most a few -seconds, you should be presented a list of sets to be downloaded. Do not -uncheck the ones you already have: they should be downloaded again. Click -'Start download' and wait. When the process is finished, the database should -be up to date. - - -Change Card Art / Custom Card Art ---- - -Is it possible to change the card art for a specif card, not by changing the -set order in my deck editor? - -YES! It is actually very simple. - -All you have to do is get a .JPG of the card you wish to change and name the -actual file of the image to the cards (exact) name and add ”.Full” to the end. -You can then take this new card image and replace the old one in your -downloaded image folder. - -*NOTE: Image size does not matter. Try to use a high resolution .JPG of the -card you want to use for best quality.* - -EXAMPLE: Let's say you wish to turn your M11 Sun Titan from its original art -to a promo version. - -If you are using Windows 7, you can find the default location for the -Cockatrice card art files under - -``` -C:\Program Files (x86)\Cockatrice\pics\downloadedPics -``` - -Once there, look for the folder of the -set for the card we are replacing. For this example, Sun Titan is in the M11 -folder. Open the folder and paste/replace your new “Promo” Sun Titan.full[.JPG] -into this folder, discarding the original copy. (If you have -not yet downloaded the original image of a card by selecting it in the deck -editor or playing it in an online game, the card image will not yet be there) - -Once you have the new card image in the proper folder, you can now start -Cockatrice (Or re-start if you already had the program open)and select the -deck editor to see your new card image. - -If you are changing the art of a card that has multiple versions in other -sets, make sure you put the new card in whatever folder (or set) is highest -on your card database set list. - -*NOTE: Other players on Cockatrice will NOT see your new card image. They will -only see whatever version of the card is in their database.* - - -Use Higher Resolution Cards ---- - -Can you get better/higher resolution card art than the default downloaded card -images already used? - -Yes! - -When you click on a card for the first time in the Deck Editor, Cockatrice -goes onto the internet and finds an image of that card from a database on -another website. - -If you find a higher resolution .JPG of a card that you wish to use on your -Cockatrice, you can replace the image with no problem. If you can find a card -image in the 3,000 by 1,000 pixel range, and save it as the cards (exact) name -and add .Full to the end. - -If you are using Windows 7, you can find the default location for the -Cockatrice card art files under - -``` -C:\Program Files(x86)\Cockatrice\pics\downloadedPics -``` - -Once there, look for the folder of the set for the card you are replacing. If you are not running Cockatrice as an -administrator, look in - -``` -C:\Users\username\AppData\Local\VirtualStore\Program Files (x86)\Cockatrice\pics\downloadedPics -``` - -Open the folder of the set with the card, and paste/replace the new higher -resolution .JPG image with the old low quality one. - -This process is the same for changing cards to custom images. - - -*NOTE: Other Cockatrice players will not see your higher resolution card art, -they will only see the image they have in their own database.* - - -Booster Drafts ---- -Cockatrice does not support booster drafts, so players need to use an -external service for this. - -Most people use [ccgdecks.com](http://ccgdecks.com/) so it's probably not a bad idea to -register an account there before joining a game in Cockatrice. - - -How to link a card in the Cockatrice chat. ---- - -To link a card in the Cockatrice chat, type out the full name of the card, -surrounded by the [card] and [/card] tags. - -For example: - -``` -[card]Black Lotus[/card] -``` - -How to link a URL in the Cockatrice chat. ---- -To link a URL in the Cockatrice chat, type out the url, surrounded by the -[url] and [/url] tags. - -For example: - -``` -[url]http://www.cockatrice.de[/url] -[url]cockatrice.de[/url] -``` - diff --git a/doc/sets.xml b/doc/sets.xml deleted file mode 100644 index bcc7c20ef..000000000 --- a/doc/sets.xml +++ /dev/null @@ -1,514 +0,0 @@ - - -http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!cardid!&type=card - -http://mtgjson.com/json/!name!.json - - ARB - Alara Reborn - - - ALL - Alliances - - - ATQ - Antiquities - - - APC - Apocalypse - - - ARN - Arabian Nights - - - ARC - Archenemy - - - AVR - Avacyn Restored - - - BRB - Battle Royale Box Set - - - BTD - Beatdown Box Set - - - BOK - Betrayers of Kamigawa - - - BNG - Born of the Gods - - - CHK - Champions of Kamigawa - - - CHR - Chronicles - - - 6ED - Classic Sixth Edition - - - CSP - Coldsnap - - - CON - Conflux - - - CMA - Commander's Arsenal - - ^M - C13^M - Commander 2013 Edition^M - - - DST - Darksteel - - - DKA - Dark Ascension - - - DIS - Dissension - - - DGM - Dragon's Maze - - - DDH - Duel Decks: Ajani vs. Nicol Bolas - - - DDC - Duel Decks: Divine vs. Demonic - - - DDF - Duel Decks: Elspeth vs. Tezzeret - - - EVG - Duel Decks: Elves vs. Goblins - - - DDD - Duel Decks: Garruk vs. Liliana - - - DDL - Duel Decks: Heroes vs. Monsters - - - DDJ - Duel Decks: Izzet vs. Golgari - - - DD2 - Duel Decks: Jace vs. Chandra - - - DDM - Duel Decks: Jace vs. Vraska - - - DDG - Duel Decks: Knights vs. Dragons - - - DDE - Duel Decks: Phyrexia vs. the Coalition - - - DDK - Duel Decks: Sorin vs. Tibalt - - - DDI - Duel Decks: Venser vs. Koth - - - 8ED - Eighth Edition - - - EVE - Eventide - - - EXO - Exodus - - - FEM - Fallen Empires - - - 5DN - Fifth Dawn - - - 5ED - Fifth Edition - - - 4ED - Fourth Edition - - - DRB - From the Vault: Dragons - - - V09 - From the Vault: Exiled - - - V11 - From the Vault: Legends - - - V12 - From the Vault: Realms - - - V10 - From the Vault: Relics - - - V13 - From the Vault: Twenty - - - FUT - Future Sight - - - GPT - Guildpact - - - GTC - Gatecrash - - - HML - Homelands - - - ICE - Ice Age - - - ISD - Innistrad - - - INV - Invasion - - - JOU - Journey into Nyx - - - JUD - Judgment - - - LEG - Legends - - - LGN - Legions - - - LEA - Limited Edition Alpha - - - LEB - Limited Edition Beta - - - LRW - Lorwyn - - - M10 - Magic 2010 - - - M11 - Magic 2011 - - - M12 - Magic 2012 - - - M13 - Magic 2013 - - - M14 - Magic 2014 Core Set - - - CMD - Magic: The Gathering-Commander - - - CNS - Magic: The Gathering—Conspiracy - - - MED - Masters Edition - - - ME2 - Masters Edition II - - - ME3 - Masters Edition III - - - ME4 - Masters Edition IV - - - MMQ - Mercadian Masques - - - MIR - Mirage - - - MRD - Mirrodin - - - MBS - Mirrodin Besieged - - ^M - MMA^M - Modern Masters^M - ^ - - MOR - Morningtide - - - NMS - Nemesis - - - NPH - New Phyrexia - - - 9ED - Ninth Edition - - - ODY - Odyssey - - - ONS - Onslaught - - - PLC - Planar Chaos - - - HOP - Planechase - - - PC2 - Planechase 2012 Edition - - - PLS - Planeshift - - - POR - Portal - - - PO2 - Portal Second Age - - - PTK - Portal Three Kingdoms - - - PD2 - Premium Deck Series: Fire and Lightning - - - PD3 - Premium Deck Series: Graveborn - - - H09 - Premium Deck Series: Slivers - - - PPR - Promo set for Gatherer - - - PCY - Prophecy - - - RAV - Ravnica: City of Guilds - - - RTR - Return to Ravnica - - - 3ED - Revised Edition - - - ROE - Rise of the Eldrazi - - - SOK - Saviors of Kamigawa - - - SOM - Scars of Mirrodin - - - SCG - Scourge - - - 7ED - Seventh Edition - - - SHM - Shadowmoor - - - ALA - Shards of Alara - - - S99 - Starter 1999 - - - S00 - Starter 2000 - - - STH - Stronghold - - - TMP - Tempest - - - 10E - Tenth Edition - - - DRK - The Dark - - - THS - Theros - - - TSP - Time Spiral - - - TSB - Time Spiral "Timeshifted" - - - TOR - Torment - - - UGL - Unglued - - - UNH - Unhinged - - - 2ED - Unlimited Edition - - - UDS - Urza's Destiny - - - ULG - Urza's Legacy - - - USG - Urza's Saga - - - VAN - Vanguard - - - VIS - Visions - - - WTH - Weatherlight - - - WWK - Worldwake - - - ZEN - Zendikar - - diff --git a/doc/shortcuts.md b/doc/shortcuts.md deleted file mode 100644 index 68e512e92..000000000 --- a/doc/shortcuts.md +++ /dev/null @@ -1,48 +0,0 @@ -Keyboard Shortcuts -================== - -Keyboard Shortcuts can be used while playing a game on Cockatrice. Players use -them to do simple and recurring tasks without the use of looking though menus -to find the proper action. - -Shortcut Action ----------------------- - -| key | action -| ------------------------ | --------------------------------------------------------------------- -| F3 | View library -| F4 | View graveyard -| Ctrl+F3 | View sideboard -| Ctrl+W | View top car(s) of library (Choose a number) -| Ctrl+S | Shuffle deck -| Ctrl+I | Roll dice -| Ctrl+M | Draw 7, then mulligan successively -| Ctrl+D | Draw a card -| Ctrl+E | Draw Cards (Choose a number) -| Ctrl+Shift+D | Undo last draw -| Ctrl+L | Set Life (useful when adding or subtracting a large number of life) -| F11 | Minus 1 life -| F12 | Add 1 life -| Ctrl+U | Untap all permanents you control -| Ctrl+R | Remove all local arrows -| Ctrl+T | Brings up menu to Create a token -| Ctrl+G | Creates last token you made -| (Select card) Ctrl+Del | Move to graveyard -| (Select card) Ctrl+A | Attach Card (For equipment and auras) -| (Select card) Ctrl+H | Clone (Make copy of) selected card -| (Select card) Ctrl++ | Increase power -| (Select card) Ctrl+- | Decrease power -| (Select card) Alt++ | Increase toughness -| (Select card) Alt+- | Decrease toughness -| (Select card) Ctrl+Alt++ | Increase power and toughness -| (Select card) Ctrl+Alt+- | Decrease power and toughness -| (Select card) Ctrl+P | Set power and toughness -| Ctrl+Enter (Return) | Next turn -| Ctrl+Space | Move to next phase -| F5 | Untap step -| F6 | Draw step -| F7 | First main phase -| F8 | Beginning of combat step -| F9 | Second main phase -| F10 | End of turn step -| F2 | Concede game diff --git a/doc/usermanual/Usermanual.md b/doc/usermanual/Usermanual.md deleted file mode 100644 index 1656be143..000000000 --- a/doc/usermanual/Usermanual.md +++ /dev/null @@ -1,1257 +0,0 @@ -Preface -======= - -This manual is basically a dump from the cockatrice.de dokuwiki. -Cockatrice has some legal problems right now and the page is down. This -document tries to save the documentation about the software, beautify -and extend it in the future. Please contribute to the project, it is -much too precious to be destroyed. - -Getting Started -=============== - -Making A User Profile ---------------------- - -Not available anymore, site is down. If someone runs his own server -where you can register a user profile, read his documentation. - -Downloading and Installing the Cockatrice Program -------------------------------------------------- - -Due to a legal dispute there are currently no official builds left, so -currently you have to build your own binaries. - -### Client compilation - -#### Windows - -There should be two ways to compile Cockatrice: With Visual Studio 2010 -and with MinGW. As the Visual Studio method is more complicated and -might not work, you should do it with MinGW. The following howto has -been tested with Windows 7 64Bit. - -##### Prerequisites - -We need the Cockatrice sourcecode, it’s dependencies and build tools. -Everything is freely available: - -1. MinGW is needed to compile everything, it ships the compiler and - other tools for this task. - -2. git is needed to download the latest Cockatrice source code. - -3. cmake is needed to create Cockatrice’s project files for MinGW. - -4. Qt 4.8 is a dependency for Cockatrice (download the MinGW version!) - -5. protobuf 2.5 is another dependency for Cockatrice (download the zip - file with the sourcecode). - -6. Nullsoft Scriptable Install System (NSIS) which can be used to - create an installer for your own Cockatrice version. - -All downloadlinks together: - -##### Installation of the Prerequisites - -1. Download MinGW (mingw-get-inst), git, cmake, protobuf 2.5 sources - zip and Qt 4.8.4 MinGW (see links above). - -2. Make a standard installation for git, cmake and NSIS. - -3. Install MinGW: - - 1. Select default values everywhere, except: Also check the C++ - Compiler, check the MSYS Basis System and MinGW Developer - Toolkit. - - 2. Append `C:\MinGW\bin` to your PATH variable (google how to do it - for your Windows version, if you don’t know it). This is very - important and overseen many times! You can keep the PATH - configuration dialog open for the next step. - -4. Install Qt, select default values everywhere, ignore the message - regarding win32.h and append `C:\Qt\4.8.4\bin` to your PATH variable - like you did for MinGW. - -5. Unpack protobuf-2.5.0.zip in `C:\MinGW\msys\1.0\home\YOURLOGIN` - - 1. Open MinGW Shell, type the following commands exactly: - - 2. `cd protobuf-2.5.0/protobuf-2.5.0` - - 3. `./configure –prefix=‘cd /mingw; pwd -W‘` (these apostrophs are - backticks and do not forget the dot at the beginning!) - - 4. `make; make install` (this builds and installs protobuf, which - needs some time) - - 5. Close MinGW Shell - -Your system is now able to compile Cockatrice. You do not have to repeat -the process so far ever again. - -##### Cockatrice Compilation - -1. Checkout Cockatrice: - - 1. Start Git Bash and type the following command exactly: - - 2. `git clone https://github.com/Daenyth/Cockatrice` - - 3. Close Git Bash - -2. Start CMake (cmake-gui) and do the following: - - 1. Where is the source code: Point to the Cockatrice directory - - 2. Where to build the binaries: Point to the Cockatrice/build - directory (it doesn’t matter if it exists; if it doesn’t, cmake - will ask later if it shall create this directory) - - 3. Check Advanced - - 4. Click Configure, choose MinGW, leave the rest default - - 5. An error will occur, set the following: - - - Set PROTOBUF\_INCLUDE\_DIR to - C:/MinGW/include/google/protobuf - - - Set PROTOBUF\_LIBRARY to C:/MinGW/lib/libprotobuf.dll.a - - - Set PROTOBUF\_LITE\_LIBRARY to - C:/MinGW/lib/libprotobuf-lite.dll.a - - - Set PROTOBUF\_PROTOC\_EXECUTABLE to C:/MinGW/bin/protoc.exe - - - Set PROTOBUF\_PROTOC\_LIBRARY to - C:/MinGW/lib/libprotoc.dll.a - - 6. Click Configure again, then Generate - - 7. Close CMake - -3. Start cmd.exe (the Windows Command Prompt) and type the following - commands: - - 1. `cd Cockatrice/build` (this changes to the build directory) - - 2. `make` (this builds everything and might need some time) - -Cockatrice has now been downloaded and built for the first time. You do -not have to repeat the process so far ever again. - -##### Updating your Cockatrice build - -If you just compiled Cockatrice for the first time, you skip this step -obviously. But if you want to update Cockatrice after the source code -changed on github, do it like this: - -1. Start Git Bash and update Cockatrice: - - 1. `git pull origin master` - - 2. `Close Git Bash` - -2. Start cmd.exe, change to Cockatrice/build, type make like you did - previously. - -Cockatrice has now been updated and built. You may repeat this process -every time when the source code changed. - -##### Cockatrice installation - -To install Cockatrice, you have to create an installer with NSIS now. -Change to the directory `nsis` in the Cockatrice root directory, right -click the cockatrice.nsi file and select `Compile NSIS Script`. The NSIS -program then creates a file called -cockatrice\_win32\_YYYYmmdd\_git-xxxxxxx.exe. This is the complete, -redistributable installer for your Cockatrice build. - -Now install Cockatrice by executing the installer. Note: if you -installed MinGW or Qt in other than the default paths, you have to fix -the paths in the cockatrice.nsi file (also if some libraries change); -you can edit this file with a text editor. - -##### Create a card database - -Start the oracle.exe (the installer does this automatically) and let it -generate a current cards.xml file: - -1. File $\to$ Download Sets Information $\to$ OK (if there are no MtG - sets listed) - -2. Check All, Start Download - -Congratulations, you may now use Cockatrice! - -#### Linux and BSD - -The following procedures have been tested with Debian Wheezy, Fedora 18 -and FreeBSD 9.1. If you use Gentoo with KDE you have the needed -prerequisites and may continue with downloading the source. If you use -Bodhi or Arch Linux (AUR) or another distribution that includes -Cockatrice, you might install Cockatrice from the default packages – -though the package might be old, so you probably should continue with -this howto. - -Before you install new software, you should update your system. The -following instructions failed on a fresh installation of Fedora 18 and -FreeBSD 9.1 until the systems were updated. - -1. You need to install the build tools and dependencies. This varies - between the Linux distributions. - - Debian, Ubuntu and spin-offs - : `sudo apt-get install build-essential git libqt4-dev qtmobility-dev libprotobuf-dev protobuf-compiler cmake` - - Fedora - : `sudo yum groupinstall "Development Tools" yum install qt-devel qt-mobility-devel protobuf-devel protobuf-compiler cmake` - - FreeBSD - : `pkg_add -r qt4 qt4-linguist qt4-moc qt4-qmake qt4-rcc qt4-uic git cmake protobuf` - -2. Download the sources from github via - `git clone https://github.com/Daenyth/Cockatrice.git` - -3. To compile the sources, change into the newly created directory, - create a build directory and invoke cmake: - - i. `cd Cockatrice` - - ii. `mkdir build` - - iii. `cd build` - - iv. `cmake ..` - - v. `make` - - If you have some issues with pthread\_ add ’pthread’ to the - “target\_link\_libraries” entry in the `CMakeFiles.txt` in - `Cockatrice/common`. - -4. You may install the program into the directory `/usr/local` by - typing `sudo make install` but you should also be able to start - cockatrice and the oracle from the build directory. - -5. Before you start Cockatrice for the first time, run `oracle -dlsets` - and download available cards, denn run `cockatrice`. The default - paths for decks, pics, cards and tokens are located in - `/home//.local/share/data/Cockatrice/Cockatrice`. - -#### MacOS X - -TODO, please contribute this section! See Linux section, then use the -`prepareMacRelease.sh` script from Cockatrice. - -### Server compilation - -You don’t need your own server if you plan to play only. But as -Cockatrice is open source you are free to run your own. The compilation -works like already written above, but instead of invoking `cmake ..`, -you have to do it like this: - -- If you want to build the server, use: - `cmake -DWITH_SERVER=1 ..` - -- If you want to build the server, but not the client, use: - `cmake -DWITH_SERVER=1 -DWITHOUT_CLIENT=1 ..` - -There is more information on compiling and running Servatrice on CentOS -6 in chapter [servatrice] on page . - -Downloading Card Database Using the Oracle ------------------------------------------- - -If you are installing Cockatrice for the first time, changing what sets -are in your database or even adding the newest set to your database, -this tutorial will show you how to do it properly. - -The Oracle will automatically run after the initial setup of Cockatrice. -If you would like to re-install your database or add new sets you can -find it (for Windows) by clicking the start menu, going to all programs, -selecting the Cockatrice folder, and in there you will find the Oracle -tool. - -- When the Oracle importer opens, click on “File” in the top left - corner and select “Download sets information…” - - ![image](pics/fetch554a.jpg) - -- This will bring up a box where you can input the URL of a card - database. The default address is - this was an XML file found - on the Cockatrice website that has the current set listings for - Magic the Gathering. As the page is down, you have to import the - file which is distributed with the Cockatrice sources. This can also - be done from the file menu. - -- Select “OK” to load the set listings. - -- A list of all current sets will be brought up. A default selection - of sets will automatically be checked. From here you can check or - uncheck all sets, or you may only download specific sets that you - wish to play with. - - ![image](pics/fetchfc3d.jpg) - - NOTE: If you are playing against someone who is using a card that is - not in your database, you will not see a card image or oracle text - for that card. Some players like to download all sets to avoid this - issue, but other players who only play specific formats (Like T2, - Standard, or Extended) wish to keep their database small with only - cards they will be using. - -- After you select which sets you wish to download, select “Start - download” at the bottom of the Oracle to download the selected sets - information. - -- After download is complete, close the Oracle and run Cockatrice. - -- We are now ready to sort our set information in our deck editor. - -Editing Set Order and Preference of Card Art --------------------------------------------- - -Many cards have been re-printed in different sets, and in return have -different versions of artwork (like the card “Cancel” which can be found -in many sets, but has different artwork for each, e.g. Zendikar versus -M11: - -![image](pics/fetchc18b.jpg) ![image](pics/fetche1f4.jpg) - -Some players like to have the most current artwork displayed on their -cards, while other players have a favorite set they wish to display -instead. - -- Run Cockatrice and select “Deck editor” from the top right - Cockatrice menu. This will bring up the Deck Editor along with a - list of all cards that are currently in your database which you - downloaded using the Oracle Tool. - -- To change what version of the cards will be shown, click on “Card - database” on the top left of the Deck editor window, and select - “Edit sets…” - - ![image](pics/fetchf924.jpg) - -- This will bring up a new window that has a list of all sets you - currently have downloaded to your database. To change the order of - the sets, simply drag and drop them into place. This will determine - which artwork is shown for your cards. If a card is found in - multiple sets, whichever set is closest to the top of this list will - be the art displayed. Example: If M11 is above Zendikar, The M11 - version of the card “Cancel” will be displayed in your Deck editor - and Cockatrice games. - -NOTE: Your opponent will NOT see what artwork you have selected for each -of your cards. They will only see what they have selected for their own. - -The Deck Editor / Making a Deck -------------------------------- - -The Cockatrice Deck Editor is a tool you can use to make decks to play -online. The cards shown in the Deck Editor are from a database that you -downloaded with the Oracle Tool. If you are missing cards or a new set -has come out, you must re-run the Oracle and download set information. - -![image](pics/fetch52e0.jpg) - -1. Search Bar -: The search bar lets you type in the name of a card and the editor - will only show cards that start with whatever you typed in. Example: - Typing in ‘B’ will show all cards that start with the letter ‘B’ and - typing in ‘Dark’ will show you all cards that start with ‘Dark’ and - so on. If you were looking for the card “Sun Titan”, you would not - type in ‘Titan’ you would have to type in ‘Sun’ first. Typing in - ‘Titan’ will only show you any cards that start with ‘Titan’. - -2. Card Search/Filter -: The Card search button will bring up a new window that helps you - filter out cards more specifically. A variety of check boxes will - help find what you need. Card name lets you filter out only cards - that have a cretin word in them. Card text can help you find key - words like “Haste” or “Infect”. If you were to uncheck all boxes - except for “Instant” along with “Artifact” and “U”, the Deck editor - will only show you all Blue Instant and Artifact cards. - - |Letter | Card Type | - |-------|-----------| - | U | Blue | - | W | White | - | X | Colorless | - | G | Green | - | R | Red | - | B | Black | - -3. Card Data -: This section shows the Oracle text for the card that you currently - have selected. It will show you up-to-date information on the card - such as the Name, Mana cost, Card type, Power/Toughness, and any - abilities the card has. It will not show you flavor text. - -4. Adding/Removing Card Buttons -: The buttons in the bottom middle will add or remove cards from your - Deck List, as well as ad a card specifically to your Sideboard. - Having a card selected on the left column and hitting the Enter key - will also add it to your deck list. - -5. Deck Name/Comments -: The area in the top right lets you name your deck as well as give - any comments or descriptions such as how to play the deck, where the - deck came from, or explain a theme. Putting something in the “Deck - Name” space will NOT be what the file name of the file for your - deck. That is spate in the “save” selection under the “Deck” menu - found at the top left of the Deck Editor window. - -6. Main Deck List -: This area will show you what cards you currently have added to your - deck list. It is sorted by card type and also shows you how many of - each card and card type you have added, as well as keeps track of - how many total cards you have added to your main deck list. This - will not add any number of cards you have added to your Sideboard. - -7. Sideboard List -: The bottom section of the deck list shows any cards you have added - to your sideboard. Again these are split into card types and it will - keep track of how many of each card you have as well as how many of - each card type and total cards in sideboard. This section will not - add any cards from the main deck. Once you have all of your cards - added to your deck, you must save it as a file Cockatrice can read. - Select “Deck” from the top left corner of the Deck Editor screen, - and select “Save Deck” or “Save Deck As…” and it will bring up a new - window where you can select where you would like to save your deck, - as well as assign it a file name. Cockatrice decks are saved as .cod - files. - -Loading a deck list from your clipboard ---------------------------------------- - -If you find a deck online, or you have a deck list saved in a word -document, it is easy to transfer it over into a Cockatrice deck file as -long as it is in a simple deck list format. The simple deck list format -is a list where each line begins with a number, followed by a -whitespace, followed by the cardname, e.g. - - 2 Doom Blade - 13 Island - 10 Swamp - 4 Cancel - ... - -Simply find the word document or deck list online that you wish to save -as a deck, and select the text and copy it to your clipboard. Next, open -the Deck Editor screen, and click on the Deck menu from the top left -corner. Select “Load deck from clipboard…” and the deck editor will -bring up a new window that has the deck list you had copied to your -clipboard. Make sure the Deck list looks correct and hit “OK” in the -bottom right corner of the window. The Deck editor will now add all the -cards in the list to your main deck list. - -![image](pics/loaddeck_clip.jpg) - -![image](pics/okdecklist.jpg) - -NOTE: If you add a card to a deck list with this function that you do -not have downloaded to your personal cockatrice database though the -Oracle tool, the card will take a spot in your main deck list, and count -toward the total number of cards, but it will show up as a blank image -with no Oracle data or card information. - -Cockatrice Settings -------------------- - -TODO - -Learning the Ropes / Starting a Solitaire Game ----------------------------------------------- - -The best way to get familiar with the way Cockatrice plays is to start a -local game that you can play around in by yourself. You could also jump -online and start slowly learning, and let other players help you. - -To start a Solo Local game, in the main Cockatrice window, click on -“Cockatrice” on the top left, and select “Start local game…”. - -![image](pics/fetch2ab8.jpg) - -This will bring up a small window that lets you select how many players -will be in this local game you are creating. For right now, since we -want to do a solo Solitaire game, select one player and hit “OK”. - -![image](pics/fetchf010.jpg) - -### Loading a Deck / Using Sideboard - -This will now bring you to a screen where you load a deck to play with. -On the top left part of this screen you will find a button that is -labeled “Load Local Deck”. Click that button and it will bring up a -window where you can find and select what deck you would like to play -with. - -![image](pics/fetchf0d2.jpg) ![image](pics/fetch55a7.jpg) - -Select a deck or a .cod file and click “Open”. - -![image](pics/fetch9b89.jpg) - -After the file has loaded you will see all of the cards in that deck -laid out on the table. If you hover your mouse over a card, the card -image and Oracle info will show on the right side of the screen. If you -have a sideboard made for the deck there will be a second section on the -table for this sideboard. This screen gives you the ability to double -check your deck to make sure it is not only the correct deck you want to -play with, but it lets you see that all card images have downloaded -properly. If you have cards not showing up at all or they are just blank -cards with names on them, you may not have that set downloaded with your -Oracle tool. If you have a sideboard, you can drag and drop cards from -your main deck to your sideboard or vice-versa. You can do this by -clicking and dragging a card to or from your main deck or sideboard. - -NOTE: Moving cards from your main deck to sideboard will NOT change how -your deck file is saved, it will only change it temporary for the game -you are playing or until you load a new deck. - -When you are satisfied with your deck choice and/or sideboarding -options, click on the red outlined “Ready to start” button found a the -top of the screen. - -### Finding Your Way Around - -The main game screenlooks like this - -![image](pics/fetch7cf0.jpg) - -(Please note your screen will look different due to background image -options.) - -#### Main Table / Play area - -Split into four areas, this is where all the action will go down. - -The Stack -: The area on the left side of the table where Instant and Sorcery - cards will be played. This is for things that will only temporarily - be put on the table, then into the graveyard. Multiple cards may be - added to this area at the same time. Anything on this part of the - table will be seen by all players. - -Battlefield -: This is the soul part of the game table. this is where creatures, - enchantments, artifacts, and even plainswalkers will be placed. As - cards are moved from your had to the table, they will be aligned to - an invisible grid and moved around from there. Tap cards by double - clicking them. Anything on this part of the table will be seen by - all players. - -Land -: This space is for land cards, but any card may be placed here. Tap - cards here by double clicking them. Anything on this part of the - table will be seen by all players. - -Hand -: Every time you draw a card it will go here to your hand. You may - also drag cards from the table back to your hand. Your opponents can - not see what is in your hand. - -#### Player Info Section - -![image](pics/fetch0300.jpg) - -Player Avatar -: This is a $156\times 60$ pixel JPG image that can be uploaded though - the main Cockatrice website. All players in the game room can see - this image. It serves nothing more than an online identity for you - and other players. - -Player Name -: Your online name that you picked though the main Cockatrice website. - -Life Total -: Your in-game life. Using your mouse, if left-clicked will raise this - number by one, and if right-clicked lowered by one. There are also - keyboard shortcuts to change your life total. - -Counters -: These seven multicolored circles are used as counters. They can be - seen by all players and can be changed by left or right clicking on - them to add or subtract a number. Players use them for various digit - counting but primarily used for adding and subtracting floating mana - produced by card effects. The bottom two white counters can be used - for other things like Poison. - -Library -: This is your deck of cards. The number in the middle reflects how - many cards are left in your library. Double clicking the deck lets - you draw a card and add it to your hand, you can also drag cards off - the top into the battlefield or to your hand. Right-clicking the - deck brings up a menu that allows other things to happen like - reviling the top number of cards, shuffling, or moving cards - directly into the graveyard. - -Number of cards in hand -: The number in the middle represents how many cards are currently in - your hand. Other players can see this number but can not see the - cards actually in your hand. - -Graveyard -: Cards can be dragged and dropped into your graveyard from play or - vise-versa, the stack, your hand, or even your library. The number - in the middle represents how many cards are currently in your - graveyard. Any player may right-click on the graveyard and bring up - a menu that shows what cards are in it. - -Exile -: Cards can be dragged and dropped into exile from play or vise-versa, - the stack, your hand, or even your library. The number in the middle - represents how many cards are currently in your exile. Any player - may right-click on the exile and bring up a menu that shows what - cards are in it. - -#### Turn Phases - -![image](pics/fetchfebd.jpg) - -This bar located on the left most side of the screen represents the 11 -steps in a players turn. To go from one phase to the next, you can click -on the square of the phase you want to move to, or you can hit -Ctrl+space to move down to the next. Some phases even have their own -keyboard shortcut. Going from one phase to the next does not actually do -anything to your or your cards, it is only a place marker for your -opponents to see and keep up with what you are doing in your turn. For -example, clicking to the “Draw Phase” will not automatically draw you a -card. It is customary for a player to end their turn on the “End of turn -step” and let their opponent hit the “next turn” button. This is a -courtesy for other players if they wish to do something like use an -instant at the end of your turn, or in response to something you did. - -NOTE: Players sometimes use the term EOT which stands for “End Of Turn”. -This is to let other players know they are doing something in response -to the end of the current turn. - -#### Info/Chat Bar - -![image](pics/fetcha0de.jpg) - -Split into three sections, the Info/Chat bar lets you see a close-up -image of the card your mouse was last over, as well as gives you the -card info for that card. At the bottom of this bar there is a chat log -that helps keep track of events during the game as well as lets you -communicate with other players. if a card is placed on the table, -pointed at, or tapped it will get noted in the chat log as well as has a -link to the card that you can hover over and see an image of at the top -of the bar. - -### Basic Functions - -#### Rolling Dice - -At the beginning of a game players decide who is going first by rolling -a 20 sided die. In Cockatrice we do this by pressing Ctrl-I and hitting -enter. Hitting Ctrl-I brings up a die window and lets you select how -many sides you want on your die. Default is 20, and pressing enter will -“roll” the die. This action will show up in the cat log at on the bottom -right of the screen. You can also find this in the “game” menu at the -top of the window, selecting “player” and clicking on “roll die…” - -![image](pics/fetch7486.jpg) - -![image](pics/fetch3705.jpg) - -#### Draw Cards / Mulligan - -When a game starts and the first player has been selected, all players -will draw seven cards. this can be done by pressing Ctrl-M. Seven cards -will go from your library to your hand. Pressing Ctrl-M again will put -the seven cards from your hand back into your library, shuffle your -library and deal out six new cards to you. Each time you press Ctrl-M it -will give you one less card until you get down to one card, then it will -re-start at seven cards. This function can be found by clicking the -“game” menu on the top of the window, selecting “player” then selecting -“hand” and then “take Mulligan”. If you are playing a friendly game, -press Ctrl-M as normal, but then press Ctrl-D to draw cards until you -have a total of seven again. - -![image](pics/fetchfe20.jpg) - -#### Tapping - -Tapping cards is very basic. If a card is on the table under your -control, you can double click it to tap it and then double click again -to untap it. You can select multiple cards on the table by clicking and -dragging your mouse, then tap or untap all of the selected cards at the -same time. Other players can not tap or untap your cards. Pressing -Ctrl-U will untap everything you control. - -- Untapped - - ![image](pics/fetchb6fe.jpg) - -- Tapped - - ![image](pics/fetch867f.jpg) - -#### Attaching Cards to Cards - -Sometimes an Enchantment -Aura or Equipment cards need to be attached to -other cards that are already on the table. simply put the enchantment or -equipment on the table. Right-click the card and select “attach” (this -can also be done with Ctrl-A). A green arrow will appear, point and -click on the card you wish to attach. You can also attach cards to other -people’s cards. - -![image](pics/fetch100e.jpg) - -![image](pics/fetchb17a.jpg) - -#### Changing Power/Toughness - -Enchantments, Equipment, and other effects sometimes change a creatures -power or toughness. This can be done by right-clicking the card, and -selecting “power / toughness” then selecting which one you wish to do. -Other players can not change your creatures power and toughness. This -can also be done though a series of keyboard shortcuts seen below. - - -------------------------- ------------------------------ - (Select card) Ctrl++ Increase power - (Select card) Ctrl+- Decrease power - (Select card) Alt++ Increase toughness - (Select card) Alt+- Decrease toughness - (Select card) Ctrl+Alt++ Increase power and toughness - (Select card) Ctrl+Alt+- Decrease power and toughness - -------------------------- ------------------------------ - -![image](pics/fetche3fc.jpg) - -![image](pics/fetchd922.jpg) - -#### Adding Counters to Cards - -Sometimes Counters are needed to be placed on cards that the counters on -the side of the screen are not able to track. Cockatrice offers three -different counter color options, Red, Green, and Yellow. Although there -is no set standard on what color stands for what, it is mostly player -preference. Green could be used for +1/+1, red -1/-1, leaving yellow for -charge and quest counters, this is not a set rule. Adding counters is as -simple as right clicking on the card you wish to add counters too, and -currently there is no keyboard shortcut for this process. Removing -counters is the same process, right click and select remove. Other -players can not add or remove counters to or from your cards. - -![image](pics/fetch5170.jpg) - -(One of each counter) - -#### Pointing at Cards / Arrows - -Pointing at cards is needed for resolving spells, or declaring attackers -and blockers. All you need to do is right-click over a card and drag an -arrow over to what you are pointing at. Permanents, spells in the stack, -and even a players life total can be pointed at. You can point at your -opponents cards and life total, and they can point at yours. When your -arrows are no loner needed, press Ctrl-R to remove them from the screen. - -![image](pics/fetch98fd.jpg) - -![image](pics/fetch74b2.jpg) - -#### Creating Tokens - -Creating tokens can sometimes be tedious, but is well worth the effort -to keep a clean and organized game. Pressing Ctrl-T will bring up a -small window to assist you in creating a token. Simply enter the name of -the token you are creating, select its color, and give it a power and -toughness (\#/\#). You can also bring up this token window by selecting -“game” from the top menu, selecting “Player” then clicking on “Create -Token…”. A copy of the Last token made can be done by pressing \*Ctrl+G -or right-clicking on a already made token (or any card on the table) and -selecting “clone” or pressing Ctrl-H\*\*. When a token or clone leaves -play, it will be destroyed and vanish. - -![image](pics/fetch2c36.jpg) - -![image](pics/fetch9bff.jpg) - -![image](pics/fetche6b2.jpg) - -![image](pics/fetch84a2.jpg) - -Make copies of your last token by pressing Ctrl-G. - -![image](pics/fetch6847.jpg) - -Playing Online -============== - -With Cockatrice you will most likely play Magic games over the Internet -with real people all around the world. In order to help maintain a -pleasant environment for users, please read the messages below: - -- User Code of Conduct[^1] – Must Read for all Users - -- How to Report Abuse[^2] – It is recommended to read this as well - -Connect to Server ------------------ - -To connect to the Cockatrice server, launch the Cockatrice program, go -to the “Cockatrice” menu at the top left, and select “Connect”. A window -will appear (see image below). - -![image](pics/fetch23f3.jpg) - -If you have registered with Cockatrice, then enter your Username in the -“Player Name” field and your password in the “Password” field then click -“OK”. You may check the “Remember Password” box if you wish. If you do, -then the next time “Connect” is selected from the “Cockatrice” menu, the -window that appears will already have your Username and Password already -filled. Please take this into consideration if you share a computer with -other people, seeing that you are responsible for anything that happens -on the server with your username (As noted here). If you did not -register with Cockatrice, then simply fill in the Username with whatever -you like and click “OK”. If you would like to become a registered user, -read the instructions from the server’s website. - -Once you are connected to the server, more tabs will appear at the top -of the screen next to the “Deck” tab that you are already on. - -All About Games ---------------- - -This page is about creating, joining, watching, and searching for games -on the Cockatrice Server. In order to participate in any games, you have -to be connected to the server. The games on the server are where all of -the action take place. There will be many games happening on the server -at the same time. Basically, first a game is created by a player (it -could be you). Then other players join the game until the number of -players reaches the number set by that game’s creator. When the game has -no players participating in it, the game disappears. Creating a Game - -To Create a game, go to the “MTG room” tab. Click on the “Create” button -below the Games list. A window will appear (see below). - -![image](pics/fetch54df.jpg) - -Here are all of the options for creating a game: - -Description -: Describe the game in your own words (i.e. “Competitive Standard”, - “Casual EDH- No Infinites”, “RavnicaDraft”, “Here is Chris”) - -Players -: Specify the number of Players in the game. This cannot be changed - after the game is created. The game can only begin when the - specified number of players join. - -Spectators -: Spectators are users that are in a game, but they are not one of the - players. Spectators can see all of the public zones of the game and - everything displayed in the Info/Chat Bar. Any number of users can - join a game as a Spectator (as long as the “Spectators Allowed” box - is checked). - - Spectators Allowed - : Unchecking this box will prohibit any/all users from joining the - game as a Spectator. - - Spectators Need a Password to Join - : Checking this box will make it so that in order for a user to - join as a Spectator, they need to type the password you specify - in the Password Field. - - Spectators can Chat - : Checking this box will allow Spectators to type comments in the - Chat bar during the game. - - Spectators See Everything - : Checking this box will allow Spectators to view cards in all - private zones of all players (hands, libraries, face-down - cards). - -Password Field -: If you type anything in this field, a Player (or Spectator if the - “Spectators Need Password” box is checked) will need to type the - exact same thing you typed in order to join the game (and it is - case-sensitive). - -Only Buddies Can Join -: Checking this box will prevent any user who is not in your Buddy - List from joining the game as a Player or Spectator. NOTE: Your - username is not on your buddy list. If you leave a game you created, - and this box is checked, you will not be able to rejoin. - -Only Registered Users Can Join -: Checking this box will prevent anyone who has not registered on the - Cockatrice website from joining as a Player or Spectator. - -Game Type -: These check boxes have no effect on the game. They inform other - Users browsing the Games list of what format your game is. Users can - choose to view only games of a certain Type/Format. - () - -### Joining a Game - -Most of the time, to join a game you click on the “MTG Room” tab, click -on a game in the Games list, then click Join. If the Game’s creator -specified a password then you will have to type that password in a small -window that appears after you click Join (the password is -case-sensitive). If your User Profile meets the criteria of the Game’s -creator then a new tab will open with that game. There is also an easy -way to join a game in which a User in your Buddy List is playing. Go to -the “User Lists” tab, right click any Username from the Buddies Online -list (at the left of the window) to make a menu appear, and select “Show -this user’s games”. A window will open with a list of games that the -User is either playing or watching. In the same manner as described with -the “MTG Room” tab, simply click on a game and click Join. To watch a -game, the instructions are the same except that you click the “Join as -Spectator” button instead of the Join button. NOTE: If you are a player -in a game and you wish to become a spectator in that game, you must -first leave the game then rejoin as a Spectator. Same thing if you are a -Spectator and wish to play. - -### Searching for Games - -The Games list in the “MTG Room” tab displays by default all games that -have not reached the specified number of players. The “Filter Games” -button makes looking through this list easier if you are looking to join -a particular kind of game. When this button is selected, a window -appears (see below). - -![image](pics/fetchd30e.jpg) - -Game Description -: Displays games with certain descriptions. You can even search - partial names. - -Creator -: Displays games created by Users with that username. It even searches - for partial names. - -Player Count -: Displays all games where the specified number of players is - greater-than or equal to the “at least” number and less-than or - equal to the “at most” number. For instance, setting both numbers to - 3 will display all games whose creators made as 3-player games. - -Show Unavailable Games -: Checking this box will display games that are full and in progress. - You can still join these games as a Spectator if the game’s creator - allows it. - -Game Types -: Displays games with the selected types. Bear in mind that the - Cockatrice software does not enforce deck construction for formats. - So just because a game’s type is EDH/Commander, doesn’t necessarily - mean that is what’s being played in the game. Players can agree to - switch formats in a game. - -Keeping Track of Buddies ------------------------- - -Cockatrice allows registered users to keep track of other registered -users in a buddy list. You won’t be able to do anything with this list -(or other registered users at all) unless you are a registered user and -connected to the server. - -To add a User to your Buddy list, right-click their username and, in the -menu that appears, select “Add to buddy list”. You can right-click and -add a User in this manner anywhere you see their username (under the -“User lists” tab, the “MTG Room” tab, in a game, or in a direct chat). -When you add a User, their username will appear in the list “Buddies -Online” located under the “User lists” tab. If the username appears in a -light shade of gray, then that User is not connected to the server. If -it appears in black, then that User is connected to the server. - -If you see that one of your Buddies is connected, you can see the games -he/she is currently in. Right-click their username, and in the menu that -appears select “Show this user’s games”. A window will appear from which -you can watch or play in a game that your buddy is currently in. - -You can Direct Chat with a Buddy (or any user for that matter) by -right-clicking their username and, from the menu that appears, selecting -“Direct Chat”. When you do this, a new tab will open (both on your -window and on the other User’s window) containing a chat room that can -only be seen and used by the two of you. The place where you type in -your messages is at the very bottom of the window. - -To remove a User from your Buddy list, right-click their username, and -from the menu that appears select “Remove from buddy list”. - -Dealing with and Preventing Unpleasantries ------------------------------------------- - -Make a screenshot, there are several free programs available. -Right-Click a username to add to Ignore List. - -Frequently Asked Questions (FAQ) -================================ - -How to update the card database -------------------------------- - -When a new set comes out, you need to update your card database to be -able to see the new cards in the deck editor and use them in the game. -To do this, open the Oracle tool in the Cockatrice folder. It’s a -stand-alone program and is run directly out of the folder without -running Cockatrice. - -When the Oracle tool is running, open the menu and select ’Download sets -information’. Use the suggested address and click OK. After at most a -few seconds, you should be presented a list of sets to be downloaded. Do -not uncheck the ones you already have: they should be downloaded again. -Click ’Start download’ and wait. When the process is finished, the -database should be up to date. - -Change Card Art / Custom Card Art ---------------------------------- - -Is it possible to change the card art for a specif card, not by changing -the set order in my deck editor? - -YES! It is actually very simple. - -All you have to do is get a .JPG of the card you wish to change and name -the actual file of the image to the cards (exact) name and add ”.Full” -to the end. You can then take this new card image and replace the old -one in your downloaded image folder. - -NOTE: Image size does not matter. Try to use a high resolution .JPG of -the card you want to use for best quality. - -EXAMPLE: Let’s say you wish to turn your M11 Sun Titan from its original -art to a promo version. - -![image](pics/fetch74e3.jpg) - -If you are using Windows 7, you can find the default location for the -Cockatrice card art files under -`C:\Program Files (x86)\Cockatrice\pics\downloadedPics` Once there, look -for the folder of the set for the card we are replacing. For this -example, Sun Titan is in the M11 folder. Open the folder and -paste/replace your new “Promo” Sun Titan.full[.JPG] into this folder, -discarding the original copy. (If you have not yet downloaded the -original image of a card by selecting it in the deck editor or playing -it in an online game, the card image will not yet be there) - -Once you have the new card image in the proper folder, you can now start -Cockatrice (Or re-start if you already had the program open)and select -the deck editor to see your new card image. - -If you are changing the art of a card that has multiple versions in -other sets, make sure you put the new card in whatever folder (or set) -is highest on your card database set list. - -NOTE: Other players on Cockatrice will NOT see your new card image. They -will only see whatever version of the card is in their database. - -Use Higher Resolution Cards ---------------------------- - -Can you get better/higher resolution card art than the default -downloaded card images already used? - -Yes! - -When you click on a card for the first time in the Deck Editor, -Cockatrice goes onto the internet and finds an image of that card from a -database on another website. - -If you find a higher resolution .JPG of a card that you wish to use on -your Cockatrice, you can replace the image with no problem. If you can -find a card image in the 3,000 by 1,000 pixel range, and save it as the -cards (exact) name and add .Full to the end. - -If you are using Windows 7, you can find the default location for the -Cockatrice card art files under -`C:\Program Files (x86)\Cockatrice\pics\downloadedPics`. Once there, -look for the folder of the set for the card you are replacing. (If you -are not running Cockatrice as an administrator, look in -`C:\Users\username\AppData\Local\VirtualStore\Program Files (x86)\Cockatrice\pics\downloadedPics`.) - -Open the folder of the set with the card, and paste/replace the new -higher resolution .JPG image with the old low quality one. - -This process is the same for changing cards to custom images. - -NOTE: Other Cockatrice players will not see your higher resolution card -art, they will only see the image they have in their own database. - -Booster drafts --------------- - -Cockatrice does not support booster drafts, so players need to use an -external service for this. - -Most people use so it’s probably not a bad idea -to register an account there before joining a game in Cockatrice. - -What is Legacy / Vintage / EDH? -------------------------------- - -See . - -Linking cards and URLs in the Cockatrice chat. ----------------------------------------------- - -The Cockatrice chat supports linking of cards and URLs by use of certain -tags around a word or phrase. - -### How to link a card in the Cockatrice chat - -To link a card in the Cockatrice chat, type out the full name of the -card, surrounded by the [card] and [/card] tags. - -For example: - - [card]Black Lotus[/card] - -### How to link a URL in the Cockatrice chat - -To link a URL in the Cockatrice chat, type out the url, surrounded by -the [url] and [/url] tags. - -For example: - - [url]http://www.cockatrice.de[/url] - [url]cockatrice.de[/url] - -Servatrice on CentOS 6 -====================== - -[servatrice] This Howto from woogerworks.com will help you to run your -own Cockatrice server (called Servatrice). An installation script can be -downloaded at: - -BEFORE CONTINUING, PLEASE UNDERSTAND THESE ARE THE VERY MOST BASIC -STEPS. THIS WILL NOT CONFIGURE SECURITY SETTINGS (YOU ARE RESPONSIBLE -FOR YOUR OWN SECURITY)! THIS WILL NOT CONNECT THE SERVER TO A DATABASE! -USE AT YOUR OWN RISK. - -1. Open a command shell and install the prerequisites - - 1. `yum -y groupinstall "development tools"` - - 2. `rpm -Uvh http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm` - - 3. `yum -y install qt-mysql qt-devel qt-mobility-devel protobuf-devel protobuf-compiler cmake28 libgcrypt-devel` - - 4. `cd` - -2. Download the source code: - - 1. `git clone https://github.com/Daenyth/Cockatrice` - - 2. `cd Cockatrice` - - 3. `mkdir build` - - 4. `cd build` - - 5. `cmake28 -DWITH_SERVER=1 ..` - - 6. `make` - - 7. `sudo make install` - -3. Create a servatrice.ini file. There is an example file you can - convert: - `sed -e "s/number_pools=1/number_pools=0/" ../servatrice/servatrice.ini.example > /servatrice.ini` - Here is one example file: - - [server] - port=4747 - statusupdate=15000 - logfile=server.log - name="My own Cockatrice server" - id=1 - number_pools=0 - - [servernetwork] - active=0 - port=14747 - ssl_cert=ssl_cert.pem - ssl_key=ssl_key.pem - - [authentication] - method=none - - [database] - type=none - prefix=cockatrice - hostname=localhost - database=servatrice - user=MYUSERNAMEHERE - password=MYSECUREPASSWORDHERE - - [rooms] - method=config - roomlist\size=1 - roomlist\1\name="FIRST ROOM NAME" - roomlist\1\description="FIRST ROOM DESCRIPTION" - roomlist\1\autojoin=true - roomlist\1\joinmessage="THIS IS A JOIN MESSAGE" - roomlist\1\game_types\size=11 - roomlist\1\game_types\1\name="Vintage (T1)" - roomlist\1\game_types\2\name="Legacy (T1.5)" - roomlist\1\game_types\3\name="Extended (T1.X)" - roomlist\1\game_types\4\name="Modern" - roomlist\1\game_types\5\name="Standard (T2)" - roomlist\1\game_types\6\name="Block Constructed" - roomlist\1\game_types\7\name="EDH/Commander" - roomlist\1\game_types\8\name="Highlander" - roomlist\1\game_types\9\name="2HG" - roomlist\1\game_types\10\name="Draft/Sealed" - roomlist\1\game_types\11\name="Other" - - [game] - max_game_inactivity_time=120 - max_player_inactivity_time=15 - - [security] - max_users_per_address=8 - message_counting_interval=10 - max_message_size_per_interval=1000 - max_message_count_per_interval=10 - max_games_per_user=5 - -4. Install the database server (optional) - - 1. `sudo yum -y install mysql-server mysql php-mysql` - - 2. `sudo service mysqld start` - - 3. `sudo chkconfig mysqld on` - - 4. `mysqladmin -u root password ’password’` - - 5. `mysql -u root -ppassword -e "create database servatrice;"` - - 6. `mysql -u root -ppassword -e "create user ’servatrice’@’localhost’ identified by ’password’;"` - - 7. `mysql -u root -ppassword -e "grant all privileges on servatrice.* to ’servatrice’@’localhost’;"` - - 8. `mysql -u servatrice -ppassword servatrice < ../servatrice/servatrice.sql` - - 9. `mysql -u servatrice -ppassword -e "insert into servatrice.cockatrice_users \ (admin,name,password_sha512,active) values (1,’servatrice’,’MYSHA512PASSWORD’,1);"` - - 10. `sed -i.bak -e "s/password=foobar/password=password/" -e "s/type=none/type=mysql/" \ -e "s/method=none/method=sql/"  /servatrice.ini` - -5. Start Servatrice for testing: `/usr/local/bin/servatrice`. I - recommend to run it later within a `screen` session or write an - init-script. - -You should be able to log in as a regular user using any account name. -You can login using the username *servatrice* and the password -*password* as the first moderator. - -If everything succeeded, you should tweak the servatrice.ini and set -your root password to something strong if not already done (at least 8 -characters, upper case, lower case, numbers, special characters). - -[^1]: TODO, dead forum link - -[^2]: TODO, dead forum link diff --git a/doc/usermanual/Usermanual.pdf b/doc/usermanual/Usermanual.pdf deleted file mode 100644 index 3d2d562da..000000000 Binary files a/doc/usermanual/Usermanual.pdf and /dev/null differ diff --git a/doc/usermanual/Usermanual.tex b/doc/usermanual/Usermanual.tex deleted file mode 100644 index 23fa5580c..000000000 --- a/doc/usermanual/Usermanual.tex +++ /dev/null @@ -1,814 +0,0 @@ -\documentclass[a4paper]{scrbook} -\usepackage[T1]{fontenc} % Fontencoding for pdf searches -\usepackage{textcomp} % EUR symbol for OT and T1 -\usepackage[osf,sc]{mathpazo} % Palatinofont -\usepackage{ellipsis} % better spaces for ellipses (…) -\usepackage{microtype} % Typographic finetuning -\usepackage{fixltx2e} % LaTeX fixes - -\usepackage[euler]{textgreek} -\usepackage[utf8]{inputenc} % utf8 input -\usepackage{graphicx,framed} % pictures, frames -\usepackage{makeidx} % Index -%\usepackage{fancyhdr} -\usepackage{listings} % source listings, e.g. for shell commands - -\usepackage{flafter} % floating objects -\usepackage{placeins} % \FloatBarrier command, http://www.tex.ac.uk/cgi-bin/texfaq2html?label=floats -\usepackage{color} % colors -\usepackage{tikz} % draw stuff -\usepackage{enumerate} % more enumerations -\usepackage{longtable} % multipage tables -\usepackage{booktabs} % better tables: http://www.ctan.org/tex-archive/macros/latex/contrib/booktabs/ - -\usepackage[nottoc]{tocbibind} % Index + Bibliographie -> toc - -%\setcounter{tocdepth}{3} % Depth for tocs; default = 2 - -%PDF stuff -\usepackage[unicode,linktocpage, -colorlinks, -bookmarks, -bookmarksopen, -pdfpagelabels=true]{hyperref} - -% pdf-specific, no line breaks! -\usepackage{xcolor} -\hypersetup{ - pdftitle = {Cockatrice Usermanual}, - pdfsubject = {Cockatrice, Servatrice, Manual}, - pdfauthor = {}, - pdfkeywords = {Cockatrice, Servatrice, Magic}, - linkcolor=blue!30!black, - citecolor=green!30!black, - urlcolor=blue!25!green!25!black, - % do not underline links - frenchlinks, - % break long links - breaklinks = true, -} - -% better Text/Float-ratio -\setcounter{topnumber}{3} -\renewcommand\topfraction{1} -\setcounter{bottomnumber}{3} -\renewcommand\bottomfraction{1} -\setcounter{totalnumber}{9} -\renewcommand\textfraction{.01} -\renewcommand\floatpagefraction{1} - -\makeindex -%opening -\title{Cockatrice} -\subtitle{Usermanual} - -%\pagestyle{fancy} - -\newcommand{\shellcmd}[1]{\texttt{\scriptsize #1}} - -\begin{document} -\maketitle -\tableofcontents - -\chapter{Preface} -This manual is basically a dump from the cockatrice.de dokuwiki. Cockatrice has some legal problems right now and the page is down. -This document tries to save the documentation about the software, beautify and extend it in the future. -Please contribute to the project, it is much too precious to be destroyed. - -\chapter{Getting Started} -\section{Making A User Profile} -Not available anymore, site is down. -If someone runs his own server where you can register a user profile, read his documentation. - -\section{Downloading and Installing the Cockatrice Program} -Due to a legal dispute there are currently no official builds left, so currently you have to build your own binaries. - -\subsection{Building the Client} -\subsubsection{Windows} -To build Cockatrice, we need the Cockatrice sourcecode, its dependencies and build tools of course. -Everything is freely available. - -There should be two ways to compile Cockatrice: -With Microsoft Visual Studio 2010 (Visual C++ 2010) and with MinGW, a minimalist GNU environment for Windows. - -We suggest to use Visual Studio because there is a severe problem with MinGW: -It doesn't offer a compatible version of its tools. -The Qt library needs a certain, old MinGW version\footnote{The officially needed version is MinGW 4.8.2, the last known working version was MinGW 20120426} which is unavailable on the internet because they use an online installer and don't offer old, stable releases. -Trying to build with the current version of MinGW will result in application crashes! -So MinGW and Qt force us to focus on Visual Studio and don't support MinGW. - -Gladly, Microsoft offers Visual Studio Express free of charge, which is a limited but sufficient version of Visual Studio. -It only requires a free of charge registration after 30 days. - -The following howto which uses Visual C++ 2010 Express has been tested with an up to date Windows 7 64Bit. -The resulting build is a 32bit binary, which runs on both 32bit and 64bit systems. - -\paragraph{Prerequisites} -Here is an introduction of all dependencies and tools needed for Cockatrice, followed by a list of downloadlinks in the same order. -\begin{enumerate} - \item The Microsoft Windows SDK for Windows 7 and .NET Framework 4 provides tools and libraries to create Windows applications. - \item The Visual C++ 2010 SP 1 Compiler Update for Windows SDK 7.1 (KB2519277) is a necessary update for the SDK. - \item Microsoft Visual C++ 2010 Express is the actual development environment. - \item Microsoft Visual Studio 2010 Service Pack 1 (VS10sp1-KB983509) is an update for Visual Studio. - \item The Qt libraries 4.8.x for Windows (VS 2010) are the main dependency for Cockatrice. - \item protobuf 2.5.0 is another dependency for Cockatrice (it has no installer, you need to download the zip file with the sourcecode). - \item cmake version 2.8.12.x is needed to create Cockatrice's project files for Visual Studio. Version 3 has not been tested yet with Cockatrice. - \item git is needed to download the latest Cockatrice source code. - \item Nullsoft Scriptable Install System (NSIS 3.0b0) is a program to create the Windows installer for Cockatrice. -\end{enumerate} - -All downloadlinks together: -\footnotesize{\begin{enumerate} - \item \url{http://www.microsoft.com/en-us/download/details.aspx?id=8279} % Win SDK - \item \url{http://www.microsoft.com/en-us/download/details.aspx?displaylang=en&id=4422} % SDK Update - \item \url{http://go.microsoft.com/?linkid=9709949} % MSVC - \item \url{http://www.microsoft.com/en-us/download/details.aspx?id=23691} % MSVC SP1 - \item \url{http://download.qt-project.org/official_releases/qt/4.8/4.8.6/qt-opensource-windows-x86-vs2010-4.8.6.exe} - \item \url{http://www.cmake.org/files/v2.8/cmake-2.8.12.2-win32-x86.exe} - \item \url{http://git-scm.com/download/win} - \item \url{http://protobuf.googlecode.com/files/protobuf-2.5.0.zip} - \item \url{http://nsis.sourceforge.net/Download} -\end{enumerate}} - -\paragraph{Installation of the Prerequisites} -Problems will occur if you don't install the first four steps (all the Microsoft SDK and Visual Studio packages) in the exact same order as printed here! -\begin{enumerate} - \item Install Microsoft Windows SDK for Windows 7 and .NET Framework 4. - \item Apply the Visual C++ 2010 SP 1 Compiler Update for Windows SDK 7.1 - \item Install Visual C++ 2010 Express (Start the web installer, you may disable Silverlight) - \item Apply Visual C++ 2010 Express Service Pack 1 - \item Install Qt4 VS 2010 edition. - \item Install NSIS. - \item Install cmake. Choose to add CMake to the System path. - \item Install git. Choose Run Git from Windows Command Prompt. -\end{enumerate} - -As protobuf does neither provide an installer nor the libraries needed for Cockatrice, you have to build those with Visual Studio from the protobuf sources. -The Cockatrice installer will search for the protobuf libraries within the Cockatrice sources, so before unpacking and building protobuf, you need to download the Cockatrice sources first: -\begin{enumerate} - \item Right click into a folder, select Git Gui, then Clone Existing Directory, - \item enter Source Location \url{https://github.com/Daenyth/Cockatrice} - \item enter target directory (from now on called Cockatrice), click clone and wait until the sources have been downloaded, - \item close Git Gui. -\end{enumerate} - -Now we prepare protobuf: -\begin{enumerate} - \item Create a \shellcmd{build} directory inside the Cockatrice directory. - \item Copy the protobuf-2.5.0.zip into the build directory. - \item Rightclick the zip, choose Extract all. This uses the Windows included zip unpacker to extract the archive. -\end{enumerate} -To be clear: you will (and must) have a Cockatrice/build/protobuf-2.5.0/protobuf-2.5.0 directory hierarchy after that! - -Now the protobuf dependencies for Cockatrice will be built: -\begin{enumerate} - \item Start Visual C++ 2010 Express and from the File menu Open Project/Solution, move to the Cockatrice/build/protobuf-2.5.0/protobuf-2.5.0/vsprojects directory and choose protobuf.sln. - \item Let you guide through the Conversion Wizard, you don't need to create a backup. - \item After the conversion is finished, open the projects (uncheck ``Ask me for every project in this solution'' and click OK). - \item You don't need to look at the conversion report. - \item Select Release or Debug in the toolbar. Release optimizes the code for size and speed, while Debug creates larger, slower binaries, used for development. You need to use the same setting for Cockatrice later! We suggest to use Release. - \item Rightclick on libprotobuf, choose Build. Note that the output should say: Build started: Project libprotobuf, Configuration: Release Win32 - \item Then rightclick on protoc, choose Build. This will build libprotoc and the protoc executable, also as Release Win32. - \item After the build succeeded, close Visual C++. -\end{enumerate} -The Cockatrice/build/protobuf-2.5.0/protobuf-2.5.0/vsprojects/Release directory now contains libprotobuf.dll, libprotobuf.lib and protoc.exe which are needed for Cockatrice. - -\paragraph{Cockatrice Compilation} -Now everything is ready to compile Cockatrice. -\begin{enumerate} - \item Start CMake GUI, locate the Cockatrice directory and locate the Cockatrice/build directory. - Then click Configure and choose Visual Studio 10. - An error will occur during the Configure process because CMake does not know the location of the protobuf library. - \item To satisfy CMake, check Advanced, enter ``protobuf'' in the Search field, - \begin{enumerate} - \item Set PROTOBUF\_INCLUDE\_DIR to Cockatrice/build/protobuf-2.5.0/protobuf-2.5.0/src/ - \item Set PROTOBUF\_LIBRARY to Cockatrice/build/protobuf-2.5.0/protobuf-2.5.0/vsprojects/Release/libprotobuf.lib - \item Set PROTOBUF\_PROTOC\_EXECUTABLE to Cockatrice/build/protobuf-2.5.0/protobuf-2.5.0/vsprojects/Release/protoc.exe - \item Set PROTOBUF\_PROTOC\_LIBRARY to Cockatrice/build/protobuf-2.5.0/protobuf-2.5.0/vsprojects/Release/libprotoc.lib % TODO: Is this needed? I don't think so; but it doesn't hurt either. - \item Click Configure again. - \end{enumerate} - \item Click Generate, then close CMake GUI. The project files have been generated. - \item Start Visual C++ 2010 Express and from the File menu select Open Project/Solution, point to the Cockatrice/build directory and choose Cockatrice.sln. - \item Select Release or Debug in the toolbar exactly as you did for protobuf. We suggest Release. - \item Rightclick on ALL\_BUILD, choose Build. -\end{enumerate} - -\paragraph{Updating your Cockatrice build} -If you just compiled Cockatrice for the first time, you skip this step obviously. -But if you want to update Cockatrice after the source code changed on github, do it like this: -\begin{enumerate} - \item Start Git Bash and update Cockatrice: - \begin{enumerate} - \item \shellcmd{git pull origin master} - \item \shellcmd{Close Git Bash} - \end{enumerate} - \item Start Visual C++ 2010 Express and build the Cockatrice sources again. -\end{enumerate} -Cockatrice has now been updated and built. You may repeat this process every time when the source code changed. - -\paragraph{Cockatrice installation} -To install Cockatrice, you have to create an installer with NSIS now. -Change to the directory \shellcmd{nsis} in the Cockatrice directory, right click the cockatrice.nsi file and select \shellcmd{Compile NSIS Script}. -The NSIS program then creates a file called cockatrice\_win32\_YYYYmmdd\_git-xxxxxxx.exe. This is the complete, redistributable installer for your Cockatrice build. - -Now install Cockatrice by executing the installer. -Note: if you installed Qt in other than the default path, you have to fix the paths in the cockatrice.nsi file; you can edit this file with a text editor. - -We will remove this static NSIS file in the future and let CMake create the NSIS file on the fly. - -\paragraph{Create a card database} -Start the oracle.exe (the installer does this automatically) and let it generate a current cards.xml file: -\begin{enumerate} - \item File $\to$ Download Sets Information $\to$ OK (if there are no MtG sets listed) - \item Check All, Start Download -\end{enumerate} -Congratulations, you may now use Cockatrice! - -\subsubsection{Linux, BSD, OS X} -The following procedures have been tested with Debian Wheezy, Fedora 18, XUbuntu 13.10, FreeBSD 9.1 and 10.0. -If you use Gentoo with KDE you have the needed prerequisites and may continue with downloading the source. -If you use Bodhi or Arch Linux (AUR) or another distribution that includes Cockatrice, you might install Cockatrice from the default packages -- though the package might be old, -so you probably should continue with this howto. - -Before you install new software, you should update your system. The following instructions failed on a fresh installation of Fedora 18 and FreeBSD 9.1 until the systems were updated. -\begin{enumerate} - \item You need to install the build tools and dependencies. This varies between the Linux distributions. - \begin{description} - \item[Debian, Ubuntu and spin-offs] \shellcmd{sudo apt-get install build-essential git libqt4-dev qtmobility-dev libprotobuf-dev protobuf-compiler cmake} - \item[Fedora] \shellcmd{sudo yum groupinstall "Development Tools"\\ - yum install qt-devel qt-mobility-devel protobuf-devel protobuf-compiler cmake} - \item[FreeBSD 9] \shellcmd{pkg\_add -r qt4 qt4-linguist qt4-moc qt4-qmake qt4-rcc qt4-uic git cmake protobuf} - \item[FreeBSD 10] \shellcmd{pkg install qt4 qt4-linguist qt4-moc qt4-qmake qt4-rcc qt4-uic git cmake protobuf} - \item[OS X] \shellcmd{brew install qt cmake protobuf} - \end{description} - \item Download the sources from github via \\ \shellcmd{cd\\ git clone https://github.com/Daenyth/Cockatrice.git} - \item To compile the sources, change into the newly created directory, create a build directory and invoke cmake:\\ - \shellcmd{cd Cockatrice \\ -mkdir build \\ -cd build \\ -cmake ..\\ -make}\\ - \item You may install the program into the directory \shellcmd{/usr/local} by typing \shellcmd{sudo make install} but you should also be able to start - cockatrice and the oracle from the build directory. - \item Before you start Cockatrice for the first time, run \shellcmd{oracle -dlsets} and download available cards, then run \shellcmd{cockatrice}. -The default paths for decks, pics, cards and tokens are located in \\ \shellcmd{/home//.local/share/data/Cockatrice/Cockatrice}. -\end{enumerate} - -\subsection{Building the Server} -You don't need your own server if you plan to play only. But as Cockatrice is open source you are free to run your own. -The compilation works like already written above, but instead of invoking \shellcmd{cmake ..}, you have to do it like this: -\begin{itemize} - \item If you want to build the server, use:\\ \shellcmd{cmake -DWITH\_SERVER=1 ..} - \item If you want to build the server, but not the client, use:\\ \shellcmd{cmake -DWITH\_SERVER=1 -DWITHOUT\_CLIENT=1 ..} -\end{itemize} -Further, the server has a dependency on libgcrypt, so you need to install it as well: -\begin{description} - \item[FreeBSD 10] \shellcmd{pkg install libgcrypt} -\end{description} -There is more information on compiling and running Servatrice on CentOS 6 in chapter \ref{servatrice} on page \pageref{servatrice}. - -\section{Downloading Card Database Using the Oracle} -If you are installing Cockatrice for the first time, changing what sets are in your database or even adding the newest set to your database, this tutorial will show you how to do it properly. - -The Oracle will automatically run after the initial setup of Cockatrice. If you would like to re-install your database or add new sets you can find it (for Windows) by clicking the start menu, going to all programs, selecting the Cockatrice folder, and in there you will find the Oracle tool. -\begin{itemize} - \item When the Oracle importer opens, click on “File” in the top left corner and select “Download sets information…” - \begin{center} -\includegraphics[scale=0.8]{pics/fetch554a} - \end{center} - \item This will bring up a box where you can input the URL of a card database. The default address is \url{http://www.cockatrice.de/files/sets.xml} this was an XML file found on the Cockatrice website that has the current set listings for Magic the Gathering. As the page is down, you have to import the file which is distributed with the Cockatrice sources. - This can also be done from the file menu. - \item Select “OK” to load the set listings. - \item A list of all current sets will be brought up. A default selection of sets will automatically be checked. From here you can check or uncheck all sets, or you may only download specific sets that you wish to play with. - \begin{center} -\includegraphics[scale=0.8]{pics/fetchfc3d} - \end{center} -NOTE: If you are playing against someone who is using a card that is not in your database, you will not see a card image or oracle text for that card. Some players like to download all sets to avoid this issue, but other players who only play specific formats (Like \textsc{T2}, \textsc{Standard}, or \textsc{Extended}) wish to keep their database small with only cards they will be using. - \item After you select which sets you wish to download, select “Start download” at the bottom of the Oracle to download the selected sets information. - \item After download is complete, close the Oracle and run Cockatrice. - \item We are now ready to sort our set information in our deck editor. -\end{itemize} - -\section{Editing Set Order and Preference of Card Art} -Many cards have been re-printed in different sets, and in return have different versions of artwork (like the card “Cancel” which can be found in many sets, but has different artwork for each, e.g. \textsc{Zendikar} versus \textsc{M11}: -\begin{center} -\includegraphics{pics/fetchc18b} -\includegraphics{pics/fetche1f4} -\end{center} - -Some players like to have the most current artwork displayed on their cards, while other players have a favorite set they wish to display instead. - -\begin{itemize} - \item Run Cockatrice and select “Deck editor” from the top right Cockatrice menu. This will bring up the Deck Editor along with a list of all cards that are currently in your database which you downloaded using the Oracle Tool. - \item To change what version of the cards will be shown, click on “Card database” on the top left of the Deck editor window, and select “Edit sets…” - \begin{center} -\includegraphics[scale=0.55]{pics/fetchf924} - \end{center} - \item This will bring up a new window that has a list of all sets you currently have downloaded to your database. To change the order of the sets, simply drag and drop them into place. - This will determine which artwork is shown for your cards. If a card is found in multiple sets, whichever set is closest to the top of this list will be the art displayed. - Example: If \textsc{M11} is above \textsc{Zendikar}, The M11 version of the card “Cancel” will be displayed in your Deck editor and Cockatrice games. -\end{itemize} -NOTE: Your opponent will NOT see what artwork you have selected for each of your cards. They will only see what they have selected for their own. - -\section{The Deck Editor / Making a Deck} -The Cockatrice Deck Editor is a tool you can use to make decks to play online. -The cards shown in the Deck Editor are from a database that you downloaded with the Oracle Tool. If you are missing cards or a new set has come out, you must re-run the Oracle and download set information. -\begin{center} -\includegraphics[scale=0.55]{pics/fetch52e0} -\end{center} - -\begin{description} - \item[1. Search Bar] The search bar lets you type in the name of a card and the editor will only show cards that start with whatever you typed in. Example: Typing in ‘B’ will show all cards that start with the letter ‘B’ and typing in ‘Dark’ will show you all cards that start with ‘Dark’ and so on. If you were looking for the card “Sun Titan”, you would not type in ‘Titan’ you would have to type in ‘Sun’ first. Typing in ‘Titan’ will only show you any cards that start with ‘Titan’. - \item[2. Card Search/Filter] The Card search button will bring up a new window that helps you filter out cards more specifically. A variety of check boxes will help find what you need. Card name lets you filter out only cards that have a cretin word in them. Card text can help you find key words like “Haste” or “Infect”. If you were to uncheck all boxes except for “Instant” along with “Artifact” and “U”, the Deck editor will only show you all Blue Instant and Artifact cards. -\begin{center} -\begin{tabular}{ll} -\toprule Letter & Card Type \\ \midrule -U & Blue \\ -W & White \\ -X & Colorless \\ -G & Green \\ -R & Red\\ -B & Black \\ \bottomrule -\end{tabular} -\end{center} - \item[3. Card Data] This section shows the Oracle text for the card that you currently have selected. It will show you up-to-date information on the card such as the Name, Mana cost, Card type, Power/Toughness, and any abilities the card has. It will not show you flavor text. - \item[4. Adding/Removing Card Buttons] -The buttons in the bottom middle will add or remove cards from your Deck List, as well as ad a card specifically to your Sideboard. Having a card selected on the left column and hitting the Enter key will also add it to your deck list. - \item[5. Deck Name/Comments] -The area in the top right lets you name your deck as well as give any comments or descriptions such as how to play the deck, where the deck came from, or explain a theme. Putting something in the “Deck Name” space will NOT be what the file name of the file for your deck. That is spate in the “save” selection under the “Deck” menu found at the top left of the Deck Editor window. - \item[6. Main Deck List] -This area will show you what cards you currently have added to your deck list. It is sorted by card type and also shows you how many of each card and card type you have added, as well as keeps track of how many total cards you have added to your main deck list. This will not add any number of cards you have added to your Sideboard. - \item[7. Sideboard List] -The bottom section of the deck list shows any cards you have added to your sideboard. Again these are split into card types and it will keep track of how many of each card you have as well as how many of each card type and total cards in sideboard. This section will not add any cards from the main deck. Once you have all of your cards added to your deck, you must save it as a file Cockatrice can read. Select “Deck” from the top left corner of the Deck Editor screen, and select “Save Deck” or “Save Deck As…” and it will bring up a new window where you can select where you would like to save your deck, as well as assign it a file name. Cockatrice decks are saved as .cod files. -\end{description} - -\section{Loading a deck list from your clipboard} -If you find a deck online, or you have a deck list saved in a word document, it is easy to transfer it over into a Cockatrice deck file as long as it is in a simple deck list format. -The simple deck list format is a list where each line begins with a number, followed by a whitespace, followed by the cardname, e.g. -\begin{verbatim} -2 Doom Blade -13 Island -10 Swamp -4 Cancel -... -\end{verbatim} - -Simply find the word document or deck list online that you wish to save as a deck, and select the text and copy it to your clipboard. Next, open the Deck Editor screen, and click on the Deck menu from the top left corner. Select “Load deck from clipboard…” and the deck editor will bring up a new window that has the deck list you had copied to your clipboard. Make sure the Deck list looks correct and hit “OK” in the bottom right corner of the window. The Deck editor will now add all the cards in the list to your main deck list. - -\begin{center} -\includegraphics{pics/loaddeck_clip} -\end{center} - -\begin{center} -\includegraphics{pics/okdecklist} -\end{center} - -NOTE: If you add a card to a deck list with this function that you do not have downloaded to your personal cockatrice database though the Oracle tool, the card will take a spot in your main deck list, and count toward the total number of cards, but it will show up as a blank image with no Oracle data or card information. - -\section{Cockatrice Settings} -TODO - -\section{Learning the Ropes / Starting a Solitaire Game} -The best way to get familiar with the way Cockatrice plays is to start a local game that you can play around in by yourself. You could also jump online and start slowly learning, and let other players help you. - -To start a Solo Local game, in the main Cockatrice window, click on “Cockatrice” on the top left, and select “Start local game…”. - -\begin{center} -\includegraphics{pics/fetch2ab8} -\end{center} - -This will bring up a small window that lets you select how many players will be in this local game you are creating. For right now, since we want to do a solo Solitaire game, select one player and hit “OK”. - -\begin{center} -\includegraphics{pics/fetchf010} -\end{center} - -\subsection{Loading a Deck / Using Sideboard} - -This will now bring you to a screen where you load a deck to play with. On the top left part of this screen you will find a button that is labeled “Load Local Deck”. Click that button and it will bring up a window where you can find and select what deck you would like to play with. - -\begin{center} -\includegraphics{pics/fetchf0d2} -\includegraphics[scale=0.8]{pics/fetch55a7} -\end{center} -Select a deck or a .cod file and click “Open”. - -\begin{center} -\includegraphics[scale=0.4]{pics/fetch9b89} -\end{center} - -After the file has loaded you will see all of the cards in that deck laid out on the table. If you hover your mouse over a card, the card image and Oracle info will show on the right side of the screen. If you have a sideboard made for the deck there will be a second section on the table for this sideboard. This screen gives you the ability to double check your deck to make sure it is not only the correct deck you want to play with, but it lets you see that all card images have downloaded properly. If you have cards not showing up at all or they are just blank cards with names on them, you may not have that set downloaded with your Oracle tool. If you have a sideboard, you can drag and drop cards from your main deck to your sideboard or vice-versa. You can do this by clicking and dragging a card to or from your main deck or sideboard. - -NOTE: Moving cards from your main deck to sideboard will NOT change how your deck file is saved, it will only change it temporary for the game you are playing or until you load a new deck. - -When you are satisfied with your deck choice and/or sideboarding options, click on the red outlined “Ready to start” button found a the top of the screen. - -\subsection{Finding Your Way Around} -The main game screenlooks like this -\begin{center} -\includegraphics[scale=0.4]{pics/fetch7cf0} -\end{center} -(Please note your screen will look different due to background image options.) - -\subsubsection{Main Table / Play area} -Split into four areas, this is where all the action will go down. -\begin{description} - \item[The Stack] The area on the left side of the table where Instant and Sorcery cards will be played. This is for things that will only temporarily be put on the table, then into the graveyard. Multiple cards may be added to this area at the same time. Anything on this part of the table will be seen by all players. - \item[Battlefield] This is the soul part of the game table. this is where creatures, enchantments, artifacts, and even plainswalkers will be placed. As cards are moved from your had to the table, they will be aligned to an invisible grid and moved around from there. Tap cards by double clicking them. Anything on this part of the table will be seen by all players. - \item[Land] This space is for land cards, but any card may be placed here. Tap cards here by double clicking them. Anything on this part of the table will be seen by all players. - \item[Hand] Every time you draw a card it will go here to your hand. You may also drag cards from the table back to your hand. Your opponents can not see what is in your hand. -\end{description} - -\subsubsection{Player Info Section} -\begin{center} -\includegraphics[scale=0.7]{pics/fetch0300} -\end{center} - -\begin{description} - \item[Player Avatar] This is a $156\times 60$ pixel JPG image that can be uploaded though the main Cockatrice website. All players in the game room can see this image. It serves nothing more than an online identity for you and other players. - \item[Player Name] Your online name that you picked though the main Cockatrice website. - \item[Life Total] Your in-game life. Using your mouse, if left-clicked will raise this number by one, and if right-clicked lowered by one. There are also keyboard shortcuts to change your life total. - \item[Counters] These seven multicolored circles are used as counters. They can be seen by all players and can be changed by left or right clicking on them to add or subtract a number. Players use them for various digit counting but primarily used for adding and subtracting floating mana produced by card effects. The bottom two white counters can be used for other things like Poison. - \item[Library] This is your deck of cards. The number in the middle reflects how many cards are left in your library. Double clicking the deck lets you draw a card and add it to your hand, you can also drag cards off the top into the battlefield or to your hand. Right-clicking the deck brings up a menu that allows other things to happen like reviling the top number of cards, shuffling, or moving cards directly into the graveyard. - \item[Number of cards in hand] The number in the middle represents how many cards are currently in your hand. Other players can see this number but can not see the cards actually in your hand. - \item[Graveyard] Cards can be dragged and dropped into your graveyard from play or vise-versa, the stack, your hand, or even your library. The number in the middle represents how many cards are currently in your graveyard. Any player may right-click on the graveyard and bring up a menu that shows what cards are in it. - \item[Exile] Cards can be dragged and dropped into exile from play or vise-versa, the stack, your hand, or even your library. The number in the middle represents how many cards are currently in your exile. Any player may right-click on the exile and bring up a menu that shows what cards are in it. -\end{description} - -\subsubsection{Turn Phases} -\begin{center} -\includegraphics[scale=0.7]{pics/fetchfebd} -\end{center} -This bar located on the left most side of the screen represents the 11 steps in a players turn. To go from one phase to the next, you can click on the square of the phase you want to move to, or you can hit Ctrl+space to move down to the next. Some phases even have their own keyboard shortcut. Going from one phase to the next does not actually do anything to your or your cards, it is only a place marker for your opponents to see and keep up with what you are doing in your turn. For example, clicking to the “Draw Phase” will not automatically draw you a card. It is customary for a player to end their turn on the “End of turn step” and let their opponent hit the “next turn” button. This is a courtesy for other players if they wish to do something like use an instant at the end of your turn, or in response to something you did. - -NOTE: Players sometimes use the term EOT which stands for “End Of Turn”. This is to let other players know they are doing something in response to the end of the current turn. - -\subsubsection{Info/Chat Bar} -\begin{center} -\includegraphics[scale=0.8]{pics/fetcha0de} -\end{center} -Split into three sections, the Info/Chat bar lets you see a close-up image of the card your mouse was last over, as well as gives you the card info for that card. At the bottom of this bar there is a chat log that helps keep track of events during the game as well as lets you communicate with other players. if a card is placed on the table, pointed at, or tapped it will get noted in the chat log as well as has a link to the card that you can hover over and see an image of at the top of the bar. - -\subsection{Basic Functions} -\subsubsection{Rolling Dice} -At the beginning of a game players decide who is going first by rolling a 20 sided die. In Cockatrice we do this by pressing Ctrl-I and hitting enter. Hitting Ctrl-I brings up a die window and lets you select how many sides you want on your die. Default is 20, and pressing enter will “roll” the die. This action will show up in the cat log at on the bottom right of the screen. You can also find this in the “game” menu at the top of the window, selecting “player” and clicking on “roll die…” -\begin{center} - \includegraphics{pics/fetch7486}\\ - \includegraphics{pics/fetch3705} -\end{center} - -\subsubsection{Draw Cards / Mulligan} -When a game starts and the first player has been selected, all players will draw seven cards. this can be done by pressing Ctrl-M. Seven cards will go from your library to your hand. Pressing Ctrl-M again will put the seven cards from your hand back into your library, shuffle your library and deal out six new cards to you. Each time you press Ctrl-M it will give you one less card until you get down to one card, then it will re-start at seven cards. This function can be found by clicking the “game” menu on the top of the window, selecting “player” then selecting “hand” and then “take Mulligan”. If you are playing a friendly game, press Ctrl-M as normal, but then press Ctrl-D to draw cards until you have a total of seven again. -\begin{center} -\includegraphics[scale=0.5]{pics/fetchfe20} -\end{center} - - -\subsubsection{Tapping} -Tapping cards is very basic. If a card is on the table under your control, you can double click it to tap it and then double click again to untap it. You can select multiple cards on the table by clicking and dragging your mouse, then tap or untap all of the selected cards at the same time. Other players can not tap or untap your cards. Pressing Ctrl-U will untap everything you control. - -\begin{itemize} - \item Untapped - \begin{center} - \includegraphics{pics/fetchb6fe} - \end{center} - - \item Tapped - \begin{center} - \includegraphics{pics/fetch867f} - \end{center} -\end{itemize} - -\subsubsection{Attaching Cards to Cards} -Sometimes an Enchantment -Aura or Equipment cards need to be attached to other cards that are already on the table. simply put the enchantment or equipment on the table. Right-click the card and select “attach” (this can also be done with Ctrl-A). A green arrow will appear, point and click on the card you wish to attach. You can also attach cards to other people's cards. -\begin{center} - \includegraphics{pics/fetch100e}\\ - \includegraphics{pics/fetchb17a} -\end{center} - - -\subsubsection{Changing Power/Toughness} -Enchantments, Equipment, and other effects sometimes change a creatures power or toughness. This can be done by right-clicking the card, and selecting “power / toughness” then selecting which one you wish to do. Other players can not change your creatures power and toughness. This can also be done though a series of keyboard shortcuts seen below. - -\begin{center} -\begin{tabular}{ll} -(Select card) Ctrl++ & Increase power \\ -(Select card) Ctrl+- & Decrease power \\ -(Select card) Alt++ & Increase toughness \\ -(Select card) Alt+- & Decrease toughness \\ -(Select card) Ctrl+Alt++ & Increase power and toughness \\ -(Select card) Ctrl+Alt+- & Decrease power and toughness -\end{tabular} -\includegraphics[scale=0.5]{pics/fetche3fc}\\ -\includegraphics{pics/fetchd922} -\end{center} - -\subsubsection{Adding Counters to Cards} -Sometimes Counters are needed to be placed on cards that the counters on the side of the screen are not able to track. Cockatrice offers three different counter color options, Red, Green, and Yellow. Although there is no set standard on what color stands for what, it is mostly player preference. Green could be used for +1/+1, red -1/-1, leaving yellow for charge and quest counters, this is not a set rule. Adding counters is as simple as right clicking on the card you wish to add counters too, and currently there is no keyboard shortcut for this process. Removing counters is the same process, right click and select remove. Other players can not add or remove counters to or from your cards. - -\begin{center} -\includegraphics{pics/fetch5170} -\end{center} -(One of each counter) - -\subsubsection{Pointing at Cards / Arrows} -Pointing at cards is needed for resolving spells, or declaring attackers and blockers. All you need to do is right-click over a card and drag an arrow over to what you are pointing at. Permanents, spells in the stack, and even a players life total can be pointed at. You can point at your opponents cards and life total, and they can point at yours. When your arrows are no loner needed, press Ctrl-R to remove them from the screen. - -\begin{center} -\includegraphics[scale=0.5]{pics/fetch98fd} \\ -\includegraphics[scale=0.5]{pics/fetch74b2} -\end{center} - -\subsubsection{Creating Tokens} -Creating tokens can sometimes be tedious, but is well worth the effort to keep a clean and organized game. Pressing Ctrl-T will bring up a small window to assist you in creating a token. Simply enter the name of the token you are creating, select its color, and give it a power and toughness (\#/\#). You can also bring up this token window by selecting “game” from the top menu, selecting “Player” then clicking on “Create Token…”. A copy of the Last token made can be done by pressing *Ctrl+G or right-clicking on a already made token (or any card on the table) and selecting “clone” or pressing Ctrl-H**. When a token or clone leaves play, it will be destroyed and vanish. - -\begin{center} -\includegraphics[scale=0.5]{pics/fetch2c36} \\ -\includegraphics[scale=0.5]{pics/fetch9bff} \\ -\includegraphics[scale=0.5]{pics/fetche6b2} \\ -\includegraphics[scale=0.5]{pics/fetch84a2} -\end{center} - -Make copies of your last token by pressing Ctrl-G. -\begin{center} -\includegraphics[scale=0.5]{pics/fetch6847} -\end{center} - -\chapter{Playing Online} - With Cockatrice you will most likely play Magic games over the Internet with real people all around the world. In order to help maintain a pleasant environment for users, please read the messages below: - -\begin{itemize} - \item User Code of Conduct\footnote{TODO, dead forum link} -- Must Read for all Users - \item How to Report Abuse\footnote{TODO, dead forum link} -- It is recommended to read this as well -\end{itemize} - -\section{Connect to Server} -To connect to the Cockatrice server, launch the Cockatrice program, go to the “Cockatrice” menu at the top left, and select “Connect”. A window will appear (see image below). -\begin{center} -\includegraphics[scale=0.5]{pics/fetch23f3} -\end{center} -If you have registered with Cockatrice, then enter your Username in the “Player Name” field and your password in the “Password” field then click “OK”. You may check the “Remember Password” box if you wish. If you do, then the next time “Connect” is selected from the “Cockatrice” menu, the window that appears will already have your Username and Password already filled. Please take this into consideration if you share a computer with other people, seeing that you are responsible for anything that happens on the server with your username (As noted here). If you did not register with Cockatrice, then simply fill in the Username with whatever you like and click “OK”. If you would like to become a registered user, read the instructions from the server's website. - -Once you are connected to the server, more tabs will appear at the top of the screen next to the “Deck” tab that you are already on. - -\section{All About Games} -This page is about creating, joining, watching, and searching for games on the Cockatrice Server. In order to participate in any games, you have to be connected to the server. The games on the server are where all of the action take place. There will be many games happening on the server at the same time. Basically, first a game is created by a player (it could be you). Then other players join the game until the number of players reaches the number set by that game's creator. When the game has no players participating in it, the game disappears. -Creating a Game - -To Create a game, go to the “MTG room” tab. Click on the “Create” button below the Games list. A window will appear (see below). -\begin{center} -\includegraphics[scale=0.5]{pics/fetch54df} -\end{center} - -Here are all of the options for creating a game: -\begin{description} - \item[Description] Describe the game in your own words (i.e. “Competitive Standard”, “Casual EDH- No Infinites”, “RavnicaDraft”, “Here is Chris”) - \item[Players] Specify the number of Players in the game. This cannot be changed after the game is created. The game can only begin when the specified number of players join. - \item[Spectators] Spectators are users that are in a game, but they are not one of the players. Spectators can see all of the public zones of the game and everything displayed in the Info/Chat Bar. Any number of users can join a game as a Spectator (as long as the “Spectators Allowed” box is checked). - \begin{description} - \item[Spectators Allowed] Unchecking this box will prohibit any/all users from joining the game as a Spectator. - \item[Spectators Need a Password to Join] Checking this box will make it so that in order for a user to join as a Spectator, they need to type the password you specify in the Password Field. - \item[Spectators can Chat] Checking this box will allow Spectators to type comments in the Chat bar during the game. - \item[Spectators See Everything] Checking this box will allow Spectators to view cards in all private zones of all players (hands, libraries, face-down cards). - \end{description} - \item[Password Field] If you type anything in this field, a Player (or Spectator if the “Spectators Need Password” box is checked) will need to type the exact same thing you typed in order to join the game (and it is case-sensitive). - \item[Only Buddies Can Join] Checking this box will prevent any user who is not in your Buddy List from joining the game as a Player or Spectator. NOTE: Your username is not on your buddy list. If you leave a game you created, and this box is checked, you will not be able to rejoin. - \item[Only Registered Users Can Join] Checking this box will prevent anyone who has not registered on the Cockatrice website from joining as a Player or Spectator. - \item[Game Type] These check boxes have no effect on the game. They inform other Users browsing the Games list of what format your game is. Users can choose to view only games of a certain Type/Format. (\url{http://en.wikipedia.org/wiki/Magic:_The_Gathering_formats}) -\end{description} - -\subsection{Joining a Game} -Most of the time, to join a game you click on the “MTG Room” tab, click on a game in the Games list, then click Join. -If the Game's creator specified a password then you will have to type that password in a small window that appears after you click Join (the password is case-sensitive). -If your User Profile meets the criteria of the Game's creator then a new tab will open with that game. -There is also an easy way to join a game in which a User in your Buddy List is playing. -Go to the “User Lists” tab, right click any Username from the Buddies Online list (at the left of the window) to make a menu appear, and select “Show this user's games”. -A window will open with a list of games that the User is either playing or watching. In the same manner as described with the “MTG Room” tab, simply click on a game and click Join. -To watch a game, the instructions are the same except that you click the “Join as Spectator” button instead of the Join button. -NOTE: If you are a player in a game and you wish to become a spectator in that game, you must first leave the game then rejoin as a Spectator. Same thing if you are a Spectator and wish to play. - -\subsection{Searching for Games} -The Games list in the “MTG Room” tab displays by default all games that have not reached the specified number of players. The “Filter Games” button makes looking through this list easier if you are looking to join a particular kind of game. When this button is selected, a window appears (see below). -\begin{center} -\includegraphics[scale=0.5]{pics/fetchd30e} -\end{center} -\begin{description} - \item[Game Description] Displays games with certain descriptions. You can even search partial names. - \item[Creator] Displays games created by Users with that username. It even searches for partial names. - \item[Player Count] Displays all games where the specified number of players is greater-than or equal to the “at least” number and less-than or equal to the “at most” number. For instance, setting both numbers to 3 will display all games whose creators made as 3-player games. - \item[Show Unavailable Games] Checking this box will display games that are full and in progress. You can still join these games as a Spectator if the game's creator allows it. - \item[Game Types] Displays games with the selected types. Bear in mind that the Cockatrice software does not enforce deck construction for formats. So just because a game's type is EDH/Commander, doesn't necessarily mean that is what's being played in the game. Players can agree to switch formats in a game. -\end{description} - -\section{Keeping Track of Buddies} - - -Cockatrice allows registered users to keep track of other registered users in a buddy list. You won't be able to do anything with this list (or other registered users at all) unless you are a registered user and connected to the server. - -To add a User to your Buddy list, right-click their username and, in the menu that appears, select “Add to buddy list”. You can right-click and add a User in this manner anywhere you see their username (under the “User lists” tab, the “MTG Room” tab, in a game, or in a direct chat). When you add a User, their username will appear in the list “Buddies Online” located under the “User lists” tab. If the username appears in a light shade of gray, then that User is not connected to the server. If it appears in black, then that User is connected to the server. - -If you see that one of your Buddies is connected, you can see the games he/she is currently in. Right-click their username, and in the menu that appears select “Show this user's games”. A window will appear from which you can watch or play in a game that your buddy is currently in. - -You can Direct Chat with a Buddy (or any user for that matter) by right-clicking their username and, from the menu that appears, selecting “Direct Chat”. When you do this, a new tab will open (both on your window and on the other User's window) containing a chat room that can only be seen and used by the two of you. The place where you type in your messages is at the very bottom of the window. - -To remove a User from your Buddy list, right-click their username, and from the menu that appears select “Remove from buddy list”. - -\section{Dealing with and Preventing Unpleasantries} -Make a screenshot, there are several free programs available. -Right-Click a username to add to Ignore List. - -\chapter{Frequently Asked Questions (FAQ)} -\section{How to update the card database} -When a new set comes out, you need to update your card database to be able to see the new cards in the deck editor and use them in the game. To do this, open the Oracle tool in the Cockatrice folder. It's a stand-alone program and is run directly out of the folder without running Cockatrice. - -When the Oracle tool is running, open the menu and select 'Download sets information'. Use the suggested address and click OK. After at most a few seconds, you should be presented a list of sets to be downloaded. Do not uncheck the ones you already have: they should be downloaded again. Click 'Start download' and wait. When the process is finished, the database should be up to date. - -\section{Change Card Art / Custom Card Art} - Is it possible to change the card art for a specif card, not by changing the set order in my deck editor? - -YES! It is actually very simple. - -All you have to do is get a .JPG of the card you wish to change and name the actual file of the image to the cards (exact) name and add ”.Full” to the end. You can then take this new card image and replace the old one in your downloaded image folder. - -NOTE: Image size does not matter. Try to use a high resolution .JPG of the card you want to use for best quality. - -EXAMPLE: Let's say you wish to turn your M11 Sun Titan from its original art to a promo version. - -\begin{center} - \includegraphics[scale=0.5]{pics/fetch74e3} -\end{center} -If you are using Windows 7, you can find the default location for the Cockatrice card art files under \shellcmd{C:\textbackslash Program Files (x86)\textbackslash Cockatrice\textbackslash pics\textbackslash downloadedPics} Once there, look for the folder of the set for the card we are replacing. For this example, Sun Titan is in the M11 folder. Open the folder and paste/replace your new “Promo” Sun Titan.full[.JPG] into this folder, discarding the original copy. (If you have not yet downloaded the original image of a card by selecting it in the deck editor or playing it in an online game, the card image will not yet be there) - -Once you have the new card image in the proper folder, you can now start Cockatrice (Or re-start if you already had the program open)and select the deck editor to see your new card image. - -If you are changing the art of a card that has multiple versions in other sets, make sure you put the new card in whatever folder (or set) is highest on your card database set list. - -NOTE: Other players on Cockatrice will NOT see your new card image. They will only see whatever version of the card is in their database. - -\section{Use Higher Resolution Cards} -Can you get better/higher resolution card art than the default downloaded card images already used? - -Yes! - -When you click on a card for the first time in the Deck Editor, Cockatrice goes onto the internet and finds an image of that card from a database on another website. - -If you find a higher resolution .JPG of a card that you wish to use on your Cockatrice, you can replace the image with no problem. If you can find a card image in the 3,000 by 1,000 pixel range, and save it as the cards (exact) name and add .Full to the end. - -If you are using Windows 7, you can find the default location for the Cockatrice card art files under \shellcmd{C:\textbackslash Program Files (x86)\textbackslash Cockatrice\textbackslash pics\textbackslash downloadedPics}. Once there, look for the folder of the set for the card you are replacing. (If you are not running Cockatrice as an administrator, look in \shellcmd{C:\textbackslash Users\textbackslash username\textbackslash AppData\textbackslash Local\textbackslash VirtualStore\textbackslash Program Files (x86)\textbackslash Cockatrice\textbackslash pics\textbackslash downloadedPics}.) - -Open the folder of the set with the card, and paste/replace the new higher resolution .JPG image with the old low quality one. - -This process is the same for changing cards to custom images. - -NOTE: Other Cockatrice players will not see your higher resolution card art, they will only see the image they have in their own database. - -\section{Booster drafts} - Cockatrice does not support booster drafts, so players need to use an external service for this. - -Most people use \url{http://ccgdecks.com/} so it's probably not a bad idea to register an account there before joining a game in Cockatrice. - -\section{What is Legacy / Vintage / EDH?} -See \url{http://en.wikipedia.org/wiki/Magic:_The_Gathering_formats}. - -\section{Linking cards and URLs in the Cockatrice chat.} -The Cockatrice chat supports linking of cards and URLs by use of certain tags around a word or phrase. - -\subsection{How to link a card in the Cockatrice chat} -To link a card in the Cockatrice chat, type out the full name of the card, surrounded by the [card] and [/card] tags. - -For example: -\begin{verbatim} -[card]Black Lotus[/card] -\end{verbatim} - -\subsection{How to link a URL in the Cockatrice chat} -To link a URL in the Cockatrice chat, type out the url, surrounded by the [url] and [/url] tags. - -For example: -\begin{verbatim} -[url]http://www.cockatrice.de[/url] -[url]cockatrice.de[/url] -\end{verbatim} - - -\chapter{Servatrice on CentOS 6} -\label{servatrice} -This Howto from woogerworks.com will help you to run your own Cockatrice server (called Servatrice). -An installation script can be downloaded at: \url{http://www.woogerworks.com/cockatrice/installatrice} - -BEFORE CONTINUING, PLEASE UNDERSTAND THESE ARE THE VERY MOST BASIC STEPS. -THIS WILL NOT CONFIGURE SECURITY SETTINGS (YOU ARE RESPONSIBLE FOR YOUR OWN SECURITY)! -THIS WILL NOT CONNECT THE SERVER TO A DATABASE! -USE AT YOUR OWN RISK. - -\begin{enumerate} - \item Open a command shell and install the prerequisites -\begin{enumerate} - \item \shellcmd{cd /etc/yum.repos.d/} - \item \shellcmd{sudo wget http://kdeforge.unl.edu/apt/kde-redhat/epel/kde.repo} - \item \shellcmd{yum -y groupinstall "development tools"} - \item \shellcmd{rpm -Uvh http://dl.fedoraproject.org/pub/epel/6/x86\_64/epel-release-6-8.noarch.rpm} - \item \shellcmd{yum -y install qt-mysql qt-devel qt-mobility-devel protobuf-devel protobuf-compiler cmake28 libgcrypt-devel} - \item \shellcmd{cd} -\end{enumerate} - - \item Download the source code: -\begin{enumerate} - \item \shellcmd{git clone https://github.com/Daenyth/Cockatrice} - \item \shellcmd{cd Cockatrice} - \item \shellcmd{mkdir build} - \item \shellcmd{cd build} - \item \shellcmd{cmake28 -DWITH\_SERVER=1 ..} - \item \shellcmd{make} - \item \shellcmd{sudo make install} -\end{enumerate} - - \item Create a servatrice.ini file. There is an example file you can convert: -\shellcmd{sed -e "s/number\_pools=1/number\_pools=0/" ../servatrice/servatrice.ini.example > \textasciitilde/servatrice.ini} -Here is one example file: -\begin{framed} -\begin{verbatim} -[server] -port=4747 -statusupdate=15000 -logfile=server.log -name="My own Cockatrice server" -id=1 -number_pools=0 - -[servernetwork] -active=0 -port=14747 -ssl_cert=ssl_cert.pem -ssl_key=ssl_key.pem - -[authentication] -method=none - -[database] -type=none -prefix=cockatrice -hostname=localhost -database=servatrice -user=MYUSERNAMEHERE -password=MYSECUREPASSWORDHERE - -[rooms] -method=config -roomlist\size=1 -roomlist\1\name="FIRST ROOM NAME" -roomlist\1\description="FIRST ROOM DESCRIPTION" -roomlist\1\autojoin=true -roomlist\1\joinmessage="THIS IS A JOIN MESSAGE" -roomlist\1\game_types\size=11 -roomlist\1\game_types\1\name="Vintage (T1)" -roomlist\1\game_types\2\name="Legacy (T1.5)" -roomlist\1\game_types\3\name="Extended (T1.X)" -roomlist\1\game_types\4\name="Modern" -roomlist\1\game_types\5\name="Standard (T2)" -roomlist\1\game_types\6\name="Block Constructed" -roomlist\1\game_types\7\name="EDH/Commander" -roomlist\1\game_types\8\name="Highlander" -roomlist\1\game_types\9\name="2HG" -roomlist\1\game_types\10\name="Draft/Sealed" -roomlist\1\game_types\11\name="Other" - -[game] -max_game_inactivity_time=120 -max_player_inactivity_time=15 - -[security] -max_users_per_address=8 -message_counting_interval=10 -max_message_size_per_interval=1000 -max_message_count_per_interval=10 -max_games_per_user=5 -\end{verbatim} -\end{framed} - - \item Install the database server (optional) -\begin{enumerate} - \item \shellcmd{sudo yum -y install mysql-server mysql php-mysql} - \item \shellcmd{sudo service mysqld start} - \item \shellcmd{sudo chkconfig mysqld on} - \item \shellcmd{mysqladmin -u root password 'password'} - \item \shellcmd{mysql -u root -ppassword -e "create database servatrice;"} - \item \shellcmd{mysql -u root -ppassword -e "create user 'servatrice'@'localhost' identified by 'password';"} - \item \shellcmd{mysql -u root -ppassword -e "grant all privileges on servatrice.* to 'servatrice'@'localhost';"} - \item \shellcmd{mysql -u servatrice -ppassword servatrice < ../servatrice/servatrice.sql} - \item \shellcmd{mysql -u servatrice -ppassword -e "insert into servatrice.cockatrice\_users \textbackslash\\ (admin,name,password\_sha512,active) values (1,'servatrice','MYSHA512PASSWORD',1);"} - \item \shellcmd{sed -i.bak -e "s/password=foobar/password=password/" -e "s/type=none/type=mysql/" \textbackslash\\ -e "s/method=none/method=sql/" ~/servatrice.ini} -\end{enumerate} - - \item Start Servatrice for testing: \shellcmd{/usr/local/bin/servatrice}. I recommend to run it later within a \shellcmd{screen} session or write an init-script. -\end{enumerate} -You should be able to log in as a regular user using any account name. -You can login using the username \textit{servatrice} and the password \textit{password} as the first moderator. - -If everything succeeded, you should tweak the servatrice.ini and set your root password to something strong if not already done (at least 8 characters, upper case, lower case, numbers, special characters). - -%\listoffigures -%\listoftables -%\printindex - -\end{document} diff --git a/doc/usermanual/pics/fetch0300.jpg b/doc/usermanual/pics/fetch0300.jpg deleted file mode 100644 index 90bdcf44e..000000000 Binary files a/doc/usermanual/pics/fetch0300.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch100e.jpg b/doc/usermanual/pics/fetch100e.jpg deleted file mode 100644 index 00e9c9ad8..000000000 Binary files a/doc/usermanual/pics/fetch100e.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch23f3.jpg b/doc/usermanual/pics/fetch23f3.jpg deleted file mode 100644 index eccdc58ed..000000000 Binary files a/doc/usermanual/pics/fetch23f3.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch2ab8.jpg b/doc/usermanual/pics/fetch2ab8.jpg deleted file mode 100644 index fdb4e61eb..000000000 Binary files a/doc/usermanual/pics/fetch2ab8.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch2c36.jpg b/doc/usermanual/pics/fetch2c36.jpg deleted file mode 100644 index d64557791..000000000 Binary files a/doc/usermanual/pics/fetch2c36.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch3705.jpg b/doc/usermanual/pics/fetch3705.jpg deleted file mode 100644 index e0262bd16..000000000 Binary files a/doc/usermanual/pics/fetch3705.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch5170.jpg b/doc/usermanual/pics/fetch5170.jpg deleted file mode 100644 index 23b04704c..000000000 Binary files a/doc/usermanual/pics/fetch5170.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch52e0.jpg b/doc/usermanual/pics/fetch52e0.jpg deleted file mode 100644 index 73548870e..000000000 Binary files a/doc/usermanual/pics/fetch52e0.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch54df.jpg b/doc/usermanual/pics/fetch54df.jpg deleted file mode 100644 index a6d990ae2..000000000 Binary files a/doc/usermanual/pics/fetch54df.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch554a.jpg b/doc/usermanual/pics/fetch554a.jpg deleted file mode 100644 index 95e3c39df..000000000 Binary files a/doc/usermanual/pics/fetch554a.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch55a7.jpg b/doc/usermanual/pics/fetch55a7.jpg deleted file mode 100644 index a16ac41a3..000000000 Binary files a/doc/usermanual/pics/fetch55a7.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch6847.jpg b/doc/usermanual/pics/fetch6847.jpg deleted file mode 100644 index 3e489efb2..000000000 Binary files a/doc/usermanual/pics/fetch6847.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch7486.jpg b/doc/usermanual/pics/fetch7486.jpg deleted file mode 100644 index 30cbf1a81..000000000 Binary files a/doc/usermanual/pics/fetch7486.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch74b2.jpg b/doc/usermanual/pics/fetch74b2.jpg deleted file mode 100644 index 019f1e99c..000000000 Binary files a/doc/usermanual/pics/fetch74b2.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch74e3.jpg b/doc/usermanual/pics/fetch74e3.jpg deleted file mode 100644 index ddfaef2ec..000000000 Binary files a/doc/usermanual/pics/fetch74e3.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch7cf0.jpg b/doc/usermanual/pics/fetch7cf0.jpg deleted file mode 100644 index 342474b1b..000000000 Binary files a/doc/usermanual/pics/fetch7cf0.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch84a2.jpg b/doc/usermanual/pics/fetch84a2.jpg deleted file mode 100644 index bbf941db3..000000000 Binary files a/doc/usermanual/pics/fetch84a2.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch867f.jpg b/doc/usermanual/pics/fetch867f.jpg deleted file mode 100644 index 2b11b7244..000000000 Binary files a/doc/usermanual/pics/fetch867f.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch98fd.jpg b/doc/usermanual/pics/fetch98fd.jpg deleted file mode 100644 index 36b3f9184..000000000 Binary files a/doc/usermanual/pics/fetch98fd.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch9b89.jpg b/doc/usermanual/pics/fetch9b89.jpg deleted file mode 100644 index 424f449bf..000000000 Binary files a/doc/usermanual/pics/fetch9b89.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetch9bff.jpg b/doc/usermanual/pics/fetch9bff.jpg deleted file mode 100644 index 8334c3137..000000000 Binary files a/doc/usermanual/pics/fetch9bff.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetcha0de.jpg b/doc/usermanual/pics/fetcha0de.jpg deleted file mode 100644 index 6f756f763..000000000 Binary files a/doc/usermanual/pics/fetcha0de.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetchb17a.jpg b/doc/usermanual/pics/fetchb17a.jpg deleted file mode 100644 index 9953a2548..000000000 Binary files a/doc/usermanual/pics/fetchb17a.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetchb6fe.jpg b/doc/usermanual/pics/fetchb6fe.jpg deleted file mode 100644 index bcafcb289..000000000 Binary files a/doc/usermanual/pics/fetchb6fe.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetchc18b.jpg b/doc/usermanual/pics/fetchc18b.jpg deleted file mode 100644 index 843741383..000000000 Binary files a/doc/usermanual/pics/fetchc18b.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetchd30e.jpg b/doc/usermanual/pics/fetchd30e.jpg deleted file mode 100644 index 63dc69616..000000000 Binary files a/doc/usermanual/pics/fetchd30e.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetchd922.jpg b/doc/usermanual/pics/fetchd922.jpg deleted file mode 100644 index 368363572..000000000 Binary files a/doc/usermanual/pics/fetchd922.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetche1f4.jpg b/doc/usermanual/pics/fetche1f4.jpg deleted file mode 100644 index 456d77466..000000000 Binary files a/doc/usermanual/pics/fetche1f4.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetche3fc.jpg b/doc/usermanual/pics/fetche3fc.jpg deleted file mode 100644 index 565d8737a..000000000 Binary files a/doc/usermanual/pics/fetche3fc.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetche6b2.jpg b/doc/usermanual/pics/fetche6b2.jpg deleted file mode 100644 index ea64940bc..000000000 Binary files a/doc/usermanual/pics/fetche6b2.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetchf010.jpg b/doc/usermanual/pics/fetchf010.jpg deleted file mode 100644 index 28edbbaf8..000000000 Binary files a/doc/usermanual/pics/fetchf010.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetchf0d2.jpg b/doc/usermanual/pics/fetchf0d2.jpg deleted file mode 100644 index 2046c0e0f..000000000 Binary files a/doc/usermanual/pics/fetchf0d2.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetchf924.jpg b/doc/usermanual/pics/fetchf924.jpg deleted file mode 100644 index 494c1f422..000000000 Binary files a/doc/usermanual/pics/fetchf924.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetchfc3d.jpg b/doc/usermanual/pics/fetchfc3d.jpg deleted file mode 100644 index 879c9c058..000000000 Binary files a/doc/usermanual/pics/fetchfc3d.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetchfe20.jpg b/doc/usermanual/pics/fetchfe20.jpg deleted file mode 100644 index c5b1730ea..000000000 Binary files a/doc/usermanual/pics/fetchfe20.jpg and /dev/null differ diff --git a/doc/usermanual/pics/fetchfebd.jpg b/doc/usermanual/pics/fetchfebd.jpg deleted file mode 100644 index f44cbbe61..000000000 Binary files a/doc/usermanual/pics/fetchfebd.jpg and /dev/null differ diff --git a/doc/usermanual/pics/loaddeck_clip.jpg b/doc/usermanual/pics/loaddeck_clip.jpg deleted file mode 100644 index 5c0da095b..000000000 Binary files a/doc/usermanual/pics/loaddeck_clip.jpg and /dev/null differ diff --git a/doc/usermanual/pics/okdecklist.jpg b/doc/usermanual/pics/okdecklist.jpg deleted file mode 100644 index 306fe6259..000000000 Binary files a/doc/usermanual/pics/okdecklist.jpg and /dev/null differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..6cbac61f0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +version: '3' + +services: + mysql: + image: mysql + command: --sql_mode= + environment: + - MYSQL_ROOT_PASSWORD=root-password + - MYSQL_DATABASE=servatrice + - MYSQL_USER=servatrice + - MYSQL_PASSWORD=password + volumes: + - $PWD/servatrice/servatrice.sql:/docker-entrypoint-initdb.d/servatrice.sql + + servatrice: + build: + context: . + dockerfile: Dockerfile + image: ghcr.io/cockatrice/servatrice:latest + depends_on: + - mysql + ports: + - "4748:4748" + entrypoint: "/bin/bash -c 'sleep 10; servatrice --config /tmp/servatrice.ini --log-to-console'" + restart: always + volumes: + - $PWD/servatrice/docker/servatrice-docker.ini:/tmp/servatrice.ini diff --git a/docker-compose.yml.windows b/docker-compose.yml.windows new file mode 100644 index 000000000..3d29b1e5f --- /dev/null +++ b/docker-compose.yml.windows @@ -0,0 +1,27 @@ +version: '3' + +services: + mysql: + image: mysql + environment: + - MYSQL_ROOT_PASSWORD=root-password + - MYSQL_DATABASE=servatrice + - MYSQL_USER=servatrice + - MYSQL_PASSWORD=password + volumes: + - ./servatrice/servatrice.sql:/docker-entrypoint-initdb.d/servatrice.sql + - ./servatrice/mysql-storage:/var/lib/mysql + + servatrice: + build: + context: . + dockerfile: Dockerfile + image: ghcr.io/cockatrice/servatrice:latest + depends_on: + - mysql + ports: + - "4748:4748" + entrypoint: "/bin/bash -c 'sleep 10; servatrice --config /tmp/servatrice.ini --log-to-console'" + restart: always + volumes: + - ./servatrice/docker/servatrice-docker.ini:/tmp/servatrice.ini diff --git a/format.sh b/format.sh new file mode 100755 index 000000000..f8c183dfc --- /dev/null +++ b/format.sh @@ -0,0 +1,394 @@ +#!/bin/bash + +# This script will run clang-format on all modified, non-3rd-party C++/Header files. +# Optionally runs cmake-format on all modified cmake files. +# Optionally runs shellcheck on all modified shell files. +# Uses clang-format cmake-format git diff find shellcheck +# Never, ever, should this receive a path with a newline in it. Don't bother proofing it for that. + +set -o pipefail + +# go to the project root directory, this file should be located in the project root directory +olddir="$PWD" +cd "${BASH_SOURCE%/*}/" || exit 2 # could not find path, this could happen with special links etc. + +# defaults +include=("cockatrice/src" \ +"libcockatrice_card" \ +"libcockatrice_deck_list" \ +"libcockatrice_network" \ +"libcockatrice_protocol" \ +"libcockatrice_rng" \ +"libcockatrice_settings" \ +"libcockatrice_utility" \ +"oracle/src" \ +"servatrice/src" \ +"tests") +exclude=("libcockatrice_rng/libcockatrice/rng/sfmt/" \ +"libcockatrice_utility/libcockatrice/utility/peglib.h" \ +"oracle/src/lzma/" \ +"oracle/src/qt-json/" \ +"oracle/src/zip/" \ +"servatrice/src/smtp/") +exts=("cpp" "h" "proto") +cf_cmd="clang-format" +branch="origin/master" +cmakefile="CMakeLists.txt" +cmakedir="cmake/.*\\.cmake" +cmakeinclude=("cmake/gtest-CMakeLists.txt.in") +scripts="*.sh" +color="--" +verbosity=0 +sep="----------" + +# parse options +while [[ $* ]]; do + case "$1" in + '-b'|'--branch') + branch=$2 + set_branch=1 + shift 2 + ;; + '--cmake') + do_cmake=1 + shift + ;; + '-c'|'--color-diff') + color="--color=always" + mode="diff" + shift + ;; + '-d'|'--diff') + mode="diff" + shift + ;; + '-h'|'--help') + cat <s are given, all source files in those directories of the project root +path are formatted. To only format changed files in these directories use the +--branch option in combination. has to be a path relative to the project +root path or a full path inside $PWD. + +USAGE: $0 [option] [--branch ] [ ...] + +DEFAULTS: +Current includes are: + ${include[@]/%/ + } +Default excludes are: + ${exclude[@]/%/ + } +OPTIONS: + -b, --branch + Compare to this git branch and format only files that differ. + If unspecified it defaults to origin/master. + To not compare to a branch this has to be explicitly set to "". + When not comparing to a branch, git will not be used at all and every + source file in the entire project will be parsed. + + --cmake + Use cmake-format to format cmake files as well. + + -c, --color-diff + Display a colored diff. Implies --diff. + Only available on systems which support 'diff --color'. + + -d, --diff + Display a diff. Implies --test. + + -h, --help + Display this message and exit. + + -n, --names + Display a list of filenames that require formatting. Implies --test. + + --no-clang-format + Do not check any source files for clang-format. + + --print-version + Print the version of clang-format being used before continuing. + + --shell + Use shellcheck to lint shell files. Not available in the default inline + mode. + + -t, --test + Do not edit files in place. Set exit code to 1 if changes are required. + + -v, --verbose + Display output on successes. + +EXIT CODES: + 0 on a successful format or if no files require formatting. + 1 if a file requires formatting. + 2 if given incorrect arguments. + 3 if clang-format could not be found. + +EXAMPLES: + $0 --branch $USER/patch-2 ${include[0]} + Formats all changed files compared to the git branch "$USER/patch-2" + in the directory ${include[0]}. + + $0 --test . || echo "code requires formatting" + Tests if the source files in the current directory are correctly + formatted and prints an error message if formatting is required. + + $0 --cmake --branch "" --no-clang-format + Unconditionally format all cmake files and no source files. +EOM + exit 0 + ;; + '-n'|'--names') + mode="name" + shift + ;; + '--no-clang-format') + include=() # do not check any dirs + shift + ;; + '--print-version') + print_version=1 + shift + ;; + '--shell') + do_shell=1 + shift + ;; + '-t'|'--test') + mode="code" + shift + ;; + '-v'|'--verbose') + verbosity=1 + shift + ;; + '--') + dashdash=1 + shift + ;; + *) + if [[ ! $dashdash && $1 =~ ^-- ]]; then + echo "error in parsing arguments of $0: $1 is an unrecognized option" >&2 + exit 2 # input error + fi + if [[ ! $1 ]] || next_dir=$(cd "$olddir" && cd -- "$1" && pwd); then + if ! [[ $set_include ]]; then + include=() # remove default includes + set_include=1 + fi + if [[ $1 ]]; then + if [[ $next_dir != $PWD/* ]]; then + echo "error in parsing arguments of $0: $next_dir is not in $PWD" >&2 + exit 2 # input error + fi + include+=("$next_dir") + fi + else + echo "error in parsing arguments of $0: $1 is not a directory" >&2 + exit 2 # input error + fi + if ! [[ $set_branch ]]; then + unset branch # unset branch if not set explicitly + fi + shift + ;; + esac +done + +# check availability of clang-format +if ! hash $cf_cmd 2>/dev/null; then + echo "could not find $cf_cmd" >&2 + # find any clang-format-x.x in /usr/bin + cf_cmd=$(find /usr/bin -regex '.*/clang-format-[0-9]+\.[0-9]+' -print -quit) + if [[ $cf_cmd ]]; then + echo "found $cf_cmd instead" >&2 + else + exit 3 # special exit code for missing dependency + fi +fi + +# check availability of cmake-format +if [[ $do_cmake ]] && ! hash cmake-format 2>/dev/null; then + echo "could not find cmake-format" >&2 + exit 3 +fi + +# check availability of shellcheck +if [[ $do_shell ]] && ! hash shellcheck 2>/dev/null; then + echo "could not find shellcheck" >&2 + exit 3 +fi + +if [[ $branch ]]; then + # get all dirty files through git + if ! base=$(git merge-base "$branch" HEAD); then + echo "could not find git merge base" >&2 + exit 2 # input error + fi + mapfile -t basenames < <(git diff --diff-filter=d --name-only "$base") + names=() + for ex in "${exts[@]}"; do + for path in "${include[@]}"; do + for name in "${basenames[@]}"; do + rx="^$path/.*\\.$ex$" + if [[ $name =~ $rx ]]; then + names+=("$name") + fi + done + done + done + if [[ $do_cmake ]]; then + cmake_names=() + for name in "${basenames[@]}"; do + dirrx="^$cmakedir$" + filerx="(^|/)$cmakefile$" + if [[ $name =~ $dirrx || $name =~ $filerx ]]; then + cmake_names+=("$name") + fi + for include in "${cmakeinclude[@]}"; do + if [[ $name == "$include" ]]; then + cmake_names+=("$name") + fi + done + done + fi + if [[ $do_shell ]]; then + shell_names=() + for name in "${basenames[@]}"; do + filerx="(^|/)$scripts$" + if [[ $name =~ $filerx ]]; then + shell_names+=("$name") + fi + done + fi +else + exts_o=() + for ext in "${exts[@]}"; do + exts_o+=(-o -name "*\\.$ext") + done + unset "exts_o[0]" # remove first -o + mapfile -t names < <(find "${include[@]}" -type f "${exts_o[@]}") + if [[ $do_cmake ]]; then + mapfile -t cmake_names < <(find . -maxdepth 2 -type f -name "$cmakefile" -o -path "./${cmakedir/.}") + cmake_names+=("${cmakeinclude[@]}") + fi + if [[ $do_shell ]]; then + mapfile -t shell_names < <(find . -maxdepth 5 -type f -name "$scripts") + fi +fi + +# filter excludes +for path in "${exclude[@]}"; do + for i in "${!names[@]}"; do + rx="^$path" + if [[ ${names[$i]} =~ $rx ]]; then + unset "names[$i]" + fi + done +done + +# optionally print version +if [[ $print_version ]]; then + $cf_cmd -version + [[ $do_cmake ]] && echo "cmake-format version $(cmake-format --version)" + [[ $do_shell ]] && echo "shellcheck $(shellcheck --version | grep "version:")" + echo "$sep" +fi + +if [[ ! ${cmake_names[*]} ]]; then + unset do_cmake +fi +if [[ ! ${shell_names[*]} ]]; then + unset do_shell +fi +if [[ ! ( ${names[*]} || $do_cmake || $do_shell ) ]]; then + exit 0 # nothing to format means format is successful! +fi + +# format +case $mode in + diff) + declare -i code=0 + files_to_format=() + for name in "${names[@]}"; do + if ! $cf_cmd "$name" | diff "$name" - -p "$color"; then + code=1 + files_to_format+=("$name") + fi + done + for name in "${cmake_names[@]}"; do + if ! cmake-format "$name" | diff "$name" - -p "$color"; then + code=1 + files_to_format+=("$name") + fi + done + for name in "${shell_names[@]}"; do + if ! shellcheck "$name"; then + code=1 + files_to_format+=("$name") + fi + done + if (( code>0 )); then + echo "$sep" + for name in "${files_to_format[@]}"; do + echo "$name" + done + fi + exit $code + ;; + name) + declare -i code=0 + for name in "${names[@]}"; do + if ! $cf_cmd "$name" | diff "$name" - -q >/dev/null; then + echo "$name" + code=1 + fi + done + for name in "${cmake_names[@]}"; do + if ! cmake-format "$name" --check; then + echo "$name" + code=1 + fi + done + for name in "${shell_names[@]}"; do + if ! shellcheck "$name" >/dev/null; then + echo "$name" + code=1 + fi + done + exit $code + ;; + code) + for name in "${names[@]}"; do + $cf_cmd "$name" | diff "$name" - -q >/dev/null || exit 1 + done + for name in "${cmake_names[@]}"; do + cmake-format "$name" --check || exit 1 + done + for name in "${shell_names[@]}"; do + shellcheck "$name" >/dev/null || exit 1 + done + ;; + *) + if [[ "${names[*]}" ]]; then + $cf_cmd -i "${names[@]}" + fi + if [[ $do_cmake ]]; then + cmake-format -i "${cmake_names[@]}" + fi + if [[ $do_shell ]]; then + echo "warning: --shell is not compatible with the current mode but shell files were modified!" >&2 + echo "recommendation: try $0 --diff --shell" >&2 + fi + if (( verbosity>0 )); then + count="${#names[*]}" + if [[ $do_cmake ]]; then + (( count+=${#cmake_names[*]} )) + fi + echo "parsed $count files that differ from base $branch" + fi + ;; +esac diff --git a/libcockatrice_card/CMakeLists.txt b/libcockatrice_card/CMakeLists.txt new file mode 100644 index 000000000..dd3799e33 --- /dev/null +++ b/libcockatrice_card/CMakeLists.txt @@ -0,0 +1,61 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS + libcockatrice/card/card_info.h + libcockatrice/card/card_info_comparator.h + libcockatrice/card/database/card_database.h + libcockatrice/card/database/card_database_loader.h + libcockatrice/card/database/card_database_manager.h + libcockatrice/card/database/card_database_querier.h + libcockatrice/card/database/parser/card_database_parser.h + libcockatrice/card/database/parser/cockatrice_xml_3.h + libcockatrice/card/database/parser/cockatrice_xml_4.h + libcockatrice/card/import/card_name_normalizer.h + libcockatrice/card/printing/exact_card.h + libcockatrice/card/printing/printing_info.h + libcockatrice/card/set/card_set.h + libcockatrice/card/relation/card_relation.h +) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library( + libcockatrice_card STATIC + ${MOC_SOURCES} + libcockatrice/card/card_info.cpp + libcockatrice/card/card_info_comparator.cpp + libcockatrice/card/database/card_database.cpp + libcockatrice/card/database/card_database_loader.cpp + libcockatrice/card/database/card_database_manager.cpp + libcockatrice/card/database/card_database_querier.cpp + libcockatrice/card/database/parser/card_database_parser.cpp + libcockatrice/card/database/parser/cockatrice_xml_3.cpp + libcockatrice/card/database/parser/cockatrice_xml_4.cpp + libcockatrice/card/import/card_name_normalizer.cpp + libcockatrice/card/printing/exact_card.cpp + libcockatrice/card/printing/printing_info.cpp + libcockatrice/card/relation/card_relation.cpp + libcockatrice/card/set/card_set.cpp + libcockatrice/card/set/card_set_list.cpp + libcockatrice/card/format/format_legality_rules.cpp + libcockatrice/card/format/format_legality_rules.h +) + +target_include_directories( + libcockatrice_card + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} + PUBLIC ${CMAKE_SOURCE_DIR}/cockatrice/src/filters +) + +target_link_libraries( + libcockatrice_card + PUBLIC libcockatrice_interfaces + PUBLIC libcockatrice_utility + PUBLIC ${QT_CORE_MODULE} +) diff --git a/libcockatrice_card/libcockatrice/card/card_info.cpp b/libcockatrice_card/libcockatrice/card/card_info.cpp new file mode 100644 index 000000000..1aaec85b8 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/card_info.cpp @@ -0,0 +1,235 @@ +#include "card_info.h" + +#include "game_specific_terms.h" +#include "printing/printing_info.h" +#include "relation/card_relation.h" +#include "set/card_set.h" + +#include +#include +#include +#include +#include +#include +#include + +class CardRelation; +class CardSet; +class CardInfo; + +using CardInfoPtr = QSharedPointer; + +CardInfo::CardInfo(const QString &_name, + const QString &_text, + bool _isToken, + QVariantHash _properties, + const QList &_relatedCards, + const QList &_reverseRelatedCards, + SetToPrintingsMap _sets, + const UiAttributes _uiAttributes) + : name(_name), text(_text), isToken(_isToken), properties(std::move(_properties)), relatedCards(_relatedCards), + reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), uiAttributes(_uiAttributes) +{ + simpleName = CardInfo::simplifyName(name); + + refreshCachedSets(); +} + +CardInfoPtr CardInfo::newInstance(const QString &_name) +{ + return newInstance(_name, "", false, {}, {}, {}, {}, {}); +} + +CardInfoPtr CardInfo::newInstance(const QString &_name, + const QString &_text, + bool _isToken, + QVariantHash _properties, + const QList &_relatedCards, + const QList &_reverseRelatedCards, + SetToPrintingsMap _sets, + const UiAttributes _uiAttributes) +{ + CardInfoPtr ptr(new CardInfo(_name, _text, _isToken, std::move(_properties), _relatedCards, _reverseRelatedCards, + _sets, _uiAttributes)); + ptr->setSmartPointer(ptr); + + for (const auto &printings : _sets) { + for (const PrintingInfo &printing : printings) { + printing.getSet()->append(ptr); + break; + } + } + + return ptr; +} + +QString CardInfo::getCorrectedName() const +{ + // remove all the characters reserved in windows file paths, + // other oses only disallow a subset of these so it covers all + static const QRegularExpression rmrx(R"(( // |[*<>:"\\?\x00-\x08\x10-\x1f]))"); + static const QRegularExpression spacerx(R"([/\x09-\x0f])"); + static const QString space(' '); + QString result = name; + // Fire // Ice, Circle of Protection: Red, "Ach! Hans, Run!", Who/What/When/Where/Why, Question Elemental? + return result.remove(rmrx).replace(spacerx, space); +} + +QString CardInfo::getLegalityProp(const QString &format) const +{ + return getProperty("format-" + format); +} + +bool CardInfo::isLegalInFormat(const QString &format) const +{ + if (format.isEmpty()) { + return true; + } + + QString formatLegality = getLegalityProp(format); + return formatLegality == "legal" || formatLegality == "restricted"; +} + +void CardInfo::addToSet(const CardSetPtr &_set, const PrintingInfo &_info) +{ + if (!_set->contains(smartThis)) { + _set->append(smartThis); + } + if (!setsToPrintings[_set->getShortName()].contains(_info)) { + setsToPrintings[_set->getShortName()].append(_info); + } + + refreshCachedSets(); +} + +void CardInfo::combineLegalities(const QVariantHash &props) +{ + QHashIterator it(props); + while (it.hasNext()) { + it.next(); + if (it.key().startsWith("format-")) { + smartThis->setProperty(it.key(), it.value().toString()); + } + } +} + +void CardInfo::refreshCachedSets() +{ + refreshCachedSetNames(); + refreshCachedAltNames(); +} + +void CardInfo::refreshCachedSetNames() +{ + QStringList setList; + // update the cached list of set names + for (const auto &printings : setsToPrintings) { + for (const auto &printing : printings) { + if (printing.getSet()->getEnabled()) { + setList << printing.getSet()->getShortName(); + } + break; + } + } + setsNames = setList.join(", "); +} + +void CardInfo::refreshCachedAltNames() +{ + altNames.clear(); + + // update the altNames with the flavorNames + for (const auto &printings : setsToPrintings) { + for (const auto &printing : printings) { + QString flavorName = printing.getFlavorName(); + if (!flavorName.isEmpty()) { + altNames.insert(flavorName); + } + } + } +} + +QString CardInfo::simplifyName(const QString &name) +{ + static const QRegularExpression spaceOrSplit("(\\s+|\\/\\/.*)"); + static const QRegularExpression nonAlnum("[^a-z0-9]"); + + QString simpleName = name.toLower(); + + // remove spaces and right halves of split cards + simpleName.remove(spaceOrSplit); + + // So Aetherling would work, but not Ætherling since 'Æ' would get replaced + // with nothing. + simpleName.replace("æ", "ae"); + + // Replace Jötun Grunt with Jotun Grunt. + simpleName = simpleName.normalized(QString::NormalizationForm_KD); + + // remove all non alphanumeric characters from the name + simpleName.remove(nonAlnum); + return simpleName; +} + +QChar CardInfo::getColorChar() const +{ + QString colors = getColors(); + switch (colors.size()) { + case 0: + return QChar(); + case 1: + return colors.at(0); + default: + return QChar('m'); + } +} + +void CardInfo::resetReverseRelatedCards2Me() +{ + for (CardRelation *cardRelation : this->getReverseRelatedCards2Me()) { + cardRelation->deleteLater(); + } + reverseRelatedCardsToMe = QList(); +} + +// Back-compatibility methods. Remove ASAP +QString CardInfo::getCardType() const +{ + return getProperty(Mtg::CardType); +} +void CardInfo::setCardType(const QString &value) +{ + setProperty(Mtg::CardType, value); +} +QString CardInfo::getCmc() const +{ + return getProperty(Mtg::ConvertedManaCost); +} +QString CardInfo::getColors() const +{ + return getProperty(Mtg::Colors); +} +void CardInfo::setColors(const QString &value) +{ + setProperty(Mtg::Colors, value); +} +QString CardInfo::getLoyalty() const +{ + return getProperty(Mtg::Loyalty); +} +QString CardInfo::getMainCardType() const +{ + return getProperty(Mtg::MainCardType); +} +QString CardInfo::getManaCost() const +{ + return getProperty(Mtg::ManaCost); +} +QString CardInfo::getPowTough() const +{ + return getProperty(Mtg::PowTough); +} +void CardInfo::setPowTough(const QString &value) +{ + setProperty(Mtg::PowTough, value); +} diff --git a/libcockatrice_card/libcockatrice/card/card_info.h b/libcockatrice_card/libcockatrice/card/card_info.h new file mode 100644 index 000000000..654ac1f63 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/card_info.h @@ -0,0 +1,377 @@ +#ifndef CARD_INFO_H +#define CARD_INFO_H + +#include "format/format_legality_rules.h" +#include "printing/printing_info.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +inline Q_LOGGING_CATEGORY(CardInfoLog, "card_info"); + +class CardInfo; +class CardSet; +class CardRelation; +class ICardDatabaseParser; + +typedef QSharedPointer CardInfoPtr; +typedef QSharedPointer CardSetPtr; +typedef QSharedPointer FormatRulesPtr; +typedef QMap> SetToPrintingsMap; + +typedef QHash CardNameMap; +typedef QHash SetNameMap; +typedef QHash FormatRulesNameMap; + +Q_DECLARE_METATYPE(CardInfoPtr) + +/** + * @class CardInfo + * @ingroup Cards + * + * @brief Represents a card and its associated metadata, properties, and relationships. + * + * CardInfo holds both static information (name, text, flags) and dynamic data + * (properties, set memberships, relationships). It also integrates with + * signals/slots, allowing observers to react to property or visual updates. + * + * Each CardInfo may belong to multiple sets through its printings, and can + * be related to other cards through defined relationships. + */ +class CardInfo : public QObject +{ + Q_OBJECT + +public: + /** + * @class CardInfo::UiAttributes + * @ingroup Cards + * + * @brief Attributes of the card that affect display and game logic. + */ + struct UiAttributes + { + bool cipt = false; ///< Positioning flag used by UI. + bool landscapeOrientation = false; ///< Orientation flag for rendering. + int tableRow = 0; ///< Row index in a table or visual representation. + bool upsideDownArt = false; ///< Whether artwork is flipped for visual purposes. + }; + +private: + /** @name Private Card Properties + * @anchor PrivateCardProperties + */ + ///@{ + CardInfoPtr smartThis; ///< Smart pointer to self for safe cross-references. + QString name; ///< Full name of the card. + QString simpleName; ///< Simplified name for fuzzy matching. + QString text; ///< Text description or rules text of the card. + bool isToken; ///< Whether this card is a token or not. + QVariantHash properties; ///< Key-value store of dynamic card properties. + QList relatedCards; ///< Forward references to related cards. + QList reverseRelatedCards; ///< Cards that refer back to this card. + QList reverseRelatedCardsToMe; ///< Cards that consider this card as related. + SetToPrintingsMap setsToPrintings; ///< Mapping from set names to printing variations. + UiAttributes uiAttributes; ///< Attributes that affect display and game logic + QString setsNames; ///< Cached, human-readable list of set names. + QSet altNames; ///< Cached set of alternate names, used when searching + ///@} + +public: + /** + * @brief Constructs a CardInfo with full initialization. + * + * @param _name The name of the card. + * @param _text Rules text or description of the card. + * @param _isToken Flag indicating whether the card is a token. + * @param _properties Arbitrary key-value properties. + * @param _relatedCards Forward references to related cards. + * @param _reverseRelatedCards Backward references to related cards. + * @param _sets Map of set names to printing information. + * @param _uiAttributes Attributes that affect display and game logic + */ + explicit CardInfo(const QString &_name, + const QString &_text, + bool _isToken, + QVariantHash _properties, + const QList &_relatedCards, + const QList &_reverseRelatedCards, + SetToPrintingsMap _sets, + UiAttributes _uiAttributes); + + /** + * @brief Copy constructor for CardInfo. + * + * Performs a deep copy of properties, sets, and related card lists. + * + * @param other Another CardInfo to copy. + */ + CardInfo(const CardInfo &other) + : QObject(other.parent()), name(other.name), simpleName(other.simpleName), text(other.text), + isToken(other.isToken), properties(other.properties), relatedCards(other.relatedCards), + reverseRelatedCards(other.reverseRelatedCards), reverseRelatedCardsToMe(other.reverseRelatedCardsToMe), + setsToPrintings(other.setsToPrintings), uiAttributes(other.uiAttributes), setsNames(other.setsNames), + altNames(other.altNames) + { + } + + /** + * @brief Creates a new instance with only the card name. + * + * All other fields are set to defaults. + * + * @param _name The card name. + * @return Shared pointer to the new CardInfo instance. + */ + static CardInfoPtr newInstance(const QString &_name); + + /** + * @brief Creates a new instance with full initialization. + * + * @param _name Name of the card. + * @param _text Rules text or description. + * @param _isToken Token flag. + * @param _properties Arbitrary properties. + * @param _relatedCards Forward relationships. + * @param _reverseRelatedCards Reverse relationships. + * @param _sets Printing information per set. + * @param _uiAttributes Attributes that affect display and game logic + * @return Shared pointer to the new CardInfo instance. + */ + static CardInfoPtr newInstance(const QString &_name, + const QString &_text, + bool _isToken, + QVariantHash _properties, + const QList &_relatedCards, + const QList &_reverseRelatedCards, + SetToPrintingsMap _sets, + UiAttributes _uiAttributes); + + /** + * @brief Clones the current CardInfo instance. + * + * Uses the copy constructor and ensures the smart pointer is properly set. + * + * @return Shared pointer to the cloned CardInfo. + */ + [[nodiscard]] CardInfoPtr clone() const + { + auto newCardInfo = CardInfoPtr(new CardInfo(*this)); + newCardInfo->setSmartPointer(newCardInfo); // Set the smart pointer for the new instance + return newCardInfo; + } + + /** + * @brief Sets the internal smart pointer to self. + * + * Used internally to allow safe cross-references among CardInfo and CardSet. + * + * @param _ptr Shared pointer pointing to this instance. + */ + void setSmartPointer(CardInfoPtr _ptr) + { + smartThis = std::move(_ptr); + } + + /** @name Basic Properties Accessors */ //@{ + [[nodiscard]] inline const QString &getName() const + { + return name; + } + [[nodiscard]] const QString &getSimpleName() const + { + return simpleName; + } + const QSet &getAltNames() + { + return altNames; + } + [[nodiscard]] const QString &getText() const + { + return text; + } + void setText(const QString &_text) + { + text = _text; + emit cardInfoChanged(smartThis); + } + [[nodiscard]] bool getIsToken() const + { + return isToken; + } + [[nodiscard]] QStringList getProperties() const + { + return properties.keys(); + } + [[nodiscard]] QString getProperty(const QString &propertyName) const + { + return properties.value(propertyName).toString(); + } + void setProperty(const QString &_name, const QString &_value) + { + properties.insert(_name, _value); + emit cardInfoChanged(smartThis); + } + [[nodiscard]] bool hasProperty(const QString &propertyName) const + { + return properties.contains(propertyName); + } + [[nodiscard]] const SetToPrintingsMap &getSets() const + { + return setsToPrintings; + } + [[nodiscard]] const QString &getSetsNames() const + { + return setsNames; + } + //@} + + /** @name Related Cards Accessors */ //@{ + [[nodiscard]] const QList &getRelatedCards() const + { + return relatedCards; + } + [[nodiscard]] const QList &getReverseRelatedCards() const + { + return reverseRelatedCards; + } + [[nodiscard]] const QList &getReverseRelatedCards2Me() const + { + return reverseRelatedCardsToMe; + } + [[nodiscard]] QList getAllRelatedCards() const + { + QList result; + result.append(getRelatedCards()); + result.append(getReverseRelatedCards2Me()); + return result; + } + void resetReverseRelatedCards2Me(); + void addReverseRelatedCards2Me(CardRelation *cardRelation) + { + reverseRelatedCardsToMe.append(cardRelation); + } + //@} + + /** @name UI Positioning */ //@{ + [[nodiscard]] const UiAttributes &getUiAttributes() const + { + return uiAttributes; + } + //@} + + [[nodiscard]] QChar getColorChar() const; + + /** @name Legacy/Convenience Property Accessors */ //@{ + [[nodiscard]] QString getCardType() const; + void setCardType(const QString &value); + [[nodiscard]] QString getCmc() const; + [[nodiscard]] QString getColors() const; + void setColors(const QString &value); + [[nodiscard]] QString getLoyalty() const; + [[nodiscard]] QString getMainCardType() const; + [[nodiscard]] QString getManaCost() const; + [[nodiscard]] QString getPowTough() const; + void setPowTough(const QString &value); + //@} + + /** + * @brief Returns a version of the card name safe for file storage or fuzzy matching. + * + * Removes invalid characters, replaces spacing markers, and normalizes diacritics. + * + * @return Corrected card name as a QString. + */ + [[nodiscard]] QString getCorrectedName() const; + + /** + * @brief Gets the card's legality value for the given format. + * The legality prop for a format is stored in the property map under the key "format-" + * @param format The format's name. + * @return The card's legality value for the format. Empty if not found. + */ + [[nodiscard]] QString getLegalityProp(const QString &format) const; + + /** + * @brief Checks if the card is legal in the given format. + * A card is considered legal in a format if its properties map contains an entry for "format-", with value + * "legal" or "restricted". + * @param format The format's name. If empty, will always return true. + * @return Whether the card is legal in the given format. + */ + [[nodiscard]] bool isLegalInFormat(const QString &format) const; + + /** + * @brief Adds a printing to a specific set. + * + * Updates the mapping and refreshes the cached list of set names. + * + * @param _set The set to which the card should be added. + * @param _info Optional printing information. + */ + void addToSet(const CardSetPtr &_set, const PrintingInfo &_info = PrintingInfo()); + + /** + * @brief Combines legality properties from a provided map. + * + * Useful for merging format legality flags from multiple sources. + * + * @param props Key-value mapping of format legalities. + */ + void combineLegalities(const QVariantHash &props); + + /** + * @brief Refreshes all cached fields that are calculated from the contained sets and printings. + * + * Typically called after adding or modifying set memberships or printings. + */ + void refreshCachedSets(); + + /** + * @brief Simplifies a name for fuzzy matching. + * + * Converts to lowercase, removes punctuation/spacing. + * + * @param name Original name string. + * @return Simplified name string. + */ + static QString simplifyName(const QString &name); + +private: + /** + * @brief Refreshes the cached, human-readable list of set names. + * + * Typically called after adding or modifying set memberships. + */ + void refreshCachedSetNames(); + + /** + * @brief Refreshes the cached list of alt names for the card. + * + * Typically called after adding or modifying the contained printings. + */ + void refreshCachedAltNames(); + +signals: + /** + * @brief Emitted when a pixmap for this card has been updated or finished loading. + * + * @param printing Specific printing for which the pixmap has updated. + */ + void pixmapUpdated(const PrintingInfo &printing); + + /** + * @brief Emitted when card properties or state have changed. + * + * @param card Shared pointer to the CardInfo instance that changed. + */ + void cardInfoChanged(CardInfoPtr card); +}; +#endif diff --git a/libcockatrice_card/libcockatrice/card/card_info_comparator.cpp b/libcockatrice_card/libcockatrice/card/card_info_comparator.cpp new file mode 100644 index 000000000..821cc8675 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/card_info_comparator.cpp @@ -0,0 +1,75 @@ +#include "card_info_comparator.h" + +CardInfoComparator::CardInfoComparator(const QStringList &properties, Qt::SortOrder order) + : m_properties(properties), m_order(order) +{ +} + +bool CardInfoComparator::operator()(const CardInfoPtr &a, const CardInfoPtr &b) const +{ + // Iterate over each property in the list + for (const QString &property : m_properties) { + QVariant valueA = getProperty(a, property); + QVariant valueB = getProperty(b, property); + + // Compare the current property + if (valueA != valueB) { + // If values differ, perform comparison + return compareVariants(valueA, valueB) ? (m_order == Qt::AscendingOrder) : (m_order == Qt::DescendingOrder); + } + } + + // If all properties are equal, return false (indicating they are considered equal for sorting purposes) + return false; +} + +bool CardInfoComparator::compareVariants(const QVariant &a, const QVariant &b) const +{ + // Determine the type of QVariant based on Qt version +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + if (a.typeId() != b.typeId()) { +#else + if (a.type() != b.type()) { +#endif + // If they are not the same type, compare as strings + return a.toString() < b.toString(); + } + + // Perform type-specific comparison +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + switch (static_cast(a.typeId())) { +#else + switch (static_cast(a.type())) { +#endif + case static_cast(QMetaType::Int): + return a.toInt() < b.toInt(); + case static_cast(QMetaType::Double): + return a.toDouble() < b.toDouble(); + case static_cast(QMetaType::QString): + return a.toString() < b.toString(); + case static_cast(QMetaType::Bool): + return a.toBool() < b.toBool(); + default: + // Default to comparing as strings + return a.toString() < b.toString(); + } +} + +QVariant CardInfoComparator::getProperty(const CardInfoPtr &card, const QString &property) const +{ + // Check if the property exists in the main fields of the class + if (property == "name") { + return card->getName(); + } else if (property == "text") { + return card->getText(); + } else if (property == "isToken") { + return card->getIsToken(); + } + + // Otherwise, check if it's a custom property in the QVariantHash + if (card->hasProperty(property)) { + return card->getProperty(property); + } + + return QVariant(); // Return an invalid variant if the property does not exist +} diff --git a/libcockatrice_card/libcockatrice/card/card_info_comparator.h b/libcockatrice_card/libcockatrice/card/card_info_comparator.h new file mode 100644 index 000000000..b8d9c47e6 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/card_info_comparator.h @@ -0,0 +1,29 @@ +/** + * @file card_info_comparator.h + * @ingroup Cards + * @brief TODO: Document this. + */ + +#ifndef CARD_INFO_COMPARATOR_H +#define CARD_INFO_COMPARATOR_H + +#include "card_info.h" + +#include +#include + +class CardInfoComparator +{ +public: + explicit CardInfoComparator(const QStringList &properties, Qt::SortOrder order = Qt::AscendingOrder); + bool operator()(const CardInfoPtr &a, const CardInfoPtr &b) const; + +private: + QStringList m_properties; // List of properties to sort by + Qt::SortOrder m_order; + + [[nodiscard]] QVariant getProperty(const CardInfoPtr &card, const QString &property) const; + [[nodiscard]] bool compareVariants(const QVariant &a, const QVariant &b) const; +}; + +#endif // CARD_INFO_COMPARATOR_H diff --git a/libcockatrice_card/libcockatrice/card/database/card_database.cpp b/libcockatrice_card/libcockatrice/card/database/card_database.cpp new file mode 100644 index 000000000..5c4b408b3 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/card_database.cpp @@ -0,0 +1,206 @@ +#include "card_database.h" + +#include "../relation/card_relation.h" +#include "parser/cockatrice_xml_4.h" + +#include +#include +#include +#include +#include +#include +#include + +CardDatabase::CardDatabase(QObject *parent, + ICardPreferenceProvider *prefs, + ICardDatabasePathProvider *pathProvider, + ICardSetPriorityController *_setPriorityController) + : QObject(parent), setPriorityController(_setPriorityController), loadStatus(NotLoaded) +{ + qRegisterMetaType("CardInfoPtr"); + qRegisterMetaType("CardSetPtr"); + + // create loader and wire it up + loader = new CardDatabaseLoader(this, this, pathProvider, prefs, setPriorityController); + // re-emit loader signals (so other code doesn't need to know about internals) + connect(loader, &CardDatabaseLoader::loadingFinished, this, &CardDatabase::cardDatabaseLoadingFinished); + connect(loader, &CardDatabaseLoader::loadingFailed, this, &CardDatabase::cardDatabaseLoadingFailed); + connect(loader, &CardDatabaseLoader::newSetsFound, this, &CardDatabase::cardDatabaseNewSetsFound); + connect(loader, &CardDatabaseLoader::allNewSetsEnabled, this, &CardDatabase::cardDatabaseAllNewSetsEnabled); + + querier = new CardDatabaseQuerier(this, this, prefs); +} + +CardDatabase::~CardDatabase() +{ + clear(); +} + +void CardDatabase::clear() +{ + QMutexLocker locker(clearDatabaseMutex); + + for (const auto &card : cards.values()) { + if (card) { + removeCard(card); + } + } + + cards.clear(); + simpleNameCards.clear(); + + sets.clear(); + ICardDatabaseParser::clearSetlist(); + + loadStatus = NotLoaded; +} + +void CardDatabase::loadCardDatabases() +{ + loadStatus = loader->loadCardDatabases(); +} + +bool CardDatabase::saveCustomTokensToFile() +{ + return loader->saveCustomTokensToFile(); +} + +void CardDatabase::refreshCachedReverseRelatedCards() +{ + for (const auto &card : cards) { + card->resetReverseRelatedCards2Me(); + } + + for (const auto &card : cards) { + for (auto *rel : card->getReverseRelatedCards()) { + if (auto target = cards.value(rel->getName())) { + auto *newRel = new CardRelation(card->getName(), rel->getAttachType(), rel->getIsCreateAllExclusion(), + rel->getIsVariable(), rel->getDefaultCount(), rel->getIsPersistent()); + target->addReverseRelatedCards2Me(newRel); + } + } + } +} + +void CardDatabase::addCard(CardInfoPtr card) +{ + if (card == nullptr) { + qCWarning(CardDatabaseLog) << "CardDatabase::addCard(nullptr)"; + return; + } + + auto name = card->getName(); + + // If a card already exists, just add the new set property. + if (auto existing = cards.value(name)) { + for (const auto &printings : card->getSets()) + for (const auto &printing : printings) + existing->addToSet(printing.getSet(), printing); + return; + } + + QMutexLocker locker(addCardMutex); + cards.insert(name, card); + simpleNameCards.insert(card->getSimpleName(), card); + + emit cardAdded(card); +} + +void CardDatabase::removeCard(CardInfoPtr card) +{ + if (card.isNull()) { + qCWarning(CardDatabaseLog) << "CardDatabase::removeCard(nullptr)"; + return; + } + + for (auto *cardRelation : card->getRelatedCards()) + cardRelation->deleteLater(); + + for (auto *cardRelation : card->getReverseRelatedCards()) + cardRelation->deleteLater(); + + for (auto *cardRelation : card->getReverseRelatedCards2Me()) + cardRelation->deleteLater(); + + QMutexLocker locker(removeCardMutex); + cards.remove(card->getName()); + simpleNameCards.remove(card->getSimpleName()); + emit cardRemoved(card); +} + +void CardDatabase::addSet(CardSetPtr set) +{ + sets.insert(set->getShortName(), set); +} + +CardSetPtr CardDatabase::getSet(const QString &setName) +{ + if (sets.contains(setName)) { + return sets.value(setName); + } else { + CardSetPtr newSet = CardSet::newInstance(setPriorityController, setName); + sets.insert(setName, newSet); + return newSet; + } +} + +CardSetList CardDatabase::getSetList() const +{ + CardSetList result; + for (auto set : sets.values()) { + result << set; + } + return result; +} + +void CardDatabase::checkUnknownSets() +{ + auto _sets = getSetList(); + + if (_sets.getEnabledSetsNum()) { + // if some sets are first found on this run, ask the user + int numUnknownSets = _sets.getUnknownSetsNum(); + QStringList unknownSetNames = _sets.getUnknownSetsNames(); + if (numUnknownSets > 0) { + emit cardDatabaseNewSetsFound(numUnknownSets, unknownSetNames); + } else { + _sets.markAllAsKnown(); + } + } else { + // No set enabled. Probably this is the first time running trice + _sets.guessSortKeys(); + _sets.sortByKey(); + _sets.enableAll(); + notifyEnabledSetsChanged(); + + emit cardDatabaseAllNewSetsEnabled(); + } +} + +void CardDatabase::enableAllUnknownSets() +{ + auto _sets = getSetList(); + _sets.enableAllUnknown(); +} + +void CardDatabase::markAllSetsAsKnown() +{ + auto _sets = getSetList(); + _sets.markAllAsKnown(); +} + +void CardDatabase::notifyEnabledSetsChanged() +{ + // refresh the list of cached set names + for (const CardInfoPtr &card : cards) { + card->refreshCachedSets(); + } + + // inform the carddatabasemodels that they need to re-check their list of cards + emit cardDatabaseEnabledSetsChanged(); +} + +void CardDatabase::addFormat(FormatRulesPtr format) +{ + formats.insert(format->formatName.toLower(), format); +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/database/card_database.h b/libcockatrice_card/libcockatrice/card/database/card_database.h new file mode 100644 index 000000000..7f8fc39db --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/card_database.h @@ -0,0 +1,186 @@ +#ifndef CARDDATABASE_H +#define CARDDATABASE_H + +#include "../set/card_set_list.h" +#include "card_database_loader.h" +#include "card_database_querier.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +inline Q_LOGGING_CATEGORY(CardDatabaseLog, "card_database"); + +/** + * @class CardDatabase + * @ingroup CardDatabase + * @brief Core in-memory container for card and set data. + * + * Responsible for maintaining CardInfo objects, CardSet objects, and + * providing access to CardDatabaseQuerier for query operations. + * Handles addition, removal, and clearing of cards and sets. + */ +class CardDatabase : public QObject +{ + Q_OBJECT + +protected: + /// Controller to determine set priority when choosing preferred printings. + ICardSetPriorityController *setPriorityController; + + /// Cards indexed by exact name + CardNameMap cards; + + /// Cards indexed by simplified name (normalized) + CardNameMap simpleNameCards; + + /// Sets indexed by short name + SetNameMap sets; + + FormatRulesNameMap formats; + + /// Loader responsible for file discovery and parsing + CardDatabaseLoader *loader; + + /// Current load status of the database + LoadStatus loadStatus; + + /// Querier for higher-level card lookups + CardDatabaseQuerier *querier; + +private: + /** + * @brief Check for sets that are unknown and emit signals if needed. + */ + void checkUnknownSets(); + + /** + * @brief Refreshes the cached reverse-related cards for all cards. + */ + void refreshCachedReverseRelatedCards(); + + /// Mutexes for thread safety + QBasicMutex *clearDatabaseMutex = new QBasicMutex(), *addCardMutex = new QBasicMutex(), + *removeCardMutex = new QBasicMutex(); + +public: + /** + * @brief Constructs a new CardDatabase instance. + * @param parent QObject parent. + * @param prefs Optional card preference provider. + * @param pathProvider Optional database path provider. + * @param setPriorityController Optional controller for set priority. + */ + explicit CardDatabase(QObject *parent = nullptr, + ICardPreferenceProvider *prefs = nullptr, + ICardDatabasePathProvider *pathProvider = nullptr, + ICardSetPriorityController *setPriorityController = nullptr); + + /** @brief Destructor clears all internal data. */ + ~CardDatabase() override; + + /** + * @brief Removes a card from the database. + * @param card Pointer to the card to remove. + */ + void removeCard(CardInfoPtr card); + + /** @brief Clears all cards, sets, and internal state. */ + void clear(); + + /** @brief Returns the map of cards by name. */ + [[nodiscard]] const CardNameMap &getCardList() const + { + return cards; + } + + /** + * @brief Retrieves a set by short name, creating a new one if missing. + * @param setName Short name of the set. + * @return Pointer to the CardSet. + */ + CardSetPtr getSet(const QString &setName); + + /** @brief Returns a list of all sets in the database. */ + [[nodiscard]] CardSetList getSetList() const; + + /** @brief Returns the current load status. */ + [[nodiscard]] LoadStatus getLoadStatus() const + { + return loadStatus; + } + + /** @brief Returns the querier for performing card lookups. */ + [[nodiscard]] CardDatabaseQuerier *query() const + { + return querier; + } + + /** @brief Enables all unknown sets in the database. */ + void enableAllUnknownSets(); + + /** @brief Marks all sets as known. */ + void markAllSetsAsKnown(); + + /** @brief Notifies listeners that enabled sets changed. */ + void notifyEnabledSetsChanged(); + +public slots: + /** + * @brief Adds a card to the database. + * @param card CardInfoPtr to add. + */ + void addCard(CardInfoPtr card); + + /** + * @brief Adds a set to the database. + * @param set Pointer to CardSet to add. + */ + void addSet(CardSetPtr set); + + void addFormat(FormatRulesPtr format); + + /** @brief Loads card databases from configured paths. */ + void loadCardDatabases(); + + /** @brief Saves custom tokens to file. + * @return True if successful. + */ + bool saveCustomTokensToFile(); + +signals: + /** @brief Emitted when the card database has finished loading successfully. */ + void cardDatabaseLoadingFinished(); + + /** @brief Emitted when the card database fails to load. */ + void cardDatabaseLoadingFailed(); + + /** + * @brief Emitted when new sets are found. + * @param numUnknownSets Number of unknown sets. + * @param unknownSetsNames Names of unknown sets. + */ + void cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames); + + /** @brief Emitted when all new sets have been enabled. */ + void cardDatabaseAllNewSetsEnabled(); + + /** @brief Emitted when enabled sets have changed. */ + void cardDatabaseEnabledSetsChanged(); + + /** @brief Emitted when a new card is added. */ + void cardAdded(CardInfoPtr card); + + /** @brief Emitted when a card is removed. */ + void cardRemoved(CardInfoPtr card); + + friend class CardDatabaseLoader; + friend class CardDatabaseQuerier; +}; + +#endif // CARDDATABASE_H diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_loader.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_loader.cpp new file mode 100644 index 000000000..716477a59 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/card_database_loader.cpp @@ -0,0 +1,156 @@ +#include "card_database_loader.h" + +#include "card_database.h" +#include "parser/cockatrice_xml_3.h" +#include "parser/cockatrice_xml_4.h" + +#include +#include +#include +#include + +CardDatabaseLoader::CardDatabaseLoader(QObject *parent, + CardDatabase *db, + ICardDatabasePathProvider *_pathProvider, + ICardPreferenceProvider *_preferenceProvider, + ICardSetPriorityController *_priorityController) + : QObject(parent), database(db), pathProvider(_pathProvider) +{ + // instantiate available parsers here and connect them to the database + availableParsers << new CockatriceXml4Parser(_preferenceProvider, _priorityController); + availableParsers << new CockatriceXml3Parser(_priorityController); + + for (auto *p : availableParsers) { + // connect parser outputs to the database adders + connect(p, &ICardDatabaseParser::addCard, database, &CardDatabase::addCard, Qt::DirectConnection); + connect(p, &ICardDatabaseParser::addSet, database, &CardDatabase::addSet, Qt::DirectConnection); + connect(p, &ICardDatabaseParser::addFormat, database, &CardDatabase::addFormat, Qt::DirectConnection); + } + + // when SettingsCache's path changes, trigger reloads + connect(pathProvider, &ICardDatabasePathProvider::cardDatabasePathChanged, this, + &CardDatabaseLoader::loadCardDatabases); +} + +CardDatabaseLoader::~CardDatabaseLoader() +{ + qDeleteAll(availableParsers); + availableParsers.clear(); +} + +LoadStatus CardDatabaseLoader::loadFromFile(const QString &fileName) +{ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + return FileError; + } + + for (auto parser : availableParsers) { + file.reset(); + if (parser->getCanParseFile(fileName, file)) { + file.reset(); + parser->parseFile(file); + return Ok; + } + } + + return Invalid; +} + +LoadStatus CardDatabaseLoader::loadCardDatabase(const QString &path) +{ + auto startTime = QTime::currentTime(); + LoadStatus tempLoadStatus = NotLoaded; + if (!path.isEmpty()) { + QMutexLocker locker(loadFromFileMutex); + tempLoadStatus = loadFromFile(path); + } + + int msecs = startTime.msecsTo(QTime::currentTime()); + qCInfo(CardDatabaseLoadingLog) << "Loaded card database: Path =" << path << "Status =" << tempLoadStatus + << "Cards =" << (database ? database->cards.size() : 0) + << "Sets =" << (database ? database->sets.size() : 0) << QString("%1ms").arg(msecs); + + return tempLoadStatus; +} + +LoadStatus CardDatabaseLoader::loadCardDatabases() +{ + QMutexLocker locker(reloadDatabaseMutex); + + if (!database) { + qCWarning(CardDatabaseLoadingLog) << "Loader has no database pointer"; + emit loadingFailed(); + return FileError; + } + emit loadingStarted(); + qCInfo(CardDatabaseLoadingLog) << "Card Database Loading Started"; + + database->clear(); // remove old db + + LoadStatus loadStatus = loadCardDatabase(pathProvider->getCardDatabasePath()); // load main card database + loadCardDatabase(pathProvider->getTokenDatabasePath()); // load tokens database + loadCardDatabase(pathProvider->getSpoilerCardDatabasePath()); // load spoilers database + + // find all custom card databases, recursively & following symlinks + // then load them alphabetically + const QStringList customPaths = collectCustomDatabasePaths(); + for (int i = 0; i < customPaths.size(); ++i) { + const auto &p = customPaths.at(i); + qCInfo(CardDatabaseLoadingLog) << "Loading Custom Set" << i << "(" << p << ")"; + loadCardDatabase(p); + } + + // AFTER all the cards have been loaded + + // resolve the reverse-related tags + + database->refreshCachedReverseRelatedCards(); + + if (loadStatus == Ok) { + database->checkUnknownSets(); // update deck editors, etc + qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Success"; + emit loadingFinished(); + } else { + qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Failed"; + emit loadingFailed(); // bring up the settings dialog + } + + return loadStatus; +} + +QStringList CardDatabaseLoader::collectCustomDatabasePaths() const +{ + QDirIterator it(pathProvider->getCustomCardDatabasePath(), {"*.xml"}, QDir::Files, + QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); + + QStringList paths; + while (it.hasNext()) + paths << it.next(); + paths.sort(); + return paths; +} + +bool CardDatabaseLoader::saveCustomTokensToFile() +{ + if (!database) { + qCWarning(CardDatabaseLog) << "saveCustomTokensToFile: database pointer missing"; + return false; + } + + QString fileName = pathProvider->getCustomCardDatabasePath() + "/" + CardSet::TOKENS_SETNAME + ".xml"; + + SetNameMap tmpSets; + CardSetPtr customTokensSet = database->getSet(CardSet::TOKENS_SETNAME); + tmpSets.insert(CardSet::TOKENS_SETNAME, customTokensSet); + + CardNameMap tmpCards; + for (const CardInfoPtr &card : database->cards) { + if (card->getSets().contains(CardSet::TOKENS_SETNAME)) { + tmpCards.insert(card->getName(), card); + } + } + + availableParsers.first()->saveToFile(FormatRulesNameMap(), tmpSets, tmpCards, fileName); + return true; +} diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_loader.h b/libcockatrice_card/libcockatrice/card/database/card_database_loader.h new file mode 100644 index 000000000..861cc95b0 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/card_database_loader.h @@ -0,0 +1,125 @@ +#ifndef COCKATRICE_CARD_DATABASE_LOADER_H +#define COCKATRICE_CARD_DATABASE_LOADER_H + +#include +#include +#include +#include +#include +#include + +inline Q_LOGGING_CATEGORY(CardDatabaseLoadingLog, "card_database.loading"); +inline Q_LOGGING_CATEGORY(CardDatabaseLoadingSuccessOrFailureLog, "card_database.loading.success_or_failure"); + +class CardDatabase; +class ICardDatabaseParser; + +/** + * @enum LoadStatus + * @brief Represents the result of attempting to load a card database. + */ +enum LoadStatus +{ + Ok, /**< Database loaded successfully. */ + VersionTooOld, /**< Database version is too old to load. */ + Invalid, /**< Database is invalid or unparsable. */ + NotLoaded, /**< Database has not been loaded. */ + FileError, /**< Error opening or reading the file. */ + NoCards /**< Database contains no cards. */ +}; + +/** + * @class CardDatabaseLoader + * @ingroup CardDatabase + * @brief Handles loading card databases from disk and saving custom tokens. + * + * This class is responsible for: + * - Discovering configured card database paths. + * - Loading main, token, spoiler, and custom databases. + * - Populating a CardDatabase instance using connected parsers. + * - Emitting signals about loading progress and new sets. + */ +class CardDatabaseLoader : public QObject +{ + Q_OBJECT +public: + /** + * @brief Constructs a CardDatabaseLoader. + * @param parent QObject parent. + * @param db Pointer to the CardDatabase to populate (non-owning). + * @param pathProvider Provider for card database file paths. + * @param preferenceProvider Optional card preference provider for pinned printings. + */ + explicit CardDatabaseLoader(QObject *parent, + CardDatabase *db, + ICardDatabasePathProvider *pathProvider, + ICardPreferenceProvider *preferenceProvider, + ICardSetPriorityController *_priorityController); + + /** @brief Destructor cleans up allocated parsers. */ + ~CardDatabaseLoader() override; + +public slots: + /** + * @brief Loads all configured card databases. + * @return Status of the main database load. + */ + LoadStatus loadCardDatabases(); + + /** + * @brief Loads a single card database file. + * @param path Path to the database file. + * @return LoadStatus indicating success or failure. + */ + LoadStatus loadCardDatabase(const QString &path); + + /** + * @brief Saves custom tokens to the user-defined custom database path. + * @return True if the save was successful. + */ + bool saveCustomTokensToFile(); + +signals: + /** @brief Emitted when loading starts. */ + void loadingStarted(); + + /** @brief Emitted when loading finishes successfully. */ + void loadingFinished(); + + /** @brief Emitted when loading fails. */ + void loadingFailed(); + + /** + * @brief Emitted when new sets are discovered during loading. + * @param numSets Number of new sets. + * @param setNames Names of the discovered sets. + */ + void newSetsFound(int numSets, const QStringList &setNames); + + /** @brief Emitted when all newly discovered sets have been enabled. */ + void allNewSetsEnabled(); + +private: + /** + * @brief Loads a database from a single file using the available parsers. + * @param fileName Path to the database file. + * @return LoadStatus indicating success or failure. + */ + LoadStatus loadFromFile(const QString &fileName); + + /** + * @brief Collects custom card database paths recursively. + * @return Sorted list of file paths to custom databases. + */ + [[nodiscard]] QStringList collectCustomDatabasePaths() const; + +private: + CardDatabase *database; /**< Non-owning pointer to the target CardDatabase. */ + ICardDatabasePathProvider *pathProvider; /**< Pointer to the path provider. */ + QList availableParsers; /**< List of available parsers for different formats. */ + + QBasicMutex *loadFromFileMutex = new QBasicMutex(); /**< Mutex for single-file loading. */ + QBasicMutex *reloadDatabaseMutex = new QBasicMutex(); /**< Mutex for reloading entire database. */ +}; + +#endif // COCKATRICE_CARD_DATABASE_LOADER_H diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_manager.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_manager.cpp new file mode 100644 index 000000000..614b4a7f8 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/card_database_manager.cpp @@ -0,0 +1,35 @@ +#include "card_database_manager.h" + +#include +#include +#include + +ICardPreferenceProvider *CardDatabaseManager::cardPreferenceProvider = new NoopCardPreferenceProvider(); +ICardDatabasePathProvider *CardDatabaseManager::pathProvider = new NoopCardDatabasePathProvider(); +ICardSetPriorityController *CardDatabaseManager::setPriorityController = new NoopCardSetPriorityController(); + +void CardDatabaseManager::setCardPreferenceProvider(ICardPreferenceProvider *provider) +{ + cardPreferenceProvider = provider; +} + +void CardDatabaseManager::setCardDatabasePathProvider(ICardDatabasePathProvider *provider) +{ + pathProvider = provider; +} + +void CardDatabaseManager::setCardSetPriorityController(ICardSetPriorityController *controller) +{ + setPriorityController = controller; +} + +CardDatabase *CardDatabaseManager::getInstance() +{ + static CardDatabase instance(nullptr, cardPreferenceProvider, pathProvider, setPriorityController); + return &instance; +} + +CardDatabaseQuerier *CardDatabaseManager::query() +{ + return getInstance()->query(); +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_manager.h b/libcockatrice_card/libcockatrice/card/database/card_database_manager.h new file mode 100644 index 000000000..58a744fbb --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/card_database_manager.h @@ -0,0 +1,80 @@ +#ifndef CARD_DATABASE_ACCESSOR_H +#define CARD_DATABASE_ACCESSOR_H + +#pragma once +#include "card_database.h" + +/** + * @class CardDatabaseManager + * @ingroup CardDatabase + * @brief The CardDatabaseManager is responsible for managing the global CardDatabase singleton. + * + * This class provides a static interface for accessing the global CardDatabase instance + * and its CardDatabaseQuerier. It also allows the configuration of optional providers: + * - ICardPreferenceProvider + * - ICardDatabasePathProvider + * - ICardSetPriorityController + * + * Only a single instance of CardDatabase exists, enforced via a private constructor and + * deleted copy/move operations. + */ +class CardDatabaseManager +{ +public: + /** @brief Deleted copy constructor to enforce singleton. */ + CardDatabaseManager(const CardDatabaseManager &) = delete; + + /** @brief Deleted assignment operator to enforce singleton. */ + CardDatabaseManager &operator=(const CardDatabaseManager &) = delete; + + /** + * @brief Sets the card preference provider. + * @param provider Pointer to an ICardPreferenceProvider. + * @note Must be called before the first call to getInstance(). + */ + static void setCardPreferenceProvider(ICardPreferenceProvider *provider); + + /** + * @brief Sets the card database path provider. + * @param provider Pointer to an ICardDatabasePathProvider. + * @note Must be called before the first call to getInstance(). + */ + static void setCardDatabasePathProvider(ICardDatabasePathProvider *provider); + + /** + * @brief Sets the card set priority controller. + * @param controller Pointer to an ICardSetPriorityController. + * @note Must be called before the first call to getInstance(). + */ + static void setCardSetPriorityController(ICardSetPriorityController *controller); + + /** + * @brief Returns the singleton CardDatabase instance. + * @return Pointer to the global CardDatabase. + */ + static CardDatabase *getInstance(); + + /** + * @brief Returns the CardDatabaseQuerier of the singleton database. + * @return Pointer to CardDatabaseQuerier. + */ + static CardDatabaseQuerier *query(); + +private: + /** @brief Private default constructor to enforce singleton. */ + CardDatabaseManager() = default; + + /** @brief Private destructor. */ + ~CardDatabaseManager() = default; + + /// Static card preference provider pointer (default: Noop) + static ICardPreferenceProvider *cardPreferenceProvider; + + /// Static path provider pointer (default: Noop) + static ICardDatabasePathProvider *pathProvider; + + /// Static set priority controller pointer (default: Noop) + static ICardSetPriorityController *setPriorityController; +}; + +#endif // CARD_DATABASE_ACCESSOR_H diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp new file mode 100644 index 000000000..021e8d12d --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp @@ -0,0 +1,363 @@ +#include "card_database_querier.h" + +#include "../card_info.h" +#include "../printing/exact_card.h" +#include "../set/card_set_comparator.h" +#include "card_database.h" + +#include + +CardDatabaseQuerier::CardDatabaseQuerier(QObject *_parent, + const CardDatabase *_db, + const ICardPreferenceProvider *prefs) + : QObject(_parent), db(_db), prefs(prefs) +{ +} + +/** + * Looks up the cardInfo corresponding to the cardName. + * + * @param cardName The card name to look up + * @return A CardInfoPtr, or null if not corresponding CardInfo is found. + */ +CardInfoPtr CardDatabaseQuerier::getCardInfo(const QString &cardName) const +{ + return db->cards.value(cardName); +} + +/** + * Looks up the cardInfos for a list of card names. + * + * @param cardNames The card names to look up + * @return A List of CardInfoPtr. Any failed lookups will be ignored and dropped from the resulting list + */ +QList CardDatabaseQuerier::getCardInfos(const QStringList &cardNames) const +{ + QList cardInfos; + for (const QString &cardName : cardNames) { + CardInfoPtr ptr = db->cards.value(cardName); + if (ptr) + cardInfos.append(ptr); + } + + return cardInfos; +} + +CardInfoPtr CardDatabaseQuerier::getCardBySimpleName(const QString &cardName) const +{ + return db->simpleNameCards.value(CardInfo::simplifyName(cardName)); +} + +CardInfoPtr CardDatabaseQuerier::lookupCardByName(const QString &name) const +{ + if (auto info = getCardInfo(name)) + return info; + if (auto info = getCardBySimpleName(name)) + return info; + return getCardBySimpleName(CardInfo::simplifyName(name)); +} + +/** + * Looks up the cards corresponding to the CardRefs. + * If the providerId is empty, will default to the preferred printing. + * If providerId is given but not found, the PrintingInfo will be empty. + * + * @param cardRefs The cards to look up. If providerId is empty for an entry, will default to the preferred printing for + * that entry. If providerId is given but not found, the PrintingInfo will be empty for that entry. + * @return A list of cards. Any failed lookups will be ignored and dropped from the resulting list. + */ +QList CardDatabaseQuerier::getCards(const QList &cardRefs) const +{ + QList cards; + for (const auto &cardRef : cardRefs) { + ExactCard card = getCard(cardRef); + if (card) + cards.append(card); + } + + return cards; +} + +/** + * Looks up the card corresponding to the CardRef. + * If the providerId is empty, will default to the preferred printing. + * If providerId is given but not found, the PrintingInfo will be empty. + * + * @param cardRef The card to look up. + * @return A specific printing of a card, or empty if not found. + */ +ExactCard CardDatabaseQuerier::getCard(const CardRef &cardRef) const +{ + auto info = getCardInfo(cardRef.name); + if (info.isNull()) { + return {}; + } + + if (cardRef.providerId.isEmpty() || cardRef.providerId.isNull()) { + return ExactCard(info, getPreferredPrinting(info)); + } + + return ExactCard(info, findPrintingWithId(info, cardRef.providerId)); +} + +/** + * Looks up the card by CardRef, simplifying the name if required. + * If the providerId is empty, will default to the preferred printing. + * If providerId is given but not found, the PrintingInfo will be empty. + * + * @param cardRef The card to look up. + * @return A specific printing of a card, or empty if not found. + */ +ExactCard CardDatabaseQuerier::guessCard(const CardRef &cardRef) const +{ + auto card = lookupCardByName(cardRef.name); + auto printing = + cardRef.providerId.isEmpty() ? getPreferredPrinting(card) : findPrintingWithId(card, cardRef.providerId); + + return ExactCard(card, printing); +} + +ExactCard CardDatabaseQuerier::getRandomCard() const +{ + if (db->cards.isEmpty()) + return {}; + + const auto keys = db->cards.keys(); + int randomIndex = QRandomGenerator::global()->bounded(keys.size()); + const QString &randomKey = keys.at(randomIndex); + CardInfoPtr randomCard = getCardInfo(randomKey); + + return ExactCard{randomCard, getPreferredPrinting(randomCard)}; +} + +ExactCard CardDatabaseQuerier::getCardFromSameSet(const QString &cardName, const PrintingInfo &otherPrinting) const +{ + // The source card does not have a printing defined, which means we can't get a card from the same set. + if (otherPrinting.isEmpty()) { + return getCard({cardName}); + } + + // The source card does have a printing defined, which means we can attempt to get a card from the same set. + PrintingInfo relatedPrinting = getSpecificPrinting(cardName, otherPrinting.getSet()->getCorrectedShortName(), ""); + ExactCard relatedCard(guessCard({cardName}).getCardPtr(), relatedPrinting); + + // If we didn't find a card from the same set, just try to find any card with the same name. + return relatedCard ? relatedCard : getCard({cardName}); +} + +/** + * Finds the PrintingInfo in the cardInfo that has the given uuid field. + * + * @param cardInfo The CardInfo to search + * @param providerId The uuid to look for + * @return The PrintingInfo, or a default-constructed PrintingInfo if not found. + */ +PrintingInfo CardDatabaseQuerier::findPrintingWithId(const CardInfoPtr &cardInfo, const QString &providerId) const +{ + for (const auto &printings : cardInfo->getSets()) { + for (const auto &printing : printings) { + if (printing.getUuid() == providerId) { + return printing; + } + } + } + + return PrintingInfo(); +} + +PrintingInfo CardDatabaseQuerier::getSpecificPrinting(const CardRef &cardRef) const +{ + CardInfoPtr cardInfo = getCardInfo(cardRef.name); + if (!cardInfo) { + return PrintingInfo(nullptr); + } + + return findPrintingWithId(cardInfo, cardRef.providerId); +} + +PrintingInfo CardDatabaseQuerier::getSpecificPrinting(const QString &cardName, + const QString &setShortName, + const QString &collectorNumber) const +{ + CardInfoPtr cardInfo = getCardInfo(cardName); + if (!cardInfo) { + return PrintingInfo(nullptr); + } + + SetToPrintingsMap setMap = cardInfo->getSets(); + if (setMap.empty()) { + return PrintingInfo(nullptr); + } + + for (const auto &printings : setMap) { + for (auto &cardInfoForSet : printings) { + if (!collectorNumber.isEmpty()) { + if (cardInfoForSet.getSet()->getShortName() == setShortName && + cardInfoForSet.getProperty("num") == collectorNumber) { + return cardInfoForSet; + } + } else { + if (cardInfoForSet.getSet()->getShortName() == setShortName) { + return cardInfoForSet; + } + } + } + } + + return PrintingInfo(nullptr); +} + +/** + * Gets the card representing the preferred printing of the cardInfo + * + * @param cardName The cardName to find the preferred card and printing for + * @return A specific printing of a card + */ +ExactCard CardDatabaseQuerier::getPreferredCard(const QString &cardName) const +{ + return getPreferredCard(getCardInfo(cardName)); +} + +/** + * Gets the card representing the preferred printing of the cardInfo + * + * @param cardInfo The cardInfo to find the preferred printing for + * @return A specific printing of a card + */ +ExactCard CardDatabaseQuerier::getPreferredCard(const CardInfoPtr &cardInfo) const +{ + return ExactCard(cardInfo, getPreferredPrinting(cardInfo)); +} + +bool CardDatabaseQuerier::isPreferredPrinting(const CardRef &cardRef) const +{ + if (cardRef.providerId.startsWith("card_")) { + return cardRef.providerId == + QLatin1String("card_") + cardRef.name + QString("_") + getPreferredPrintingProviderId(cardRef.name); + } + return cardRef.providerId == getPreferredPrintingProviderId(cardRef.name); +} + +PrintingInfo CardDatabaseQuerier::getPreferredPrinting(const QString &cardName) const +{ + CardInfoPtr cardInfo = getCardInfo(cardName); + return getPreferredPrinting(cardInfo); +} + +PrintingInfo CardDatabaseQuerier::getPreferredPrinting(const CardInfoPtr &cardInfo) const +{ + if (!cardInfo) { + return PrintingInfo(nullptr); + } + + const auto &pinnedPrintingProviderId = prefs->getCardPreferenceOverride(cardInfo->getName()); + + if (!pinnedPrintingProviderId.isEmpty()) { + return getSpecificPrinting({cardInfo->getName(), pinnedPrintingProviderId}); + } + + SetToPrintingsMap setMap = cardInfo->getSets(); + if (setMap.empty()) { + return PrintingInfo(nullptr); + } + + CardSetPtr preferredSet = nullptr; + PrintingInfo preferredPrinting; + SetPriorityComparator comparator; + + for (const auto &printings : setMap) { + for (auto &printing : printings) { + CardSetPtr currentSet = printing.getSet(); + if (!preferredSet || comparator(currentSet, preferredSet)) { + preferredSet = currentSet; + preferredPrinting = printing; + } + } + } + + if (preferredSet) { + return preferredPrinting; + } + + return PrintingInfo(nullptr); +} + +QString CardDatabaseQuerier::getPreferredPrintingProviderId(const QString &cardName) const +{ + PrintingInfo preferredPrinting = getPreferredPrinting(cardName); + QString uuid = preferredPrinting.getUuid(); + if (!uuid.isEmpty()) { + return uuid; + } + + CardInfoPtr defaultCardInfo = getCardInfo(cardName); + if (defaultCardInfo.isNull()) { + return cardName; + } + return defaultCardInfo->getName(); +} + +QStringList CardDatabaseQuerier::getAllMainCardTypes() const +{ + QSet types; + for (const auto &card : db->cards.values()) { + types.insert(card->getMainCardType()); + } + return types.values(); +} + +QMap CardDatabaseQuerier::getAllMainCardTypesWithCount() const +{ + QMap typeCounts; + + for (const auto &card : db->cards.values()) { + QString type = card->getMainCardType(); + typeCounts[type]++; + } + + return typeCounts; +} + +QMap CardDatabaseQuerier::getAllSubCardTypesWithCount() const +{ + QMap typeCounts; + + for (const auto &card : db->cards.values()) { + QString type = card->getCardType(); + + QStringList parts = type.split(" — "); + + if (parts.size() > 1) { // Ensure there are subtypes + QStringList subtypes = parts[1].split(" ", Qt::SkipEmptyParts); + + for (const QString &subtype : subtypes) { + typeCounts[subtype]++; + } + } + } + + return typeCounts; +} + +FormatRulesPtr CardDatabaseQuerier::getFormat(const QString &formatName) const +{ + return db->formats.value(formatName.toLower()); +} + +QMap CardDatabaseQuerier::getAllFormatsWithCount() const +{ + QMap formatCounts; + + for (const auto &card : db->cards.values()) { + QStringList allProps = card->getProperties(); + + for (const QString &prop : allProps) { + if (prop.startsWith("format-")) { + QString formatName = prop.mid(QStringLiteral("format-").size()); + formatCounts[formatName]++; + } + } + } + + return formatCounts; +} diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_querier.h b/libcockatrice_card/libcockatrice/card/database/card_database_querier.h new file mode 100644 index 000000000..ff8d7958b --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/card_database_querier.h @@ -0,0 +1,225 @@ +#ifndef COCKATRICE_CARD_DATABASE_QUERIER_H +#define COCKATRICE_CARD_DATABASE_QUERIER_H + +#include "../card_info.h" +#include "../printing/exact_card.h" + +#include +#include +#include + +class CardDatabase; + +/** + * @class CardDatabaseQuerier + * @ingroup CardDatabase + * @brief Provides lookup and convenience functions for querying cards and their printings. + * + * The CardDatabaseQuerier class offers various lookup helpers for retrieving card information + * (e.g., CardInfoPtr, ExactCard, and PrintingInfo) from a CardDatabase. It also applies user + * printing preferences via ICardPreferenceProvider when determining preferred printings. + */ +class CardDatabaseQuerier : public QObject +{ + Q_OBJECT + +public: + /** + * @brief Constructs a CardDatabaseQuerier. + * + * @param parent Parent QObject. + * @param db Pointer to the CardDatabase used for lookups. + * @param prefs Pointer to card preference provider which supplies user-preference for printings. + */ + explicit CardDatabaseQuerier(QObject *parent, const CardDatabase *db, const ICardPreferenceProvider *prefs); + + /** + * @brief Retrieves a card by its exact name. + * + * @param cardName Exact card name. + * @return A CardInfoPtr, or null if no matching card exists. + */ + [[nodiscard]] CardInfoPtr getCardInfo(const QString &cardName) const; + + /** + * @brief Retrieves multiple cards by their exact names. + * + * Failed lookups are skipped and not included in the result. + * + * @param cardNames List of exact card names. + * @return List of CardInfoPtr objects for which a match was found. + */ + [[nodiscard]] QList getCardInfos(const QStringList &cardNames) const; + + /** + * @brief Retrieves a card using simplified name matching. + * + * The name is automatically normalized, so callers do not need to simplify it. + * + * @param cardName A (possibly simplified or misspelled) card name. + * @return A CardInfoPtr, or null if not found. + */ + [[nodiscard]] CardInfoPtr getCardBySimpleName(const QString &cardName) const; + + /** + * @brief Looks up a card using exact name first, then simplified matching as fallback. + * + * @param name Raw card name input. + * @return The best-match CardInfoPtr, or null if no match is found. + */ + [[nodiscard]] CardInfoPtr lookupCardByName(const QString &name) const; + + /** + * @brief Converts a CardRef into an ExactCard. + * + * If the providerId is empty, the preferred printing is used. + * If providerId exists but cannot be found, an ExactCard with an empty PrintingInfo is returned. + * + * @param cardRef Card reference with name and optional providerId. + * @return The resolved ExactCard, or empty if no card was found. + */ + [[nodiscard]] ExactCard getCard(const CardRef &cardRef) const; + + /** + * @brief Resolves multiple CardRefs into ExactCards. + * + * Failed entries are not included in the result. + * + * @param cardRefs List of card references. + * @return List of successfully resolved ExactCards. + */ + [[nodiscard]] QList getCards(const QList &cardRefs) const; + + /** + * @brief Attempts a more flexible card lookup using both simple name matching and CardRef rules. + * + * If providerId is missing, uses preferred printing. If lookup fails, attempts simplified name. + * + * @param cardRef Card reference to resolve. + * @return The best-guess ExactCard, or empty if unresolved. + */ + [[nodiscard]] ExactCard guessCard(const CardRef &cardRef) const; + + /** + * @brief Returns a random card from the database using the preferred printing. + * + * @return A random ExactCard, or empty if the database is empty. + */ + [[nodiscard]] ExactCard getRandomCard() const; + + /** + * @brief Returns a printing of a card from the same set as another given printing when possible. + * + * If no matching printing exists, falls back to a standard lookup. + * + * @param cardName Card to retrieve. + * @param otherPrinting Printing to match the set against. + * @return Matching ExactCard if found, otherwise fallback ExactCard. + */ + [[nodiscard]] ExactCard getCardFromSameSet(const QString &cardName, const PrintingInfo &otherPrinting) const; + + /** + * @brief Returns the preferred printing of a card based on user preferences and set priority. + * + * @param cardName Name of the card. + * @return The preferred ExactCard. + */ + [[nodiscard]] ExactCard getPreferredCard(const QString &cardName) const; + + /** + * @brief Returns the preferred printing of a card based on user preferences and set priority. + * + * @param cardInfo Card information object. + * @return The preferred ExactCard. + */ + [[nodiscard]] ExactCard getPreferredCard(const CardInfoPtr &cardInfo) const; + + /** + * @brief Checks whether the CardRef refers to the preferred printing. + * + * @param cardRef Card reference to test. + * @return True if providerId matches the preferred printing. + */ + [[nodiscard]] bool isPreferredPrinting(const CardRef &cardRef) const; + + /** + * @brief Returns the preferred printing for the given card name. + * + * @param cardName Card name. + * @return Preferred PrintingInfo, or empty if not found. + */ + [[nodiscard]] PrintingInfo getPreferredPrinting(const QString &cardName) const; + + /** + * @brief Returns the preferred printing for the given card. + * + * @param cardInfo Card information object. + * @return Preferred PrintingInfo, or empty if not applicable. + */ + [[nodiscard]] PrintingInfo getPreferredPrinting(const CardInfoPtr &cardInfo) const; + + /** + * @brief Returns the providerId of the preferred printing. + * + * @param cardName Card name. + * @return ProviderId string for preferred printing. + */ + [[nodiscard]] QString getPreferredPrintingProviderId(const QString &cardName) const; + + /** + * @brief Retrieves a specific printing referenced by CardRef. + * + * @param cardRef Card reference including providerId. + * @return Matching PrintingInfo, or empty if not found. + */ + [[nodiscard]] PrintingInfo getSpecificPrinting(const CardRef &cardRef) const; + + /** + * @brief Searches for a specific printing by set code and collector number. + * + * @param cardName Card name to search. + * @param setCode Set (short) code to match. + * @param collectorNumber Collector number. If empty, any printing from the set is returned. + * @return Matching PrintingInfo, or empty if not found. + */ + [[nodiscard]] PrintingInfo + getSpecificPrinting(const QString &cardName, const QString &setCode, const QString &collectorNumber) const; + + /** + * @brief Searches for a printing that matches a given providerId. + * + * @param card Card to search. + * @param providerId Provider identifier to match. + * @return Matching PrintingInfo, or empty if not found. + */ + [[nodiscard]] PrintingInfo findPrintingWithId(const CardInfoPtr &card, const QString &providerId) const; + + /** + * @brief Returns a list of all main card types present in the database. + * + * @return List of main card type strings. + */ + [[nodiscard]] QStringList getAllMainCardTypes() const; + + /** + * @brief Returns a mapping of main card types to their occurrence counts. + * + * @return Map of main card type to count. + */ + [[nodiscard]] QMap getAllMainCardTypesWithCount() const; + + /** + * @brief Returns a mapping of card subtypes to their occurrence counts. + * + * @return Map of subtype string to count. + */ + [[nodiscard]] QMap getAllSubCardTypesWithCount() const; + FormatRulesPtr getFormat(const QString &formatName) const; + QMap getAllFormatsWithCount() const; + +private: + const CardDatabase *db; //!< Card database used for all lookups. + const ICardPreferenceProvider *prefs; //!< Preference provider for preferred printings. +}; + +#endif // COCKATRICE_CARD_DATABASE_QUERIER_H diff --git a/libcockatrice_card/libcockatrice/card/database/parser/card_database_parser.cpp b/libcockatrice_card/libcockatrice/card/database/parser/card_database_parser.cpp new file mode 100644 index 000000000..f7e6bbfcf --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/parser/card_database_parser.cpp @@ -0,0 +1,35 @@ +#include "card_database_parser.h" + +#include + +SetNameMap ICardDatabaseParser::sets; + +ICardDatabaseParser::ICardDatabaseParser(ICardSetPriorityController *_cardSetPriorityController) + : cardSetPriorityController(_cardSetPriorityController) +{ +} +void ICardDatabaseParser::clearSetlist() +{ + sets.clear(); +} + +CardSetPtr ICardDatabaseParser::internalAddSet(const QString &setName, + const QString &longName, + const QString &setType, + const QDate &releaseDate, + const CardSet::Priority priority) +{ + if (sets.contains(setName)) { + return sets.value(setName); + } + + CardSetPtr newSet = CardSet::newInstance(cardSetPriorityController, setName); + newSet->setLongName(longName); + newSet->setSetType(setType); + newSet->setReleaseDate(releaseDate); + newSet->setPriority(priority); + + sets.insert(setName, newSet); + emit addSet(newSet); + return newSet; +} diff --git a/libcockatrice_card/libcockatrice/card/database/parser/card_database_parser.h b/libcockatrice_card/libcockatrice/card/database/parser/card_database_parser.h new file mode 100644 index 000000000..a8eceab5a --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/parser/card_database_parser.h @@ -0,0 +1,92 @@ +#ifndef CARDDATABASE_PARSER_H +#define CARDDATABASE_PARSER_H + +#include "../../card_info.h" + +#include +#include + +#define COCKATRICE_XML_XSI_NAMESPACE "http://www.w3.org/2001/XMLSchema-instance" + +/** + * @class ICardDatabaseParser + * @ingroup CardDatabase + * @brief Defines the base parser interface (ICardDatabaseParser) for all card database parsers. + * + * Provides methods for checking file compatibility, parsing, and saving card databases. + * Also provides shared access to the global set list for cross-referencing. + */ +class ICardDatabaseParser : public QObject +{ + Q_OBJECT +public: + ICardDatabaseParser(ICardSetPriorityController *cardSetPriorityController); + ~ICardDatabaseParser() override = default; + + /** + * @brief Checks whether this parser can parse the given file. + * @param name File name (used for extension checks). + * @param device QIODevice representing the file content. + * @return true if the parser can handle this file. + */ + virtual bool getCanParseFile(const QString &name, QIODevice &device) = 0; + + /** + * @brief Parses a database file and emits addCard/addSet signals. + * @param device QIODevice representing the file content. + */ + virtual void parseFile(QIODevice &device) = 0; + + /** + * @brief Saves card and set data to a file. + * @param _formats + * @param sets Map of sets to save. + * @param cards Map of cards to save. + * @param fileName Target file path. + * @param sourceUrl Optional source URL of the database. + * @param sourceVersion Optional version string of the source. + * @return true if save succeeded. + */ + virtual bool saveToFile(FormatRulesNameMap _formats, + SetNameMap sets, + CardNameMap cards, + const QString &fileName, + const QString &sourceUrl = "unknown", + const QString &sourceVersion = "unknown") = 0; + + /** @brief Clears the cached global set list. */ + static void clearSetlist(); + +protected: + /** @brief Cached global list of sets shared between all parsers. */ + static SetNameMap sets; + ICardSetPriorityController *cardSetPriorityController; + + /** + * @brief Internal helper to add a set to the global set cache. + * @param setName Short set name. + * @param longName Optional full name. + * @param setType Optional set type string. + * @param releaseDate Optional release date. + * @param priority Optional priority (fallback if not specified). + * @return Pointer to the added or existing CardSet instance. + */ + CardSetPtr internalAddSet(const QString &setName, + const QString &longName = "", + const QString &setType = "", + const QDate &releaseDate = QDate(), + const CardSet::Priority priority = CardSet::PriorityFallback); + +signals: + /** Emitted when a card is loaded from the database. */ + void addCard(CardInfoPtr card); + + /** Emitted when a set is loaded from the database. */ + void addSet(CardSetPtr set); + + void addFormat(FormatRulesPtr format); +}; + +Q_DECLARE_INTERFACE(ICardDatabaseParser, "ICardDatabaseParser") + +#endif diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp new file mode 100644 index 000000000..ba27d63c4 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp @@ -0,0 +1,496 @@ +#include "cockatrice_xml_3.h" + +#include "../../relation/card_relation.h" +#include "../../relation/card_relation_type.h" + +#include +#include +#include +#include +#include + +#define COCKATRICE_XML3_TAGNAME "cockatrice_carddatabase" +#define COCKATRICE_XML3_TAGVER 3 +#define COCKATRICE_XML3_SCHEMALOCATION \ + "https://raw.githubusercontent.com/Cockatrice/Cockatrice/master/doc/carddatabase_v3/cards.xsd" + +CockatriceXml3Parser::CockatriceXml3Parser(ICardSetPriorityController *_cardSetPriorityController) + : ICardDatabaseParser(_cardSetPriorityController) +{ +} + +bool CockatriceXml3Parser::getCanParseFile(const QString &fileName, QIODevice &device) +{ + qCInfo(CockatriceXml3Log) << "Trying to parse: " << fileName; + + if (!fileName.endsWith(".xml", Qt::CaseInsensitive)) { + qCInfo(CockatriceXml3Log) << "Parsing failed: wrong extension"; + return false; + } + + QXmlStreamReader xml(&device); + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::StartElement) { + if (xml.name().toString() == COCKATRICE_XML3_TAGNAME) { + int version = xml.attributes().value("version").toString().toInt(); + if (version == COCKATRICE_XML3_TAGVER) { + return true; + } else { + qCInfo(CockatriceXml3Log) << "Parsing failed: wrong version" << version; + return false; + } + + } else { + qCInfo(CockatriceXml3Log) << "Parsing failed: wrong element tag" << xml.name(); + return false; + } + } + } + + return true; +} + +void CockatriceXml3Parser::parseFile(QIODevice &device) +{ + QXmlStreamReader xml(&device); + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::StartElement) { + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + + auto name = xml.name().toString(); + if (name == "sets") { + loadSetsFromXml(xml); + } else if (name == "cards") { + loadCardsFromXml(xml); + } else if (!name.isEmpty()) { + qCInfo(CockatriceXml3Log) << "Unknown item" << name << ", trying to continue anyway"; + xml.skipCurrentElement(); + } + } + } + } + + if (xml.hasError()) { + QString preamble = tr("Parse error at line %1 col %2:").arg(xml.lineNumber()).arg(xml.columnNumber()); + qCWarning(CockatriceXml3Log).noquote() << preamble << xml.errorString(); + } +} + +void CockatriceXml3Parser::loadSetsFromXml(QXmlStreamReader &xml) +{ + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + + auto name = xml.name().toString(); + if (name == "set") { + QString shortName, longName, setType; + QDate releaseDate; + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + name = xml.name().toString(); + + if (name == "name") { + shortName = xml.readElementText(QXmlStreamReader::IncludeChildElements); + } else if (name == "longname") { + longName = xml.readElementText(QXmlStreamReader::IncludeChildElements); + } else if (name == "settype") { + setType = xml.readElementText(QXmlStreamReader::IncludeChildElements); + } else if (name == "releasedate") { + releaseDate = + QDate::fromString(xml.readElementText(QXmlStreamReader::IncludeChildElements), Qt::ISODate); + } else if (!name.isEmpty()) { + qCInfo(CockatriceXml3Log) << "Unknown set property" << name << ", trying to continue anyway"; + xml.skipCurrentElement(); + } + } + + internalAddSet(shortName, longName, setType, releaseDate); + } + } +} + +QString CockatriceXml3Parser::getMainCardType(QString &type) +{ + QString result = type; + /* + Legendary Artifact Creature - Golem + Instant // Instant + */ + + int pos; + if ((pos = result.indexOf('-')) != -1) { + result.remove(pos, result.length()); + } + + if ((pos = result.indexOf("—")) != -1) { + result.remove(pos, result.length()); + } + + if ((pos = result.indexOf("//")) != -1) { + result.remove(pos, result.length()); + } + + result = result.simplified(); + /* + Legendary Artifact Creature + Instant + */ + + if ((pos = result.lastIndexOf(' ')) != -1) { + result = result.mid(pos + 1); + } + /* + Creature + Instant + */ + + return result; +} + +void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) +{ + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + + auto xmlName = xml.name().toString(); + if (xmlName == "card") { + QString name = QString(""); + QString text = QString(""); + QVariantHash properties = QVariantHash(); + QString colors = QString(""); + QList relatedCards, reverseRelatedCards; + auto _sets = SetToPrintingsMap(); + int tableRow = 0; + bool cipt = false; + bool landscapeOrientation = false; + bool isToken = false; + bool upsideDown = false; + + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + xmlName = xml.name().toString(); + + // variable - assigned properties + if (xmlName == "name") { + name = xml.readElementText(QXmlStreamReader::IncludeChildElements); + } else if (xmlName == "text") { + text = xml.readElementText(QXmlStreamReader::IncludeChildElements); + } else if (xmlName == "color" || xmlName == "colors") { + colors.append(xml.readElementText(QXmlStreamReader::IncludeChildElements)); + } else if (xmlName == "token") { + isToken = static_cast(xml.readElementText(QXmlStreamReader::IncludeChildElements).toInt()); + // generic properties + } else if (xmlName == "manacost") { + properties.insert("manacost", xml.readElementText(QXmlStreamReader::IncludeChildElements)); + } else if (xmlName == "cmc") { + properties.insert("cmc", xml.readElementText(QXmlStreamReader::IncludeChildElements)); + } else if (xmlName == "type") { + QString type = xml.readElementText(QXmlStreamReader::IncludeChildElements); + properties.insert("type", type); + properties.insert("maintype", getMainCardType(type)); + } else if (xmlName == "pt") { + properties.insert("pt", xml.readElementText(QXmlStreamReader::IncludeChildElements)); + } else if (xmlName == "loyalty") { + properties.insert("loyalty", xml.readElementText(QXmlStreamReader::IncludeChildElements)); + // positioning info + } else if (xmlName == "tablerow") { + tableRow = xml.readElementText(QXmlStreamReader::IncludeChildElements).toInt(); + } else if (xmlName == "cipt") { + cipt = (xml.readElementText(QXmlStreamReader::IncludeChildElements) == "1"); + } else if (xmlName == "landscapeOrientation") { + landscapeOrientation = (xml.readElementText(QXmlStreamReader::IncludeChildElements) == "1"); + } else if (xmlName == "upsidedown") { + upsideDown = (xml.readElementText(QXmlStreamReader::IncludeChildElements) == "1"); + // sets + } else if (xmlName == "set") { + // NOTE: attributes must be read before readElementText() + QXmlStreamAttributes attrs = xml.attributes(); + QString setName = xml.readElementText(QXmlStreamReader::IncludeChildElements); + PrintingInfo setInfo(internalAddSet(setName)); + if (attrs.hasAttribute("muId")) { + setInfo.setProperty("muid", attrs.value("muId").toString()); + } + + if (attrs.hasAttribute("muId")) { + setInfo.setProperty("uuid", attrs.value("uuId").toString()); + } + + if (attrs.hasAttribute("picURL")) { + setInfo.setProperty("picurl", attrs.value("picURL").toString()); + } + + if (attrs.hasAttribute("num")) { + setInfo.setProperty("num", attrs.value("num").toString()); + } + + if (attrs.hasAttribute("rarity")) { + setInfo.setProperty("rarity", attrs.value("rarity").toString()); + } + _sets[setName].append(setInfo); + // related cards + } else if (xmlName == "related" || xmlName == "reverse-related") { + CardRelationType attach = CardRelationType::DoesNotAttach; + bool exclude = false; + bool variable = false; + int count = 1; + QXmlStreamAttributes attrs = xml.attributes(); + QString cardName = xml.readElementText(QXmlStreamReader::IncludeChildElements); + if (attrs.hasAttribute("count")) { + if (attrs.value("count").toString().indexOf("x=") == 0) { + variable = true; + count = attrs.value("count").toString().remove(0, 2).toInt(); + } else if (attrs.value("count").toString().indexOf("x") == 0) { + variable = true; + } else { + count = attrs.value("count").toString().toInt(); + } + + if (count < 1) { + count = 1; + } + } + + if (attrs.hasAttribute("attach")) { + attach = CardRelationType::AttachTo; + } + + if (attrs.hasAttribute("exclude")) { + exclude = true; + } + + auto *relation = new CardRelation(cardName, attach, exclude, variable, count); + if (xmlName == "reverse-related") { + reverseRelatedCards << relation; + } else { + relatedCards << relation; + } + } else if (!xmlName.isEmpty()) { + qCInfo(CockatriceXml3Log) << "Unknown card property" << xmlName << ", trying to continue anyway"; + xml.skipCurrentElement(); + } + } + + if (name.isEmpty()) { + qCWarning(CockatriceXml3Log) << "Encountered card with empty name; skipping"; + continue; + } + + properties.insert("colors", colors); + + CardInfo::UiAttributes attributes = {.cipt = cipt, + .landscapeOrientation = landscapeOrientation, + .tableRow = tableRow, + .upsideDownArt = upsideDown}; + CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards, + reverseRelatedCards, _sets, attributes); + emit addCard(newCard); + } + } +} + +static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardSetPtr &set) +{ + if (set.isNull()) { + qCWarning(CockatriceXml3Log) << "&operator<< set is nullptr"; + return xml; + } + + xml.writeStartElement("set"); + xml.writeTextElement("name", set->getShortName()); + xml.writeTextElement("longname", set->getLongName()); + xml.writeTextElement("settype", set->getSetType()); + xml.writeTextElement("releasedate", set->getReleaseDate().toString(Qt::ISODate)); + xml.writeEndElement(); + + return xml; +} + +static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &info) +{ + if (info.isNull()) { + qCWarning(CockatriceXml3Log) << "operator<< info is nullptr"; + return xml; + } + + QString tmpString; + + xml.writeStartElement("card"); + + // variable - assigned properties + xml.writeTextElement("name", info->getName()); + xml.writeTextElement("text", info->getText()); + if (info->getIsToken()) { + xml.writeTextElement("token", "1"); + } + + // generic properties + xml.writeTextElement("manacost", info->getProperty("manacost")); + xml.writeTextElement("cmc", info->getProperty("cmc")); + xml.writeTextElement("type", info->getProperty("type")); + + int colorSize = info->getColors().size(); + for (int i = 0; i < colorSize; ++i) { + xml.writeTextElement("color", info->getColors().at(i)); + } + + tmpString = info->getProperty("pt"); + if (!tmpString.isEmpty()) { + xml.writeTextElement("pt", tmpString); + } + + tmpString = info->getProperty("loyalty"); + if (!tmpString.isEmpty()) { + xml.writeTextElement("loyalty", tmpString); + } + + // sets + const SetToPrintingsMap setMap = info->getSets(); + for (const auto &printings : setMap) { + for (const PrintingInfo &set : printings) { + xml.writeStartElement("set"); + xml.writeAttribute("rarity", set.getProperty("rarity")); + xml.writeAttribute("muId", set.getProperty("muid")); + xml.writeAttribute("uuId", set.getProperty("uuid")); + + tmpString = set.getProperty("num"); + if (!tmpString.isEmpty()) { + xml.writeAttribute("num", tmpString); + } + + tmpString = set.getProperty("picurl"); + if (!tmpString.isEmpty()) { + xml.writeAttribute("picURL", tmpString); + } + + xml.writeCharacters(set.getSet()->getShortName()); + xml.writeEndElement(); + } + } + + // related cards + const QList related = info->getRelatedCards(); + for (auto i : related) { + xml.writeStartElement("related"); + if (i->getDoesAttach()) { + xml.writeAttribute("attach", "attach"); + } + if (i->getIsCreateAllExclusion()) { + xml.writeAttribute("exclude", "exclude"); + } + + if (i->getIsVariable()) { + if (1 == i->getDefaultCount()) { + xml.writeAttribute("count", "x"); + } else { + xml.writeAttribute("count", "x=" + QString::number(i->getDefaultCount())); + } + } else if (1 != i->getDefaultCount()) { + xml.writeAttribute("count", QString::number(i->getDefaultCount())); + } + xml.writeCharacters(i->getName()); + xml.writeEndElement(); + } + const QList reverseRelated = info->getReverseRelatedCards(); + for (auto i : reverseRelated) { + xml.writeStartElement("reverse-related"); + if (i->getDoesAttach()) { + xml.writeAttribute("attach", "attach"); + } + + if (i->getIsCreateAllExclusion()) { + xml.writeAttribute("exclude", "exclude"); + } + + if (i->getIsVariable()) { + if (1 == i->getDefaultCount()) { + xml.writeAttribute("count", "x"); + } else { + xml.writeAttribute("count", "x=" + QString::number(i->getDefaultCount())); + } + } else if (1 != i->getDefaultCount()) { + xml.writeAttribute("count", QString::number(i->getDefaultCount())); + } + xml.writeCharacters(i->getName()); + xml.writeEndElement(); + } + + // positioning + const CardInfo::UiAttributes &attributes = info->getUiAttributes(); + xml.writeTextElement("tablerow", QString::number(attributes.tableRow)); + if (attributes.cipt) { + xml.writeTextElement("cipt", "1"); + } + if (attributes.landscapeOrientation) { + xml.writeTextElement("landscapeOrientation", "1"); + } + if (attributes.upsideDownArt) { + xml.writeTextElement("upsidedown", "1"); + } + + xml.writeEndElement(); // card + + return xml; +} + +bool CockatriceXml3Parser::saveToFile(FormatRulesNameMap _formats, + SetNameMap _sets, + CardNameMap cards, + const QString &fileName, + const QString &sourceUrl, + const QString &sourceVersion) +{ + Q_UNUSED(_formats); + + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + return false; + } + + QXmlStreamWriter xml(&file); + + xml.setAutoFormatting(true); + xml.writeStartDocument(); + xml.writeStartElement(COCKATRICE_XML3_TAGNAME); + xml.writeAttribute("version", QString::number(COCKATRICE_XML3_TAGVER)); + xml.writeAttribute("xmlns:xsi", COCKATRICE_XML_XSI_NAMESPACE); + xml.writeAttribute("xsi:schemaLocation", COCKATRICE_XML3_SCHEMALOCATION); + + xml.writeStartElement("info"); + xml.writeTextElement("author", QCoreApplication::applicationName() + QString(" %1").arg(VERSION_STRING)); + xml.writeTextElement("createdAt", QDateTime::currentDateTimeUtc().toString(Qt::ISODate)); + xml.writeTextElement("sourceUrl", sourceUrl); + xml.writeTextElement("sourceVersion", sourceVersion); + xml.writeEndElement(); + + if (_sets.count() > 0) { + xml.writeStartElement("sets"); + for (CardSetPtr set : _sets) { + xml << set; + } + xml.writeEndElement(); + } + + if (cards.count() > 0) { + xml.writeStartElement("cards"); + for (CardInfoPtr card : cards) { + xml << card; + } + xml.writeEndElement(); + } + + xml.writeEndElement(); // cockatrice_carddatabase + xml.writeEndDocument(); + + return true; +} diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.h b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.h new file mode 100644 index 000000000..a01b705aa --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.h @@ -0,0 +1,79 @@ +#ifndef COCKATRICE_XML3_H +#define COCKATRICE_XML3_H + +#include "card_database_parser.h" + +#include +#include + +inline Q_LOGGING_CATEGORY(CockatriceXml3Log, "cockatrice_xml.xml_3_parser"); + +/** + * @class CockatriceXml3Parser + * @ingroup CardDatabase + * @brief Parses version 3 of the Cockatrice XML Schema. + * + * This parser reads a Cockatrice XML3 database and emits CardInfoPtr + * and CardSetPtr objects. All card properties are read individually. + * + * @note Differences from v4: + * - No block; properties are hardcoded (manacost, cmc, type, pt, loyalty, etc.). + * - No set priority field. + * - No support for rebalanced cards or preferences. + * - Related cards support only attach, exclude, variable, and count attributes. + */ +class CockatriceXml3Parser : public ICardDatabaseParser +{ + Q_OBJECT +public: + CockatriceXml3Parser(ICardSetPriorityController *cardSetPriorityController); + ~CockatriceXml3Parser() override = default; + + /** + * @brief Determines if the parser can handle this file. + * @param name File name. + * @param device Open QIODevice containing the XML. + * @return True if the file is a Cockatrice XML3 database. + */ + bool getCanParseFile(const QString &name, QIODevice &device) override; + + /** + * @brief Parse the XML database. + * @param device Open QIODevice positioned at start of file. + */ + void parseFile(QIODevice &device) override; + + /** + * @brief Save sets and cards back to an XML3 file. + */ + bool saveToFile(FormatRulesNameMap _formats, + SetNameMap _sets, + CardNameMap cards, + const QString &fileName, + const QString &sourceUrl = "unknown", + const QString &sourceVersion = "unknown") override; + +private: + /** + * @brief Load all elements from the XML stream. + * @param xml The open QXmlStreamReader positioned at the element. + * Parses each node and emits addCard signals for each CardInfoPtr created. + */ + void loadCardsFromXml(QXmlStreamReader &xml); + + /** + * @brief Load all elements from the XML stream. + * @param xml The open QXmlStreamReader positioned at the element. + * Parses each node and adds them to the shared set cache. + */ + void loadSetsFromXml(QXmlStreamReader &xml); + + /** + * @brief Extracts the main card type from a full type string. + * @param type The full type string (e.g., "Legendary Artifact Creature - Golem") + * @return The primary type (e.g., "Creature"). + */ + QString getMainCardType(QString &type); +}; + +#endif \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp new file mode 100644 index 000000000..cc0220526 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp @@ -0,0 +1,625 @@ +#include "cockatrice_xml_4.h" + +#include "../../relation/card_relation.h" + +#include +#include +#include +#include +#include +#include + +#define COCKATRICE_XML4_TAGNAME "cockatrice_carddatabase" +#define COCKATRICE_XML4_TAGVER 4 +#define COCKATRICE_XML4_SCHEMALOCATION \ + "https://raw.githubusercontent.com/Cockatrice/Cockatrice/master/doc/carddatabase_v4/cards.xsd" + +CockatriceXml4Parser::CockatriceXml4Parser(ICardPreferenceProvider *_cardPreferenceProvider, + ICardSetPriorityController *_cardSetPriorityController) + : ICardDatabaseParser(_cardSetPriorityController), cardPreferenceProvider(_cardPreferenceProvider) +{ +} + +bool CockatriceXml4Parser::getCanParseFile(const QString &fileName, QIODevice &device) +{ + qCInfo(CockatriceXml4Log) << "Trying to parse: " << fileName; + + if (!fileName.endsWith(".xml", Qt::CaseInsensitive)) { + qCInfo(CockatriceXml4Log) << "Parsing failed: wrong extension"; + return false; + } + + QXmlStreamReader xml(&device); + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::StartElement) { + if (xml.name().toString() == COCKATRICE_XML4_TAGNAME) { + int version = xml.attributes().value("version").toString().toInt(); + if (version == COCKATRICE_XML4_TAGVER) { + return true; + } else { + qCInfo(CockatriceXml4Log) << "Parsing failed: wrong version" << version; + return false; + } + + } else { + qCInfo(CockatriceXml4Log) << "Parsing failed: wrong element tag" << xml.name(); + return false; + } + } + } + + return true; +} + +void CockatriceXml4Parser::parseFile(QIODevice &device) +{ + QXmlStreamReader xml(&device); + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::StartElement) { + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + + auto xmlName = xml.name().toString(); + if (xmlName == "formats") { + loadFormats(xml); + } else if (xmlName == "sets") { + loadSetsFromXml(xml); + } else if (xmlName == "cards") { + loadCardsFromXml(xml); + } else if (!xmlName.isEmpty()) { + qCInfo(CockatriceXml4Log) << "Unknown item" << xmlName << ", trying to continue anyway"; + xml.skipCurrentElement(); + } + } + } + } + + if (xml.hasError()) { + QString preamble = tr("Parse error at line %1 col %2:").arg(xml.lineNumber()).arg(xml.columnNumber()); + qCWarning(CockatriceXml4Log).noquote() << preamble << xml.errorString(); + } +} + +static QSharedPointer parseFormat(QXmlStreamReader &xml) +{ + auto rulesPtr = FormatRulesPtr(new FormatRules()); + + if (xml.attributes().hasAttribute("formatName")) { + rulesPtr->formatName = xml.attributes().value("formatName").toString(); + } + + while (!xml.atEnd()) { + auto token = xml.readNext(); + + if (token == QXmlStreamReader::EndElement && xml.name().toString() == "format") { + break; + } + + if (token != QXmlStreamReader::StartElement) { + continue; + } + + QString xmlName = xml.name().toString(); + + if (xmlName == "minDeckSize") { + rulesPtr->minDeckSize = xml.readElementText().toInt(); + } else if (xmlName == "maxDeckSize") { + QString text = xml.readElementText(); + rulesPtr->maxDeckSize = text.toInt(); + } else if (xmlName == "maxSideboardSize") { + rulesPtr->maxSideboardSize = xml.readElementText().toInt(); + } else if (xmlName == "allowedCounts") { + while (!xml.atEnd()) { + token = xml.readNext(); + + if (token == QXmlStreamReader::EndElement && xml.name().toString() == "allowedCounts") { + break; + } + + if (token == QXmlStreamReader::StartElement && xml.name().toString() == "count") { + + AllowedCount c; + + QString maxAttr = xml.attributes().value("max").toString(); + c.max = (maxAttr == "unlimited") ? -1 : maxAttr.toInt(); + + c.label = xml.readElementText().trimmed(); + + rulesPtr->allowedCounts.append(c); + } + } + } else if (xmlName == "exceptions") { + while (!xml.atEnd()) { + token = xml.readNext(); + + if (token == QXmlStreamReader::EndElement && xml.name().toString() == "exceptions") { + break; + } + + if (token == QXmlStreamReader::StartElement && xml.name().toString() == "exception") { + ExceptionRule ex; + + while (!xml.atEnd()) { + token = xml.readNext(); + + if (token == QXmlStreamReader::EndElement && xml.name().toString() == "exception") { + break; + } + + if (token == QXmlStreamReader::StartElement) { + QString ename = xml.name().toString(); + + if (ename == "maxCopies") { + QString text = xml.readElementText(); + ex.maxCopies = (text == "unlimited") ? -1 : text.toInt(); + } else if (ename == "cardCondition") { + CardCondition cond; + cond.field = xml.attributes().value("field").toString(); + cond.matchType = xml.attributes().value("match").toString(); + cond.value = xml.attributes().value("value").toString(); + ex.conditions.append(cond); + xml.skipCurrentElement(); + } else { + xml.skipCurrentElement(); + } + } + } + + rulesPtr->exceptions.append(ex); + } + } + } else { + xml.skipCurrentElement(); + } + } + + return rulesPtr; +} + +void CockatriceXml4Parser::loadFormats(QXmlStreamReader &xml) +{ + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + + if (xml.name().toString() == "format") { + auto rulesPtr = parseFormat(xml); + emit addFormat(rulesPtr); + } + } +} + +void CockatriceXml4Parser::loadSetsFromXml(QXmlStreamReader &xml) +{ + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + + auto xmlName = xml.name().toString(); + if (xmlName == "set") { + QString shortName, longName, setType; + QDate releaseDate; + short priority; + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + xmlName = xml.name().toString(); + + if (xmlName == "name") { + shortName = xml.readElementText(QXmlStreamReader::IncludeChildElements); + } else if (xmlName == "longname") { + longName = xml.readElementText(QXmlStreamReader::IncludeChildElements); + } else if (xmlName == "settype") { + setType = xml.readElementText(QXmlStreamReader::IncludeChildElements); + } else if (xmlName == "releasedate") { + releaseDate = + QDate::fromString(xml.readElementText(QXmlStreamReader::IncludeChildElements), Qt::ISODate); + } else if (xmlName == "priority") { + priority = xml.readElementText(QXmlStreamReader::IncludeChildElements).toShort(); + } else if (!xmlName.isEmpty()) { + qCInfo(CockatriceXml4Log) << "Unknown set property" << xmlName << ", trying to continue anyway"; + xml.skipCurrentElement(); + } + } + + internalAddSet(shortName, longName, setType, releaseDate, static_cast(priority)); + } + } +} + +QVariantHash CockatriceXml4Parser::loadCardPropertiesFromXml(QXmlStreamReader &xml) +{ + QVariantHash properties = QVariantHash(); + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + + auto xmlName = xml.name().toString(); + if (!xmlName.isEmpty()) { + properties.insert(xmlName, xml.readElementText(QXmlStreamReader::IncludeChildElements)); + } + } + return properties; +} + +void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) +{ + bool includeRebalancedCards = cardPreferenceProvider->getIncludeRebalancedCards(); + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + + auto xmlName = xml.name().toString(); + + if (xmlName == "card") { + QString name = QString(""); + QString text = QString(""); + QVariantHash properties = QVariantHash(); + QList relatedCards, reverseRelatedCards; + auto _sets = SetToPrintingsMap(); + int tableRow = 0; + bool cipt = false; + bool landscapeOrientation = false; + bool isToken = false; + bool upsideDown = false; + + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + + xmlName = xml.name().toString(); + + // variable - assigned properties + if (xmlName == "name") { + name = xml.readElementText(QXmlStreamReader::IncludeChildElements); + } else if (xmlName == "text") { + text = xml.readElementText(QXmlStreamReader::IncludeChildElements); + } else if (xmlName == "token") { + isToken = static_cast(xml.readElementText(QXmlStreamReader::IncludeChildElements).toInt()); + // generic properties + } else if (xmlName == "prop") { + properties = loadCardPropertiesFromXml(xml); + // positioning info + } else if (xmlName == "tablerow") { + tableRow = xml.readElementText(QXmlStreamReader::IncludeChildElements).toInt(); + } else if (xmlName == "cipt") { + cipt = (xml.readElementText(QXmlStreamReader::IncludeChildElements) == "1"); + } else if (xmlName == "landscapeOrientation") { + landscapeOrientation = (xml.readElementText(QXmlStreamReader::IncludeChildElements) == "1"); + } else if (xmlName == "upsidedown") { + upsideDown = (xml.readElementText(QXmlStreamReader::IncludeChildElements) == "1"); + // sets + } else if (xmlName == "set") { + // NOTE: attributes but be read before readElementText() + QXmlStreamAttributes attrs = xml.attributes(); + QString setName = xml.readElementText(QXmlStreamReader::IncludeChildElements); + auto set = internalAddSet(setName); + if (set->getEnabled()) { + PrintingInfo printingInfo(set); + for (QXmlStreamAttribute attr : attrs) { + QString attrName = attr.name().toString(); + if (attrName == "picURL") + attrName = "picurl"; + printingInfo.setProperty(attrName, attr.value().toString()); + } + + // This is very much a hack and not the right place to + // put this check, as it requires a reload of Cockatrice + // to be apply. + // + // However, this is also true of the `set->getEnabled()` + // check above (which is currently bugged as well), so + // we'll fix both at the same time. + if (includeRebalancedCards || printingInfo.getProperty("isRebalanced") != "true") { + _sets[setName].append(printingInfo); + } + } + // related cards + } else if (xmlName == "related" || xmlName == "reverse-related") { + CardRelationType attachType = CardRelationType::DoesNotAttach; + bool exclude = false; + bool variable = false; + bool persistent = false; + int count = 1; + QXmlStreamAttributes attrs = xml.attributes(); + QString cardName = xml.readElementText(QXmlStreamReader::IncludeChildElements); + if (attrs.hasAttribute("count")) { + if (attrs.value("count").toString().indexOf("x=") == 0) { + variable = true; + count = attrs.value("count").toString().remove(0, 2).toInt(); + } else if (attrs.value("count").toString().indexOf("x") == 0) { + variable = true; + } else { + count = attrs.value("count").toString().toInt(); + } + + if (count < 1) { + count = 1; + } + } + + if (attrs.hasAttribute("attach")) { + attachType = attrs.value("attach").toString() == "transform" ? CardRelationType::TransformInto + : CardRelationType::AttachTo; + } + + if (attrs.hasAttribute("exclude")) { + exclude = true; + } + + if (attrs.hasAttribute("persistent")) { + persistent = true; + } + + auto *relation = new CardRelation(cardName, attachType, exclude, variable, count, persistent); + if (xmlName == "reverse-related") { + reverseRelatedCards << relation; + } else { + relatedCards << relation; + } + } else if (!xmlName.isEmpty()) { + qCInfo(CockatriceXml4Log) << "Unknown card property" << xmlName << ", trying to continue anyway"; + xml.skipCurrentElement(); + } + } + + if (name.isEmpty()) { + qCWarning(CockatriceXml4Log) << "Encountered card with empty name; skipping"; + continue; + } + + CardInfo::UiAttributes attributes = {.cipt = cipt, + .landscapeOrientation = landscapeOrientation, + .tableRow = tableRow, + .upsideDownArt = upsideDown}; + CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards, + reverseRelatedCards, _sets, attributes); + emit addCard(newCard); + } + } +} + +static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const QSharedPointer &rulesPtr) +{ + if (rulesPtr.isNull()) { + qCWarning(CockatriceXml4Log) << "&operator<< FormatRules is nullptr"; + return xml; + } + + const FormatRules &rules = *rulesPtr; + + xml.writeStartElement("format"); + if (!rules.formatName.isEmpty()) { + xml.writeAttribute("formatName", rules.formatName); + } + + xml.writeTextElement("minDeckSize", QString::number(rules.minDeckSize)); + xml.writeTextElement("maxDeckSize", rules.maxDeckSize >= 0 ? QString::number(rules.maxDeckSize) : "0"); + xml.writeTextElement("maxSideboardSize", QString::number(rules.maxSideboardSize)); + if (!rules.allowedCounts.isEmpty()) { + xml.writeStartElement("allowedCounts"); + + for (const AllowedCount &c : rules.allowedCounts) { + xml.writeStartElement("count"); + xml.writeAttribute("max", c.max == -1 ? "unlimited" : QString::number(c.max)); + xml.writeCharacters(c.label); + xml.writeEndElement(); // count + } + + xml.writeEndElement(); // allowedCounts + } + + if (!rules.exceptions.isEmpty()) { + xml.writeStartElement("exceptions"); + for (const ExceptionRule &ex : rules.exceptions) { + xml.writeStartElement("exception"); + xml.writeTextElement("maxCopies", ex.maxCopies == -1 ? "unlimited" : QString::number(ex.maxCopies)); + + for (const CardCondition &cond : ex.conditions) { + xml.writeStartElement("cardCondition"); + xml.writeAttribute("field", cond.field); + xml.writeAttribute("match", cond.matchType); + xml.writeAttribute("value", cond.value); + xml.writeEndElement(); // cardCondition + } + + xml.writeEndElement(); // exception + } + xml.writeEndElement(); // exceptions + } + + xml.writeEndElement(); // format + return xml; +} + +static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardSetPtr &set) +{ + if (set.isNull()) { + qCWarning(CockatriceXml4Log) << "&operator<< set is nullptr"; + return xml; + } + + xml.writeStartElement("set"); + xml.writeTextElement("name", set->getShortName()); + xml.writeTextElement("longname", set->getLongName()); + xml.writeTextElement("settype", set->getSetType()); + xml.writeTextElement("releasedate", set->getReleaseDate().toString(Qt::ISODate)); + xml.writeTextElement("priority", QString::number(set->getPriority())); + xml.writeEndElement(); + + return xml; +} + +static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &info) +{ + if (info.isNull()) { + qCWarning(CockatriceXml4Log) << "operator<< info is nullptr"; + return xml; + } + + QString tmpString; + + xml.writeStartElement("card"); + + // variable - assigned properties + xml.writeTextElement("name", info->getName()); + xml.writeTextElement("text", info->getText()); + if (info->getIsToken()) { + xml.writeTextElement("token", "1"); + } + + // generic properties + xml.writeStartElement("prop"); + for (QString propName : info->getProperties()) { + xml.writeTextElement(propName, info->getProperty(propName)); + } + xml.writeEndElement(); + + // sets + for (const auto &printings : info->getSets()) { + for (const PrintingInfo &set : printings) { + xml.writeStartElement("set"); + for (const QString &propName : set.getProperties()) { + xml.writeAttribute(propName, set.getProperty(propName)); + } + + xml.writeCharacters(set.getSet()->getShortName()); + xml.writeEndElement(); + } + } + + // related cards + const QList related = info->getRelatedCards(); + for (auto i : related) { + xml.writeStartElement("related"); + if (i->getDoesAttach()) { + xml.writeAttribute("attach", i->getAttachTypeAsString()); + } + if (i->getIsCreateAllExclusion()) { + xml.writeAttribute("exclude", "exclude"); + } + if (i->getIsPersistent()) { + xml.writeAttribute("persistent", "persistent"); + } + if (i->getIsVariable()) { + if (1 == i->getDefaultCount()) { + xml.writeAttribute("count", "x"); + } else { + xml.writeAttribute("count", "x=" + QString::number(i->getDefaultCount())); + } + } else if (1 != i->getDefaultCount()) { + xml.writeAttribute("count", QString::number(i->getDefaultCount())); + } + xml.writeCharacters(i->getName()); + xml.writeEndElement(); + } + const QList reverseRelated = info->getReverseRelatedCards(); + for (auto i : reverseRelated) { + xml.writeStartElement("reverse-related"); + if (i->getDoesAttach()) { + xml.writeAttribute("attach", i->getAttachTypeAsString()); + } + + if (i->getIsCreateAllExclusion()) { + xml.writeAttribute("exclude", "exclude"); + } + + if (i->getIsPersistent()) { + xml.writeAttribute("persistent", "persistent"); + } + if (i->getIsVariable()) { + if (1 == i->getDefaultCount()) { + xml.writeAttribute("count", "x"); + } else { + xml.writeAttribute("count", "x=" + QString::number(i->getDefaultCount())); + } + } else if (1 != i->getDefaultCount()) { + xml.writeAttribute("count", QString::number(i->getDefaultCount())); + } + xml.writeCharacters(i->getName()); + xml.writeEndElement(); + } + + // positioning + const CardInfo::UiAttributes &attributes = info->getUiAttributes(); + xml.writeTextElement("tablerow", QString::number(attributes.tableRow)); + if (attributes.cipt) { + xml.writeTextElement("cipt", "1"); + } + if (attributes.landscapeOrientation) { + xml.writeTextElement("landscapeOrientation", "1"); + } + if (attributes.upsideDownArt) { + xml.writeTextElement("upsidedown", "1"); + } + + xml.writeEndElement(); // card + + return xml; +} + +bool CockatriceXml4Parser::saveToFile(FormatRulesNameMap _formats, + SetNameMap _sets, + CardNameMap cards, + const QString &fileName, + const QString &sourceUrl, + const QString &sourceVersion) +{ + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + return false; + } + + QXmlStreamWriter xml(&file); + + xml.setAutoFormatting(true); + xml.writeStartDocument(); + xml.writeStartElement(COCKATRICE_XML4_TAGNAME); + xml.writeAttribute("version", QString::number(COCKATRICE_XML4_TAGVER)); + xml.writeAttribute("xmlns:xsi", COCKATRICE_XML_XSI_NAMESPACE); + xml.writeAttribute("xsi:schemaLocation", COCKATRICE_XML4_SCHEMALOCATION); + + xml.writeStartElement("info"); + xml.writeTextElement("author", QCoreApplication::applicationName() + QString(" %1").arg(VERSION_STRING)); + xml.writeTextElement("createdAt", QDateTime::currentDateTimeUtc().toString(Qt::ISODate)); + xml.writeTextElement("sourceUrl", sourceUrl); + xml.writeTextElement("sourceVersion", sourceVersion); + xml.writeEndElement(); + + if (_formats.count() > 0) { + xml.writeStartElement("formats"); + for (FormatRulesPtr format : _formats) { + xml << format; + } + xml.writeEndElement(); + } + + if (_sets.count() > 0) { + xml.writeStartElement("sets"); + for (CardSetPtr set : _sets) { + xml << set; + } + xml.writeEndElement(); + } + + if (cards.count() > 0) { + xml.writeStartElement("cards"); + for (CardInfoPtr card : cards) { + xml << card; + } + xml.writeEndElement(); + } + + xml.writeEndElement(); // cockatrice_carddatabase + xml.writeEndDocument(); + + return true; +} diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.h b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.h new file mode 100644 index 000000000..189e79535 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.h @@ -0,0 +1,86 @@ +#ifndef COCKATRICE_XML4_H +#define COCKATRICE_XML4_H + +#include "card_database_parser.h" + +#include +#include +#include + +inline Q_LOGGING_CATEGORY(CockatriceXml4Log, "cockatrice_xml.xml_4_parser"); + +/** + * @class CockatriceXml4Parser + * @ingroup CardDatabase + * @brief Parses version 4 of the Cockatrice XML Schema. + * + * This parser reads a Cockatrice XML4 database and emits CardInfoPtr + * and CardSetPtr objects. Card properties are read inside blocks, + * making the parser more extensible and schema-compliant. + * + * @note Differences from v3: + * - Card properties are stored in blocks as a QVariantHash. + * - Sets can include a element. + * - Supports user preferences via ICardPreferenceProvider (e.g., skipping rebalanced cards). + * - Related cards support persistent relations and multiple attach types (e.g., transform). + * - More robust serialization; easier to extend schema in the future. + */ +class CockatriceXml4Parser : public ICardDatabaseParser +{ + Q_OBJECT +public: + explicit CockatriceXml4Parser(ICardPreferenceProvider *cardPreferenceProvider, + ICardSetPriorityController *cardSetPriorityController); + ~CockatriceXml4Parser() override = default; + + /** + * @brief Determines if the parser can handle this file. + * @param name File name. + * @param device Open QIODevice containing the XML. + * @return True if the file is a Cockatrice XML4 database. + */ + bool getCanParseFile(const QString &name, QIODevice &device) override; + + /** + * @brief Parse the XML database. + * @param device Open QIODevice positioned at start of file. + */ + void parseFile(QIODevice &device) override; + + /** + * @brief Save sets and cards back to an XML4 file. + */ + bool saveToFile(FormatRulesNameMap _formats, + SetNameMap _sets, + CardNameMap cards, + const QString &fileName, + const QString &sourceUrl = "unknown", + const QString &sourceVersion = "unknown") override; + +private: + ICardPreferenceProvider *cardPreferenceProvider; ///< Interface to handle user preferences + + /** + * @brief Loads a generic block from a element. + * @param xml The open QXmlStreamReader positioned at a element. + * @return A QVariantHash mapping property names to values. + */ + QVariantHash loadCardPropertiesFromXml(QXmlStreamReader &xml); + + /** + * @brief Load all elements from the XML stream. + * @param xml The open QXmlStreamReader positioned at the element. + * Honors the user's preference regarding rebalanced cards. + */ + void loadCardsFromXml(QXmlStreamReader &xml); + + void loadFormats(QXmlStreamReader &xml); + /** + * @brief Load all elements from the XML stream. + * @param xml The open QXmlStreamReader positioned at the element. + * Parses nodes including priority information. + */ + void loadSetsFromXml(QXmlStreamReader &xml); +}; + +#endif \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/format/format_legality_rules.cpp b/libcockatrice_card/libcockatrice/card/format/format_legality_rules.cpp new file mode 100644 index 000000000..a6b3bb038 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/format/format_legality_rules.cpp @@ -0,0 +1,53 @@ +#include "format_legality_rules.h" + +#include + +bool cardMatchesCondition(const CardInfo &card, const CardCondition &cond) +{ + CardMatchType type = matchTypeFromString(cond.matchType); + QString fieldValue; + if (cond.field == "name") { + fieldValue = card.getName(); + } else if (cond.field == "text") { + fieldValue = card.getText(); + } else { + fieldValue = card.getProperty(cond.field); + } + + switch (type) { + case CardMatchType::Equals: + return fieldValue == cond.value; + case CardMatchType::NotEquals: + return fieldValue != cond.value; + case CardMatchType::Contains: + return fieldValue.contains(cond.value, Qt::CaseInsensitive); + case CardMatchType::NotContains: + return !fieldValue.contains(cond.value, Qt::CaseInsensitive); + case CardMatchType::Regex: { + QRegularExpression re(cond.value, QRegularExpression::CaseInsensitiveOption); + return re.match(fieldValue).hasMatch(); + } + default: + return false; + } +} + +bool exceptionAppliesToCard(const CardInfo &card, const ExceptionRule &rule) +{ + for (const CardCondition &cond : rule.conditions) { + if (!cardMatchesCondition(card, cond)) { + return false; // all conditions must match + } + } + return true; +} + +bool cardHasAnyException(const CardInfo &card, const FormatRules &format) +{ + for (const ExceptionRule &rule : format.exceptions) { + if (exceptionAppliesToCard(card, rule)) { + return true; + } + } + return false; +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/format/format_legality_rules.h b/libcockatrice_card/libcockatrice/card/format/format_legality_rules.h new file mode 100644 index 000000000..16f2359ab --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/format/format_legality_rules.h @@ -0,0 +1,73 @@ +#ifndef COCKATRICE_FORMAT_LEGALITY_RULES_H +#define COCKATRICE_FORMAT_LEGALITY_RULES_H + +#include +#include +#include + +class CardInfo; +using CardInfoPtr = QSharedPointer; + +struct CardCondition +{ + QString field; // e.g. "type", "maintype", "text" + QString matchType; // "contains", "equals", "regex", "notContains", etc. + QString value; // e.g. "Basic Land" +}; + +struct AllowedCount +{ + int max = 0; // 4, 1, 0, or -1 for unlimited + QString label; // "legal", "restricted", "banned" +}; + +struct ExceptionRule +{ + QList conditions; // All must match + int maxCopies = -1; // -1 = unlimited +}; + +struct FormatRules +{ + QString formatName; + int minDeckSize = 60; + int maxDeckSize = -1; // -1 = unlimited + int maxSideboardSize = 15; + + QList allowedCounts; + + QList exceptions; // Cards allowed to break maxCopies +}; + +enum class CardMatchType +{ + Equals, + NotEquals, + Contains, + NotContains, + Regex +}; + +// convert string to enum +inline CardMatchType matchTypeFromString(const QString &str) +{ + if (str == "equals") + return CardMatchType::Equals; + if (str == "notEquals") + return CardMatchType::NotEquals; + if (str == "contains") + return CardMatchType::Contains; + if (str == "notContains") + return CardMatchType::NotContains; + if (str == "regex") + return CardMatchType::Regex; + return CardMatchType::Equals; // fallback default +} + +bool cardMatchesCondition(const CardInfo &card, const CardCondition &cond); + +bool exceptionAppliesToCard(const CardInfo &card, const ExceptionRule &rule); + +bool cardHasAnyException(const CardInfo &card, const FormatRules &format); + +#endif // COCKATRICE_FORMAT_LEGALITY_RULES_H diff --git a/libcockatrice_card/libcockatrice/card/game_specific_terms.h b/libcockatrice_card/libcockatrice/card/game_specific_terms.h new file mode 100644 index 000000000..2931365ad --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/game_specific_terms.h @@ -0,0 +1,58 @@ +/** + * @file game_specific_terms.h + * @ingroup Cards + * @brief TODO: Document this. + */ + +#ifndef GAME_SPECIFIC_TERMS_H +#define GAME_SPECIFIC_TERMS_H + +#include +#include + +/* + * Collection of traslatable property names used in games, + * so we can use Game::Property instead of hardcoding strings. + * Note: Mtg = "Maybe that game" + */ + +namespace Mtg +{ +QString const CardType("type"); +QString const ConvertedManaCost("cmc"); +QString const Colors("colors"); +QString const Loyalty("loyalty"); +QString const MainCardType("maintype"); +QString const ManaCost("manacost"); +QString const PowTough("pt"); +QString const Side("side"); +QString const Layout("layout"); +QString const ColorIdentity("coloridentity"); + +inline static const QString getNicePropertyName(QString key) +{ + if (key == CardType) + return QCoreApplication::translate("Mtg", "Card Type"); + if (key == ConvertedManaCost) + return QCoreApplication::translate("Mtg", "Mana Value"); + if (key == Colors) + return QCoreApplication::translate("Mtg", "Color(s)"); + if (key == Loyalty) + return QCoreApplication::translate("Mtg", "Loyalty"); + if (key == MainCardType) + return QCoreApplication::translate("Mtg", "Main Card Type"); + if (key == ManaCost) + return QCoreApplication::translate("Mtg", "Mana Cost"); + if (key == PowTough) + return QCoreApplication::translate("Mtg", "P/T"); + if (key == Side) + return QCoreApplication::translate("Mtg", "Side"); + if (key == Layout) + return QCoreApplication::translate("Mtg", "Layout"); + if (key == ColorIdentity) + return QCoreApplication::translate("Mtg", "Color Identity"); + return key; +} +} // namespace Mtg + +#endif diff --git a/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.cpp b/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.cpp new file mode 100644 index 000000000..91ebd6647 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.cpp @@ -0,0 +1,66 @@ +#include "card_name_normalizer.h" + +#include "../database/card_database_manager.h" +#include "../printing/exact_card.h" + +#include + +/** + * @brief Resolves the complete display name of a card. + * @param cardName Base name. + * @return Full display name, or the cardName unchanged if a display name is not found. + */ +static QString getCompleteCardName(const QString &cardName) +{ + ExactCard temp = CardDatabaseManager::query()->guessCard({cardName}); + if (temp) { + return temp.getName(); + } + + return cardName; +} + +QString CardNameNormalizer::operator()(const QString &cardNameString) const +{ + QString cardName = cardNameString; + + // Regex for advanced card parsing + static const QRegularExpression reSplitCard(R"( ?\/\/ ?)"); + static const QRegularExpression reBrace(R"( ?[\[\{][^\]\}]*[\]\}] ?)"); // not nested + static const QRegularExpression reRoundBrace(R"(^\([^\)]*\) ?)"); // () are only matched at start of string + static const QRegularExpression reDigitBrace(R"( ?\(\d*\) ?)"); // () are matched if containing digits + static const QRegularExpression reBraceDigit( + R"( ?\([\dA-Z]+\) *\d+$)"); // () are matched if containing setcode then a number + static const QRegularExpression reDoubleFacedMarker(R"( ?\(Transform\) ?)"); + + static const QHash differences{{QRegularExpression("’"), "'"}, + {QRegularExpression("Æ"), "Ae"}, + {QRegularExpression("æ"), "ae"}, + {QRegularExpression(" ?[|/]+ ?"), " // "}}; + + // Handle advanced card types + if (cardName.contains(reSplitCard)) { + cardName = cardName.split(reSplitCard).join(" // "); + } + + if (cardName.contains(reDoubleFacedMarker)) { + QStringList faces = cardName.split(reDoubleFacedMarker); + cardName = faces.first().trimmed(); + } + + // Remove unnecessary characters + cardName.remove(reBrace); + cardName.remove(reRoundBrace); // I'll be entirely honest here, these are split to accommodate just three cards + cardName.remove(reDigitBrace); // from un-sets that have a word in between round braces at the end + cardName.remove(reBraceDigit); // very specific format with the set code in () and collectors number after + + // Normalize characters + for (auto diff = differences.constBegin(); diff != differences.constEnd(); ++diff) { + cardName.replace(diff.key(), diff.value()); + } + + // Resolve complete card name + cardName = getCompleteCardName(cardName); + + return cardName; +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.h b/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.h new file mode 100644 index 000000000..716e7da80 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.h @@ -0,0 +1,15 @@ +#ifndef COCKATRICE_CARD_NAME_NORMALIZER_H +#define COCKATRICE_CARD_NAME_NORMALIZER_H + +#include + +/** + * Functor that normalizes the raw card name parsed during a plaintext deck import into the card name that Cockatrice + * uses. + */ +struct CardNameNormalizer +{ + QString operator()(const QString &cardNameString) const; +}; + +#endif // COCKATRICE_CARD_NAME_NORMALIZER_H diff --git a/libcockatrice_card/libcockatrice/card/printing/exact_card.cpp b/libcockatrice_card/libcockatrice/card/printing/exact_card.cpp new file mode 100644 index 000000000..993b5b96e --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/printing/exact_card.cpp @@ -0,0 +1,82 @@ +#include "exact_card.h" + +#include "../card_info.h" +#include "printing_info.h" + +/** + * Default constructor. + * This will set the CardInfoPtr to null. + * The printing will be the default-constructed PrintingInfo. + */ +ExactCard::ExactCard() +{ +} + +/** + * @param _card The card. Can be null. + * @param _printing The printing. Can be empty. + */ +ExactCard::ExactCard(const CardInfoPtr &_card, const PrintingInfo &_printing) : card(_card), printing(_printing) +{ +} + +bool ExactCard::operator==(const ExactCard &other) const +{ + return this->card == other.card && this->printing == other.printing; +} + +/** + * Convenience method to safely get the card's name. + * @return The name in the CardInfo, or an empty string if card is null + */ +QString ExactCard::getName() const +{ + return card.isNull() ? "" : card->getName(); +} + +/** + * Gets a view of the underlying cardInfoPtr. + * @return A const reference to the CardInfo, or an empty CardInfo if card is null + */ +const CardInfo &ExactCard::getInfo() const +{ + if (card.isNull()) { + static CardInfoPtr emptyCard = CardInfo::newInstance(""); + return *emptyCard; + } + return *card; +} + +/** + * The key used to identify this exact printing in the cache + */ +QString ExactCard::getPixmapCacheKey() const +{ + QString uuid = printing.getUuid(); + QString suffix = uuid.isEmpty() ? "" : "_" + uuid; + return QLatin1String("card_") + card->getName() + suffix; +} + +/** + * Checks if the card is null or empty. + */ +bool ExactCard::isEmpty() const +{ + return card.isNull() || card->getName().isEmpty(); +} + +/** + * Returns true if isEmpty() is false + */ +ExactCard::operator bool() const +{ + return !isEmpty(); +} + +/** + * Gets the CardInfo to emit the pixmapUpdated signal + */ +void ExactCard::emitPixmapUpdated() const +{ + emit card->pixmapUpdated(printing); +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/printing/exact_card.h b/libcockatrice_card/libcockatrice/card/printing/exact_card.h new file mode 100644 index 000000000..74fc75da3 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/printing/exact_card.h @@ -0,0 +1,119 @@ +#ifndef EXACT_CARD_H +#define EXACT_CARD_H + +#include "../card_info.h" + +/** + * @class ExactCard + * @ingroup CardPrintings + * + * @brief Represents a specific card instance, defined by its CardInfo + * and a particular printing. + * + * An ExactCard identifies a card not only by its underlying CardInfoPtr + * (which may be null), but also by its PrintingInfo, which specifies the + * exact printing/variant. This allows distinguishing between different + * printings of the same logical card (e.g., different sets, promos, foils). + */ +class ExactCard +{ + CardInfoPtr card; + PrintingInfo printing; + +public: + /** + * @brief Constructs an empty ExactCard. + * + * The CardInfoPtr will be null, and PrintingInfo will be default-constructed. + * An empty ExactCard represents "no card". + */ + ExactCard(); + + /** + * @brief Constructs an ExactCard from a card and printing. + * + * @param _card The card info pointer. May be null. + * @param _printing The printing details. Defaults to an empty PrintingInfo. + */ + explicit ExactCard(const CardInfoPtr &_card, const PrintingInfo &_printing = PrintingInfo()); + + /** + * @brief Returns the underlying CardInfoPtr. + * + * May be null if the ExactCard is empty. + */ + [[nodiscard]] CardInfoPtr getCardPtr() const + { + return card; + } + + /** + * @brief Returns the printing information associated with this card. + * + * May be empty if no specific printing was assigned. + */ + [[nodiscard]] PrintingInfo getPrinting() const + { + return printing; + } + + /** + * @brief Compares both card pointer and printing for equality. + * + * Two ExactCard objects are equal only if both their CardInfoPtr and + * PrintingInfo values are equal. + */ + bool operator==(const ExactCard &other) const; + + /** + * @brief Convenience helper to get the card's display name. + * + * @return The card's name, or an empty string if the CardInfoPtr is null. + */ + [[nodiscard]] QString getName() const; + + /** + * @brief Returns a reference to the underlying CardInfo object. + * + * If the CardInfoPtr is null, returns a reference to a static empty CardInfo + * instance instead. This avoids null-dereferencing but means modifications + * to the returned object do not affect the ExactCard. + * + * @return A const reference to the CardInfo object. + */ + [[nodiscard]] const CardInfo &getInfo() const; + + /** + * @brief Generates a stable cache key for pixmap caching. + * + * The key includes the card's name and (if present) the printing UUID, + * allowing different printings of the same card to map to different cache entries. + */ + [[nodiscard]] QString getPixmapCacheKey() const; + + /** + * @brief Indicates whether this ExactCard represents no valid card. + * + * An ExactCard is considered empty if the CardInfoPtr is null or the + * card's name is empty. + */ + [[nodiscard]] bool isEmpty() const; + + /** + * @brief Boolean conversion indicating whether the card is valid (non-empty). + * + * @return true if not empty, false otherwise. + */ + explicit operator bool() const; + + /** + * @brief Emits the pixmapUpdated signal on the underlying CardInfo. + * + * Assumes CardInfoPtr is non-null. If called on an empty ExactCard, + * the behavior is undefined. + */ + void emitPixmapUpdated() const; +}; +Q_DECLARE_METATYPE(ExactCard) + +#endif // EXACT_CARD_H diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp new file mode 100644 index 000000000..340998565 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp @@ -0,0 +1,20 @@ +#include "printing_info.h" + +#include "../set/card_set.h" + +PrintingInfo::PrintingInfo(const CardSetPtr &_set) : set(_set) +{ +} + +/** + * Gets the uuid property of the printing, or an empty string if the property isn't present + */ +QString PrintingInfo::getUuid() const +{ + return properties.value("uuid").toString(); +} + +QString PrintingInfo::getFlavorName() const +{ + return properties.value("flavorName").toString(); +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.h b/libcockatrice_card/libcockatrice/card/printing/printing_info.h new file mode 100644 index 000000000..ad7b33654 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.h @@ -0,0 +1,131 @@ +#ifndef COCKATRICE_PRINTING_INFO_H +#define COCKATRICE_PRINTING_INFO_H + +#include "../set/card_set.h" + +#include +#include +#include + +class PrintingInfo; + +using SetToPrintingsMap = QMap>; + +/** + * @class PrintingInfo + * @ingroup CardPrintings + * + * @brief Represents metadata for a specific variation of a card within a set. + * + * A card can have multiple variations across sets. PrintingInfo associates + * a card with one such variation, and provides per-printing attributes + * such as identifiers or additional properties. + * + * Equality is defined as both the set and the property values being equal. + */ +class PrintingInfo +{ +public: + /** + * @brief Constructs a PrintingInfo associated with a specific set. + * + * @param _set The set this printing belongs to (defaults to null). + */ + explicit PrintingInfo(const CardSetPtr &_set = nullptr); + + /** + * @brief Destroys the PrintingInfo. + * + * Defaulted since no special cleanup is required. + */ + ~PrintingInfo() = default; + + /** + * @brief Equality operator. + * + * Two PrintingInfo objects are equal if they refer to the same set + * and contain the exact same property key/value pairs. + * + * @param other Another PrintingInfo to compare against. + * @return True if both set and properties are equal, otherwise false. + */ + bool operator==(const PrintingInfo &other) const + { + return this->set == other.set && this->properties == other.properties; + } + + /** + * @brief check if the info is empty, as if default constructed. + * + * @return True if both set and properties are empty, otherwise false. + */ + bool isEmpty() const + { + return set == nullptr && properties.isEmpty(); + } + +private: + CardSetPtr set; ///< The set this variation belongs to. + QVariantHash properties; ///< Key-value store for variation-specific attributes. + +public: + /** + * @brief Returns the set this printing belongs to. + * + * @return Pointer to the associated CardSet. + */ + [[nodiscard]] CardSetPtr getSet() const + { + return set; + } + + /** + * @brief Returns the list of property names defined for this printing. + * + * @return List of keys stored in the properties map. + */ + [[nodiscard]] QStringList getProperties() const + { + return properties.keys(); + } + + /** + * @brief Retrieves the value of a specific property. + * + * @param propertyName The key name of the property to query. + * @return The property value as a string, or an empty string if not set. + */ + [[nodiscard]] QString getProperty(const QString &propertyName) const + { + return properties.value(propertyName).toString(); + } + + /** + * @brief Sets or updates the value of a specific property. + * + * If the property already exists, its value is replaced. + * + * @param _name The name of the property. + * @param _value The string value to assign. + */ + void setProperty(const QString &_name, const QString &_value) + { + properties.insert(_name, _value); + } + + /** + * @brief Returns the providerID for this printing. + * + * @return A string representing the providerID. + */ + [[nodiscard]] QString getUuid() const; + + /** + * @brief Returns the flavorName for this printing. + * + * @return The flavorName, or empty if it isn't present. + */ + [[nodiscard]] QString getFlavorName() const; +}; + +#endif // COCKATRICE_PRINTING_INFO_H diff --git a/libcockatrice_card/libcockatrice/card/relation/card_relation.cpp b/libcockatrice_card/libcockatrice/card/relation/card_relation.cpp new file mode 100644 index 000000000..90e59e439 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/relation/card_relation.cpp @@ -0,0 +1,14 @@ +#include "card_relation.h" + +#include "card_relation_type.h" + +CardRelation::CardRelation(const QString &_name, + CardRelationType _attachType, + bool _isCreateAllExclusion, + bool _isVariableCount, + int _defaultCount, + bool _isPersistent) + : name(_name), attachType(_attachType), isCreateAllExclusion(_isCreateAllExclusion), + isVariableCount(_isVariableCount), defaultCount(_defaultCount), isPersistent(_isPersistent) +{ +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/relation/card_relation.h b/libcockatrice_card/libcockatrice/card/relation/card_relation.h new file mode 100644 index 000000000..9ff704097 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/relation/card_relation.h @@ -0,0 +1,156 @@ +#ifndef COCKATRICE_CARD_RELATION_H +#define COCKATRICE_CARD_RELATION_H + +#include "card_relation_type.h" + +#include +#include + +/** + * @class CardRelation + * @ingroup Cards + * + * @brief Represents a relationship between two cards. + * + * CardRelation objects define directional relationships, such as: + * - One card attaching to another. + * - One card transforming into another. + * - One card creating another instance. + * + * Relations may also define metadata such as whether multiple creations + * are possible, whether the relation is persistent, and default counts. + */ +class CardRelation : public QObject +{ + Q_OBJECT + +private: + QString name; ///< Name of the related card. + CardRelationType attachType; ///< Type of attachment. + bool isCreateAllExclusion; ///< True if this relation should exclude multiple creations in "create all" operations. + bool isVariableCount; ///< True if the number of creations is variable. + int defaultCount; ///< Default number of cards created or involved. + bool isPersistent; ///< True if this relation persists (i.e. is not destroyed) on zone change. + +public: + /** + * @brief Constructs a CardRelation with optional parameters. + * + * @param _name Name of the related card. + * @param _attachType Type of attachment. + * @param _isCreateAllExclusion Whether this relation excludes mass creation. + * @param _isVariableCount Whether the count is variable. + * @param _defaultCount Default number for creations or transformations. + * @param _isPersistent Whether the relation persists across zone changes. + */ + explicit CardRelation(const QString &_name = QString(), + CardRelationType _attachType = CardRelationType::DoesNotAttach, + bool _isCreateAllExclusion = false, + bool _isVariableCount = false, + int _defaultCount = 1, + bool _isPersistent = false); + + /** + * @brief Returns the name of the related card. + * + * @return Name as QString reference. + */ + [[nodiscard]] inline const QString &getName() const + { + return name; + } + + /** + * @brief Returns the type of attachment. + * + * @return Enum value representing the attachment type. + */ + [[nodiscard]] CardRelationType getAttachType() const + { + return attachType; + } + + /** + * @brief Returns true if the card is attached to another. + * + * @return True if attached, false otherwise. + */ + [[nodiscard]] bool getDoesAttach() const + { + return attachType != CardRelationType::DoesNotAttach; + } + + /** + * @brief Returns true if this card transforms into another card. + * + * @return True if it transforms, false otherwise. + */ + [[nodiscard]] bool getDoesTransform() const + { + return attachType == CardRelationType::TransformInto; + } + + /** + * @brief Returns a string description of the attachment type. + * + * @return "attach" for AttachTo, "transform" for TransformInto, empty string otherwise. + */ + [[nodiscard]] QString getAttachTypeAsString() const + { + return cardAttachTypeToString(attachType); + } + + /** + * @brief Determines whether another instance can be created. + * + * @return True if creation is allowed, false if constrained by attachment. + */ + [[nodiscard]] bool getCanCreateAnother() const + { + return !getDoesAttach(); + } + + /** + * @brief Returns whether this relation is excluded from "create all" operations. + * + * @return True if excluded, false otherwise. + */ + [[nodiscard]] bool getIsCreateAllExclusion() const + { + return isCreateAllExclusion; + } + + /** + * @brief Returns whether the relation count is variable. + * + * @return True if variable, false otherwise. + */ + [[nodiscard]] bool getIsVariable() const + { + return isVariableCount; + } + + /** + * @brief Returns the default count of related cards. + * + * @return Integer representing default number. + */ + [[nodiscard]] int getDefaultCount() const + { + return defaultCount; + } + + /** + * @brief Returns whether the relation is persistent. + * + * Persistent relations are not destroyed on zone changes. + * + * @return True if persistent, false otherwise. + */ + [[nodiscard]] bool getIsPersistent() const + { + return isPersistent; + } +}; + +#endif // COCKATRICE_CARD_RELATION_H diff --git a/libcockatrice_card/libcockatrice/card/relation/card_relation_type.h b/libcockatrice_card/libcockatrice/card/relation/card_relation_type.h new file mode 100644 index 000000000..9bd85f3ac --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/relation/card_relation_type.h @@ -0,0 +1,35 @@ +#ifndef COCKATRICE_CARD_RELATION_TYPE_H +#define COCKATRICE_CARD_RELATION_TYPE_H + +#include + +/** + * @enum CardRelationType + * @ingroup Cards + * @brief Types of attachments between cards. + * + * DoesNotAttach: No attachment is present. + * AttachTo: This card attaches to another card. + * TransformInto: This card transforms into another card. + */ +enum class CardRelationType +{ + DoesNotAttach = 0, + AttachTo = 1, + TransformInto = 2, +}; + +// Helper function to transform the enum values into human-readable strings +inline QString cardAttachTypeToString(CardRelationType type) +{ + switch (type) { + case CardRelationType::AttachTo: + return "attach"; + case CardRelationType::TransformInto: + return "transform"; + default: + return ""; + } +} + +#endif // COCKATRICE_CARD_RELATION_TYPE_H diff --git a/libcockatrice_card/libcockatrice/card/set/card_set.cpp b/libcockatrice_card/libcockatrice/card/set/card_set.cpp new file mode 100644 index 000000000..20d0aced8 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/set/card_set.cpp @@ -0,0 +1,85 @@ +#include "card_set.h" + +#include +#include + +const char *CardSet::TOKENS_SETNAME = "TK"; + +CardSet::CardSet(ICardSetPriorityController *_priorityController, + const QString &_shortName, + const QString &_longName, + const QString &_setType, + const QDate &_releaseDate, + const CardSet::Priority _priority) + : priorityController(std::move(_priorityController)), shortName(_shortName), longName(_longName), + releaseDate(_releaseDate), setType(_setType), priority(_priority) +{ + loadSetOptions(); +} + +CardSetPtr CardSet::newInstance(ICardSetPriorityController *_priorityController, + const QString &_shortName, + const QString &_longName, + const QString &_setType, + const QDate &_releaseDate, + const Priority _priority) +{ + CardSetPtr ptr(new CardSet(_priorityController, _shortName, _longName, _setType, _releaseDate, _priority)); + // ptr->setSmartPointer(ptr); + return ptr; +} + +QString CardSet::getCorrectedShortName() const +{ + // For Windows machines. + QSet invalidFileNames; + invalidFileNames << "CON" + << "PRN" + << "AUX" + << "NUL" + << "COM1" + << "COM2" + << "COM3" + << "COM4" + << "COM5" + << "COM6" + << "COM7" + << "COM8" + << "COM9" + << "LPT1" + << "LPT2" + << "LPT3" + << "LPT4" + << "LPT5" + << "LPT6" + << "LPT7" + << "LPT8" + << "LPT9"; + + return invalidFileNames.contains(shortName) ? shortName + "_" : shortName; +} + +void CardSet::loadSetOptions() +{ + sortKey = priorityController->getSortKey(shortName); + enabled = priorityController->isEnabled(shortName); + isknown = priorityController->isKnown(shortName); +} + +void CardSet::setSortKey(unsigned int _sortKey) +{ + sortKey = _sortKey; + priorityController->setSortKey(shortName, _sortKey); +} + +void CardSet::setEnabled(bool _enabled) +{ + enabled = _enabled; + priorityController->setEnabled(shortName, _enabled); +} + +void CardSet::setIsKnown(bool _isknown) +{ + isknown = _isknown; + priorityController->setIsKnown(shortName, _isknown); +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/set/card_set.h b/libcockatrice_card/libcockatrice/card/set/card_set.h new file mode 100644 index 000000000..fe4b66522 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/set/card_set.h @@ -0,0 +1,231 @@ +#ifndef COCKATRICE_CARD_SET_H +#define COCKATRICE_CARD_SET_H + +#include +#include +#include +#include +#include + +class CardInfo; +using CardInfoPtr = QSharedPointer; + +class CardSet; +using CardSetPtr = QSharedPointer; + +/** + * @class CardSet + * @ingroup CardSets + * + * @brief A collection of cards grouped under a common identifier. + * + * A set serves both as metadata (identifier, title, category, release date, and priority) + * and as a container of all cards that belong to it. Each set can be enabled/disabled + * and marked as known/unknown depending on context. + * + * The class inherits from `QList`, so it can be iterated over directly + * to access its contents. + * + * Typical usage: + * - Query metadata such as identifier, category, or release date. + * - Enable or disable sets according to user preference. + * - Store and retrieve CardInfo objects associated with the set. + */ +class CardSet : public QList +{ +public: + /** + * @enum Priority + * @brief Defines relative ordering and importance of sets. + */ + enum Priority + { + PriorityFallback = 0, ///< Used when no other priority is defined. + PriorityPrimary = 10, ///< Primary, canonical set. + PrioritySecondary = 20, ///< Secondary but relevant. + PriorityReprint = 30, ///< Duplicate or reprint category. + PriorityOther = 40, ///< Miscellaneous grouping. + PriorityLowest = 100, ///< Lowest sorting priority. + }; + + static const char *TOKENS_SETNAME; ///< Reserved identifier for token-like sets. + +private: + ICardSetPriorityController *priorityController; ///< Interface to the card set priority controller. + QString shortName; ///< Short identifier for the set. + QString longName; ///< Full name for the set. + unsigned int sortKey; ///< Custom numeric sort key. + QDate releaseDate; ///< Release date, may be empty if unknown. + QString setType; ///< Type/category label for the set. + Priority priority; ///< Priority level for sorting and relevance. + bool enabled; ///< Whether the set is active/enabled. + bool isknown; ///< Whether the set is considered known. + +public: + /** + * @brief Constructs a CardSet. + * + * @param priorityController Interface to a card set priority controller. + * @param _shortName Identifier string. + * @param _longName Full descriptive name. + * @param _setType Type/category string. + * @param _releaseDate Release date (optional). + * @param _priority Sorting/priority level. + */ + explicit CardSet(ICardSetPriorityController *priorityController, + const QString &_shortName = QString(), + const QString &_longName = QString(), + const QString &_setType = QString(), + const QDate &_releaseDate = QDate(), + const Priority _priority = PriorityFallback); + + /** + * @brief Creates and returns a new shared CardSet instance. + * + * @param priorityController Interface to a card set priority controller. + * @param _shortName Identifier string. + * @param _longName Full descriptive name. + * @param _setType Type/category string. + * @param _releaseDate Release date (optional). + * @param _priority Sorting/priority level. + * @return A shared pointer to the new CardSet. + */ + static CardSetPtr newInstance(ICardSetPriorityController *priorityController, + const QString &_shortName = QString(), + const QString &_longName = QString(), + const QString &_setType = QString(), + const QDate &_releaseDate = QDate(), + const Priority _priority = PriorityFallback); + + /** + * @brief Returns a safe, sanitized version of the short name. + * + * Intended for file paths or identifiers where only certain + * characters are allowed. + * + * @return Sanitized short name. + */ + [[nodiscard]] QString getCorrectedShortName() const; + + /// @return Short identifier of the set. + [[nodiscard]] QString getShortName() const + { + return shortName; + } + + /// @return Descriptive name of the set. + [[nodiscard]] QString getLongName() const + { + return longName; + } + + /// @return Type/category string of the set. + [[nodiscard]] QString getSetType() const + { + return setType; + } + + /// @return Release date of the set. + [[nodiscard]] QDate getReleaseDate() const + { + return releaseDate; + } + + /// @return Priority level of the set. + [[nodiscard]] Priority getPriority() const + { + return priority; + } + + /** + * @brief Sets the full name of the set. + * @param _longName New full name. + */ + void setLongName(const QString &_longName) + { + longName = _longName; + } + + /** + * @brief Sets the category/type of the set. + * @param _setType New category string. + */ + void setSetType(const QString &_setType) + { + setType = _setType; + } + + /** + * @brief Sets the release date of the set. + * @param _releaseDate New release date. + */ + void setReleaseDate(const QDate &_releaseDate) + { + releaseDate = _releaseDate; + } + + /** + * @brief Updates the priority of the set. + * @param _priority New priority value. + */ + + void setPriority(const Priority _priority) + { + priority = _priority; + } + + /** + * @brief Loads state values (enabled, known, sort key) from configuration. + * + * Reads external configuration and applies it to this set. + */ + void loadSetOptions(); + + /// @return The sort key assigned to this set. + [[nodiscard]] int getSortKey() const + { + return sortKey; + } + + /** + * @brief Assigns a new sort key to this set. + * @param _sortKey The numeric key to use for sorting. + */ + void setSortKey(unsigned int _sortKey); + + /// @return True if the set is enabled. + [[nodiscard]] bool getEnabled() const + { + return enabled; + } + + /** + * @brief Enables or disables the set. + * @param _enabled True to enable, false to disable. + */ + void setEnabled(bool _enabled); + + /// @return True if the set is considered known. + [[nodiscard]] bool getIsKnown() const + { + return isknown; + } + + /** + * @brief Marks the set as known or unknown. + * @param _isknown True if known, false if unknown. + */ + void setIsKnown(bool _isknown); + + /** + * @brief Determines whether the set has incomplete metadata and should be ignored. + * + * @return True if the long name, type, and release date are all empty. + */ + [[nodiscard]] bool getIsKnownIgnored() const + { + return longName.length() + setType.length() + releaseDate.toString().length() == 0; + } +}; + +#endif // COCKATRICE_CARD_SET_H diff --git a/libcockatrice_card/libcockatrice/card/set/card_set_comparator.h b/libcockatrice_card/libcockatrice/card/set/card_set_comparator.h new file mode 100644 index 000000000..96f1052a9 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/set/card_set_comparator.h @@ -0,0 +1,66 @@ +/** + * @file card_set_comparator.h + * @ingroup CardSets + * @brief TODO: Document this. + */ + +#ifndef SET_PRIORITY_COMPARATOR_H +#define SET_PRIORITY_COMPARATOR_H + +#include "../card_info.h" + +class SetPriorityComparator +{ +public: + /* + * Returns true if a has higher download priority than b + * Enabled sets have priority over disabled sets + * Both groups follow the user-defined order + */ + inline bool operator()(const CardSetPtr &a, const CardSetPtr &b) const + { + if (a->getEnabled()) { + return !b->getEnabled() || a->getSortKey() < b->getSortKey(); + } else { + return !b->getEnabled() && a->getSortKey() < b->getSortKey(); + } + } +}; + +class SetReleaseDateComparator +{ +public: + /* + * Returns true if a has higher download priority than b + * Enabled sets have priority over disabled sets + * Both groups follow the user-defined order + */ + inline bool operator()(const CardSetPtr &a, const CardSetPtr &b) const + { + if (a->getEnabled()) { + return !b->getEnabled() || a->getReleaseDate() < b->getReleaseDate(); + } else { + return !b->getEnabled() && a->getReleaseDate() < b->getReleaseDate(); + } + } +}; + +class CardSetPriorityComparator +{ +public: + /* + * Returns true if a has higher download priority than b + * Enabled sets have priority over disabled sets + * Both groups follow the user-defined order + */ + inline bool operator()(const PrintingInfo &a, const PrintingInfo &b) const + { + if (a.getSet()->getEnabled()) { + return !b.getSet()->getEnabled() || a.getSet()->getSortKey() < b.getSet()->getSortKey(); + } else { + return !b.getSet()->getEnabled() && a.getSet()->getSortKey() < b.getSet()->getSortKey(); + } + } +}; + +#endif // SET_PRIORITY_COMPARATOR_H \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/set/card_set_list.cpp b/libcockatrice_card/libcockatrice/card/set/card_set_list.cpp new file mode 100644 index 000000000..8d6aa7365 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/set/card_set_list.cpp @@ -0,0 +1,127 @@ +#include "card_set_list.h" + +class CardSetList::KeyCompareFunctor +{ +public: + inline bool operator()(const CardSetPtr &a, const CardSetPtr &b) const + { + if (a.isNull() || b.isNull()) { + // qCWarning(CardInfoLog) << "SetList::KeyCompareFunctor a or b is null"; + return false; + } + + return a->getSortKey() < b->getSortKey(); + } +}; + +void CardSetList::sortByKey() +{ + std::sort(begin(), end(), KeyCompareFunctor()); +} + +int CardSetList::getEnabledSetsNum() +{ + int num = 0; + for (int i = 0; i < size(); ++i) { + CardSetPtr set = at(i); + if (set && set->getEnabled()) { + ++num; + } + } + return num; +} + +int CardSetList::getUnknownSetsNum() +{ + int num = 0; + for (int i = 0; i < size(); ++i) { + CardSetPtr set = at(i); + if (set && !set->getIsKnown() && !set->getIsKnownIgnored()) { + ++num; + } + } + return num; +} + +QStringList CardSetList::getUnknownSetsNames() +{ + QStringList sets = QStringList(); + for (int i = 0; i < size(); ++i) { + CardSetPtr set = at(i); + if (set && !set->getIsKnown() && !set->getIsKnownIgnored()) { + sets << set->getShortName(); + } + } + return sets; +} + +void CardSetList::enableAllUnknown() +{ + for (int i = 0; i < size(); ++i) { + CardSetPtr set = at(i); + if (set && !set->getIsKnown() && !set->getIsKnownIgnored()) { + set->setIsKnown(true); + set->setEnabled(true); + } else if (set && set->getIsKnownIgnored() && !set->getEnabled()) { + set->setEnabled(true); + } + } +} + +void CardSetList::enableAll() +{ + for (int i = 0; i < size(); ++i) { + CardSetPtr set = at(i); + + if (set == nullptr) { + // qCWarning(CardInfoLog) << "enabledAll has null"; + continue; + } + + if (!set->getIsKnownIgnored()) { + set->setIsKnown(true); + } + + set->setEnabled(true); + } +} + +void CardSetList::markAllAsKnown() +{ + for (int i = 0; i < size(); ++i) { + CardSetPtr set = at(i); + if (set && !set->getIsKnown() && !set->getIsKnownIgnored()) { + set->setIsKnown(true); + set->setEnabled(false); + } else if (set && set->getIsKnownIgnored() && !set->getEnabled()) { + set->setEnabled(true); + } + } +} + +void CardSetList::guessSortKeys() +{ + defaultSort(); + for (int i = 0; i < size(); ++i) { + CardSetPtr set = at(i); + if (set.isNull()) { + // qCWarning(CardInfoLog) << "guessSortKeys set is null"; + continue; + } + set->setSortKey(i); + } +} + +void CardSetList::defaultSort() +{ + std::sort(begin(), end(), [](const CardSetPtr &a, const CardSetPtr &b) { + // Sort by priority, then by release date, then by short name + if (a->getPriority() != b->getPriority()) { + return a->getPriority() < b->getPriority(); // lowest first + } else if (a->getReleaseDate() != b->getReleaseDate()) { + return a->getReleaseDate() > b->getReleaseDate(); // most recent first + } else { + return a->getShortName() < b->getShortName(); // alphabetically + } + }); +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/set/card_set_list.h b/libcockatrice_card/libcockatrice/card/set/card_set_list.h new file mode 100644 index 000000000..81d605374 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/set/card_set_list.h @@ -0,0 +1,102 @@ +#ifndef COCKATRICE_CARD_SET_LIST_H +#define COCKATRICE_CARD_SET_LIST_H + +#include "card_set.h" + +#include + +/** + * @class CardSetList + * @ingroup CardSets + * + * @brief A list-like container for CardSet objects with extended management methods. + * + * Extends `QList` by adding convenience operations for sorting, + * enabling/disabling sets, and tracking known/unknown status. Unlike a plain + * list, this container provides domain-specific functionality for handling + * groups of sets in bulk. + */ +class CardSetList : public QList +{ +private: + /** + * @class KeyCompareFunctor + * @brief Internal comparison functor for sorting by sort key. + * + * Used internally by `sortByKey()` to order sets consistently + * according to their assigned numeric sort keys. + */ + class KeyCompareFunctor; + +public: + /** + * @brief Sorts the set list by each set’s assigned sort key. + * + * Uses KeyCompareFunctor internally. If two sets share the + * same sort key, their relative order is unspecified. + */ + void sortByKey(); + + /** + * @brief Reassigns sort keys based on the current order. + * + * Calls defaultSort() and then assigns sequential sort keys + * to all sets according to their resulting positions, replacing + * any existing sort keys to ensure consistent ordering. + */ + void guessSortKeys(); + + /** + * @brief Enables all sets that are unknown or ignored. + * + * Sets that are not marked as known and not ignored are marked as known + * and enabled. Ignored-known sets are also enabled, but remain ignored. + */ + void enableAllUnknown(); + + /** + * @brief Enables all sets in the list. + * + * Equivalent to calling `setEnabled(true)` on each entry. + */ + void enableAll(); + + /** + * @brief Marks all sets as known and adjusts their enabled state. + * + * Unknown, non-ignored sets become known and disabled. + * Ignored-known sets are enabled if they were previously disabled. + */ + void markAllAsKnown(); + + /** + * @brief Counts the number of sets that are currently enabled. + * + * @return Integer count of enabled sets. + */ + int getEnabledSetsNum(); + + /** + * @brief Counts the number of sets that are currently unknown. + * + * @return Integer count of unknown sets. + */ + int getUnknownSetsNum(); + + /** + * @brief Collects the short names of all sets marked as unknown. + * + * @return A list of unknown set names. + */ + QStringList getUnknownSetsNames(); + + /** + * @brief Sorts the list by default rules. + * + * Orders sets first by priority (ascending), then by release date + * (most recent first), and finally alphabetically by short name. + */ + void defaultSort(); +}; + +#endif // COCKATRICE_CARD_SET_LIST_H diff --git a/libcockatrice_deck_list/CMakeLists.txt b/libcockatrice_deck_list/CMakeLists.txt new file mode 100644 index 000000000..5ccdb5f66 --- /dev/null +++ b/libcockatrice_deck_list/CMakeLists.txt @@ -0,0 +1,40 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS + libcockatrice/deck_list/tree/abstract_deck_list_card_node.h + libcockatrice/deck_list/tree/abstract_deck_list_node.h + libcockatrice/deck_list/tree/deck_list_card_node.h + libcockatrice/deck_list/tree/inner_deck_list_node.h + libcockatrice/deck_list/deck_list.h + libcockatrice/deck_list/deck_list_history_manager.h + libcockatrice/deck_list/deck_list_node_tree.h + libcockatrice/deck_list/deck_list_memento.h + libcockatrice/deck_list/sideboard_plan.h +) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library( + libcockatrice_deck_list STATIC + ${MOC_SOURCES} + libcockatrice/deck_list/tree/abstract_deck_list_card_node.cpp + libcockatrice/deck_list/tree/abstract_deck_list_node.cpp + libcockatrice/deck_list/tree/deck_list_card_node.cpp + libcockatrice/deck_list/tree/inner_deck_list_node.cpp + libcockatrice/deck_list/deck_list.cpp + libcockatrice/deck_list/deck_list_history_manager.cpp + libcockatrice/deck_list/deck_list_node_tree.cpp + libcockatrice/deck_list/sideboard_plan.cpp +) + +add_dependencies(libcockatrice_deck_list libcockatrice_protocol) + +target_include_directories(libcockatrice_deck_list PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(libcockatrice_deck_list PUBLIC libcockatrice_protocol libcockatrice_utility ${QT_CORE_MODULE}) diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp new file mode 100644 index 000000000..e3e7b41c0 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp @@ -0,0 +1,512 @@ +#include "deck_list.h" + +#include "deck_list_memento.h" +#include "tree/abstract_deck_list_node.h" +#include "tree/deck_list_card_node.h" +#include "tree/inner_deck_list_node.h" + +#include +#include +#include +#include +#include +#include +#include + +#if QT_VERSION < 0x050600 +// qHash on QRegularExpression was added in 5.6, FIX IT +uint qHash(const QRegularExpression &key, uint seed) noexcept +{ + return qHash(key.pattern(), seed); // call qHash on pattern QString instead +} +#endif + +static const QString CURRENT_SIDEBOARD_PLAN_KEY = ""; + +bool DeckList::Metadata::isEmpty() const +{ + return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty(); +} + +DeckList::DeckList() +{ +} + +DeckList::DeckList(const QString &nativeString) +{ + loadFromString_Native(nativeString); +} + +DeckList::DeckList(const Metadata &metadata, + const DecklistNodeTree &tree, + const QMap &sideboardPlans) + : metadata(metadata), sideboardPlans(sideboardPlans), tree(tree) +{ +} + +QList DeckList::getCurrentSideboardPlan() const +{ + if (!sideboardPlans.contains(CURRENT_SIDEBOARD_PLAN_KEY)) { + return {}; + } + + return sideboardPlans.value(CURRENT_SIDEBOARD_PLAN_KEY).getMoveList(); +} + +void DeckList::setCurrentSideboardPlan(const QList &plan) +{ + sideboardPlans[CURRENT_SIDEBOARD_PLAN_KEY].setMoveList(plan); +} + +bool DeckList::readElement(QXmlStreamReader *xml) +{ + const QString childName = xml->name().toString(); + if (xml->isStartElement()) { + if (childName == "lastLoadedTimestamp") { + metadata.lastLoadedTimestamp = xml->readElementText(); + } else if (childName == "deckname") { + metadata.name = xml->readElementText(); + } else if (childName == "format") { + metadata.gameFormat = xml->readElementText(); + } else if (childName == "comments") { + metadata.comments = xml->readElementText(); + } else if (childName == "bannerCard") { + QString providerId = xml->attributes().value("providerId").toString(); + QString cardName = xml->readElementText(); + metadata.bannerCard = {cardName, providerId}; + } else if (childName == "tags") { + metadata.tags.clear(); // Clear existing tags + while (xml->readNextStartElement()) { + if (xml->name().toString() == "tag") { + metadata.tags.append(xml->readElementText()); + } + } + } else if (childName == "zone") { + tree.readZoneElement(xml); + } else if (childName == "sideboard_plan") { + SideboardPlan newSideboardPlan; + if (newSideboardPlan.readElement(xml)) { + sideboardPlans.insert(newSideboardPlan.getName(), newSideboardPlan); + } + } + } else if (xml->isEndElement() && (childName == "cockatrice_deck")) { + return false; + } + return true; +} + +static void writeMetadata(QXmlStreamWriter *xml, const DeckList::Metadata &metadata) +{ + xml->writeTextElement("lastLoadedTimestamp", metadata.lastLoadedTimestamp); + xml->writeTextElement("deckname", metadata.name); + xml->writeTextElement("format", metadata.gameFormat); + xml->writeStartElement("bannerCard"); + xml->writeAttribute("providerId", metadata.bannerCard.providerId); + xml->writeCharacters(metadata.bannerCard.name); + xml->writeEndElement(); + xml->writeTextElement("comments", metadata.comments); + + // Write tags + xml->writeStartElement("tags"); + for (const QString &tag : metadata.tags) { + xml->writeTextElement("tag", tag); + } + xml->writeEndElement(); +} + +void DeckList::write(QXmlStreamWriter *xml) const +{ + xml->writeStartElement("cockatrice_deck"); + xml->writeAttribute("version", "1"); + + writeMetadata(xml, metadata); + + // Write zones + tree.write(xml); + + // Write sideboard plans + for (auto &sideboardPlan : sideboardPlans.values()) { + sideboardPlan.write(xml); + } + + xml->writeEndElement(); // Close "cockatrice_deck" +} + +bool DeckList::loadFromXml(QXmlStreamReader *xml) +{ + if (xml->error()) { + qDebug() << "Error loading deck from xml: " << xml->errorString(); + return false; + } + + cleanList(); + while (!xml->atEnd()) { + xml->readNext(); + if (xml->isStartElement()) { + if (xml->name().toString() != "cockatrice_deck") + return false; + while (!xml->atEnd()) { + xml->readNext(); + if (!readElement(xml)) + break; + } + } + } + refreshDeckHash(); + if (xml->error()) { + qDebug() << "Error loading deck from xml: " << xml->errorString(); + return false; + } + return true; +} + +bool DeckList::loadFromString_Native(const QString &nativeString) +{ + QXmlStreamReader xml(nativeString); + return loadFromXml(&xml); +} + +QString DeckList::writeToString_Native() const +{ + QString result; + QXmlStreamWriter xml(&result); + xml.writeStartDocument(); + write(&xml); + xml.writeEndDocument(); + return result; +} + +bool DeckList::loadFromFile_Native(QIODevice *device) +{ + QXmlStreamReader xml(device); + return loadFromXml(&xml); +} + +bool DeckList::saveToFile_Native(QIODevice *device) const +{ + QXmlStreamWriter xml(device); + xml.setAutoFormatting(true); + xml.writeStartDocument(); + + write(&xml); + + xml.writeEndDocument(); + return true; +} + +/** + * Clears the decklist and loads in a new deck from text + * + * @param in The text to load + * @param preserveMetadata If true, don't clear the existing metadata + * @param cardNameNormalizer Function that takes the parsed card name string in the text and + * @return False if the input was empty, true otherwise. + */ +bool DeckList::loadFromStream_Plain(QTextStream &in, + bool preserveMetadata, + const std::function &cardNameNormalizer) +{ + const QRegularExpression reCardLine(R"(^\s*[\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption); + const QRegularExpression reEmpty("^\\s*$"); + const QRegularExpression reComment(R"([\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption); + const QRegularExpression reSBMark("^\\s*sb:\\s*(.+)", QRegularExpression::CaseInsensitiveOption); + const QRegularExpression reSBComment("^sideboard\\b.*$", QRegularExpression::CaseInsensitiveOption); + const QRegularExpression reDeckComment("^((main)?deck(list)?|mainboard)\\b", + QRegularExpression::CaseInsensitiveOption); + + // Regex for advanced card parsing + const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))"); + + // Regex for extracting set code and collector number with attached symbols + const QRegularExpression reHyphenFormat(R"(\((\w{3,})\)\s+(\w{3,})-(\d+[^\w\s]*))"); + const QRegularExpression reRegularFormat(R"(\((\w{3,})\)\s+(\d+[^\w\s]*))"); + + cleanList(preserveMetadata); + + auto inputs = in.readAll().trimmed().split('\n'); + auto max_line = inputs.size(); + + // Start at the first empty line before the first card line + auto deckStart = inputs.indexOf(reCardLine); + if (deckStart == -1) { + if (inputs.indexOf(reComment) == -1) { + return false; // Input is empty + } + deckStart = max_line; + } else { + deckStart = inputs.lastIndexOf(reEmpty, deckStart); + if (deckStart == -1) { + deckStart = 0; + } + } + + // find sideboard position, if marks are used this won't be needed + int sBStart = -1; + if (inputs.indexOf(reSBMark, deckStart) == -1) { + sBStart = inputs.indexOf(reSBComment, deckStart); + if (sBStart == -1) { + sBStart = inputs.indexOf(reEmpty, deckStart + 1); + if (sBStart == -1) { + sBStart = max_line; + } + auto nextCard = inputs.indexOf(reCardLine, sBStart + 1); + if (inputs.indexOf(reEmpty, nextCard + 1) != -1) { + sBStart = max_line; + } + } + } + + int index = 0; + QRegularExpressionMatch match; + + // Parse name and comments + while (index < deckStart) { + const auto ¤t = inputs.at(index++); + if (!current.contains(reEmpty)) { + match = reComment.match(current); + metadata.name = match.captured(); + break; + } + } + while (index < deckStart) { + const auto ¤t = inputs.at(index++); + if (!current.contains(reEmpty)) { + match = reComment.match(current); + metadata.comments += match.captured() + '\n'; + } + } + metadata.comments.chop(1); + + // Discard empty lines + while (index < max_line && inputs.at(index).contains(reEmpty)) { + ++index; + } + + // Discard line if it starts with deck or mainboard, all cards until the sideboard starts are in the mainboard + if (inputs.at(index).contains(reDeckComment)) { + ++index; + } + + // Parse decklist + for (; index < max_line; ++index) { + // check if line is a card + match = reCardLine.match(inputs.at(index)); + if (!match.hasMatch()) + continue; + + QString cardName = match.captured().simplified(); + bool sideboard = false; + + // Sideboard detection + if (sBStart < 0) { + match = reSBMark.match(cardName); + if (match.hasMatch()) { + sideboard = true; + cardName = match.captured(1); + } + } else { + if (index == sBStart) + continue; + sideboard = index > sBStart; + } + + // Extract set code, collector number, and foil + QString setCode; + QString collectorNumber; + bool isFoil = false; + + // Check for foil status at the end of the card name + if (cardName.endsWith("*F*", Qt::CaseInsensitive)) { + isFoil = true; + cardName.chop(3); // Remove the "*F*" from the card name + } + Q_UNUSED(isFoil); + + // Attempt to match the hyphen-separated format (PLST-2094) + match = reHyphenFormat.match(cardName); + if (match.hasMatch()) { + setCode = match.captured(2).toUpper(); + collectorNumber = match.captured(3); + cardName = cardName.left(match.capturedStart()).trimmed(); + } else { + // Attempt to match the regular format (PLST) 2094 + match = reRegularFormat.match(cardName); + if (match.hasMatch()) { + setCode = match.captured(1).toUpper(); + collectorNumber = match.captured(2); + cardName = cardName.left(match.capturedStart()).trimmed(); + } + } + + // check if a specific amount is mentioned + int amount = 1; + match = reMultiplier.match(cardName); + if (match.hasMatch()) { + amount = match.captured(1).toInt(); + cardName = match.captured(2); + } + + // Normalize the card name + cardName = cardNameNormalizer(cardName); + + // Determine the zone (mainboard/sideboard) + QString zoneName = sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN; + + // make new entry in decklist + tree.addCard(cardName, amount, zoneName, -1, setCode, collectorNumber); + } + + refreshDeckHash(); + return true; +} + +bool DeckList::loadFromFile_Plain(QIODevice *device, const std::function &cardNameNormalizer) +{ + QTextStream in(device); + return loadFromStream_Plain(in, false, cardNameNormalizer); +} + +bool DeckList::saveToStream_Plain(QTextStream &stream, bool prefixSideboardCards, bool slashTappedOutSplitCards) const +{ + auto writeToStream = [&stream, prefixSideboardCards, slashTappedOutSplitCards](const auto node, const auto card) { + if (prefixSideboardCards && node->getName() == DECK_ZONE_SIDE) { + stream << "SB: "; + } + if (!slashTappedOutSplitCards) { + stream << QString("%1 %2\n").arg(card->getNumber()).arg(card->getName()); + } else { + stream << QString("%1 %2\n").arg(card->getNumber()).arg(card->getName().replace("//", "/")); + } + }; + + forEachCard(writeToStream); + return true; +} + +bool DeckList::saveToFile_Plain(QIODevice *device, bool prefixSideboardCards, bool slashTappedOutSplitCards) const +{ + QTextStream out(device); + return saveToStream_Plain(out, prefixSideboardCards, slashTappedOutSplitCards); +} + +QString DeckList::writeToString_Plain(bool prefixSideboardCards, bool slashTappedOutSplitCards) const +{ + QString result; + QTextStream out(&result); + saveToStream_Plain(out, prefixSideboardCards, slashTappedOutSplitCards); + return result; +} + +/** + * Clears all cards and other data from the decklist + * + * @param preserveMetadata If true, only clear the cards + */ +void DeckList::cleanList(bool preserveMetadata) +{ + tree.clear(); + if (!preserveMetadata) { + metadata = {}; + } + refreshDeckHash(); +} + +QStringList DeckList::getCardList(const QSet &restrictToZones) const +{ + auto nodes = tree.getCardNodes(restrictToZones); + + QStringList result; + std::transform(nodes.cbegin(), nodes.cend(), std::back_inserter(result), [](auto node) { return node->getName(); }); + + return result; +} + +QList DeckList::getCardRefList(const QSet &restrictToZones) const +{ + auto nodes = tree.getCardNodes(restrictToZones); + + QList result; + std::transform(nodes.cbegin(), nodes.cend(), std::back_inserter(result), + [](auto node) { return node->toCardRef(); }); + + return result; +} + +QList DeckList::getCardNodes(const QSet &restrictToZones) const +{ + return tree.getCardNodes(restrictToZones); +} + +QList DeckList::getZoneNodes(const QSet &restrictToZones) const +{ + return tree.getZoneNodes(restrictToZones); +} + +int DeckList::getSideboardSize() const +{ + auto cards = tree.getCardNodes({DECK_ZONE_SIDE}); + + int size = 0; + for (auto card : cards) { + size += card->getNumber(); + } + + return size; +} + +DecklistCardNode *DeckList::addCard(const QString &cardName, + const QString &zoneName, + int position, + const QString &cardSetName, + const QString &cardSetCollectorNumber, + const QString &cardProviderId, + bool formatLegal) +{ + auto node = + tree.addCard(cardName, 1, zoneName, position, cardSetName, cardSetCollectorNumber, cardProviderId, formatLegal); + refreshDeckHash(); + return node; +} + +/** + * Gets the deck hash. + * The hash is computed on the first call to this method, and is cached until the decklist is modified. + * + * @return The deck hash + */ +QString DeckList::getDeckHash() const +{ + if (!cachedDeckHash.isEmpty()) { + return cachedDeckHash; + } + + cachedDeckHash = tree.computeDeckHash(); + return cachedDeckHash; +} + +/** + * Invalidates the cached deckHash. + */ +void DeckList::refreshDeckHash() +{ + cachedDeckHash = QString(); +} + +/** + * Calls a given function on each card in the deck. + */ +void DeckList::forEachCard(const std::function &func) const +{ + tree.forEachCard(func); +} + +DeckListMemento DeckList::createMemento(const QString &reason) const +{ + return DeckListMemento(writeToString_Native(), reason); +} + +void DeckList::restoreMemento(const DeckListMemento &m) +{ + cleanList(); + loadFromString_Native(m.getMemento()); +} diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h new file mode 100644 index 000000000..a96adeb38 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h @@ -0,0 +1,257 @@ +/** + * @file deck_list.h + * @brief Defines the DeckList class, which manages a full + * deck structure including cards, zones, sideboard plans, and + * serialization to/from multiple formats. This is a logic class which + * does not care about Qt or user facing views. + * See @c DeckListModel for the actual Qt Model to be used for views + */ + +#ifndef DECKLIST_H +#define DECKLIST_H + +#include "deck_list_memento.h" +#include "deck_list_node_tree.h" +#include "sideboard_plan.h" +#include "tree/inner_deck_list_node.h" + +#include +#include +#include +#include + +class AbstractDecklistNode; +class DecklistCardNode; +class CardDatabase; +class QIODevice; +class QTextStream; +class InnerDecklistNode; + +/** + * @class DeckList + * @ingroup Decks + * @brief Represents a complete deck, including metadata, zones, cards, + * and sideboard plans. + * + * A DeckList is a wrapper around an `InnerDecklistNode` tree, + * enriched with metadata like deck name, comments, tags, banner card, + * and multiple sideboard plans. + * + * ### Core responsibilities: + * - Store and manage the root node tree (zones → groups → cards). + * - Provide deck-level metadata (name, comments, tags, banner). + * - Support multiple sideboard plans (meta-game strategies). + * - Provide import/export in multiple formats: + * - Cockatrice native XML format. + * - Plain-text list format. + * - Provide hashing for deck identity (deck hash). + * + * ### Ownership: + * - Owns the `DecklistNodeTree`. + * - Owns `SideboardPlan` instances stored in `sideboardPlans`. + * + * ### Example workflow: + * ``` + * DeckList deck; + * deck.setName("Mono Red Aggro"); + * deck.addCard("Lightning Bolt", "main"); + * deck.addTag("Aggro"); + * deck.saveToFile_Native(device); + * ``` + */ +class DeckList +{ +public: + struct Metadata + { + QString name; ///< User-defined deck name. + QString comments; ///< Free-form comments or notes. + QString gameFormat; ///< The name of the game format this deck contains legal cards for + CardRef bannerCard; ///< Optional representative card for the deck. + QStringList tags; ///< User-defined tags for deck classification. + QString lastLoadedTimestamp; ///< Timestamp string of last load. + + /** + * @brief Checks if all values (except for lastLoadedTimestamp) in the metadata is empty. + */ + bool isEmpty() const; + }; + +private: + Metadata metadata; ///< Deck metadata that is stored in the deck file + QMap sideboardPlans; ///< Named sideboard plans. + DecklistNodeTree tree; ///< The deck tree (zones + cards). + + /** + * @brief Cached deck hash, recalculated lazily. + * An empty string indicates the cache is invalid. + */ + mutable QString cachedDeckHash; + +public: + /// @name Metadata setters + ///@{ + void setName(const QString &_name = QString()) + { + metadata.name = _name; + } + void setComments(const QString &_comments = QString()) + { + metadata.comments = _comments; + } + void setTags(const QStringList &_tags = QStringList()) + { + metadata.tags = _tags; + } + void addTag(const QString &_tag) + { + metadata.tags.append(_tag); + } + void clearTags() + { + metadata.tags.clear(); + } + void setBannerCard(const CardRef &_bannerCard = {}) + { + metadata.bannerCard = _bannerCard; + } + void setLastLoadedTimestamp(const QString &_lastLoadedTimestamp = QString()) + { + metadata.lastLoadedTimestamp = _lastLoadedTimestamp; + } + void setGameFormat(const QString &_gameFormat = QString()) + { + metadata.gameFormat = _gameFormat; + } + ///@} + + /// @brief Construct an empty deck. + explicit DeckList(); + /// @brief Construct from a serialized native-format string. + explicit DeckList(const QString &nativeString); + /// @brief Construct from components + DeckList(const Metadata &metadata, + const DecklistNodeTree &tree, + const QMap &sideboardPlans = {}); + + /** + * @brief Gets a pointer to the underlying node tree. + * Note: DO NOT call this method unless the object needs to have access to the underlying model. + * For now, only the DeckListModel should be calling this. + */ + DecklistNodeTree *getTree() + { + return &tree; + } + + /// @name Metadata getters + /// The individual metadata getters still exist for backwards compatibility. + ///@{ + //! \todo Figure out when we can remove them. + const Metadata &getMetadata() const + { + return metadata; + } + QString getName() const + { + return metadata.name; + } + QString getComments() const + { + return metadata.comments; + } + QStringList getTags() const + { + return metadata.tags; + } + CardRef getBannerCard() const + { + return metadata.bannerCard; + } + QString getLastLoadedTimestamp() const + { + return metadata.lastLoadedTimestamp; + } + QString getGameFormat() const + { + return metadata.gameFormat; + } + ///@} + + bool isBlankDeck() const + { + return metadata.isEmpty() && getCardList().isEmpty(); + } + + /// @name Sideboard plans + ///@{ + QList getCurrentSideboardPlan() const; + void setCurrentSideboardPlan(const QList &plan); + const QMap &getSideboardPlans() const + { + return sideboardPlans; + } + ///@} + + /// @name Serialization (XML) + ///@{ + bool readElement(QXmlStreamReader *xml); + void write(QXmlStreamWriter *xml) const; + bool loadFromXml(QXmlStreamReader *xml); + bool loadFromString_Native(const QString &nativeString); + QString writeToString_Native() const; + bool loadFromFile_Native(QIODevice *device); + bool saveToFile_Native(QIODevice *device) const; + ///@} + + /// @name Serialization (Plain text) + ///@{ + bool loadFromStream_Plain(QTextStream &stream, + bool preserveMetadata, + const std::function &cardNameNormalizer); + bool loadFromFile_Plain(QIODevice *device, const std::function &cardNameNormalizer); + bool saveToStream_Plain(QTextStream &stream, bool prefixSideboardCards, bool slashTappedOutSplitCards) const; + bool + saveToFile_Plain(QIODevice *device, bool prefixSideboardCards = true, bool slashTappedOutSplitCards = false) const; + QString writeToString_Plain(bool prefixSideboardCards = true, bool slashTappedOutSplitCards = false) const; + ///@} + + /// @name Deck manipulation + ///@{ + void cleanList(bool preserveMetadata = false); + bool isEmpty() const + { + return tree.isEmpty() && metadata.isEmpty() && sideboardPlans.isEmpty(); + } + QStringList getCardList(const QSet &restrictToZones = {}) const; + QList getCardRefList(const QSet &restrictToZones = {}) const; + QList getCardNodes(const QSet &restrictToZones = {}) const; + QList getZoneNodes(const QSet &restrictToZones = {}) const; + int getSideboardSize() const; + + DecklistCardNode *addCard(const QString &cardName, + const QString &zoneName, + int position = -1, + const QString &cardSetName = QString(), + const QString &cardSetCollectorNumber = QString(), + const QString &cardProviderId = QString(), + const bool formatLegal = true); + ///@} + + /// @name Deck identity + ///@{ + QString getDeckHash() const; + void refreshDeckHash(); + ///@} + + /** + * @brief Apply a function to every card in the deck tree. + * + * @param func Function taking (zone node, card node). + */ + void forEachCard(const std::function &func) const; + DeckListMemento createMemento(const QString &reason) const; + void restoreMemento(const DeckListMemento &m); +}; + +#endif diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.cpp new file mode 100644 index 000000000..83c9cc0bb --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.cpp @@ -0,0 +1,53 @@ +#include "deck_list_history_manager.h" + +void DeckListHistoryManager::save(const DeckListMemento &memento) +{ + undoStack.push(memento); + redoStack.clear(); + emit undoRedoStateChanged(); +} + +void DeckListHistoryManager::clear() +{ + undoStack.clear(); + redoStack.clear(); + emit undoRedoStateChanged(); +} + +void DeckListHistoryManager::undo(DeckList *deck) +{ + if (undoStack.isEmpty()) + return; + + // Peek at the memento we are going to restore + const DeckListMemento &mementoToRestore = undoStack.top(); + + // Save current state for redo + DeckListMemento currentState = deck->createMemento(mementoToRestore.getReason()); + redoStack.push(currentState); + + // Pop the last state from undo stack and restore it + DeckListMemento memento = undoStack.pop(); + deck->restoreMemento(memento); + + emit undoRedoStateChanged(); +} + +void DeckListHistoryManager::redo(DeckList *deck) +{ + if (redoStack.isEmpty()) + return; + + // Peek at the memento we are going to restore + const DeckListMemento &mementoToRestore = redoStack.top(); + + // Save current state for undo + DeckListMemento currentState = deck->createMemento(mementoToRestore.getReason()); + undoStack.push(currentState); + + // Pop the next state from redo stack and restore it + DeckListMemento memento = redoStack.pop(); + deck->restoreMemento(memento); + + emit undoRedoStateChanged(); +} diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.h new file mode 100644 index 000000000..e6bd27e2d --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.h @@ -0,0 +1,54 @@ +#ifndef COCKATRICE_DECK_LIST_HISTORY_MANAGER_H +#define COCKATRICE_DECK_LIST_HISTORY_MANAGER_H + +#include "deck_list.h" +#include "deck_list_memento.h" + +#include +#include + +class DeckListHistoryManager : public QObject +{ + Q_OBJECT + +signals: + void undoRedoStateChanged(); + +public: + explicit DeckListHistoryManager(QObject *parent = nullptr) : QObject(parent) + { + } + + void save(const DeckListMemento &memento); + + void clear(); + + [[nodiscard]] bool canUndo() const + { + return !undoStack.isEmpty(); + } + + [[nodiscard]] bool canRedo() const + { + return !redoStack.isEmpty(); + } + + void undo(DeckList *deck); + + void redo(DeckList *deck); + + [[nodiscard]] QStack getRedoStack() const + { + return redoStack; + } + [[nodiscard]] QStack getUndoStack() const + { + return undoStack; + } + +private: + QStack undoStack; + QStack redoStack; +}; + +#endif // COCKATRICE_DECK_LIST_HISTORY_MANAGER_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_memento.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_memento.h new file mode 100644 index 000000000..6ddf430c5 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_memento.h @@ -0,0 +1,28 @@ +#ifndef COCKATRICE_DECK_LIST_MEMENTO_H +#define COCKATRICE_DECK_LIST_MEMENTO_H +#include + +class DeckListMemento +{ +public: + DeckListMemento() = default; + explicit DeckListMemento(const QString &memento, const QString &reason = QString()) + : memento(memento), reason(reason) + { + } + + [[nodiscard]] QString getMemento() const + { + return memento; + } + [[nodiscard]] QString getReason() const + { + return reason; + } + +private: + QString memento; + QString reason; +}; + +#endif // COCKATRICE_DECK_LIST_MEMENTO_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp new file mode 100644 index 000000000..644e0851a --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp @@ -0,0 +1,186 @@ +#include "deck_list_node_tree.h" + +#include "tree/deck_list_card_node.h" + +#include +#include + +DecklistNodeTree::DecklistNodeTree() : root(new InnerDecklistNode()) +{ +} + +DecklistNodeTree::DecklistNodeTree(const DecklistNodeTree &other) : root(new InnerDecklistNode(other.root)) +{ +} + +DecklistNodeTree &DecklistNodeTree::operator=(const DecklistNodeTree &other) +{ + if (this != &other) { + delete root; + root = new InnerDecklistNode(other.root); + } + return *this; +} + +DecklistNodeTree::~DecklistNodeTree() +{ + delete root; +} + +bool DecklistNodeTree::isEmpty() const +{ + return root->isEmpty(); +} + +void DecklistNodeTree::clear() +{ + root->clearTree(); +} + +QList DecklistNodeTree::getCardNodes(const QSet &restrictToZones) const +{ + QList result; + + for (auto *zoneNode : getZoneNodes(restrictToZones)) { + for (auto *cardNode : *zoneNode) { + auto *cardCardNode = dynamic_cast(cardNode); + if (cardCardNode) { + result.append(cardCardNode); + } + } + } + + return result; +} + +QList DecklistNodeTree::getZoneNodes(const QSet &restrictToZones) const +{ + QList zones; + for (auto *node : *root) { + InnerDecklistNode *currentZone = dynamic_cast(node); + if (!currentZone) + continue; + if (!restrictToZones.isEmpty() && !restrictToZones.contains(currentZone->getName())) { + continue; + } + zones.append(currentZone); + } + + return zones; +} + +QString DecklistNodeTree::computeDeckHash() const +{ + auto mainDeckNodes = getCardNodes({DECK_ZONE_MAIN}); + auto sideDeckNodes = getCardNodes({DECK_ZONE_SIDE}); + + static auto nodesToCardList = [](const QList &nodes, const QString &prefix = {}) { + QStringList result; + for (auto node : nodes) { + for (int i = 0; i < node->getNumber(); ++i) { + result.append(prefix + node->getName().toLower()); + } + } + return result; + }; + + QStringList cardList = nodesToCardList(mainDeckNodes) + nodesToCardList(sideDeckNodes, "SB:"); + + cardList.sort(); + QByteArray deckHashArray = QCryptographicHash::hash(cardList.join(";").toUtf8(), QCryptographicHash::Sha1); + quint64 number = (((quint64)(unsigned char)deckHashArray[0]) << 32) + + (((quint64)(unsigned char)deckHashArray[1]) << 24) + + (((quint64)(unsigned char)deckHashArray[2] << 16)) + + (((quint64)(unsigned char)deckHashArray[3]) << 8) + (quint64)(unsigned char)deckHashArray[4]; + return QString::number(number, 32).rightJustified(8, '0'); +} + +void DecklistNodeTree::write(QXmlStreamWriter *xml) const +{ + for (int i = 0; i < root->size(); i++) { + root->at(i)->writeElement(xml); + } +} + +void DecklistNodeTree::readZoneElement(QXmlStreamReader *xml) +{ + QString zoneName = xml->attributes().value("name").toString(); + InnerDecklistNode *newZone = getZoneObjFromName(zoneName); + newZone->readElement(xml); +} + +DecklistCardNode *DecklistNodeTree::addCard(const QString &cardName, + int amount, + const QString &zoneName, + int position, + const QString &cardSetName, + const QString &cardSetCollectorNumber, + const QString &cardProviderId, + const bool formatLegal) +{ + auto *zoneNode = getZoneObjFromName(zoneName); + auto *node = new DecklistCardNode(cardName, amount, zoneNode, position, cardSetName, cardSetCollectorNumber, + cardProviderId, formatLegal); + return node; +} + +bool DecklistNodeTree::deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode) +{ + if (node == root) { + return true; + } + + if (rootNode == nullptr) { + rootNode = root; + } + + int index = rootNode->indexOf(node); + if (index != -1) { + delete rootNode->takeAt(index); + + if (rootNode->empty()) { + deleteNode(rootNode, rootNode->getParent()); + } + + return true; + } + + for (int i = 0; i < rootNode->size(); i++) { + auto *inner = dynamic_cast(rootNode->at(i)); + if (inner) { + if (deleteNode(node, inner)) { + return true; + } + } + } + + return false; +} + +void DecklistNodeTree::forEachCard(const std::function &func) const +{ + // Support for this is only possible if the internal structure + // doesn't get more complicated. + for (int i = 0; i < root->size(); i++) { + InnerDecklistNode *node = dynamic_cast(root->at(i)); + for (int j = 0; j < node->size(); j++) { + DecklistCardNode *card = dynamic_cast(node->at(j)); + func(node, card); + } + } +} + +/** + * Gets the InnerDecklistNode that is the root node for the given zone, creating a new node if it doesn't exist. + */ +InnerDecklistNode *DecklistNodeTree::getZoneObjFromName(const QString &zoneName) const +{ + for (int i = 0; i < root->size(); i++) { + auto *node = dynamic_cast(root->at(i)); + if (node->getName() == zoneName) { + return node; + } + } + + return new InnerDecklistNode(zoneName, root); +} diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h new file mode 100644 index 000000000..6de760634 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h @@ -0,0 +1,92 @@ +#ifndef COCKATRICE_DECKLIST_NODE_TREE_H +#define COCKATRICE_DECKLIST_NODE_TREE_H + +#include "libcockatrice/utility/card_ref.h" +#include "tree/deck_list_card_node.h" +#include "tree/inner_deck_list_node.h" + +#include + +class DecklistNodeTree +{ + InnerDecklistNode *root; ///< Root of the deck tree (zones + cards). + +public: + /// @brief Constructs an empty DecklistNodeTree + explicit DecklistNodeTree(); + /// @brief Copy constructor. Deep copies the tree + explicit DecklistNodeTree(const DecklistNodeTree &other); + /// @brief Copy-assignment operator. Deep copies the tree + DecklistNodeTree &operator=(const DecklistNodeTree &other); + + virtual ~DecklistNodeTree(); + + /** + * @brief Gets a pointer to the underlying root node. + * Note: DO NOT call this method unless the object needs to have access to the underlying model. + * For now, only the DeckListModel should be calling this. + */ + InnerDecklistNode *getRoot() const + { + return root; + } + + bool isEmpty() const; + + /** + * @brief Deletes all nodes except the root. + */ + void clear(); + + /** + * Gets all card nodes in the tree + * @param restrictToZones Only get the nodes in these zones + * @return A QList containing all the card nodes in the zone. + */ + QList getCardNodes(const QSet &restrictToZones = {}) const; + + /** + * Gets all zone nodes in the tree + * @param restrictToZones If not empty, only get the zone nodes with these names. + * @return A QList containing all the zone nodes in the tree. + */ + QList getZoneNodes(const QSet &restrictToZones = {}) const; + + /** + * @brief Computes the deck hash + */ + QString computeDeckHash() const; + + /** + *@brief Writes the contents of the deck to xml + */ + void write(QXmlStreamWriter *xml) const; + + /** + * @brief Reads a "zone" section of the xml to this tree + */ + void readZoneElement(QXmlStreamReader *xml); + + DecklistCardNode *addCard(const QString &cardName, + int amount, + const QString &zoneName, + int position, + const QString &cardSetName = QString(), + const QString &cardSetCollectorNumber = QString(), + const QString &cardProviderId = QString(), + const bool formatLegal = true); + bool deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode = nullptr); + + /** + * @brief Apply a function to every card in the deck tree. This can modify the cards. + * + * @param func Function taking (zone node, card node). + */ + void forEachCard(const std::function &func) const; + +private: + // Helpers for traversing the tree + InnerDecklistNode *getZoneObjFromName(const QString &zoneName) const; +}; + +#endif // COCKATRICE_DECKLIST_NODE_TREE_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp new file mode 100644 index 000000000..d991ec98e --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp @@ -0,0 +1,59 @@ +#include "sideboard_plan.h" + +#include + +SideboardPlan::SideboardPlan(const QString &_name, const QList &_moveList) + : name(_name), moveList(_moveList) +{ +} + +void SideboardPlan::setMoveList(const QList &_moveList) +{ + moveList = _moveList; +} + +bool SideboardPlan::readElement(QXmlStreamReader *xml) +{ + while (!xml->atEnd()) { + xml->readNext(); + const QString childName = xml->name().toString(); + if (xml->isStartElement()) { + if (childName == "name") + name = xml->readElementText(); + else if (childName == "move_card_to_zone") { + MoveCard_ToZone m; + while (!xml->atEnd()) { + xml->readNext(); + const QString childName2 = xml->name().toString(); + if (xml->isStartElement()) { + if (childName2 == "card_name") + m.set_card_name(xml->readElementText().toStdString()); + else if (childName2 == "start_zone") + m.set_start_zone(xml->readElementText().toStdString()); + else if (childName2 == "target_zone") + m.set_target_zone(xml->readElementText().toStdString()); + } else if (xml->isEndElement() && (childName2 == "move_card_to_zone")) { + moveList.append(m); + break; + } + } + } + } else if (xml->isEndElement() && (childName == "sideboard_plan")) + return true; + } + return false; +} + +void SideboardPlan::write(QXmlStreamWriter *xml) const +{ + xml->writeStartElement("sideboard_plan"); + xml->writeTextElement("name", name); + for (auto &i : moveList) { + xml->writeStartElement("move_card_to_zone"); + xml->writeTextElement("card_name", QString::fromStdString(i.card_name())); + xml->writeTextElement("start_zone", QString::fromStdString(i.start_zone())); + xml->writeTextElement("target_zone", QString::fromStdString(i.target_zone())); + xml->writeEndElement(); + } + xml->writeEndElement(); +} \ No newline at end of file diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.h b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.h new file mode 100644 index 000000000..524217c2d --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.h @@ -0,0 +1,70 @@ +#ifndef COCKATRICE_SIDEBOARD_PLAN_H +#define COCKATRICE_SIDEBOARD_PLAN_H + +#include +#include + +class QXmlStreamWriter; +class QXmlStreamReader; + +/** + * @class SideboardPlan + * @ingroup Decks + * @brief Represents a predefined sideboarding strategy for a deck. + * + * Sideboard plans store a named list of card movements that should be applied + * between the mainboard and sideboard for a specific matchup. Each movement + * is expressed using a `MoveCard_ToZone` protobuf message. + * + * ### Responsibilities: + * - Store the plan name and list of moves. + * - Support XML serialization/deserialization. + * + * ### Typical usage: + * A deck can contain multiple sideboard plans (e.g., "vs Aggro", "vs Control"), + * each describing how to transform the main deck into its intended configuration. + */ +class SideboardPlan +{ +private: + QString name; ///< Human-readable name of this plan. + QList moveList; ///< List of move instructions for this plan. + +public: + /** + * @brief Construct a new SideboardPlan. + * @param _name The plan name. + * @param _moveList Initial list of card move instructions. + */ + explicit SideboardPlan(const QString &_name = "", const QList &_moveList = {}); + + /** + * @brief Read a SideboardPlan from an XML stream. + * @param xml XML reader positioned at the plan element. + * @return true if parsing succeeded. + */ + bool readElement(QXmlStreamReader *xml); + + /** + * @brief Write this SideboardPlan to XML. + * @param xml Stream to append the serialized element to. + */ + void write(QXmlStreamWriter *xml) const; + + /// @return The plan name. + [[nodiscard]] QString getName() const + { + return name; + } + + /// @return Const reference to the move list. + [[nodiscard]] const QList &getMoveList() const + { + return moveList; + } + + /// @brief Replace the move list with a new one. + void setMoveList(const QList &_moveList); +}; + +#endif // COCKATRICE_SIDEBOARD_PLAN_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.cpp new file mode 100644 index 000000000..b8a497c20 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.cpp @@ -0,0 +1,62 @@ +#include "abstract_deck_list_card_node.h" + +bool AbstractDecklistCardNode::compare(AbstractDecklistNode *other) const +{ + switch (sortMethod) { + case ByNumber: + return compareNumber(other); + case ByName: + return compareName(other); + default: + return false; + } +} + +bool AbstractDecklistCardNode::compareNumber(AbstractDecklistNode *other) const +{ + auto *other2 = dynamic_cast(other); + if (other2) { + int n1 = getNumber(); + int n2 = other2->getNumber(); + return (n1 != n2) ? (n1 > n2) : compareName(other); + } else { + return true; + } +} + +bool AbstractDecklistCardNode::compareName(AbstractDecklistNode *other) const +{ + auto *other2 = dynamic_cast(other); + if (other2) { + return (getName() > other2->getName()); + } else { + return true; + } +} + +bool AbstractDecklistCardNode::readElement(QXmlStreamReader *xml) +{ + while (!xml->atEnd()) { + xml->readNext(); + if (xml->isEndElement() && xml->name().toString() == "card") + return false; + } + return true; +} + +void AbstractDecklistCardNode::writeElement(QXmlStreamWriter *xml) +{ + xml->writeEmptyElement("card"); + xml->writeAttribute("number", QString::number(getNumber())); + xml->writeAttribute("name", getName()); + + if (!getCardSetShortName().isEmpty()) { + xml->writeAttribute("setShortName", getCardSetShortName()); + } + if (!getCardCollectorNumber().isEmpty()) { + xml->writeAttribute("collectorNumber", getCardCollectorNumber()); + } + if (!getCardProviderId().isEmpty()) { + xml->writeAttribute("uuid", getCardProviderId()); + } +} \ No newline at end of file diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h new file mode 100644 index 000000000..88d8b0930 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h @@ -0,0 +1,155 @@ +/** + * @file abstract_deck_list_card_node.h + * @brief Defines the AbstractDecklistCardNode base class, which adds + * card-specific behavior on top of AbstractDecklistNode. + * + * This class is the intermediate abstract base between the generic + * AbstractDecklistNode and concrete card entries such as DecklistCardNode + * or DecklistModelCardNode. + */ + +#ifndef COCKATRICE_ABSTRACT_DECK_LIST_CARD_NODE_H +#define COCKATRICE_ABSTRACT_DECK_LIST_CARD_NODE_H + +#include "abstract_deck_list_node.h" + +/** + * @class AbstractDecklistCardNode + * @ingroup DeckModels + * @brief Abstract base class for all deck list nodes that represent + * actual card entries. + * + * While AbstractDecklistNode provides the general interface for all + * nodes in the deck tree (zones, groups, cards), this subclass refines + * the interface to cover properties specific to *cards*: + * - Quantity (number of copies). + * - Name. + * - Set code and collector number. + * - Provider ID. + * + * ### Role in the hierarchy: + * - Leaf-oriented abstract class; no children of its own. + * - Serves as the base for concrete implementations: + * - @c DecklistCardNode: Stores real card data in the deck tree. + * - @c DecklistModelCardNode: Wraps a DecklistCardNode for use + * in the Qt model layer. + * + * ### Responsibilities: + * - Defines getters/setters for all card-identifying attributes. + * - Provides comparison logic for sorting by name or number. + * - Implements XML serialization for saving/loading deck files. + * + * ### Ownership: + * - As with all nodes, owned by its parent InnerDecklistNode. + */ +class AbstractDecklistCardNode : public AbstractDecklistNode +{ +public: + /** + * @brief Construct a new AbstractDecklistCardNode. + * + * @param _parent Optional parent node. If provided, this node + * will be inserted into the parent’s children list. + * @param position Index at which to insert into parent’s children. + * If -1, the node is appended to the end. + */ + explicit AbstractDecklistCardNode(InnerDecklistNode *_parent = nullptr, int position = -1) + : AbstractDecklistNode(_parent, position) + { + } + + /// @return The number of copies of this card in the deck. + [[nodiscard]] virtual int getNumber() const = 0; + + /// @param _number Set the number of copies of this card. + virtual void setNumber(int _number) = 0; + + /// @return The display name of this card. + [[nodiscard]] QString getName() const override = 0; + + /// @param _name Set the display name of this card. + virtual void setName(const QString &_name) = 0; + + /// @return The provider identifier for this card (e.g., UUID). + [[nodiscard]] virtual QString getCardProviderId() const override = 0; + + /// @param _cardProviderId Set the provider identifier for this card. + virtual void setCardProviderId(const QString &_cardProviderId) = 0; + + /// @return The abbreviated set code (e.g., "NEO"). + [[nodiscard]] virtual QString getCardSetShortName() const override = 0; + + /// @param _cardSetShortName Set the abbreviated set code. + virtual void setCardSetShortName(const QString &_cardSetShortName) = 0; + + /// @return The collector number of the card within its set. + [[nodiscard]] virtual QString getCardCollectorNumber() const override = 0; + + /// @param _cardSetNumber Set the collector number. + virtual void setCardCollectorNumber(const QString &_cardSetNumber) = 0; + + /// @return The format legality of the card + virtual bool getFormatLegality() const = 0; + + /// @param _formatLegal If the card is considered legal + virtual void setFormatLegality(const bool _formatLegal) = 0; + + /** + * @brief Get the height of this node in the tree. + * + * For card nodes, height is always 0 because they are leaf nodes + * and do not contain children. + * + * @return 0 + */ + [[nodiscard]] int height() const override + { + return 0; + } + + /** + * @brief Compare this card node against another for sorting. + * + * Uses the node’s current @c sortMethod to determine how to compare: + * - ByName: Alphabetical comparison. + * - ByNumber: Numerical comparison. + * - Default: Falls back to implementation-defined behavior. + * + * @param other Another node to compare against. + * @return true if this node should sort before @p other. + */ + bool compare(AbstractDecklistNode *other) const override; + + /** + * @brief Compare this card node to another by quantity. + * @param other Node to compare against. + * @return true if this node’s number < other’s number. + */ + bool compareNumber(AbstractDecklistNode *other) const; + + /** + * @brief Compare this card node to another by name. + * @param other Node to compare against. + * @return true if this node’s name comes before other’s name. + */ + bool compareName(AbstractDecklistNode *other) const; + + /** + * @brief Deserialize this node’s properties from XML. + * @param xml QXmlStreamReader positioned at the element. + * @return true if parsing succeeded. + * + * This supports loading deck files from Cockatrice’s XML format. + */ + bool readElement(QXmlStreamReader *xml) override; + + /** + * @brief Serialize this node’s properties to XML. + * @param xml Writer to append this node’s XML element. + * + * This supports saving deck files to Cockatrice’s XML format. + */ + void writeElement(QXmlStreamWriter *xml) override; +}; + +#endif // COCKATRICE_ABSTRACT_DECK_LIST_CARD_NODE_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.cpp new file mode 100644 index 000000000..0f39ce71a --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.cpp @@ -0,0 +1,24 @@ +#include "abstract_deck_list_node.h" + +#include "inner_deck_list_node.h" + +AbstractDecklistNode::AbstractDecklistNode(InnerDecklistNode *_parent, int position) + : parent(_parent), sortMethod(Default) +{ + if (parent) { + if (position == -1) { + parent->append(this); + } else { + parent->insert(position, this); + } + } +} + +int AbstractDecklistNode::depth() const +{ + if (parent) { + return parent->depth() + 1; + } else { + return 0; + } +} \ No newline at end of file diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.h new file mode 100644 index 000000000..877705705 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.h @@ -0,0 +1,185 @@ +/** + * @file abstract_deck_list_node.h + * @brief Defines the AbstractDecklistNode base class used as the foundation + * for all nodes in the deck list tree (zones, groups, and cards). + * + * The deck list is modeled as a tree: + * - The invisible root node is managed by DeckListModel. + * - Top-level children are zones (e.g. Mainboard, Sideboard). + * - Zones contain grouping nodes (e.g. by type, color, or mana cost). + * - Grouping nodes contain card nodes. + * + * This abstract base class provides the interface and shared functionality + * for all node types. Concrete subclasses (InnerDecklistNode, + * DecklistCardNode, DecklistModelCardNode, etc.) implement the specifics. + */ + +#ifndef COCKATRICE_ABSTRACT_DECK_LIST_NODE_H +#define COCKATRICE_ABSTRACT_DECK_LIST_NODE_H + +#include + +/** + * @enum DeckSortMethod + * @ingroup DeckModels + * @brief Defines the different sort strategies a node may use + * to order its children. + * + * Sorting behavior is typically set by the DeckListModel when the user + * requests sorting in the UI. + * + * - ByNumber: Sort numerically (often by collector number). + * - ByName: Sort alphabetically by card name. + * - Default: No explicit sorting; insertion order is preserved. + */ +enum DeckSortMethod +{ + ByNumber, ///< Sort by numeric properties (e.g. collector number). + ByName, ///< Sort by card name (locale-aware comparison). + Default ///< Leave in insertion order. +}; + +class InnerDecklistNode; + +/** + * @class AbstractDecklistNode + * @ingroup DeckModels + * @brief Base class for all nodes in the deck list tree. + * + * This class defines the common interface for every node in the + * deck representation: zones, groupings, and cards. + * + * Responsibilities: + * - Maintain a pointer to its parent (if any). + * - Track the sorting method to be used for child nodes. + * - Provide a consistent interface for retrieving basic identifying + * properties (name, set, collector number, provider ID). + * - Define abstract methods for XML serialization, used when saving + * or loading deck files. + * + * Lifetime / Ownership: + * - Nodes are arranged hierarchically under @c InnerDecklistNode parents. + * - The parent takes ownership of its children; destruction cascades. + * - The DeckListModel holds the invisible root node, which in turn + * owns the entire hierarchy. + * + * Extension: + * - @c InnerDecklistNode is the concrete subclass representing + * "folders" in the tree (zones, groups). + * - @c DecklistCardNode and @c DecklistModelCardNode represent + * actual card entries. + */ +class AbstractDecklistNode +{ +protected: + /** + * @brief Pointer to the parent node, or nullptr if this is the root. + * + * Ownership note: The parent is responsible for destroying this node + * when it is removed from the tree. + */ + InnerDecklistNode *parent; + + /** + * @brief Current sorting strategy for this node's children. + * + * Sorting is applied recursively by the DeckListModel when + * the view requests it. + */ + DeckSortMethod sortMethod; + +public: + /** + * @brief Construct a new AbstractDecklistNode and insert it into its parent. + * + * @param _parent Parent node. May be nullptr if this is the root. + * @param position Optional index at which to insert into the parent's + * children. If -1, the node is appended to the end. + * + * If a parent is provided, the constructor automatically appends + * or inserts this node into the parent’s child list. + */ + explicit AbstractDecklistNode(InnerDecklistNode *_parent = nullptr, int position = -1); + + /// Virtual destructor. Child classes must clean up their resources. + virtual ~AbstractDecklistNode() = default; + + /** + * @brief Set the sort method for this node’s children. + * @param method The sorting strategy to use. + * + * Subclasses may override if they need to apply additional logic. + */ + virtual void setSortMethod(DeckSortMethod method) + { + sortMethod = method; + } + + /** + * @name Core identification properties + * + * These methods provide a standard way for the model to retrieve + * identifying information about a node, regardless of type. + * @{ + */ + [[nodiscard]] virtual QString getName() const = 0; + [[nodiscard]] virtual QString getCardProviderId() const = 0; + [[nodiscard]] virtual QString getCardSetShortName() const = 0; + [[nodiscard]] virtual QString getCardCollectorNumber() const = 0; + /// @} + + /** + * @brief Whether this node is the "deck header" (deck metadata). + * + * This distinguishes special nodes that represent deck-level + * information rather than cards or groupings. + */ + [[nodiscard]] virtual bool isDeckHeader() const = 0; + + /// @return The parent node, or nullptr if this is the root. + [[nodiscard]] InnerDecklistNode *getParent() const + { + return parent; + } + + /** + * @brief Compute the depth of this node in the tree. + * @return Distance from the root (root = 0, children = 1, etc.). + */ + [[nodiscard]] int depth() const; + + /** + * @brief Compute the "height" of this node. + * + * Height is defined by subclasses; it usually represents how + * many levels of descendants this node spans. + * + * For example: + * - A card node has height 1. + * - A group node containing cards has height 2. + */ + [[nodiscard]] virtual int height() const = 0; + + /** + * @brief Compare this node against another for sorting. + * + * The semantics of comparison depend on the node type and the + * current @c sortMethod. + * + * @param other The node to compare against. + * @return true if this node should come before @p other. + */ + virtual bool compare(AbstractDecklistNode *other) const = 0; + + /** + * @name XML serialization + * These methods support reading and writing decks from/to + * Cockatrice deck XML format. + * @{ + */ + virtual bool readElement(QXmlStreamReader *xml) = 0; + virtual void writeElement(QXmlStreamWriter *xml) = 0; + /// @} +}; + +#endif // COCKATRICE_ABSTRACT_DECK_LIST_NODE_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.cpp new file mode 100644 index 000000000..1791c09a6 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.cpp @@ -0,0 +1,8 @@ +#include "deck_list_card_node.h" + +DecklistCardNode::DecklistCardNode(DecklistCardNode *other, InnerDecklistNode *_parent) + : AbstractDecklistCardNode(_parent), name(other->getName()), number(other->getNumber()), + cardSetShortName(other->getCardSetShortName()), cardSetNumber(other->getCardCollectorNumber()), + cardProviderId(other->getCardProviderId()) +{ +} \ No newline at end of file diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h new file mode 100644 index 000000000..b3d42b89a --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h @@ -0,0 +1,186 @@ +/** + * @file deck_list_card_node.h + * @brief Defines the DecklistCardNode class, representing a single card entry + * in the deck list tree. + * + * DecklistCardNode is the concrete data-bearing node that corresponds to + * an individual card entry in a deck. It stores the card’s name, quantity, + * set information, and provider ID. These nodes live inside an + * InnerDecklistNode (e.g., under Mainboard → Group → Card). + */ + +#ifndef COCKATRICE_DECK_LIST_CARD_NODE_H +#define COCKATRICE_DECK_LIST_CARD_NODE_H + +#include "abstract_deck_list_card_node.h" + +#include + +/** + * @class DecklistCardNode + * @ingroup DeckModels + * @brief Concrete node type representing an actual card entry in the deck. + * + * This class extends AbstractDecklistCardNode to hold all information + * needed to uniquely identify a card printing within the deck. + * + * ### Role in the hierarchy: + * - Child of an InnerDecklistNode (which groups cards by zone or criteria). + * - Leaf node in the deck tree; it does not contain further children. + * + * ### Data stored: + * - @c name: Card’s display name. + * - @c number: Quantity of this card in the deck. + * - @c cardSetShortName: Abbreviation of the set (e.g., "NEO" for Neon Dynasty). + * - @c cardSetNumber: Collector number within the set. + * - @c cardProviderId: External provider identifier (e.g., UUID or MTGJSON ID). + * + * ### Usage: + * - Constructed directly when building a deck list from user input or file. + * - Used by DeckListModel to present cards in Qt views. + * - Convertible to @c CardRef for database lookups or cross-references. + * + * ### Ownership: + * - Owned by its parent InnerDecklistNode. + * - Destroyed automatically when its parent is destroyed. + */ +class DecklistCardNode : public AbstractDecklistCardNode +{ + QString name; ///< Display name of the card. + int number; ///< Quantity of this card in the deck. + QString cardSetShortName; ///< Short set code (e.g., "NEO"). + QString cardSetNumber; ///< Collector number within the set. + QString cardProviderId; ///< External provider identifier (e.g., UUID). + bool formatLegal; ///< Format legality + +public: + /** + * @brief Construct a new DecklistCardNode. + * + * @param _name Display name of the card. + * @param _number Quantity of this card (default = 1). + * @param _parent Parent node in the tree (zone or group). May be nullptr. + * @param position Index to insert into parent’s children. -1 = append. + * @param _cardSetShortName Short set code (e.g., "NEO"). + * @param _cardSetNumber Collector number within the set. + * @param _cardProviderId External provider ID (e.g., UUID). + * @param _formatLegality If the card is legal in the format + * + * On construction, if a parent is provided, this node is inserted into + * the parent’s children list automatically. + */ + explicit DecklistCardNode(QString _name = QString(), + int _number = 1, + InnerDecklistNode *_parent = nullptr, + int position = -1, + QString _cardSetShortName = QString(), + QString _cardSetNumber = QString(), + QString _cardProviderId = QString(), + bool _formatLegality = true) + : AbstractDecklistCardNode(_parent, position), name(std::move(_name)), number(_number), + cardSetShortName(std::move(_cardSetShortName)), cardSetNumber(std::move(_cardSetNumber)), + cardProviderId(std::move(_cardProviderId)), formatLegal(_formatLegality) + { + } + + /** + * @brief Copy constructor with new parent assignment. + * @param other Existing DecklistCardNode to copy. + * @param _parent Parent node for the copy. + * + * Creates a deep copy of the card node’s properties, but attaches + * the new instance to a different parent in the tree. + */ + explicit DecklistCardNode(DecklistCardNode *other, InnerDecklistNode *_parent); + + /// @return The quantity of this card. + [[nodiscard]] int getNumber() const override + { + return number; + } + + /// @param _number Set the quantity of this card. + void setNumber(int _number) override + { + number = _number; + } + + /// @return The display name of this card. + [[nodiscard]] QString getName() const override + { + return name; + } + + /// @param _name Set the display name of this card. + void setName(const QString &_name) override + { + name = _name; + } + + /// @return The provider identifier for this card. + [[nodiscard]] QString getCardProviderId() const override + { + return cardProviderId; + } + + /// @param _providerId Set the provider identifier for this card. + void setCardProviderId(const QString &_providerId) override + { + cardProviderId = _providerId; + } + + /// @return The short set code (e.g., "NEO"). + [[nodiscard]] QString getCardSetShortName() const override + { + return cardSetShortName; + } + + /// @param _cardSetShortName Set the short set code. + void setCardSetShortName(const QString &_cardSetShortName) override + { + cardSetShortName = _cardSetShortName; + } + + /// @return The collector number of this card within its set. + [[nodiscard]] QString getCardCollectorNumber() const override + { + return cardSetNumber; + } + + /// @param _cardSetNumber Set the collector number. + void setCardCollectorNumber(const QString &_cardSetNumber) override + { + cardSetNumber = _cardSetNumber; + } + + /// @return The format legality of the card + [[nodiscard]] bool getFormatLegality() const override + { + return formatLegal; + } + + /// @param _formatLegal If the card is considered legal + void setFormatLegality(const bool _formatLegal) override + { + formatLegal = _formatLegal; + } + + /// @return Always false; card nodes are not deck headers. + [[nodiscard]] bool isDeckHeader() const override + { + return false; + } + + /** + * @brief Convert this node to a CardRef. + * + * @return A CardRef with the card’s name and provider ID, suitable + * for database lookups or comparison with other card sources. + */ + [[nodiscard]] CardRef toCardRef() const + { + return {name, cardProviderId}; + } +}; + +#endif // COCKATRICE_DECK_LIST_CARD_NODE_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp new file mode 100644 index 000000000..eca58963a --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp @@ -0,0 +1,199 @@ +#include "inner_deck_list_node.h" + +#include "deck_list_card_node.h" + +InnerDecklistNode::InnerDecklistNode(InnerDecklistNode *other, InnerDecklistNode *_parent) + : AbstractDecklistNode(_parent), name(other->getName()) +{ + for (int i = 0; i < other->size(); ++i) { + auto *inner = dynamic_cast(other->at(i)); + if (inner) { + new InnerDecklistNode(inner, this); + } else { + new DecklistCardNode(dynamic_cast(other->at(i)), this); + } + } +} + +InnerDecklistNode::~InnerDecklistNode() +{ + clearTree(); +} + +QString InnerDecklistNode::visibleNameFromName(const QString &_name) +{ + if (_name == DECK_ZONE_MAIN) { + return QObject::tr("Maindeck"); + } else if (_name == DECK_ZONE_SIDE) { + return QObject::tr("Sideboard"); + } else if (_name == DECK_ZONE_TOKENS) { + return QObject::tr("Tokens"); + } else { + return _name; + } +} + +void InnerDecklistNode::setSortMethod(DeckSortMethod method) +{ + sortMethod = method; + for (int i = 0; i < size(); i++) { + at(i)->setSortMethod(method); + } +} + +QString InnerDecklistNode::getVisibleName() const +{ + return visibleNameFromName(name); +} + +void InnerDecklistNode::clearTree() +{ + for (int i = 0; i < size(); i++) + delete at(i); + clear(); +} + +AbstractDecklistNode *InnerDecklistNode::findChild(const QString &_name) +{ + for (int i = 0; i < size(); i++) { + if (at(i)->getName() == _name) { + return at(i); + } + } + return nullptr; +} + +AbstractDecklistNode *InnerDecklistNode::findCardChildByNameProviderIdAndNumber(const QString &_name, + const QString &_providerId, + const QString &_cardNumber) +{ + for (const auto &i : *this) { + if (!i || i->getName() != _name) { + continue; + } + if (_cardNumber != "" && i->getCardCollectorNumber() != _cardNumber) { + continue; + } + if (_providerId != "" && i->getCardProviderId() != _providerId) { + continue; + } + return i; + } + return nullptr; +} + +int InnerDecklistNode::height() const +{ + return at(0)->height() + 1; +} + +int InnerDecklistNode::recursiveCount(bool countTotalCards) const +{ + int result = 0; + for (int i = 0; i < size(); i++) { + auto *node = dynamic_cast(at(i)); + + if (node) { + result += node->recursiveCount(countTotalCards); + } else if (countTotalCards) { + result += dynamic_cast(at(i))->getNumber(); + } else { + result++; + } + } + return result; +} + +bool InnerDecklistNode::compare(AbstractDecklistNode *other) const +{ + switch (sortMethod) { + case ByNumber: + return compareNumber(other); + case ByName: + return compareName(other); + default: + return false; + } +} + +bool InnerDecklistNode::compareNumber(AbstractDecklistNode *other) const +{ + auto *other2 = dynamic_cast(other); + if (other2) { + int n1 = recursiveCount(true); + int n2 = other2->recursiveCount(true); + return (n1 != n2) ? (n1 > n2) : compareName(other); + } else { + return false; + } +} + +bool InnerDecklistNode::compareName(AbstractDecklistNode *other) const +{ + auto *other2 = dynamic_cast(other); + if (other2) { + return (getName() > other2->getName()); + } else { + return false; + } +} + +bool InnerDecklistNode::readElement(QXmlStreamReader *xml) +{ + while (!xml->atEnd()) { + xml->readNext(); + const QString childName = xml->name().toString(); + if (xml->isStartElement()) { + if (childName == "zone") { + auto *newZone = new InnerDecklistNode(xml->attributes().value("name").toString(), this); + newZone->readElement(xml); + } else if (childName == "card") { + auto *newCard = new DecklistCardNode( + xml->attributes().value("name").toString(), xml->attributes().value("number").toString().toInt(), + this, -1, xml->attributes().value("setShortName").toString(), + xml->attributes().value("collectorNumber").toString(), xml->attributes().value("uuid").toString()); + newCard->readElement(xml); + } + } else if (xml->isEndElement() && (childName == "zone")) + return false; + } + return true; +} + +void InnerDecklistNode::writeElement(QXmlStreamWriter *xml) +{ + xml->writeStartElement("zone"); + xml->writeAttribute("name", name); + for (int i = 0; i < size(); i++) + at(i)->writeElement(xml); + xml->writeEndElement(); // zone +} + +QVector> InnerDecklistNode::sort(Qt::SortOrder order) +{ + QVector> result(size()); + + // Initialize temporary list with contents of current list + QVector> tempList(size()); + for (int i = size() - 1; i >= 0; --i) { + tempList[i].first = i; + tempList[i].second = at(i); + } + + // Sort temporary list + auto cmp = [order](const auto &a, const auto &b) { + return (order == Qt::AscendingOrder) ? (b.second->compare(a.second)) : (a.second->compare(b.second)); + }; + + std::sort(tempList.begin(), tempList.end(), cmp); + + // Map old indexes to new indexes and + // copy temporary list to the current one + for (int i = size() - 1; i >= 0; --i) { + result[i].first = tempList[i].first; + result[i].second = i; + replace(i, tempList[i].second); + } + + return result; +} \ No newline at end of file diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h new file mode 100644 index 000000000..f4c48afce --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h @@ -0,0 +1,228 @@ +/** + * @file inner_deck_list_node.h + * @brief Defines the InnerDecklistNode class, which represents + * structural nodes (zones and groups) in the deck tree. + * + * The deck tree consists of: + * - A root node (invisible). + * - Zones (Main, Sideboard, Tokens). + * - Optional grouping nodes (e.g., by type, color, or mana cost). + * - Card nodes as leaves. + * + * InnerDecklistNode implements the zone/group nodes and provides + * storage and management of child nodes. + */ + +#ifndef COCKATRICE_INNER_DECK_LIST_NODE_H +#define COCKATRICE_INNER_DECK_LIST_NODE_H + +#include "abstract_deck_list_node.h" + +/// Constant for the "main" deck zone name. +#define DECK_ZONE_MAIN "main" +/// Constant for the "sideboard" zone name. +#define DECK_ZONE_SIDE "side" +/// Constant for the "tokens" zone name. +#define DECK_ZONE_TOKENS "tokens" + +/** + * @class InnerDecklistNode + * @brief Represents a container node in the deck list hierarchy + * (zones and groupings). + * + * Unlike DecklistCardNode, which holds leaf card data, this class + * manages collections of child nodes, which may themselves be + * InnerDecklistNode or DecklistCardNode objects. + * + * ### Role in the hierarchy: + * - Root node (invisible): Holds zones. + * - Zone nodes: "main", "side", "tokens". + * - Grouping nodes: Created dynamically when grouping by type, + * color, or mana cost. + * - Card nodes: Always children of an InnerDecklistNode. + * + * ### Design notes: + * - Inherits from AbstractDecklistNode (tree interface) and + * QList (storage of children). + * This allows direct QList-style manipulation of children while + * still presenting a polymorphic node interface. + * + * ### Responsibilities: + * - Store a display name. + * - Own and manage child nodes (insert, clear, find). + * - Provide recursive operations such as counting cards or computing height. + * - Implement sorting logic for reordering children. + * - Implement XML serialization for persistence. + * + * ### Ownership: + * - Owns all child nodes stored in the QList. The destructor + * recursively deletes children. + */ +class InnerDecklistNode : public AbstractDecklistNode, public QList +{ + QString name; ///< Internal identifier for this node (zone or group name). + +public: + /** + * @brief Construct a new InnerDecklistNode. + * + * @param _name Internal name (e.g., "main", "side", "tokens", or group label). + * @param _parent Parent node (may be nullptr for the root). + * @param position Optional index for insertion into parent. -1 = append. + */ + explicit InnerDecklistNode(QString _name = QString(), InnerDecklistNode *_parent = nullptr, int position = -1) + : AbstractDecklistNode(_parent, position), name(std::move(_name)) + { + } + + /** + * @brief Copy constructor with parent reassignment. + * @param other Node to copy from (deep copy of children). + * @param _parent Parent node for the copy. + */ + explicit InnerDecklistNode(InnerDecklistNode *other, InnerDecklistNode *_parent = nullptr); + + /** + * @brief Destructor. Recursively deletes all child nodes. + */ + ~InnerDecklistNode() override; + + /** + * @brief Set the sorting method for this node and all children. + * @param method Sort method to apply recursively. + */ + void setSortMethod(DeckSortMethod method) override; + + /// @return The internal name of this node. + [[nodiscard]] QString getName() const override + { + return name; + } + + /// @param _name Set the internal name of this node. + void setName(const QString &_name) + { + name = _name; + } + + /** + * @brief Translate an internal name into a user-visible name. + * + * For example, the internal string "main" is presented as + * "Mainboard" in the UI. + * + * @param _name Internal identifier. + * @return Display-friendly string. + */ + static QString visibleNameFromName(const QString &_name); + + /** + * @brief Get this node’s display-friendly name. + * @return Human-readable name (zone/group name). + */ + [[nodiscard]] virtual QString getVisibleName() const; + + /// @return Always empty for container nodes. + [[nodiscard]] QString getCardProviderId() const override + { + return ""; + } + + /// @return Always empty for container nodes. + [[nodiscard]] QString getCardSetShortName() const override + { + return ""; + } + + /// @return Always empty for container nodes. + [[nodiscard]] QString getCardCollectorNumber() const override + { + return ""; + } + + /// @return Always true; InnerDecklistNode represents deck structure. + [[nodiscard]] bool isDeckHeader() const override + { + return true; + } + + /** + * @brief Delete all children of this node, recursively. + */ + void clearTree(); + + /** + * @brief Find a direct child node by name. + * @param _name Name to match. + * @return Pointer to child node, or nullptr if not found. + */ + AbstractDecklistNode *findChild(const QString &_name); + + /** + * @brief Find a child card node by name, provider ID, and collector number. + * + * Searches immediate children only. + * + * @param _name Card name to match. + * @param _providerId Optional provider ID to match. + * @param _cardNumber Optional collector number to match. + * @return Pointer to child node if found, nullptr otherwise. + */ + AbstractDecklistNode *findCardChildByNameProviderIdAndNumber(const QString &_name, + const QString &_providerId = "", + const QString &_cardNumber = ""); + + /** + * @brief Compute the height of this node. + * @return Maximum depth of descendants + 1. + */ + [[nodiscard]] int height() const override; + + /** + * @brief Count cards recursively under this node. + * @param countTotalCards If true, sums up quantities of cards. + * If false, counts unique card nodes. + * @return Total count. + */ + [[nodiscard]] int recursiveCount(bool countTotalCards = false) const; + + /** + * @brief Compare this node against another for sorting. + * + * Uses current @c sortMethod to determine the comparison. + * + * @param other Node to compare. + * @return true if this node should sort before @p other. + */ + bool compare(AbstractDecklistNode *other) const override; + + /// @copydoc compare(AbstractDecklistNode*) const + bool compareNumber(AbstractDecklistNode *other) const; + + /// @copydoc compare(AbstractDecklistNode*) const + bool compareName(AbstractDecklistNode *other) const; + + /** + * @brief Sort this node’s children recursively. + * + * @param order Ascending or descending. + * @return A QVector of (oldIndex, newIndex) pairs indicating + * how children were reordered. + */ + QVector> sort(Qt::SortOrder order = Qt::AscendingOrder); + + /** + * @brief Deserialize this node and its children from XML. + * @param xml Reader positioned at this element. + * @return true if parsing succeeded. + */ + bool readElement(QXmlStreamReader *xml) override; + + /** + * @brief Serialize this node and its children to XML. + * @param xml Writer to append elements to. + */ + void writeElement(QXmlStreamWriter *xml) override; +}; + +#endif // COCKATRICE_INNER_DECK_LIST_NODE_H diff --git a/libcockatrice_filters/CMakeLists.txt b/libcockatrice_filters/CMakeLists.txt new file mode 100644 index 000000000..74566ca05 --- /dev/null +++ b/libcockatrice_filters/CMakeLists.txt @@ -0,0 +1,24 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS libcockatrice/filters/filter_card.h libcockatrice/filters/filter_string.h + libcockatrice/filters/filter_tree.h +) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library( + libcockatrice_filters STATIC ${MOC_SOURCES} libcockatrice/filters/filter_card.cpp + libcockatrice/filters/filter_string.cpp libcockatrice/filters/filter_tree.cpp +) + +add_dependencies(libcockatrice_filters libcockatrice_card) + +target_include_directories(libcockatrice_filters PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(libcockatrice_filters PUBLIC libcockatrice_card ${QT_CORE_MODULE}) diff --git a/libcockatrice_filters/libcockatrice/filters/filter_card.cpp b/libcockatrice_filters/libcockatrice/filters/filter_card.cpp new file mode 100644 index 000000000..5fdce7ae0 --- /dev/null +++ b/libcockatrice_filters/libcockatrice/filters/filter_card.cpp @@ -0,0 +1,94 @@ +#include "filter_card.h" + +#include + +QJsonObject CardFilter::toJson() const +{ + QJsonObject obj; + obj["term"] = trm; + obj["type"] = typeName(t); + obj["attr"] = attrName(a); + return obj; +} + +CardFilter *CardFilter::fromJson(const QJsonObject &obj) +{ + QString term = obj["term"].toString(); + QString typeStr = obj["type"].toString(); + QString attrStr = obj["attr"].toString(); + + Type type = TypeEnd; + Attr attr = AttrEnd; + + // Convert type string back to enum + for (int i = 0; i < TypeEnd; i++) { + if (typeName(static_cast(i)) == typeStr) { + type = static_cast(i); + break; + } + } + + // Convert attr string back to enum + for (int i = 0; i < AttrEnd; i++) { + if (attrName(static_cast(i)) == attrStr) { + attr = static_cast(i); + break; + } + } + + return new CardFilter(term, type, attr); +} + +const QString CardFilter::typeName(Type t) +{ + switch (t) { + case TypeAnd: + return tr("AND", "Logical conjunction operator used in card filter"); + case TypeOr: + return tr("OR", "Logical disjunction operator used in card filter"); + case TypeAndNot: + return tr("AND NOT", "Negated logical conjunction operator used in card filter"); + case TypeOrNot: + return tr("OR NOT", "Negated logical disjunction operator used in card filter"); + default: + return QString(""); + } +} + +const QString CardFilter::attrName(Attr a) +{ + switch (a) { + case AttrName: + return tr("Name"); + case AttrNameExact: + return tr("Name (Exact)"); + case AttrType: + return tr("Type"); + case AttrColor: + return tr("Color"); + case AttrText: + return tr("Text"); + case AttrSet: + return tr("Set"); + case AttrManaCost: + return tr("Mana Cost"); + case AttrCmc: + return tr("Mana Value"); + case AttrRarity: + return tr("Rarity"); + case AttrPow: + return tr("Power"); + case AttrTough: + return tr("Toughness"); + case AttrLoyalty: + return tr("Loyalty"); + case AttrFormat: + return tr("Format"); + case AttrMainType: + return tr("Main Type"); + case AttrSubType: + return tr("Sub Type"); + default: + return QString(""); + } +} diff --git a/libcockatrice_filters/libcockatrice/filters/filter_card.h b/libcockatrice_filters/libcockatrice/filters/filter_card.h new file mode 100644 index 000000000..732e43a09 --- /dev/null +++ b/libcockatrice_filters/libcockatrice/filters/filter_card.h @@ -0,0 +1,78 @@ +/** + * @file filter_card.h + * @ingroup CardDatabaseModelFilters + * @brief TODO: Document this. + */ + +#ifndef CARDFILTER_H +#define CARDFILTER_H + +#include +#include + +class CardFilter : public QObject +{ + Q_OBJECT + +public: + enum Type + { + TypeAnd = 0, + TypeOr, + TypeAndNot, + TypeOrNot, + TypeEnd + }; + + /* if you add an attribute here you also need to + * add its string representation in attrName */ + enum Attr + { + AttrCmc = 0, + AttrColor, + AttrLoyalty, + AttrManaCost, + AttrName, + AttrNameExact, + AttrPow, + AttrRarity, + AttrSet, + AttrText, + AttrTough, + AttrType, + AttrMainType, + AttrSubType, + AttrFormat, + AttrEnd, + }; + +private: + QString trm; + enum Type t; + enum Attr a; + +public: + CardFilter(QString &term, Type type, Attr attr) : trm(term), t(type), a(attr) + { + } + + [[nodiscard]] Type type() const + { + return t; + } + [[nodiscard]] const QString &term() const + { + return trm; + } + [[nodiscard]] Attr attr() const + { + return a; + } + + [[nodiscard]] QJsonObject toJson() const; + static CardFilter *fromJson(const QJsonObject &json); + static const QString typeName(Type t); + static const QString attrName(Attr a); +}; + +#endif diff --git a/libcockatrice_filters/libcockatrice/filters/filter_string.cpp b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp new file mode 100644 index 000000000..704e8fadb --- /dev/null +++ b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp @@ -0,0 +1,411 @@ +#include "filter_string.h" + +#include +#include +#include +#include +#include +#include + +static peg::parser search(R"( +Start <- QueryPartList +~ws <- [ ]+ +QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws* + +ComplexQueryPart <- SomewhatComplexQueryPart ws "OR" ws ComplexQueryPart / SomewhatComplexQueryPart + +SomewhatComplexQueryPart <- [(] QueryPartList [)] / QueryPart + +QueryPart <- NotQuery / SetQuery / RarityQuery / CMCQuery / FormatQuery / PowerQuery / ToughnessQuery / ColorQuery / TypeQuery / OracleQuery / FieldQuery / GenericQuery + +NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart +SetQuery <- ('e'/'set') [:] FlexStringValue +OracleQuery <- 'o' [:] MatcherString + + +CMCQuery <- ('cmc'/'mv') ws? NumericExpression +PowerQuery <- [Pp] 'ow' 'er'? ws? NumericExpression +ToughnessQuery <- [Tt] 'ou' 'ghness'? ws? NumericExpression + +RarityQuery <- [rR] ':' Rarity +Rarity <- [Cc] 'ommon'? / [Uu] 'ncommon'? / [Rr] 'are'? / [Mm] 'ythic'? / [Ss] 'pecial'? / [a-zA-Z] [a-z]* + +FormatQuery <- 'f' ':' Format / Legality ':' Format +Format <- [a-zA-Z] [a-z]* +Legality <- [Ll] 'egal'? / [Bb] 'anned'? / [Rr] 'estricted' + + +TypeQuery <- [tT] 'ype'? [:] StringValue + +Color <- < [Ww] 'hite'? / [Uu] / [Bb] 'lack'? / [Rr] 'ed'? / [Gg] 'reen'? / [Bb] 'lue'? > +ColorEx <- Color / [mc] + +ColorQuery <- [cC] 'olor'? <[iI]?> <[:!]> ColorEx* + +FieldQuery <- String [:] MatcherString / String ws? NumericExpression + +NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["]. +NonSingleQuoteUnlessEscaped <- "\\\'". / ![']. +UnescapedStringListPart <- !['":<>()=! ]. +SingleApostropheString <- (UnescapedStringListPart+ ws*)* ['] (UnescapedStringListPart+ ws*)* +String <- SingleApostropheString / UnescapedStringListPart+ / ["] ["] / ['] ['] +StringValue <- String / [(] StringList [)] +StringList <- StringListString (ws? [,] ws? StringListString)* +StringListString <- UnescapedStringListPart+ +GenericQuery <- MatcherString + +# A String that can either be a normal string or a regex search string +MatcherString <- RegexMatcher / NormalMatcher + +NormalMatcher <- String +RegexMatcher <- '/' RegexMatcherString '/' +RegexMatcherString <- ('\\/' / !'/' .)+ + +FlexStringValue <- CompactStringSet / String / [(] StringList [)] +CompactStringSet <- StringListString ([,+] StringListString)+ + +NumericExpression <- NumericOperator ws? NumericValue +NumericOperator <- [=:] / <[> +NumericValue <- [0-9]+ +)"); + +static std::once_flag init; + +static void setupParserRules() +{ + auto passthru = [](const peg::SemanticValues &sv) -> Filter { + return !sv.empty() ? std::any_cast(sv[0]) : nullptr; + }; + + search["Start"] = passthru; + search["QueryPartList"] = [](const peg::SemanticValues &sv) -> Filter { + return [=](const CardData &x) { + auto matchesFilter = [&x](const std::any &query) { return std::any_cast(query)(x); }; + return std::all_of(sv.begin(), sv.end(), matchesFilter); + }; + }; + search["ComplexQueryPart"] = [](const peg::SemanticValues &sv) -> Filter { + return [=](const CardData &x) { + auto matchesFilter = [&x](const std::any &query) { return std::any_cast(query)(x); }; + return std::any_of(sv.begin(), sv.end(), matchesFilter); + }; + }; + search["SomewhatComplexQueryPart"] = passthru; + search["QueryPart"] = passthru; + search["NotQuery"] = [](const peg::SemanticValues &sv) -> Filter { + const auto dependent = std::any_cast(sv[0]); + return [=](const CardData &x) -> bool { return !dependent(x); }; + }; + search["TypeQuery"] = [](const peg::SemanticValues &sv) -> Filter { + const auto matcher = std::any_cast(sv[0]); + return [=](const CardData &x) -> bool { return matcher(x->getCardType()); }; + }; + search["SetQuery"] = [](const peg::SemanticValues &sv) -> Filter { + auto matcher = std::any_cast(sv[0]); + return [=](const CardData &x) -> bool { + QList sets = x->getSets().keys(); + + auto matchesSet = [&matcher](const QString &set) { return matcher(set); }; + return std::any_of(sets.begin(), sets.end(), matchesSet); + }; + }; + search["Rarity"] = [](const peg::SemanticValues &sv) -> QString { + switch (tolower(std::string(sv.sv())[0])) { + case 'c': + return "common"; + case 'u': + return "uncommon"; + case 'r': + return "rare"; + case 'm': + return "mythic"; + case 's': + return "special"; + default: + return QString::fromStdString(std::string(sv.sv())); + } + }; + search["RarityQuery"] = [](const peg::SemanticValues &sv) -> Filter { + const auto rarity = std::any_cast(sv[0]); + return [=](const CardData &x) -> bool { + QList infos; + for (const auto &printings : x->getSets()) { + for (const auto &printing : printings) { + infos.append(printing); + } + } + + auto matchesRarity = [&rarity](const PrintingInfo &info) { return rarity == info.getProperty("rarity"); }; + return std::any_of(infos.begin(), infos.end(), matchesRarity); + }; + }; + + search["FormatQuery"] = [](const peg::SemanticValues &sv) -> Filter { + if (sv.choice() == 0) { + const auto format = std::any_cast(sv[0]); + return [=](const CardData &x) -> bool { return x->getLegalityProp(format) == "legal"; }; + } + + const auto format = std::any_cast(sv[1]); + const auto legality = std::any_cast(sv[0]); + return [=](const CardData &x) -> bool { return x->getLegalityProp(format) == legality; }; + }; + search["Legality"] = [](const peg::SemanticValues &sv) -> QString { + switch (tolower(std::string(sv.sv())[0])) { + case 'l': + return "legal"; + case 'b': + return "banned"; + case 'r': + return "restricted"; + default: + return ""; + } + }; + + search["Format"] = [](const peg::SemanticValues &sv) -> QString { + if (sv.size() == 1) { + switch (tolower(std::string(sv.sv())[0])) { + case 'm': + return "modern"; + case 's': + return "standard"; + case 'v': + return "vintage"; + case 'l': + return "legacy"; + case 'c': + return "commander"; + case 'p': + return "pioneer"; + default: + return ""; + } + } + + return QString::fromStdString(std::string(sv.sv())).toLower(); + }; + + search["StringValue"] = [](const peg::SemanticValues &sv) -> StringMatcher { + // Helper function for word boundary matching + auto createWordBoundaryMatcher = [](const QString &target) { + QString pattern = QString("\\b%1\\b").arg(QRegularExpression::escape(target)); + QRegularExpression regex(pattern, QRegularExpression::CaseInsensitiveOption); + return [regex](const QString &s) { return regex.match(s).hasMatch(); }; + }; + + if (sv.choice() == 0) { + const auto target = std::any_cast(sv[0]); + return createWordBoundaryMatcher(target); + } + + const auto target = std::any_cast(sv[0]); + return [=](const QString &s) { + auto containsString = [&s, &createWordBoundaryMatcher](const QString &str) { + return createWordBoundaryMatcher(str)(s); + }; + return std::any_of(target.begin(), target.end(), containsString); + }; + }; + + search["String"] = [](const peg::SemanticValues &sv) -> QString { + if (sv.choice() == 0) { + return QString::fromStdString(std::string(sv.sv())); + } + + return QString::fromStdString(std::string(sv.token(0))); + }; + search["FlexStringValue"] = [](const peg::SemanticValues &sv) -> StringMatcher { + if (sv.choice() != 1) { + const auto target = std::any_cast(sv[0]); + return [=](const QString &s) { + auto containsString = [&s](const QString &str) { + return s.split(" ").contains(str, Qt::CaseInsensitive); + }; + return std::any_of(target.begin(), target.end(), containsString); + }; + } + + const auto target = std::any_cast(sv[0]); + return [=](const QString &s) { return s.split(" ").contains(target, Qt::CaseInsensitive); }; + }; + search["CompactStringSet"] = [](const peg::SemanticValues &sv) -> QStringList { + QStringList result; + for (const auto &i : sv) { + result.append(std::any_cast(i)); + } + return result; + }; + search["StringList"] = [](const peg::SemanticValues &sv) -> QStringList { + QStringList result; + for (const auto &i : sv) { + result.append(std::any_cast(i)); + } + return result; + }; + search["StringListString"] = [](const peg::SemanticValues &sv) -> QString { + return QString::fromStdString(std::string(sv.sv())); + }; + + search["NumericExpression"] = [](const peg::SemanticValues &sv) -> NumberMatcher { + const auto arg = std::any_cast(sv[1]); + const auto op = std::any_cast(sv[0]); + + if (op == ">") + return [=](const int s) { return s > arg; }; + if (op == ">=") + return [=](const int s) { return s >= arg; }; + if (op == "<") + return [=](const int s) { return s < arg; }; + if (op == "<=") + return [=](const int s) { return s <= arg; }; + if (op == "=") + return [=](const int s) { return s == arg; }; + if (op == ":") + return [=](const int s) { return s == arg; }; + if (op == "!=") + return [=](const int s) { return s != arg; }; + return [](int) { return false; }; + }; + + search["NumericValue"] = [](const peg::SemanticValues &sv) -> int { + return QString::fromStdString(std::string(sv.sv())).toInt(); + }; + + search["NumericOperator"] = [](const peg::SemanticValues &sv) -> QString { + return QString::fromStdString(std::string(sv.sv())); + }; + + search["NormalMatcher"] = [](const peg::SemanticValues &sv) -> StringMatcher { + auto target = std::any_cast(sv[0]); + auto sanitizedTarget = QString(target); + sanitizedTarget.replace("\\\"", "\""); + sanitizedTarget.replace("\\'", "'"); + return [=](const QString &s) { return s.contains(sanitizedTarget, Qt::CaseInsensitive); }; + }; + + search["RegexMatcher"] = [](const peg::SemanticValues &sv) -> StringMatcher { + auto target = std::any_cast(sv[0]); + auto regex = QRegularExpression(target, QRegularExpression::CaseInsensitiveOption); + return [=](const QString &s) { return regex.match(s).hasMatch(); }; + }; + + search["RegexMatcherString"] = [](const peg::SemanticValues &sv) -> QString { + return QString::fromStdString(sv.token_to_string()); + }; + + search["OracleQuery"] = [](const peg::SemanticValues &sv) -> Filter { + const auto matcher = std::any_cast(sv[0]); + return [=](const CardData &x) { return matcher(x->getText()); }; + }; + + search["ColorQuery"] = [](const peg::SemanticValues &sv) -> Filter { + QString parts; + for (const auto &i : sv) { + parts += std::any_cast(i); + } + const bool identity = sv.tokens[0].empty() || sv.tokens[0][0] != 'i'; + if (sv.tokens[1][0] == ':') { + return [=](const CardData &x) { + QString match = identity ? x->getColors() : x->getProperty("coloridentity"); + if (parts.contains("m") && match.length() < 2) { + return false; + } + if (parts == "m") { + return true; + } + + if (parts.contains("c") && match.length() == 0) + return true; + + auto containsColor = [&parts](const QString &s) { return parts.contains(s); }; + return std::any_of(match.begin(), match.end(), containsColor); + }; + } + + return [=](const CardData &x) { + QString match = identity ? x->getColors() : x->getProperty("colorIdentity"); + if (parts.contains("m") && match.length() < 2) { + return false; + } + + if (parts.contains("c") && match.length() != 0) { + return false; + } + + for (const auto &part : parts) { + if (!match.contains(part)) { + return false; + } + } + + auto containsColor = [&parts](const QString &s) { return parts.contains(s); }; + return std::all_of(match.begin(), match.end(), containsColor); + }; + }; + + search["CMCQuery"] = [](const peg::SemanticValues &sv) -> Filter { + const auto matcher = std::any_cast(sv[0]); + return [=](const CardData &x) -> bool { return matcher(x->getProperty("cmc").toInt()); }; + }; + search["PowerQuery"] = [](const peg::SemanticValues &sv) -> Filter { + const auto matcher = std::any_cast(sv[0]); + return [=](const CardData &x) -> bool { return matcher(x->getPowTough().split("/")[0].toInt()); }; + }; + search["ToughnessQuery"] = [](const peg::SemanticValues &sv) -> Filter { + const auto matcher = std::any_cast(sv[0]); + return [=](const CardData &x) -> bool { + auto parts = x->getPowTough().split("/"); + return matcher(parts.length() == 2 ? parts[1].toInt() : 0); + }; + }; + search["FieldQuery"] = [](const peg::SemanticValues &sv) -> Filter { + const auto field = std::any_cast(sv[0]); + if (sv.choice() == 0) { + const auto matcher = std::any_cast(sv[1]); + return [=](const CardData &x) -> bool { return x->hasProperty(field) && matcher(x->getProperty(field)); }; + } + + const auto matcher = std::any_cast(sv[1]); + return + [=](const CardData &x) -> bool { return x->hasProperty(field) && matcher(x->getProperty(field).toInt()); }; + }; + search["GenericQuery"] = [](const peg::SemanticValues &sv) -> Filter { + const auto matcher = std::any_cast(sv[0]); + return [=](const CardData &x) { return matcher(x->getName()); }; + }; + + search["Color"] = [](const peg::SemanticValues &sv) -> char { return "WUBRGU"[sv.choice()]; }; + search["ColorEx"] = [](const peg::SemanticValues &sv) -> char { + return sv.choice() == 0 ? std::any_cast(sv[0]) : *std::string(sv.sv()).c_str(); + }; +} + +FilterString::FilterString() +{ + result = [](const CardData &) -> bool { return false; }; + _error = "Not initialized"; +} + +FilterString::FilterString(const QString &expr) +{ + QByteArray ba = expr.simplified().toUtf8(); + + std::call_once(init, setupParserRules); + + _error = QString(); + + if (ba.isEmpty()) { + result = [](const CardData &) -> bool { return true; }; + return; + } + + search.set_logger([&](size_t /*ln*/, size_t col, const std::string &msg) { + _error = QString("Error at position %1: %2").arg(col).arg(QString::fromStdString(msg)); + }); + + if (!search.parse(ba.data(), result)) { + qCInfo(FilterStringLog).nospace() << "FilterString error for " << expr << "; " << qPrintable(_error); + result = [](const CardData &) -> bool { return false; }; + } +} diff --git a/libcockatrice_filters/libcockatrice/filters/filter_string.h b/libcockatrice_filters/libcockatrice/filters/filter_string.h new file mode 100644 index 000000000..8fc41ce68 --- /dev/null +++ b/libcockatrice_filters/libcockatrice/filters/filter_string.h @@ -0,0 +1,62 @@ +/** + * @file filter_string.h + * @ingroup CardDatabaseModelFilters + * @brief TODO: Document this. + */ + +#ifndef FILTER_STRING_H +#define FILTER_STRING_H + +#include "filter_tree.h" + +#include +#include +#include +#include +#include +#include + +inline Q_LOGGING_CATEGORY(FilterStringLog, "filter_string"); + +typedef CardInfoPtr CardData; +typedef std::function Filter; +typedef std::function StringMatcher; +typedef std::function NumberMatcher; + +namespace peg +{ +template struct AstBase; +struct EmptyType; +typedef AstBase Ast; +} // namespace peg + +class FilterString +{ +public: + FilterString(); + explicit FilterString(const QString &exp); + [[nodiscard]] bool check(const CardData &card) const + { + if (card.isNull()) { + static CardInfoPtr blankCard = CardInfo::newInstance(""); + return result(blankCard); + } + return result(card); + } + + bool valid() + { + return _error.isEmpty(); + } + + QString error() + { + return _error; + } + +private: + QString _error; + Filter result; +}; + +#endif diff --git a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp new file mode 100644 index 000000000..19e8c2d8d --- /dev/null +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp @@ -0,0 +1,572 @@ +#include "filter_tree.h" + +#include "filter_card.h" + +#include + +template FilterTreeNode *FilterTreeBranch::nodeAt(int i) const +{ + return (childNodes.size() > i) ? childNodes.at(i) : nullptr; +} + +template void FilterTreeBranch::deleteAt(int i) +{ + preRemoveChild(this, i); + delete childNodes.takeAt(i); + postRemoveChild(this, i); + nodeChanged(); +} + +template int FilterTreeBranch::childIndex(const FilterTreeNode *node) const +{ + auto *unconst = const_cast(node); + auto downcasted = dynamic_cast(unconst); + return (downcasted) ? childNodes.indexOf(downcasted) : -1; +} + +template FilterTreeBranch::~FilterTreeBranch() +{ + while (!childNodes.isEmpty()) { + delete childNodes.takeFirst(); + } +} + +const FilterItemList *LogicMap::findTypeList(CardFilter::Type type) const +{ + QList::const_iterator i; + + for (i = childNodes.constBegin(); i != childNodes.constEnd(); ++i) { + if ((*i)->type == type) { + return *i; + } + } + + return nullptr; +} + +FilterItemList *LogicMap::typeList(CardFilter::Type type) +{ + QList::iterator i; + int count = 0; + + for (i = childNodes.begin(); i != childNodes.end(); ++i) { + if ((*i)->type == type) { + break; + } + count++; + } + + if (i == childNodes.end()) { + preInsertChild(this, count); + i = childNodes.insert(i, new FilterItemList(type, this)); + postInsertChild(this, count); + nodeChanged(); + } + + return *i; +} + +FilterTreeNode *LogicMap::parent() const +{ + return p; +} + +int FilterItemList::termIndex(const QString &term) const +{ + for (int i = 0; i < childNodes.count(); i++) { + if ((childNodes.at(i))->term == term) { + return i; + } + } + + return -1; +} + +FilterTreeNode *FilterItemList::termNode(const QString &term) +{ + int i = termIndex(term); + if (i < 0) { + FilterItem *fi = new FilterItem(term, this); + int count = childNodes.count(); + + preInsertChild(this, count); + childNodes.append(fi); + postInsertChild(this, count); + nodeChanged(); + + return fi; + } + + return childNodes.at(i); +} + +bool FilterItemList::testTypeAnd(const CardInfoPtr info, CardFilter::Attr attr) const +{ + for (auto i = childNodes.constBegin(); i != childNodes.constEnd(); i++) { + if (!(*i)->isEnabled()) { + continue; + } + + if (!(*i)->acceptCardAttr(info, attr)) { + return false; + } + } + + return true; +} + +bool FilterItemList::testTypeAndNot(const CardInfoPtr info, CardFilter::Attr attr) const +{ + // if any one in the list is true, return false + return !testTypeOr(info, attr); +} + +bool FilterItemList::testTypeOr(const CardInfoPtr info, CardFilter::Attr attr) const +{ + bool noChildEnabledChild = true; + + for (auto i = childNodes.constBegin(); i != childNodes.constEnd(); i++) { + if (!(*i)->isEnabled()) { + continue; + } + + if (noChildEnabledChild) { + noChildEnabledChild = false; + } + + if ((*i)->acceptCardAttr(info, attr)) { + return true; + } + } + + return noChildEnabledChild; +} + +bool FilterItemList::testTypeOrNot(const CardInfoPtr info, CardFilter::Attr attr) const +{ + // if any one in the list is false, return true + return !testTypeAnd(info, attr); +} + +bool FilterItem::acceptName(const CardInfoPtr info) const +{ + return info->getName().contains(term, Qt::CaseInsensitive); +} + +bool FilterItem::acceptNameExact(const CardInfoPtr info) const +{ + return info->getName() == term; +} + +bool FilterItem::acceptType(const CardInfoPtr info) const +{ + return info->getCardType().contains(term, Qt::CaseInsensitive); +} + +bool FilterItem::acceptMainType(const CardInfoPtr info) const +{ + const QStringList typeParts = info->getCardType().split(" — "); + return !typeParts.isEmpty() && typeParts[0].contains(term, Qt::CaseInsensitive); +} + +bool FilterItem::acceptSubType(const CardInfoPtr info) const +{ + const QStringList typeParts = info->getCardType().split(" — "); + return typeParts.size() > 1 && typeParts[1].contains(term, Qt::CaseInsensitive); +} + +bool FilterItem::acceptColor(const CardInfoPtr info) const +{ + QString converted_term = term.trimmed(); + + converted_term.replace("green", "g", Qt::CaseInsensitive); + converted_term.replace("grn", "g", Qt::CaseInsensitive); + converted_term.replace("blue", "u", Qt::CaseInsensitive); + converted_term.replace("blu", "u", Qt::CaseInsensitive); + converted_term.replace("black", "b", Qt::CaseInsensitive); + converted_term.replace("blk", "b", Qt::CaseInsensitive); + converted_term.replace("red", "r", Qt::CaseInsensitive); + converted_term.replace("white", "w", Qt::CaseInsensitive); + converted_term.replace("wht", "w", Qt::CaseInsensitive); + converted_term.replace("colorless", "c", Qt::CaseInsensitive); + converted_term.replace("colourless", "c", Qt::CaseInsensitive); + converted_term.replace("none", "c", Qt::CaseInsensitive); + + converted_term.replace(QString(" "), QString(""), Qt::CaseInsensitive); + + // Colorless card filter + if (converted_term.toLower() == "c" && info->getColors().length() < 1) { + return true; + } + + /* + * This is a tricky part, if the filter has multiple colors in it, like UGW, + * then we should match all of them to the card's colors + */ + int match_count = 0; + for (auto &it : converted_term) { + if (info->getColors().contains(it, Qt::CaseInsensitive)) + match_count++; + } + + return match_count == converted_term.length(); +} + +bool FilterItem::acceptText(const CardInfoPtr info) const +{ + return info->getText().contains(term, Qt::CaseInsensitive); +} + +bool FilterItem::acceptSet(const CardInfoPtr info) const +{ + bool status = false; + for (const auto &printings : info->getSets()) { + for (const auto &set : printings) { + if (set.getSet()->getShortName().compare(term, Qt::CaseInsensitive) == 0 || + set.getSet()->getLongName().compare(term, Qt::CaseInsensitive) == 0) { + status = true; + break; + } + } + } + + return status; +} + +bool FilterItem::acceptManaCost(const CardInfoPtr info) const +{ + QString partialCost = term.toUpper(); + + // Sort the mana cost so it will be easy to find + std::sort(partialCost.begin(), partialCost.end()); + + // Try to seperate the mana cost in case it's a split card + // if it's not a split card the loop will run only once + for (QString fullManaCost : info->getManaCost().split("//")) { + std::sort(fullManaCost.begin(), fullManaCost.end()); + + // If the partial is found in the full, return true + if (fullManaCost.contains(partialCost)) { + return true; + } + } + return false; +} + +bool FilterItem::acceptCmc(const CardInfoPtr info) const +{ + bool convertSuccess; + int cmcInt = info->getCmc().toInt(&convertSuccess); + // if conversion failed, check for the "//" separator used in split cards + if (!convertSuccess) { + int cmcSum = 0; + for (const QString &cmc : info->getCmc().split("//")) { + cmcInt = cmc.toInt(); + cmcSum += cmcInt; + if (relationCheck(cmcInt)) { + return true; + } + } + return relationCheck(cmcSum); + } else { + return relationCheck(cmcInt); + } +} + +bool FilterItem::acceptFormat(const CardInfoPtr info) const +{ + return info->getLegalityProp(term.toLower()) == "legal"; +} + +bool FilterItem::acceptLoyalty(const CardInfoPtr info) const +{ + if (info->getLoyalty().isEmpty()) { + return false; + } else { + bool success; + // if loyalty can't be converted to "int" it must be "X" + int loyalty = info->getLoyalty().toInt(&success); + if (success) { + return relationCheck(loyalty); + } else { + return term.trimmed().toUpper() == info->getLoyalty(); + } + } +} + +bool FilterItem::acceptPowerToughness(const CardInfoPtr info, CardFilter::Attr attr) const +{ + int slash = info->getPowTough().indexOf("/"); + if (slash == -1) { + return false; + } + QString valueString; + if (attr == CardFilter::AttrPow) { + valueString = info->getPowTough().mid(0, slash); + } else { + valueString = info->getPowTough().mid(slash + 1); + } + if (term == valueString) { + return true; + } + // advanced filtering should only happen after fast string comparison failed + bool conversion; + int value = valueString.toInt(&conversion); + return conversion ? relationCheck(value) : false; +} + +bool FilterItem::acceptRarity(const CardInfoPtr info) const +{ + QString converted_term = term.trimmed(); + + /* + * The purpose of this loop is to only apply one of the replacement + * policies and then escape. If we attempt to layer them on top of + * each other, we will get awkward results (i.e. comythic rare mythic rareon) + * Conditional statement will exit once a case is successful in + * replacement OR we go through all possible cases. + * Will also need to replace just "mythic" + */ + for (int i = 0; converted_term.length() <= 3 && i <= 6; i++) { + switch (i) { + case 0: + converted_term.replace("mr", "mythic", Qt::CaseInsensitive); + break; + case 1: + converted_term.replace("m r", "mythic", Qt::CaseInsensitive); + break; + case 2: + converted_term.replace("m", "mythic", Qt::CaseInsensitive); + break; + case 3: + converted_term.replace("c", "common", Qt::CaseInsensitive); + break; + case 4: + converted_term.replace("u", "uncommon", Qt::CaseInsensitive); + break; + case 5: + converted_term.replace("r", "rare", Qt::CaseInsensitive); + break; + case 6: + converted_term.replace("s", "special", Qt::CaseInsensitive); + break; + default: + break; + } + } + + for (const auto &printings : info->getSets()) { + for (const auto &printing : printings) { + if (printing.getProperty("rarity").compare(converted_term, Qt::CaseInsensitive) == 0) { + return true; + } + } + } + return false; +} + +bool FilterItem::relationCheck(int cardInfo) const +{ + bool result, conversion; + + // if int conversion fails, there's probably an operator at the start + result = (cardInfo == term.toInt(&conversion)); + if (!conversion) { + // leading whitespaces could cause indexing to fail + QString trimmedTerm = term.trimmed(); + // check whether it's a 2 char operator (<=, >=, or ==) + if (trimmedTerm[1] == '=') { + int termInt = trimmedTerm.mid(2).toInt(); + if (trimmedTerm.startsWith('<')) { + result = (cardInfo <= termInt); + } else if (trimmedTerm.startsWith('>')) { + result = (cardInfo >= termInt); + } else { + result = (cardInfo == termInt); + } + } else { + int termInt = trimmedTerm.mid(1).toInt(); + if (trimmedTerm.startsWith('<')) { + result = (cardInfo < termInt); + } else if (trimmedTerm.startsWith('>')) { + result = (cardInfo > termInt); + } else if (trimmedTerm.startsWith("=")) { + result = (cardInfo == termInt); + } else { + // the int conversion hasn't failed due to an operator at the start + result = false; + } + } + } + return result; +} + +bool FilterItem::acceptCardAttr(const CardInfoPtr info, CardFilter::Attr attr) const +{ + switch (attr) { + case CardFilter::AttrName: + return acceptName(info); + case CardFilter::AttrNameExact: + return acceptNameExact(info); + case CardFilter::AttrType: + return acceptType(info); + case CardFilter::AttrColor: + return acceptColor(info); + case CardFilter::AttrText: + return acceptText(info); + case CardFilter::AttrSet: + return acceptSet(info); + case CardFilter::AttrManaCost: + return acceptManaCost(info); + case CardFilter::AttrCmc: + return acceptCmc(info); + case CardFilter::AttrRarity: + return acceptRarity(info); + case CardFilter::AttrPow: + case CardFilter::AttrTough: + // intentional fallthrough + return acceptPowerToughness(info, attr); + case CardFilter::AttrLoyalty: + return acceptLoyalty(info); + case CardFilter::AttrFormat: + return acceptFormat(info); + case CardFilter::AttrMainType: + return acceptMainType(info); + case CardFilter::AttrSubType: + return acceptSubType(info); + default: + return true; /* ignore this attribute */ + } +} + +/* + * Need to define these here to make QT happy, otherwise + * moc doesnt find some of the FilterTreeBranch symbols. + */ +FilterTree::FilterTree() = default; +FilterTree::~FilterTree() = default; + +LogicMap *FilterTree::attrLogicMap(CardFilter::Attr attr) +{ + QList::iterator i; + + int count = 0; + for (i = childNodes.begin(); i != childNodes.end(); i++) { + if ((*i)->attr == attr) { + break; + } + count++; + } + + if (i == childNodes.end()) { + preInsertChild(this, count); + i = childNodes.insert(i, new LogicMap(attr, this)); + postInsertChild(this, count); + nodeChanged(); + } + + return *i; +} + +FilterItemList *FilterTree::attrTypeList(CardFilter::Attr attr, CardFilter::Type type) +{ + return attrLogicMap(attr)->typeList(type); +} + +FilterTreeNode *FilterTree::termNode(CardFilter::Attr attr, CardFilter::Type type, const QString &term) +{ + return attrTypeList(attr, type)->termNode(term); +} + +FilterTreeNode *FilterTree::termNode(const CardFilter *f) +{ + return termNode(f->attr(), f->type(), f->term()); +} + +bool FilterTree::testAttr(const CardInfoPtr info, const LogicMap *lm) const +{ + const FilterItemList *fil; + bool status = true; + + fil = lm->findTypeList(CardFilter::TypeAnd); + if (fil && fil->isEnabled() && !fil->testTypeAnd(info, lm->attr)) { + return false; + } + + fil = lm->findTypeList(CardFilter::TypeAndNot); + if (fil && fil->isEnabled() && !fil->testTypeAndNot(info, lm->attr)) { + return false; + } + + fil = lm->findTypeList(CardFilter::TypeOr); + if (fil && fil->isEnabled()) { + status = false; + + // if this is true we can return because it is OR'd with the OrNot list + if (fil->testTypeOr(info, lm->attr)) { + return true; + } + } + + fil = lm->findTypeList(CardFilter::TypeOrNot); + if (fil && fil->isEnabled() && fil->testTypeOrNot(info, lm->attr)) { + return true; + } + + return status; +} + +bool FilterTree::acceptsCard(const CardInfoPtr info) const +{ + for (auto i = childNodes.constBegin(); i != childNodes.constEnd(); i++) { + if ((*i)->isEnabled() && !testAttr(info, *i)) { + return false; + } + } + + return true; +} + +void FilterTree::removeFiltersByAttr(CardFilter::Attr filterType) +{ + for (int i = childNodes.size() - 1; i >= 0; --i) { + auto *child = dynamic_cast(childNodes.at(i)); + + if (child && child->attr == filterType) { + deleteAt(i); + } + } +} + +void FilterTree::removeFilter(const CardFilter *toRemove) +{ + for (int i = childNodes.size() - 1; i >= 0; --i) { + auto *logicMap = dynamic_cast(childNodes.at(i)); + if (!logicMap || logicMap->attr != toRemove->attr()) + continue; + + FilterItemList *typeList = logicMap->typeList(toRemove->type()); + if (!typeList) + continue; + + int termIdx = typeList->termIndex(toRemove->term()); + if (termIdx != -1) { + typeList->deleteAt(termIdx); + emit typeList->nodeChanged(); + if (typeList->childCount() == 0) { + int logicIndex = logicMap->childIndex(typeList); + if (logicIndex != -1) { + logicMap->deleteAt(logicIndex); + } + } + } + } +} + +void FilterTree::clear() +{ + while (childCount() > 0) { + deleteAt(0); + } + emit changed(); +} diff --git a/libcockatrice_filters/libcockatrice/filters/filter_tree.h b/libcockatrice_filters/libcockatrice/filters/filter_tree.h new file mode 100644 index 000000000..75d4241a5 --- /dev/null +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.h @@ -0,0 +1,283 @@ +/** + * @file filter_tree.h + * @ingroup CardDatabaseModelFilters + * @brief TODO: Document this. + */ + +#ifndef FILTERTREE_H +#define FILTERTREE_H + +#include "filter_card.h" + +#include +#include +#include +#include + +class FilterTreeNode +{ +private: + bool enabled; + +public: + FilterTreeNode() : enabled(true) + { + } + [[nodiscard]] virtual bool isEnabled() const + { + return enabled; + } + virtual void enable() + { + enabled = true; + nodeChanged(); + } + virtual void disable() + { + enabled = false; + nodeChanged(); + } + [[nodiscard]] virtual FilterTreeNode *parent() const + { + return nullptr; + } + [[nodiscard]] virtual FilterTreeNode *nodeAt(int /* i */) const + { + return nullptr; + } + virtual void deleteAt(int /* i */) + { + } + [[nodiscard]] virtual int childCount() const + { + return 0; + } + virtual int childIndex(const FilterTreeNode * /* node */) const + { + return -1; + } + [[nodiscard]] virtual int index() const + { + return (parent() != nullptr) ? parent()->childIndex(this) : -1; + } + [[nodiscard]] virtual const QString text() const + { + return QString(""); + } + [[nodiscard]] virtual bool isLeaf() const + { + return false; + } + virtual void nodeChanged() const + { + if (parent() != nullptr) + parent()->nodeChanged(); + } + virtual void preInsertChild(const FilterTreeNode *p, int i) const + { + if (parent() != nullptr) + parent()->preInsertChild(p, i); + } + virtual void postInsertChild(const FilterTreeNode *p, int i) const + { + if (parent() != nullptr) + parent()->postInsertChild(p, i); + } + virtual void preRemoveChild(const FilterTreeNode *p, int i) const + { + if (parent() != nullptr) + parent()->preRemoveChild(p, i); + } + virtual void postRemoveChild(const FilterTreeNode *p, int i) const + { + if (parent() != nullptr) + parent()->postRemoveChild(p, i); + } +}; + +template class FilterTreeBranch : public FilterTreeNode +{ +protected: + QList childNodes; + +public: + virtual ~FilterTreeBranch(); + void removeFiltersByAttr(CardFilter::Attr filterType); + [[nodiscard]] FilterTreeNode *nodeAt(int i) const override; + void deleteAt(int i) override; + [[nodiscard]] int childCount() const override + { + return childNodes.size(); + } + int childIndex(const FilterTreeNode *node) const override; +}; + +class FilterItemList; +class FilterTree; +class LogicMap : public FilterTreeBranch +{ + +private: + FilterTree *const p; + +public: + const CardFilter::Attr attr; + + LogicMap(CardFilter::Attr a, FilterTree *parent) : p(parent), attr(a) + { + } + [[nodiscard]] const FilterItemList *findTypeList(CardFilter::Type type) const; + FilterItemList *typeList(CardFilter::Type type); + [[nodiscard]] FilterTreeNode *parent() const override; + [[nodiscard]] const QString text() const override + { + return CardFilter::attrName(attr); + } +}; + +class FilterItem; +class FilterItemList : public FilterTreeBranch +{ +private: + LogicMap *const p; + +public: + const CardFilter::Type type; + + FilterItemList(CardFilter::Type t, LogicMap *parent) : p(parent), type(t) + { + } + [[nodiscard]] CardFilter::Attr attr() const + { + return p->attr; + } + [[nodiscard]] FilterTreeNode *parent() const override + { + return p; + } + [[nodiscard]] int termIndex(const QString &term) const; + FilterTreeNode *termNode(const QString &term); + [[nodiscard]] const QString text() const override + { + return CardFilter::typeName(type); + } + + [[nodiscard]] bool testTypeAnd(CardInfoPtr info, CardFilter::Attr attr) const; + [[nodiscard]] bool testTypeAndNot(CardInfoPtr info, CardFilter::Attr attr) const; + [[nodiscard]] bool testTypeOr(CardInfoPtr info, CardFilter::Attr attr) const; + [[nodiscard]] bool testTypeOrNot(CardInfoPtr info, CardFilter::Attr attr) const; +}; + +class FilterItem : public FilterTreeNode +{ +private: + FilterItemList *const p; + +public: + const QString term; + + FilterItem(QString trm, FilterItemList *parent) : p(parent), term(std::move(trm)) + { + } + virtual ~FilterItem() = default; + + [[nodiscard]] CardFilter::Attr attr() const + { + return p->attr(); + } + [[nodiscard]] CardFilter::Type type() const + { + return p->type; + } + [[nodiscard]] FilterTreeNode *parent() const override + { + return p; + } + [[nodiscard]] const QString text() const override + { + return term; + } + [[nodiscard]] bool isLeaf() const override + { + return true; + } + + [[nodiscard]] bool acceptName(CardInfoPtr info) const; + [[nodiscard]] bool acceptNameExact(CardInfoPtr info) const; + [[nodiscard]] bool acceptType(CardInfoPtr info) const; + [[nodiscard]] bool acceptMainType(CardInfoPtr info) const; + [[nodiscard]] bool acceptSubType(CardInfoPtr info) const; + [[nodiscard]] bool acceptColor(CardInfoPtr info) const; + [[nodiscard]] bool acceptText(CardInfoPtr info) const; + [[nodiscard]] bool acceptSet(CardInfoPtr info) const; + [[nodiscard]] bool acceptManaCost(CardInfoPtr info) const; + [[nodiscard]] bool acceptCmc(CardInfoPtr info) const; + [[nodiscard]] bool acceptPowerToughness(CardInfoPtr info, CardFilter::Attr attr) const; + [[nodiscard]] bool acceptLoyalty(CardInfoPtr info) const; + [[nodiscard]] bool acceptRarity(CardInfoPtr info) const; + [[nodiscard]] bool acceptCardAttr(CardInfoPtr info, CardFilter::Attr attr) const; + [[nodiscard]] bool acceptFormat(CardInfoPtr info) const; + [[nodiscard]] bool relationCheck(int cardInfo) const; +}; + +class FilterTree : public QObject, public FilterTreeBranch +{ + Q_OBJECT + +signals: + void preInsertRow(const FilterTreeNode *parent, int i) const; + void postInsertRow(const FilterTreeNode *parent, int i) const; + void preRemoveRow(const FilterTreeNode *parent, int i) const; + void postRemoveRow(const FilterTreeNode *parent, int i) const; + void changed() const; + +private: + LogicMap *attrLogicMap(CardFilter::Attr attr); + FilterItemList *attrTypeList(CardFilter::Attr attr, CardFilter::Type type); + + bool testAttr(CardInfoPtr info, const LogicMap *lm) const; + + void nodeChanged() const override + { + emit changed(); + } + void preInsertChild(const FilterTreeNode *p, int i) const override + { + emit preInsertRow(p, i); + } + void postInsertChild(const FilterTreeNode *p, int i) const override + { + emit postInsertRow(p, i); + } + void preRemoveChild(const FilterTreeNode *p, int i) const override + { + emit preRemoveRow(p, i); + } + void postRemoveChild(const FilterTreeNode *p, int i) const override + { + emit postRemoveRow(p, i); + } + +public: + FilterTree(); + ~FilterTree() override; + + FilterTreeNode *termNode(CardFilter::Attr attr, CardFilter::Type type, const QString &term); + FilterTreeNode *termNode(const CardFilter *f); + + [[nodiscard]] const QString text() const override + { + return QString("root"); + } + [[nodiscard]] int index() const override + { + return 0; + } + + [[nodiscard]] bool acceptsCard(CardInfoPtr info) const; + void removeFiltersByAttr(CardFilter::Attr filterType); + void removeFilter(const CardFilter *toRemove); + void clear(); +}; + +#endif diff --git a/libcockatrice_interfaces/CMakeLists.txt b/libcockatrice_interfaces/CMakeLists.txt new file mode 100644 index 000000000..4f34f7985 --- /dev/null +++ b/libcockatrice_interfaces/CMakeLists.txt @@ -0,0 +1,25 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS + libcockatrice/interfaces/interface_card_database_path_provider.h + libcockatrice/interfaces/interface_card_preference_provider.h + libcockatrice/interfaces/interface_card_set_priority_controller.h + libcockatrice/interfaces/interface_network_settings_provider.h + libcockatrice/interfaces/noop_card_database_path_provider.h + libcockatrice/interfaces/noop_card_preference_provider.h + libcockatrice/interfaces/noop_card_set_priority_controller.h +) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library(libcockatrice_interfaces STATIC ${MOC_SOURCES}) + +target_include_directories(libcockatrice_interfaces PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(libcockatrice_interfaces PUBLIC ${QT_CORE_MODULE}) diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_database_path_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_database_path_provider.h new file mode 100644 index 000000000..dd55ab789 --- /dev/null +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_database_path_provider.h @@ -0,0 +1,21 @@ +#ifndef COCKATRICE_INTERFACE_CARD_DATABASE_PATH_PROVIDER_H +#define COCKATRICE_INTERFACE_CARD_DATABASE_PATH_PROVIDER_H +#include + +class ICardDatabasePathProvider : public QObject +{ + Q_OBJECT + +public: + virtual ~ICardDatabasePathProvider() = default; + + [[nodiscard]] virtual QString getCardDatabasePath() const = 0; + [[nodiscard]] virtual QString getCustomCardDatabasePath() const = 0; + [[nodiscard]] virtual QString getTokenDatabasePath() const = 0; + [[nodiscard]] virtual QString getSpoilerCardDatabasePath() const = 0; + +signals: + void cardDatabasePathChanged(); +}; + +#endif // COCKATRICE_INTERFACE_CARD_DATABASE_PATH_PROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_preference_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_preference_provider.h new file mode 100644 index 000000000..a6cf941fb --- /dev/null +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_preference_provider.h @@ -0,0 +1,14 @@ +#ifndef COCKATRICE_INTERFACE_CARD_PREFERENCE_PROVIDER_H +#define COCKATRICE_INTERFACE_CARD_PREFERENCE_PROVIDER_H + +#include + +class ICardPreferenceProvider +{ +public: + virtual ~ICardPreferenceProvider() = default; + [[nodiscard]] virtual QString getCardPreferenceOverride(const QString &cardName) const = 0; + [[nodiscard]] virtual bool getIncludeRebalancedCards() const = 0; +}; + +#endif // COCKATRICE_INTERFACE_CARD_PREFERENCE_PROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_set_priority_controller.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_set_priority_controller.h new file mode 100644 index 000000000..b8fbbc74a --- /dev/null +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_set_priority_controller.h @@ -0,0 +1,20 @@ +#ifndef COCKATRICE_INTERFACE_CARD_SET_PRIORITY_CONTROLLER_H +#define COCKATRICE_INTERFACE_CARD_SET_PRIORITY_CONTROLLER_H + +#include + +class ICardSetPriorityController +{ +public: + virtual ~ICardSetPriorityController() = default; + + virtual void setSortKey(QString shortName, unsigned int sortKey) = 0; + virtual void setEnabled(QString shortName, bool enabled) = 0; + virtual void setIsKnown(QString shortName, bool isknown) = 0; + + virtual unsigned int getSortKey(QString shortName) const = 0; + virtual bool isEnabled(QString shortName) const = 0; + virtual bool isKnown(QString shortName) const = 0; +}; + +#endif // COCKATRICE_INTERFACE_CARD_SET_PRIORITY_CONTROLLER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_network_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_network_settings_provider.h new file mode 100644 index 000000000..bd136513d --- /dev/null +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_network_settings_provider.h @@ -0,0 +1,20 @@ +#ifndef COCKATRICE_INETWORKSETTINGSPROVIDER_H +#define COCKATRICE_INETWORKSETTINGSPROVIDER_H +#include + +class INetworkSettingsProvider +{ +public: + virtual ~INetworkSettingsProvider() = default; + + virtual QString getClientID() = 0; + + [[nodiscard]] virtual int getTimeOut() const = 0; + [[nodiscard]] virtual int getKeepAlive() const = 0; + [[nodiscard]] virtual bool getNotifyAboutUpdates() const = 0; + + virtual void setKnownMissingFeatures(const QString &_knownMissingFeatures) = 0; + virtual QString getKnownMissingFeatures() = 0; +}; + +#endif // COCKATRICE_INETWORKSETTINGSPROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_database_path_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_database_path_provider.h new file mode 100644 index 000000000..bacd7a2bb --- /dev/null +++ b/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_database_path_provider.h @@ -0,0 +1,26 @@ +#ifndef COCKATRICE_NOOP_CARD_DATABASE_PATH_PROVIDER_H +#define COCKATRICE_NOOP_CARD_DATABASE_PATH_PROVIDER_H +#include "interface_card_database_path_provider.h" + +class NoopCardDatabasePathProvider : public ICardDatabasePathProvider +{ +public: + [[nodiscard]] QString getCardDatabasePath() const override + { + return ""; + } + [[nodiscard]] QString getCustomCardDatabasePath() const override + { + return ""; + } + [[nodiscard]] QString getTokenDatabasePath() const override + { + return ""; + } + [[nodiscard]] QString getSpoilerCardDatabasePath() const override + { + return ""; + } +}; + +#endif // COCKATRICE_NOOP_CARD_DATABASE_PATH_PROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_preference_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_preference_provider.h new file mode 100644 index 000000000..3781f4b63 --- /dev/null +++ b/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_preference_provider.h @@ -0,0 +1,19 @@ +#ifndef COCKATRICE_NOOP_CARD_PREFERENCE_PROVIDER_H +#define COCKATRICE_NOOP_CARD_PREFERENCE_PROVIDER_H +#include "interface_card_preference_provider.h" + +class NoopCardPreferenceProvider : public ICardPreferenceProvider +{ +public: + [[nodiscard]] QString getCardPreferenceOverride(const QString &) const override + { + return {}; + } + + [[nodiscard]] bool getIncludeRebalancedCards() const override + { + return true; + } +}; + +#endif // COCKATRICE_NOOP_CARD_PREFERENCE_PROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_set_priority_controller.h b/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_set_priority_controller.h new file mode 100644 index 000000000..e5027648c --- /dev/null +++ b/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_set_priority_controller.h @@ -0,0 +1,33 @@ +#ifndef COCKATRICE_NOOP_CARD_SET_PRIORITY_CONTROLLER_H +#define COCKATRICE_NOOP_CARD_SET_PRIORITY_CONTROLLER_H + +#include "interface_card_set_priority_controller.h" + +class NoopCardSetPriorityController : public ICardSetPriorityController +{ +public: + void setSortKey(QString /* shortName */, unsigned int /* sortKey */) override + { + } + void setEnabled(QString /* shortName */, bool /* enabled */) override + { + } + void setIsKnown(QString /* shortName */, bool /* isknown */) override + { + } + + unsigned int getSortKey(QString /* shortName */) const override + { + return 0; + } + bool isEnabled(QString /* shortName */) const override + { + return true; + } + bool isKnown(QString /* shortName */) const override + { + return true; + } +}; + +#endif // COCKATRICE_NOOP_CARD_SET_PRIORITY_CONTROLLER_H diff --git a/libcockatrice_models/CMakeLists.txt b/libcockatrice_models/CMakeLists.txt new file mode 100644 index 000000000..7af35b2d5 --- /dev/null +++ b/libcockatrice_models/CMakeLists.txt @@ -0,0 +1,12 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +add_subdirectory(libcockatrice/models/database) +add_subdirectory(libcockatrice/models/deck_list) + +add_library(libcockatrice_models INTERFACE) + +target_include_directories(libcockatrice_models INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(libcockatrice_models INTERFACE libcockatrice_models_database libcockatrice_models_deck_list) diff --git a/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt b/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt new file mode 100644 index 000000000..950d6d79f --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt @@ -0,0 +1,35 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS + card_database_model.h + card_database_display_model.h + card/card_completer_proxy_model.h + card/card_search_model.h + card_set/card_sets_model.h + token/token_display_model.h + token/token_edit_model.h +) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library( + libcockatrice_models_database STATIC + ${MOC_SOURCES} + card_database_model.cpp + card_database_display_model.cpp + card/card_completer_proxy_model.cpp + card/card_search_model.cpp + card_set/card_sets_model.cpp + token/token_display_model.cpp + token/token_edit_model.cpp +) + +target_include_directories(libcockatrice_models_database PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(libcockatrice_models_database PUBLIC libcockatrice_card libcockatrice_filters ${QT_CORE_MODULE}) diff --git a/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.cpp b/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.cpp new file mode 100644 index 000000000..387eb454f --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.cpp @@ -0,0 +1,18 @@ +#include "card_completer_proxy_model.h" + +CardCompleterProxyModel::CardCompleterProxyModel(QObject *parent) : QSortFilterProxyModel(parent) +{ +} + +bool CardCompleterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + if (filterRegularExpression().pattern().isEmpty()) { + return true; + } + + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + QString data = index.data(Qt::DisplayRole).toString(); + + // Ensure substring matching + return data.contains(filterRegularExpression()); +} diff --git a/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.h b/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.h new file mode 100644 index 000000000..afb6f1fcf --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.h @@ -0,0 +1,22 @@ +/** + * @file card_completer_proxy_model.h + * @ingroup CardDatabaseModels + * @brief TODO: Document this. + */ + +#ifndef CARD_COMPLETER_PROXY_MODEL_H +#define CARD_COMPLETER_PROXY_MODEL_H + +#include + +class CardCompleterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + explicit CardCompleterProxyModel(QObject *parent = nullptr); + +protected: + [[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; +}; + +#endif // CARD_COMPLETER_PROXY_MODEL_H diff --git a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp new file mode 100644 index 000000000..6a930c1da --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp @@ -0,0 +1,73 @@ +#include "card_search_model.h" + +#include "../card_database_display_model.h" +#include "../card_database_model.h" + +#include +#include + +CardSearchModel::CardSearchModel(CardDatabaseDisplayModel *sourceModel, QObject *parent) + : QAbstractListModel(parent), sourceModel(sourceModel) +{ +} + +int CardSearchModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return searchResults.size(); +} + +QVariant CardSearchModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= searchResults.size()) + return QVariant(); + + if (role == Qt::DisplayRole) { + return searchResults.at(index.row()).card->getName(); + } + + return QVariant(); +} + +void CardSearchModel::updateSearchResults(const QString &query) +{ + beginResetModel(); + searchResults.clear(); + + if (query.isEmpty() || !sourceModel) + return; + + // Set the filter for the display model + sourceModel->setCardName(query); + + // Collect matching cards and compute Levenshtein distance + for (int i = 0; i < sourceModel->rowCount(); ++i) { + QModelIndex modelIndex = sourceModel->index(i, 0); + QModelIndex sourceIndex = sourceModel->mapToSource(modelIndex); + CardDatabaseModel *sourceDbModel = qobject_cast(sourceModel->sourceModel()); + + if (!sourceDbModel || !sourceIndex.isValid()) + return; + + CardInfoPtr card = sourceDbModel->getCard(sourceIndex.row()); + + if (!card) + continue; + + int distance = levenshteinDistance(query.toLower(), card->getName().toLower()); + searchResults.append({card, distance}); + } + + // Sort by Levenshtein distance (lower distance = better match) + std::sort(searchResults.begin(), searchResults.end(), + [](const SearchResult &a, const SearchResult &b) { return a.distance < b.distance; }); + + // Keep only the top 5 results + if (searchResults.size() > 10) + searchResults = searchResults.mid(0, 10); + + emit dataChanged(index(0, 0), index(rowCount() - 1, 0)); + emit layoutChanged(); + + endResetModel(); +} diff --git a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h new file mode 100644 index 000000000..18be2c55a --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h @@ -0,0 +1,36 @@ +/** + * @file card_search_model.h + * @ingroup CardDatabaseModels + * @brief TODO: Document this. + */ + +#ifndef CARD_SEARCH_MODEL_H +#define CARD_SEARCH_MODEL_H + +#include "../card_database_display_model.h" + +#include + +class CardSearchModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit CardSearchModel(CardDatabaseDisplayModel *sourceModel, QObject *parent = nullptr); + + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + void updateSearchResults(const QString &query); // Update results based on input + +private: + struct SearchResult + { + CardInfoPtr card; + int distance; + }; + + CardDatabaseDisplayModel *sourceModel; + QList searchResults; +}; + +#endif // CARD_SEARCH_MODEL_H diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp new file mode 100644 index 000000000..5ce63a939 --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp @@ -0,0 +1,227 @@ +#include "card_database_display_model.h" + +#include "card_database_model.h" + +CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent) + : QSortFilterProxyModel(parent), isToken(ShowAll), filterString(nullptr) +{ + filterTree = nullptr; + setFilterCaseSensitivity(Qt::CaseInsensitive); + setSortCaseSensitivity(Qt::CaseInsensitive); + + dirtyTimer.setSingleShot(true); + connect(&dirtyTimer, &QTimer::timeout, this, &CardDatabaseDisplayModel::invalidate); + + loadedRowCount = 0; +} + +QMap CardDatabaseDisplayModel::characterTranslation = {{L'“', L'\"'}, + {L'”', L'\"'}, + {L'‘', L'\''}, + {L'’', L'\''}}; + +bool CardDatabaseDisplayModel::canFetchMore(const QModelIndex &index) const +{ + return loadedRowCount < sourceModel()->rowCount(index); +} + +void CardDatabaseDisplayModel::fetchMore(const QModelIndex &index) +{ + int remainder = sourceModel()->rowCount(index) - loadedRowCount; + int itemsToFetch = qMin(100, remainder); + + if (itemsToFetch == 0) { + return; + } + + const auto startIndex = qMin(rowCount(QModelIndex()), loadedRowCount); + beginInsertRows(QModelIndex(), startIndex, startIndex + itemsToFetch - 1); + + loadedRowCount += itemsToFetch; + endInsertRows(); +} + +int CardDatabaseDisplayModel::rowCount(const QModelIndex &parent) const +{ + return QSortFilterProxyModel::rowCount(parent); +} + +bool CardDatabaseDisplayModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + + QString leftString = sourceModel()->data(left, CardDatabaseModel::SortRole).toString(); + QString rightString = sourceModel()->data(right, CardDatabaseModel::SortRole).toString(); + + if (!cardName.isEmpty() && left.column() == CardDatabaseModel::NameColumn) { + bool isLeftType = leftString.startsWith(cardName, Qt::CaseInsensitive); + bool isRightType = rightString.startsWith(cardName, Qt::CaseInsensitive); + + // test for an exact match: isLeftType && leftString.size() == cardName.size() + // or an exclusive start match: isLeftType && !isRightType + if (isLeftType && (!isRightType || leftString.size() == cardName.size())) + return true; + + // same checks for the right string + if (isRightType && (!isLeftType || rightString.size() == cardName.size())) + return false; + } else if (right.column() == CardDatabaseModel::PTColumn && left.column() == CardDatabaseModel::PTColumn) { + QStringList leftList = leftString.split("/"); + QStringList rightList = rightString.split("/"); + + if (leftList.size() == 2 && rightList.size() == 2) { + + // cool, have both P/T in list now + int lessThanNum = lessThanNumerically(leftList.at(0), rightList.at(0)); + if (lessThanNum != 0) { + return lessThanNum < 0; + } else { + // power equal, check toughness + return lessThanNumerically(leftList.at(1), rightList.at(1)) < 0; + } + } + } + return QString::localeAwareCompare(leftString, rightString) < 0; +} + +int CardDatabaseDisplayModel::lessThanNumerically(const QString &left, const QString &right) +{ + if (left == right) { + return 0; + } + + bool okLeft, okRight; + float leftNum = left.toFloat(&okLeft); + float rightNum = right.toFloat(&okRight); + + if (okLeft && okRight) { + if (leftNum < rightNum) { + return -1; + } else if (leftNum > rightNum) { + return 1; + } else { + return 0; + } + } + // try and parsing again, for weird ones like "1+*" + QString leftAfterNum = ""; + QString rightAfterNum = ""; + if (!okLeft) { + int leftNumIndex = 0; + for (; leftNumIndex < left.length(); leftNumIndex++) { + if (!left.at(leftNumIndex).isDigit()) { + break; + } + } + if (leftNumIndex != 0) { + leftNum = left.left(leftNumIndex).toFloat(&okLeft); + leftAfterNum = left.right(leftNumIndex); + } + } + if (!okRight) { + int rightNumIndex = 0; + for (; rightNumIndex < right.length(); rightNumIndex++) { + if (!right.at(rightNumIndex).isDigit()) { + break; + } + } + if (rightNumIndex != 0) { + rightNum = right.left(rightNumIndex).toFloat(&okRight); + rightAfterNum = right.right(rightNumIndex); + } + } + if (okLeft && okRight) { + + if (leftNum != rightNum) { + // both parsed as numbers, but different number + if (leftNum < rightNum) { + return -1; + } else { + return 1; + } + } else { + // both parsed, same number, but at least one has something else + // so compare the part after the number - prefer nothing + return QString::localeAwareCompare(leftAfterNum, rightAfterNum); + } + } else if (okLeft) { + return -1; + } else if (okRight) { + return 1; + } + // couldn't parse it, just return String comparison + return QString::localeAwareCompare(left, right); +} +bool CardDatabaseDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex & /*sourceParent*/) const +{ + CardInfoPtr info = static_cast(sourceModel())->getCard(sourceRow); + + if (((isToken == ShowTrue) && !info->getIsToken()) || ((isToken == ShowFalse) && info->getIsToken())) + return false; + + if (filterString != nullptr) { + if (filterTree != nullptr && !filterTree->acceptsCard(info)) { + return false; + } + return filterString->check(info); + } + + return rowMatchesCardName(info); +} + +bool CardDatabaseDisplayModel::rowMatchesCardName(CardInfoPtr info) const +{ + if (!cardName.isEmpty() && !info->getName().contains(cardName, Qt::CaseInsensitive)) + return false; + + if (!cardNameSet.isEmpty() && !cardNameSet.contains(info->getName())) + return false; + + if (filterTree != nullptr) + return filterTree->acceptsCard(info); + + return true; +} + +void CardDatabaseDisplayModel::clearFilterAll() +{ +#if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)) + beginFilterChange(); +#endif + cardName.clear(); + cardText.clear(); + cardTypes.clear(); + cardColors.clear(); + if (filterTree != nullptr) + filterTree->clear(); +#if (QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)) + endFilterChange(QSortFilterProxyModel::Direction::Rows); +#else + invalidateFilter(); +#endif +} + +void CardDatabaseDisplayModel::setFilterTree(FilterTree *_filterTree) +{ + if (this->filterTree != nullptr) + disconnect(this->filterTree, nullptr, this, nullptr); + + this->filterTree = _filterTree; + connect(this->filterTree, &FilterTree::changed, this, &CardDatabaseDisplayModel::filterTreeChanged); + invalidate(); +} + +void CardDatabaseDisplayModel::filterTreeChanged() +{ + invalidate(); +} + +const QString CardDatabaseDisplayModel::sanitizeCardName(const QString &dirtyName, const QMap &table) +{ + std::wstring toReturn = dirtyName.toStdWString(); + for (wchar_t &ch : toReturn) { + if (table.contains(ch)) { + ch = table.value(ch); + } + } + return QString::fromStdWString(toReturn); +} diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h new file mode 100644 index 000000000..0c5994a3a --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h @@ -0,0 +1,96 @@ +/** + * @file card_database_display_model.h + * @ingroup CardDatabaseModels + * @brief The CardDatabaseDisplayModel is a QSortFilterProxyModel that allows applying filters and sorting to a + * CardDatabaseModel. + */ + +#ifndef COCKATRICE_CARD_DATABASE_DISPLAY_MODEL_H +#define COCKATRICE_CARD_DATABASE_DISPLAY_MODEL_H + +#include +#include +#include + +class FilterTree; +class CardDatabaseDisplayModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + enum FilterBool + { + ShowTrue, + ShowFalse, + ShowAll + }; + +private: + FilterBool isToken; + QString cardName, cardText; + QSet cardNameSet, cardTypes, cardColors; + FilterTree *filterTree; + FilterString *filterString; + int loadedRowCount; + QTimer dirtyTimer; + + /** The translation table that will be used for sanitizeCardName. */ + static QMap characterTranslation; + +public: + explicit CardDatabaseDisplayModel(QObject *parent = nullptr); + void setFilterTree(FilterTree *_filterTree); + void setIsToken(FilterBool _isToken) + { + isToken = _isToken; + emit modelDirty(); + dirty(); + } + + void setCardName(const QString &_cardName) + { + if (filterString != nullptr) { + delete filterString; + filterString = nullptr; + } + cardName = sanitizeCardName(_cardName, characterTranslation); + emit modelDirty(); + dirty(); + } + void setStringFilter(const QString &_src) + { + delete filterString; + filterString = new FilterString(_src); + emit modelDirty(); + dirty(); + } + void setCardNameSet(const QSet &_cardNameSet) + { + cardNameSet = _cardNameSet; + emit modelDirty(); + dirty(); + } + + void dirty() + { + dirtyTimer.start(20); + } + void clearFilterAll(); + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + [[nodiscard]] bool canFetchMore(const QModelIndex &parent) const override; + void fetchMore(const QModelIndex &parent) override; +signals: + void modelDirty(); + +protected: + [[nodiscard]] bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + static int lessThanNumerically(const QString &left, const QString &right); + [[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + [[nodiscard]] bool rowMatchesCardName(CardInfoPtr info) const; + +private slots: + void filterTreeChanged(); + /** Will translate all undesirable characters in DIRTYNAME according to the TABLE. */ + const QString sanitizeCardName(const QString &dirtyName, const QMap &table); +}; + +#endif // COCKATRICE_CARD_DATABASE_DISPLAY_MODEL_H diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp new file mode 100644 index 000000000..e33156329 --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp @@ -0,0 +1,147 @@ +#include "card_database_model.h" + +#include +#include + +#define CARDDBMODEL_COLUMNS 6 + +CardDatabaseModel::CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromEnabledSets, QObject *parent) + : QAbstractListModel(parent), db(_db), showOnlyCardsFromEnabledSets(_showOnlyCardsFromEnabledSets) +{ + connect(db, &CardDatabase::cardAdded, this, &CardDatabaseModel::cardAdded); + connect(db, &CardDatabase::cardRemoved, this, &CardDatabaseModel::cardRemoved); + connect(db, &CardDatabase::cardDatabaseEnabledSetsChanged, this, + &CardDatabaseModel::cardDatabaseEnabledSetsChanged); + + cardDatabaseEnabledSetsChanged(); +} + +CardDatabaseModel::~CardDatabaseModel() = default; + +int CardDatabaseModel::rowCount(const QModelIndex & /*parent*/) const +{ + return cardList.size(); +} + +int CardDatabaseModel::columnCount(const QModelIndex & /*parent*/) const +{ + return CARDDBMODEL_COLUMNS; +} + +QVariant CardDatabaseModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= cardList.size() || index.column() >= CARDDBMODEL_COLUMNS || + (role != Qt::DisplayRole && role != SortRole)) + return QVariant(); + + CardInfoPtr card = cardList.at(index.row()); + switch (index.column()) { + case NameColumn: + return card->getName(); + case SetListColumn: + return card->getSetsNames(); + case ManaCostColumn: + return role == SortRole ? QString("%1%2").arg(card->getCmc(), 4, QChar('0')).arg(card->getManaCost()) + : card->getManaCost(); + case CardTypeColumn: + return card->getCardType(); + case PTColumn: + return card->getPowTough(); + case ColorColumn: + return card->getColors(); + default: + return QVariant(); + } +} + +QVariant CardDatabaseModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) + return QVariant(); + if (orientation != Qt::Horizontal) + return QVariant(); + switch (section) { + case NameColumn: + return QString(tr("Name")); + case SetListColumn: + return QString(tr("Sets")); + case ManaCostColumn: + return QString(tr("Mana cost")); + case CardTypeColumn: + return QString(tr("Card type")); + case PTColumn: + return QString(tr("P/T")); + case ColorColumn: + return QString(tr("Color(s)")); + default: + return QVariant(); + } +} + +void CardDatabaseModel::cardInfoChanged(CardInfoPtr card) +{ + const int row = cardList.indexOf(card); + if (row == -1) + return; + + emit dataChanged(index(row, 0), index(row, CARDDBMODEL_COLUMNS - 1)); +} + +bool CardDatabaseModel::checkCardHasAtLeastOneEnabledSet(CardInfoPtr card) +{ + if (!showOnlyCardsFromEnabledSets) + return true; + + for (const auto &printings : card->getSets()) { + for (const auto &printing : printings) { + if (printing.getSet()->getEnabled()) + return true; + } + } + + return false; +} + +void CardDatabaseModel::cardDatabaseEnabledSetsChanged() +{ + // remove all the cards no more present in at least one enabled set + for (const CardInfoPtr &card : cardList) { + if (!checkCardHasAtLeastOneEnabledSet(card)) { + cardRemoved(card); + } + } + + // re-check all the card currently not shown, maybe their part of a newly-enabled set + for (const CardInfoPtr &card : db->getCardList()) { + if (!cardListSet.contains(card)) { + cardAdded(card); + } + } +} + +void CardDatabaseModel::cardAdded(CardInfoPtr card) +{ + if (checkCardHasAtLeastOneEnabledSet(card)) { + // add the card if it's present in at least one enabled set + beginInsertRows(QModelIndex(), cardList.size(), cardList.size()); + cardList.append(card); + cardListSet.insert(card); + connect(card.data(), &CardInfo::cardInfoChanged, this, &CardDatabaseModel::cardInfoChanged); + endInsertRows(); + } +} + +void CardDatabaseModel::cardRemoved(CardInfoPtr card) +{ + const int row = cardList.indexOf(card); + if (row == -1) { + return; + } + + beginRemoveRows(QModelIndex(), row, row); + disconnect(card.data(), nullptr, this, nullptr); + cardListSet.remove(card); + card.clear(); + cardList.removeAt(row); + endRemoveRows(); +} diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_model.h b/libcockatrice_models/libcockatrice/models/database/card_database_model.h new file mode 100644 index 000000000..218cfff92 --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/card_database_model.h @@ -0,0 +1,62 @@ +/** + * @file card_database_model.h + * @ingroup CardDatabaseModels + * @brief The CardDatabaseModel maps the cardList contained in the CardDatabase as a QAbstractListModel. + */ + +#ifndef CARDDATABASEMODEL_H +#define CARDDATABASEMODEL_H + +#include +#include +#include +#include + +class CardDatabaseModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum Columns + { + NameColumn, + SetListColumn, + ManaCostColumn, + PTColumn, + CardTypeColumn, + ColorColumn + }; + enum Role + { + SortRole = Qt::UserRole + }; + CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromEnabledSets, QObject *parent = nullptr); + ~CardDatabaseModel() override; + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + [[nodiscard]] int columnCount(const QModelIndex &parent = QModelIndex()) const override; + [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; + [[nodiscard]] QVariant + headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + [[nodiscard]] CardDatabase *getDatabase() const + { + return db; + } + [[nodiscard]] CardInfoPtr getCard(int index) const + { + return cardList[index]; + } + +private: + QList cardList; + QSet cardListSet; // Supports faster lookups in cardDatabaseEnabledSetsChanged() + CardDatabase *db; + bool showOnlyCardsFromEnabledSets; + + inline bool checkCardHasAtLeastOneEnabledSet(CardInfoPtr card); +private slots: + void cardAdded(CardInfoPtr card); + void cardRemoved(CardInfoPtr card); + void cardInfoChanged(CardInfoPtr card); + void cardDatabaseEnabledSetsChanged(); +}; + +#endif diff --git a/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp new file mode 100644 index 000000000..f6dc4f9cf --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp @@ -0,0 +1,299 @@ +#include "card_sets_model.h" + +#include + +SetsModel::SetsModel(CardDatabase *_db, QObject *parent) : QAbstractTableModel(parent), sets(_db->getSetList()) +{ + sets.sortByKey(); + for (const CardSetPtr &set : sets) { + if (set->getEnabled()) + enabledSets.insert(set); + } +} + +SetsModel::~SetsModel() = default; + +int SetsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + else + return sets.size(); +} + +QVariant SetsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || (index.column() >= NUM_COLS) || (index.row() >= rowCount())) + return QVariant(); + + CardSetPtr set = sets[index.row()]; + + if (index.column() == EnabledCol) { + switch (role) { + case SortRole: + return enabledSets.contains(set) ? "1" : "0"; + case Qt::CheckStateRole: + return static_cast(enabledSets.contains(set) ? Qt::Checked : Qt::Unchecked); + case Qt::DisplayRole: + default: + return QVariant(); + } + } + + if (role != Qt::DisplayRole && role != SortRole) + return QVariant(); + + switch (index.column()) { + case SortKeyCol: + return QString("%1").arg(index.row(), 8, 10, QChar('0')); + case IsKnownCol: + return set->getIsKnown(); + case SetTypeCol: + return set->getSetType(); + case ShortNameCol: + return set->getShortName(); + case LongNameCol: + return set->getLongName(); + case ReleaseDateCol: + return set->getReleaseDate().toString(Qt::ISODate); + default: + return QVariant(); + } +} + +bool SetsModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (role == Qt::CheckStateRole && index.column() == EnabledCol) { + toggleRow(index.row(), value == Qt::Checked); + return true; + } + return false; +} + +QVariant SetsModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if ((role != Qt::DisplayRole) || (orientation != Qt::Horizontal)) + return QVariant(); + switch (section) { + case SortKeyCol: + return QString("Key"); /* no tr() for translations needed, column just used for sorting --> hidden */ + case IsKnownCol: + return QString( + "Is known"); /* no tr() for translations needed, column is just used for sorting --> hidden */ + case EnabledCol: + return tr("Enabled"); + case SetTypeCol: + return tr("Set type"); + case ShortNameCol: + return tr("Set code"); + case LongNameCol: + return tr("Long name"); + case ReleaseDateCol: + return tr("Release date"); + default: + return QVariant(); + } +} + +Qt::ItemFlags SetsModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return Qt::NoItemFlags; + + Qt::ItemFlags flags = QAbstractTableModel::flags(index) | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled; + + if (index.column() == EnabledCol) + flags |= Qt::ItemIsUserCheckable; + + return flags; +} + +Qt::DropActions SetsModel::supportedDropActions() const +{ + return Qt::MoveAction; +} + +QMimeData *SetsModel::mimeData(const QModelIndexList &indexes) const +{ + if (indexes.isEmpty()) + return 0; + + SetsMimeData *result = new SetsMimeData(indexes[0].row()); + return qobject_cast(result); +} + +bool SetsModel::dropMimeData(const QMimeData *data, + Qt::DropAction action, + int row, + int /*column*/, + const QModelIndex &parent) +{ + if (action != Qt::MoveAction) + return false; + if (row == -1) { + if (!parent.isValid()) + return false; + row = parent.row(); + } + int oldRow = qobject_cast(data)->getOldRow(); + if (oldRow < row) + row--; + + swapRows(oldRow, row); + + return true; +} + +void SetsModel::toggleRow(int row, bool enable) +{ + CardSetPtr temp = sets.at(row); + + if (enable) + enabledSets.insert(temp); + else + enabledSets.remove(temp); + + emit dataChanged(index(row, EnabledCol), index(row, EnabledCol)); +} + +void SetsModel::toggleRow(int row) +{ + CardSetPtr tmp = sets.at(row); + + if (tmp == nullptr) + return; + + if (enabledSets.contains(tmp)) + enabledSets.remove(tmp); + else + enabledSets.insert(tmp); + + emit dataChanged(index(row, EnabledCol), index(row, EnabledCol)); +} + +void SetsModel::toggleAll(bool enabled) +{ + enabledSets.clear(); + + if (enabled) + for (CardSetPtr set : sets) + enabledSets.insert(set); + + emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1)); +} + +void SetsModel::swapRows(int oldRow, int newRow) +{ + beginRemoveRows(QModelIndex(), oldRow, oldRow); + CardSetPtr temp = sets.takeAt(oldRow); + endRemoveRows(); + + beginInsertRows(QModelIndex(), newRow, newRow); + sets.insert(newRow, temp); + endInsertRows(); + + emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1)); +} + +void SetsModel::restoreOriginalOrder() +{ + int numRows = rowCount(); + sets.defaultSort(); + emit dataChanged(index(0, 0), index(numRows - 1, columnCount() - 1)); +} + +void SetsModel::sort(int column, Qt::SortOrder order) +{ + QMultiMap setMap; + int numRows = rowCount(); + int row; + + for (row = 0; row < numRows; ++row) + setMap.insert(index(row, column).data(SetsModel::SortRole).toString(), sets.at(row)); + + QList tmp = setMap.values(); + sets.clear(); + if (order == Qt::AscendingOrder) { + for (row = 0; row < tmp.size(); row++) { + sets.append(tmp.at(row)); + } + } else { + for (row = tmp.size() - 1; row >= 0; row--) { + sets.append(tmp.at(row)); + } + } + + emit dataChanged(index(0, 0), index(numRows - 1, columnCount() - 1)); +} + +void SetsModel::save(CardDatabase *db) +{ + // order + for (int i = 0; i < sets.size(); i++) + sets[i]->setSortKey(static_cast(i + 1)); + + // enabled sets + for (const CardSetPtr &set : sets) + set->setEnabled(enabledSets.contains(set)); + + sets.sortByKey(); + + db->notifyEnabledSetsChanged(); +} + +void SetsModel::restore(CardDatabase *db) +{ + // order + sets = db->getSetList(); + sets.sortByKey(); + + // enabled sets + enabledSets.clear(); + for (const CardSetPtr &set : sets) { + if (set->getEnabled()) + enabledSets.insert(set); + } + + emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1)); +} + +QStringList SetsModel::mimeTypes() const +{ + return QStringList() << "application/x-cockatricecardset"; +} + +SetsDisplayModel::SetsDisplayModel(QObject *parent) : QSortFilterProxyModel(parent) +{ + setFilterCaseSensitivity(Qt::CaseInsensitive); + setSortCaseSensitivity(Qt::CaseInsensitive); +} + +void SetsDisplayModel::fetchMore(const QModelIndex &index) +{ + int itemsToFetch = sourceModel()->rowCount(index); + + beginInsertRows(QModelIndex(), 0, itemsToFetch - 1); + + endInsertRows(); +} + +bool SetsDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + auto typeIndex = sourceModel()->index(sourceRow, SetsModel::SetTypeCol, sourceParent); + auto nameIndex = sourceModel()->index(sourceRow, SetsModel::LongNameCol, sourceParent); + auto shortNameIndex = sourceModel()->index(sourceRow, SetsModel::ShortNameCol, sourceParent); + + const auto filter = filterRegularExpression(); + + return (sourceModel()->data(typeIndex).toString().contains(filter) || + sourceModel()->data(nameIndex).toString().contains(filter) || + sourceModel()->data(shortNameIndex).toString().contains(filter)); +} + +bool SetsDisplayModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + QString leftString = sourceModel()->data(left, SetsModel::SortRole).toString(); + QString rightString = sourceModel()->data(right, SetsModel::SortRole).toString(); + + return QString::localeAwareCompare(leftString, rightString) < 0; +} diff --git a/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.h b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.h new file mode 100644 index 000000000..0ffc5a847 --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.h @@ -0,0 +1,106 @@ +/** + * @file sets_model.h + * @ingroup CardDatabaseModels + * @brief TODO: Document this. + */ + +#ifndef SETSMODEL_H +#define SETSMODEL_H + +#include +#include +#include +#include +#include + +class SetsProxyModel; + +class SetsMimeData : public QMimeData +{ + Q_OBJECT +private: + int oldRow; + +public: + SetsMimeData(int _oldRow) : oldRow(_oldRow) + { + } + [[nodiscard]] int getOldRow() const + { + return oldRow; + } + [[nodiscard]] QStringList formats() const + { + return QStringList() << "application/x-cockatricecardset"; + } +}; + +class SetsModel : public QAbstractTableModel +{ + Q_OBJECT + friend class SetsProxyModel; + +private: + static const int NUM_COLS = 7; + CardSetList sets; + QSet enabledSets; + +public: + enum SetsColumns + { + SortKeyCol, + IsKnownCol, + EnabledCol, + LongNameCol, + ShortNameCol, + SetTypeCol, + ReleaseDateCol, + PriorityCol + }; + enum Role + { + SortRole = Qt::UserRole + }; + + explicit SetsModel(CardDatabase *_db, QObject *parent = nullptr); + ~SetsModel() override; + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + [[nodiscard]] int columnCount(const QModelIndex &parent = QModelIndex()) const override + { + Q_UNUSED(parent); + return NUM_COLS; + } + [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + [[nodiscard]] QVariant + headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + [[nodiscard]] Qt::ItemFlags flags(const QModelIndex &index) const override; + [[nodiscard]] Qt::DropActions supportedDropActions() const override; + + [[nodiscard]] QMimeData *mimeData(const QModelIndexList &indexes) const override; + bool + dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; + [[nodiscard]] QStringList mimeTypes() const override; + void swapRows(int oldRow, int newRow); + void toggleRow(int row, bool enable); + void toggleRow(int row); + void toggleAll(bool); + void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override; + void save(CardDatabase *db); + void restore(CardDatabase *db); + void restoreOriginalOrder(); +}; + +class SetsDisplayModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + explicit SetsDisplayModel(QObject *parent = nullptr); + +protected: + [[nodiscard]] bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + [[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + void fetchMore(const QModelIndex &index) override; +}; + +#endif diff --git a/libcockatrice_models/libcockatrice/models/database/token/token_display_model.cpp b/libcockatrice_models/libcockatrice/models/database/token/token_display_model.cpp new file mode 100644 index 000000000..4282c6dc8 --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/token/token_display_model.cpp @@ -0,0 +1,19 @@ +#include "token_display_model.h" + +#include "../card_database_model.h" + +TokenDisplayModel::TokenDisplayModel(QObject *parent) : CardDatabaseDisplayModel(parent) +{ +} + +bool TokenDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex & /*sourceParent*/) const +{ + CardInfoPtr info = static_cast(sourceModel())->getCard(sourceRow); + return info->getIsToken() && rowMatchesCardName(info); +} + +int TokenDisplayModel::rowCount(const QModelIndex &parent) const +{ + // always load all tokens at start + return QSortFilterProxyModel::rowCount(parent); +} \ No newline at end of file diff --git a/libcockatrice_models/libcockatrice/models/database/token/token_display_model.h b/libcockatrice_models/libcockatrice/models/database/token/token_display_model.h new file mode 100644 index 000000000..f6b2fdfbb --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/token/token_display_model.h @@ -0,0 +1,23 @@ +/** + * @file token_display_model.h + * @ingroup CardDatabaseModels + * @brief TODO: Document this. + */ + +#ifndef COCKATRICE_TOKEN_DISPLAY_MODEL_H +#define COCKATRICE_TOKEN_DISPLAY_MODEL_H + +#include "../card_database_display_model.h" + +class TokenDisplayModel : public CardDatabaseDisplayModel +{ + Q_OBJECT +public: + explicit TokenDisplayModel(QObject *parent = nullptr); + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + +protected: + [[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; +}; + +#endif // COCKATRICE_TOKEN_DISPLAY_MODEL_H diff --git a/libcockatrice_models/libcockatrice/models/database/token/token_edit_model.cpp b/libcockatrice_models/libcockatrice/models/database/token/token_edit_model.cpp new file mode 100644 index 000000000..89a1f71b1 --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/token/token_edit_model.cpp @@ -0,0 +1,22 @@ +#include "token_edit_model.h" + +#include "../card_database_display_model.h" +#include "../card_database_model.h" + +#include + +TokenEditModel::TokenEditModel(QObject *parent) : CardDatabaseDisplayModel(parent) +{ +} + +bool TokenEditModel::filterAcceptsRow(int sourceRow, const QModelIndex & /*sourceParent*/) const +{ + CardInfoPtr info = static_cast(sourceModel())->getCard(sourceRow); + return info->getIsToken() && info->getSets().contains(CardSet::TOKENS_SETNAME) && rowMatchesCardName(info); +} + +int TokenEditModel::rowCount(const QModelIndex &parent) const +{ + // always load all tokens at start + return QSortFilterProxyModel::rowCount(parent); +} \ No newline at end of file diff --git a/libcockatrice_models/libcockatrice/models/database/token/token_edit_model.h b/libcockatrice_models/libcockatrice/models/database/token/token_edit_model.h new file mode 100644 index 000000000..5e5843761 --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/token/token_edit_model.h @@ -0,0 +1,23 @@ +/** + * @file token_edit_model.h + * @ingroup CardDatabaseModels + * @brief TODO: Document this. + */ + +#ifndef COCKATRICE_TOKEN_EDIT_MODEL_H +#define COCKATRICE_TOKEN_EDIT_MODEL_H + +#include "../card_database_display_model.h" + +class TokenEditModel : public CardDatabaseDisplayModel +{ + Q_OBJECT +public: + explicit TokenEditModel(QObject *parent = nullptr); + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + +protected: + [[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; +}; + +#endif // COCKATRICE_TOKEN_EDIT_MODEL_H diff --git a/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt new file mode 100644 index 000000000..851636a35 --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt @@ -0,0 +1,21 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS deck_list_model.h deck_list_sort_filter_proxy_model.h) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library( + libcockatrice_models_deck_list STATIC ${MOC_SOURCES} deck_list_model.cpp deck_list_sort_filter_proxy_model.cpp +) + +target_include_directories(libcockatrice_models_deck_list PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries( + libcockatrice_models_deck_list PUBLIC libcockatrice_card libcockatrice_deck_list ${QT_CORE_MODULE} +) diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp new file mode 100644 index 000000000..e43c612f0 --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -0,0 +1,776 @@ +#include "deck_list_model.h" + +#include + +DeckListModel::DeckListModel(QObject *parent) + : QAbstractItemModel(parent), lastKnownColumn(1), lastKnownOrder(Qt::AscendingOrder) +{ + deckList = QSharedPointer(new DeckList()); + root = new InnerDecklistNode; +} + +DeckListModel::DeckListModel(QObject *parent, const QSharedPointer &deckList) : DeckListModel(parent) +{ + setDeckList(deckList); + + // forward change signals + connect(this, &DeckListModel::cardAddedAt, this, &DeckListModel::cardsChanged); + connect(this, &DeckListModel::cardRemoved, this, &DeckListModel::cardsChanged); + connect(this, &DeckListModel::deckReplaced, this, &DeckListModel::cardsChanged); + + connect(this, &DeckListModel::cardNodeAddedAt, this, &DeckListModel::cardNodesChanged); + connect(this, &DeckListModel::cardNodeRemoved, this, &DeckListModel::cardNodesChanged); + connect(this, &DeckListModel::deckReplaced, this, &DeckListModel::cardNodesChanged); +} + +DeckListModel::~DeckListModel() +{ + delete root; +} + +/** + * @brief Extract the value from the card that is used for the group criteria. + * @param info Pointer to card information. + * @param criteria The group criteria + * @return String representing the value of the criteria. + */ +static QString extractGroupCriteriaValue(const CardInfoPtr &info, DeckListModelGroupCriteria::Type criteria) +{ + if (!info) { + return "unknown"; + } + + switch (criteria) { + case DeckListModelGroupCriteria::MAIN_TYPE: + return info->getMainCardType(); + case DeckListModelGroupCriteria::MANA_COST: + return info->getCmc(); + case DeckListModelGroupCriteria::COLOR: + return info->getColors() == "" ? "Colorless" : info->getColors(); + default: + return "unknown"; + } +} + +void DeckListModel::rebuildTree() +{ + beginResetModel(); + root->clearTree(); + + InnerDecklistNode *listRoot = deckList->getTree()->getRoot(); + + for (int i = 0; i < listRoot->size(); i++) { + auto *currentZone = dynamic_cast(listRoot->at(i)); + auto *node = new InnerDecklistNode(currentZone->getName(), root); + + for (int j = 0; j < currentZone->size(); j++) { + auto *currentCard = dynamic_cast(currentZone->at(j)); + + // TODO: better sanity checking + if (currentCard == nullptr) { + continue; + } + + CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(currentCard->getName()); + QString groupCriteria = extractGroupCriteriaValue(info, activeGroupCriteria); + + auto *groupNode = dynamic_cast(node->findChild(groupCriteria)); + + if (!groupNode) { + groupNode = new InnerDecklistNode(groupCriteria, node); + } + + new DecklistModelCardNode(currentCard, groupNode); + } + } + + endResetModel(); + + refreshCardFormatLegalities(); +} + +int DeckListModel::rowCount(const QModelIndex &parent) const +{ + // debugIndexInfo("rowCount", parent); + auto *node = getNode(parent); + if (node) { + return node->size(); + } else { + return 0; + } +} + +int DeckListModel::columnCount(const QModelIndex & /*parent*/) const +{ + return 5; +} + +QVariant DeckListModel::data(const QModelIndex &index, int role) const +{ + // debugIndexInfo("data", index); + if (!index.isValid()) { + return {}; + } + + if (index.column() >= columnCount()) { + return {}; + } + + auto *node = static_cast(index.internalPointer()); + auto *card = dynamic_cast(node); + + // Group node + if (!card) { + const auto *group = dynamic_cast(node); + + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: { + switch (index.column()) { + case DeckListModelColumns::CARD_AMOUNT: + return group->recursiveCount(true); + case DeckListModelColumns::CARD_NAME: + if (role == Qt::DisplayRole) { + return group->getVisibleName(); + } + return group->getName(); + case DeckListModelColumns::CARD_SET: + return group->getCardSetShortName(); + case DeckListModelColumns::CARD_COLLECTOR_NUMBER: + return group->getCardCollectorNumber(); + case DeckListModelColumns::CARD_PROVIDER_ID: + return group->getCardProviderId(); + default: + return {}; + } + } + case DeckRoles::IsCardRole: + return false; + + case DeckRoles::DepthRole: + return group->depth(); + + // legality does not apply to group nodes + case DeckRoles::IsLegalRole: + return true; + + default: + return {}; + } + } + + // Card node + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + switch (index.column()) { + case DeckListModelColumns::CARD_AMOUNT: + return card->getNumber(); + case DeckListModelColumns::CARD_NAME: + return card->getName(); + case DeckListModelColumns::CARD_SET: + return card->getCardSetShortName(); + case DeckListModelColumns::CARD_COLLECTOR_NUMBER: + return card->getCardCollectorNumber(); + case DeckListModelColumns::CARD_PROVIDER_ID: + return card->getCardProviderId(); + default: + return {}; + } + + case DeckRoles::IsCardRole: { + return true; + } + + case DeckRoles::DepthRole: { + return card->depth(); + } + + case DeckRoles::IsLegalRole: { + return card->getFormatLegality(); + } + + default: { + return {}; + } + } +} + +QVariant DeckListModel::headerData(const int section, const Qt::Orientation orientation, const int role) const +{ + if ((role != Qt::DisplayRole) || (orientation != Qt::Horizontal)) { + return {}; + } + + if (section >= columnCount()) { + return {}; + } + + switch (section) { + case DeckListModelColumns::CARD_AMOUNT: + return tr("Count"); + case DeckListModelColumns::CARD_NAME: + return tr("Card"); + case DeckListModelColumns::CARD_SET: + return tr("Set"); + case DeckListModelColumns::CARD_COLLECTOR_NUMBER: + return tr("Number"); + case DeckListModelColumns::CARD_PROVIDER_ID: + return tr("Provider ID"); + default: + return {}; + } +} + +QModelIndex DeckListModel::index(int row, int column, const QModelIndex &parent) const +{ + // debugIndexInfo("index", parent); + if (!hasIndex(row, column, parent)) { + return {}; + } + + auto *parentNode = getNode(parent); + return row >= parentNode->size() ? QModelIndex() : createIndex(row, column, parentNode->at(row)); +} + +QModelIndex DeckListModel::parent(const QModelIndex &ind) const +{ + if (!ind.isValid()) { + return {}; + } + + return nodeToIndex(static_cast(ind.internalPointer())->getParent()); +} + +Qt::ItemFlags DeckListModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) { + return Qt::NoItemFlags; + } + + Qt::ItemFlags result = Qt::ItemIsEnabled; + result |= Qt::ItemIsSelectable; + + return result; +} + +void DeckListModel::emitBackgroundUpdates(const QModelIndex &parent) +{ + int rows = rowCount(parent); + if (rows == 0) + return; + + QModelIndex topLeft = index(0, 0, parent); + QModelIndex bottomRight = index(rows - 1, columnCount() - 1, parent); + emit dataChanged(topLeft, bottomRight, {Qt::BackgroundRole}); + + for (int r = 0; r < rows; ++r) { + QModelIndex child = index(r, 0, parent); + emitBackgroundUpdates(child); + } +} + +void DeckListModel::emitRecursiveUpdates(const QModelIndex &index) +{ + if (!index.isValid()) { + return; + } + + emit dataChanged(index, index); + emitRecursiveUpdates(index.parent()); +} + +bool DeckListModel::setData(const QModelIndex &index, const QVariant &value, const int role) +{ + auto *node = getNode(index); + if (!node || (role != Qt::EditRole)) { + return false; + } + + switch (index.column()) { + case DeckListModelColumns::CARD_AMOUNT: + node->setNumber(value.toInt()); + refreshCardFormatLegalities(); + break; + case DeckListModelColumns::CARD_NAME: + node->setName(value.toString()); + break; + case DeckListModelColumns::CARD_SET: + node->setCardSetShortName(value.toString()); + break; + case DeckListModelColumns::CARD_COLLECTOR_NUMBER: + node->setCardCollectorNumber(value.toString()); + break; + case DeckListModelColumns::CARD_PROVIDER_ID: + node->setCardProviderId(value.toString()); + break; + default: + return false; + } + + emitRecursiveUpdates(index); + deckList->refreshDeckHash(); + emit deckHashChanged(); + + return true; +} + +bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent) +{ + auto *node = getNode(parent); + if (!node) { + return false; + } + + if (row + count > node->size()) { + return false; + } + + beginRemoveRows(parent, row, row + count - 1); + for (int i = 0; i < count; i++) { + AbstractDecklistNode *toDelete = node->takeAt(row); + if (auto *temp = dynamic_cast(toDelete)) { + deckList->getTree()->deleteNode(temp->getDataNode()); + } + delete toDelete; + } + endRemoveRows(); + + if (node->empty() && (node != root)) { + removeRows(parent.row(), 1, parent.parent()); + } else { + emitRecursiveUpdates(parent); + } + + deckList->refreshDeckHash(); + emit deckHashChanged(); + + return true; +} + +InnerDecklistNode *DeckListModel::createNodeIfNeeded(const QString &name, InnerDecklistNode *parent) +{ + auto *newNode = dynamic_cast(parent->findChild(name)); + if (!newNode) { + beginInsertRows(nodeToIndex(parent), parent->size(), parent->size()); + newNode = new InnerDecklistNode(name, parent); + endInsertRows(); + } + return newNode; +} + +DecklistModelCardNode *DeckListModel::findCardNode(const QString &cardName, + const QString &zoneName, + const QString &providerId, + const QString &cardNumber) const +{ + InnerDecklistNode *zoneNode = dynamic_cast(root->findChild(zoneName)); + if (!zoneNode) { + return nullptr; + } + + CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(cardName); + if (!info) { + return nullptr; + } + + QString groupCriteria = extractGroupCriteriaValue(info, activeGroupCriteria); + InnerDecklistNode *groupNode = dynamic_cast(zoneNode->findChild(groupCriteria)); + if (!groupNode) { + return nullptr; + } + + return dynamic_cast( + groupNode->findCardChildByNameProviderIdAndNumber(cardName, providerId, cardNumber)); +} + +QModelIndex DeckListModel::findCard(const QString &cardName, + const QString &zoneName, + const QString &providerId, + const QString &cardNumber) const +{ + DecklistModelCardNode *cardNode = findCardNode(cardName, zoneName, providerId, cardNumber); + if (!cardNode) { + return {}; + } + + return nodeToIndex(cardNode); +} + +QModelIndex DeckListModel::addPreferredPrintingCard(const QString &cardName, const QString &zoneName, bool abAddAnyway) +{ + ExactCard card = CardDatabaseManager::query()->getCard({cardName}); + + if (!card) { + if (abAddAnyway) { + // We need to keep this card added no matter what + // This is usually called from tab_deck_editor + // So we'll create a new CardInfo with the name + // and default values for all fields + card = ExactCard(CardInfo::newInstance(cardName)); + } else { + return {}; + } + } + + return addCard(card, zoneName); +} + +QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneName) +{ + if (!card) { + return {}; + } + + InnerDecklistNode *zoneNode = createNodeIfNeeded(zoneName, root); + + CardInfoPtr cardInfo = card.getCardPtr(); + PrintingInfo printingInfo = card.getPrinting(); + + QString groupCriteria = extractGroupCriteriaValue(cardInfo, activeGroupCriteria); + InnerDecklistNode *groupNode = createNodeIfNeeded(groupCriteria, zoneNode); + + const QModelIndex parentIndex = nodeToIndex(groupNode); + auto *cardNode = dynamic_cast(groupNode->findCardChildByNameProviderIdAndNumber( + card.getName(), printingInfo.getUuid(), printingInfo.getProperty("num"))); + const auto cardSetName = printingInfo.getSet().isNull() ? "" : printingInfo.getSet()->getCorrectedShortName(); + + bool cardNodeAdded = false; + if (!cardNode) { + // Determine the correct index + int insertRow = findSortedInsertRow(groupNode, cardInfo); + + auto *decklistCard = deckList->addCard(cardInfo->getName(), zoneName, insertRow, cardSetName, + printingInfo.getProperty("num"), printingInfo.getProperty("uuid")); + + beginInsertRows(parentIndex, insertRow, insertRow); + cardNode = new DecklistModelCardNode(decklistCard, groupNode, insertRow); + endInsertRows(); + + cardNodeAdded = true; + } else { + cardNode->setNumber(cardNode->getNumber() + 1); + cardNode->setCardSetShortName(cardSetName); + cardNode->setCardCollectorNumber(printingInfo.getProperty("num")); + cardNode->setCardProviderId(printingInfo.getProperty("uuid")); + // Emit dataChanged for the amount column since we modified it + QModelIndex cardIndex = nodeToIndex(cardNode); + QModelIndex amountIndex = cardIndex.sibling(cardIndex.row(), DeckListModelColumns::CARD_AMOUNT); + emit dataChanged(amountIndex, amountIndex, {Qt::EditRole}); + } + sort(lastKnownColumn, lastKnownOrder); + refreshCardFormatLegalities(); + emitRecursiveUpdates(parentIndex); + + deckList->refreshDeckHash(); + emit deckHashChanged(); + + auto index = nodeToIndex(cardNode); + + if (cardNodeAdded) { + emit cardNodeAddedAt(index); + } + + emit cardAddedAt(index); + + return index; +} + +bool DeckListModel::offsetCountAtIndex(const QModelIndex &idx, int offset) +{ + if (!idx.isValid()) { + return false; + } + + auto *node = static_cast(idx.internalPointer()); + auto *card = dynamic_cast(node); + + if (!card) { + return false; + } + + const QModelIndex numberIndex = idx.siblingAtColumn(DeckListModelColumns::CARD_AMOUNT); + const int count = numberIndex.data(Qt::EditRole).toInt(); + const int newCount = count + offset; + + if (newCount <= 0) { + removeRow(idx.row(), idx.parent()); + emit cardNodeRemoved(); + } else { + setData(numberIndex, newCount, Qt::EditRole); + } + + if (offset > 0) { + emit cardAddedAt(idx); + } else if (offset < 0) { + emit cardRemoved(); + } + + return true; +} + +bool DeckListModel::removeCardAtIndex(const QModelIndex &idx) +{ + if (!idx.isValid()) { + return false; + } + + auto *node = static_cast(idx.internalPointer()); + auto *card = dynamic_cast(node); + + if (!card) { + return false; + } + + bool success = removeRow(idx.row(), idx.parent()); + + if (success) { + emit cardRemoved(); + } + + return success; +} + +int DeckListModel::findSortedInsertRow(const InnerDecklistNode *parent, const CardInfoPtr &cardInfo) const +{ + if (!cardInfo) { + return parent->size(); // fallback: append at end + } + + for (int i = 0; i < parent->size(); ++i) { + auto *existingCard = dynamic_cast(parent->at(i)); + if (!existingCard) + continue; + + bool lessThan = false; + switch (lastKnownColumn) { + case 0: // ByNumber + lessThan = lastKnownOrder == Qt::AscendingOrder + ? cardInfo->getProperty("collectorNumber") < existingCard->getCardCollectorNumber() + : cardInfo->getProperty("collectorNumber") > existingCard->getCardCollectorNumber(); + break; + case 1: // ByName + default: + lessThan = lastKnownOrder == Qt::AscendingOrder + ? cardInfo->getName().localeAwareCompare(existingCard->getName()) < 0 + : cardInfo->getName().localeAwareCompare(existingCard->getName()) > 0; + break; + } + + if (lessThan) + return i; + } + + return parent->size(); // insert at end if no earlier match +} + +QModelIndex DeckListModel::nodeToIndex(AbstractDecklistNode *node) const +{ + if (node == nullptr || node == root) { + return {}; + } + + return createIndex(node->getParent()->indexOf(node), 0, node); +} + +void DeckListModel::sortHelper(InnerDecklistNode *node, Qt::SortOrder order) +{ + // Sort children of node and save the information needed to + // update the list of persistent indexes. + QVector> sortResult = node->sort(order); + + QModelIndexList from, to; + int columns = columnCount(); + for (int i = sortResult.size() - 1; i >= 0; --i) { + const int fromRow = sortResult[i].first; + const int toRow = sortResult[i].second; + AbstractDecklistNode *temp = node->at(toRow); + for (int j = 0; j < columns; ++j) { + from << createIndex(fromRow, j, temp); + to << createIndex(toRow, j, temp); + } + } + changePersistentIndexList(from, to); + + // Recursion + for (int i = node->size() - 1; i >= 0; --i) { + auto *subNode = dynamic_cast(node->at(i)); + if (subNode) { + sortHelper(subNode, order); + } + } +} + +void DeckListModel::sort(int column, Qt::SortOrder order) +{ + lastKnownColumn = column; + lastKnownOrder = order; + + emit layoutAboutToBeChanged(); + DeckSortMethod sortMethod; + switch (column) { + case 0: + sortMethod = ByNumber; + break; + case 1: + sortMethod = ByName; + break; + default: + sortMethod = ByName; + } + + root->setSortMethod(sortMethod); + sortHelper(root, order); + emit layoutChanged(); +} + +void DeckListModel::setActiveGroupCriteria(DeckListModelGroupCriteria::Type newCriteria) +{ + activeGroupCriteria = newCriteria; + rebuildTree(); +} + +void DeckListModel::setActiveFormat(const QString &_format) +{ + deckList->setGameFormat(_format); + refreshCardFormatLegalities(); + emitBackgroundUpdates(QModelIndex()); // start from root +} + +void DeckListModel::cleanList() +{ + setDeckList(QSharedPointer(new DeckList())); +} + +/** + * @param _deck The deck. + */ +void DeckListModel::setDeckList(const QSharedPointer &_deck) +{ + if (deckList != _deck) { + deckList = _deck; + } + rebuildTree(); + emit deckReplaced(); +} + +void DeckListModel::forEachCard(const std::function &func) +{ + deckList->forEachCard(func); +} + +QList DeckListModel::getCardNodes() const +{ + return deckList->getCardNodes(); +} + +QList DeckListModel::getCardNodesForZone(const QString &zoneName) const +{ + return deckList->getCardNodes({zoneName}); +} + +QList DeckListModel::getCardNames() const +{ + auto nodes = deckList->getCardNodes(); + + QList names; + std::transform(nodes.cbegin(), nodes.cend(), std::back_inserter(names), [](auto node) { return node->getName(); }); + + return names; +} + +QList DeckListModel::getCardRefs() const +{ + auto nodes = deckList->getCardNodes(); + + QList cardRefs; + std::transform(nodes.cbegin(), nodes.cend(), std::back_inserter(cardRefs), + [](auto node) { return node->toCardRef(); }); + + return cardRefs; +} + +QList DeckListModel::getZones() const +{ + auto zoneNodes = deckList->getZoneNodes(); + + QList zones; + std::transform(zoneNodes.cbegin(), zoneNodes.cend(), std::back_inserter(zones), + [](auto zoneNode) { return zoneNode->getName(); }); + + return zones; +} + +static int maxAllowedForLegality(const FormatRules &format, const QString &legality) +{ + for (const AllowedCount &c : format.allowedCounts) { + if (c.label == legality) { + return c.max; + } + } + return -1; // unknown legality → treat as illegal +} + +static bool isCardQuantityLegalForFormat(const QString &format, const CardInfo &cardInfo, int quantity) +{ + if (format.isEmpty()) { + return true; + } + + auto formatRules = CardDatabaseManager::query()->getFormat(format); + + // if format has no custom rules, then just do the default check + if (!formatRules) { + return cardInfo.isLegalInFormat(format); + } + + // Exceptions always win + if (cardHasAnyException(cardInfo, *formatRules)) { + return true; + } + + // check legality prop + const QString legality = cardInfo.getLegalityProp(format); + if (legality.isEmpty()) { + return false; + } + + int maxAllowed = maxAllowedForLegality(*formatRules, legality); + + if (maxAllowed == -1) { + return false; + } + + if (maxAllowed < 0) { // unlimited + return true; + } + + return quantity <= maxAllowed; +} + +static bool isCardNodeLegalForFormat(const QString &format, const InnerDecklistNode *zone, const DecklistCardNode *card) +{ + // Don't check legality for tokens + if (zone->getName() == DECK_ZONE_TOKENS) { + return true; + } + + // unknown cards are not legal + ExactCard exactCard = CardDatabaseManager::query()->getCard(card->toCardRef()); + if (!exactCard) { + return false; + } + + // actual check + return isCardQuantityLegalForFormat(format, exactCard.getInfo(), card->getNumber()); +} + +void DeckListModel::refreshCardFormatLegalities() +{ + QString format = deckList->getGameFormat(); + + deckList->forEachCard([&format](const InnerDecklistNode *zone, DecklistCardNode *card) { + bool legal = isCardNodeLegalForFormat(format, zone, card); + card->setFormatLegality(legal); + }); +} diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h new file mode 100644 index 000000000..86d36b7f9 --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h @@ -0,0 +1,438 @@ +#ifndef DECKLISTMODEL_H +#define DECKLISTMODEL_H + +#include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h> +#include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h> +#include +#include +#include +#include + +class CardDatabase; +class QPrinter; +class QTextCursor; + +/** + * @namespace DeckRoles + * @brief Custom model roles used by the DeckListModel for data retrieval. + * + * These roles extend Qt's item data roles starting at Qt::UserRole. + */ +namespace DeckRoles +{ +/** + * @enum DeckRoles + * @brief Custom data roles for deck-related model items. + * + * These roles are used to retrieve specialized data from the DeckListModel. + */ +enum +{ + IsCardRole = Qt::UserRole + 1, /**< Indicates whether the item represents a card. */ + DepthRole, /**< Depth level within the deck's grouping hierarchy. */ + IsLegalRole /**< Whether the card is legal in the current deck format. */ +}; +} // namespace DeckRoles + +/** + * @namespace DeckListModelColumns + * @brief Column indices for the DeckListModel. + * + * These values map to the columns in the deck list table representation. + */ +namespace DeckListModelColumns +{ +/** + * @enum DeckListModelColumns + * @brief Column identifiers for displaying card information in the deck list. + */ +enum +{ + CARD_AMOUNT = 0, /**< The number of copies of the card. */ + CARD_NAME = 1, /**< The card's name. */ + CARD_SET = 2, /**< The set or expansion the card belongs to. */ + CARD_COLLECTOR_NUMBER = 3, /**< Collector number of the card within the set. */ + CARD_PROVIDER_ID = 4 /**< ID used by the external data provider (e.g., Scryfall). */ +}; +} // namespace DeckListModelColumns + +/** + * @namespace DeckListModelGroupCriteria + * @brief Specifies criteria used to group cards in the DeckListModel. + * + * These values determine how cards are grouped in UI views such as the deck editor. + */ +namespace DeckListModelGroupCriteria +{ +/** + * @enum DeckListModelGroupCriteria + * @brief Available grouping strategies for deck visualization. + */ +enum Type +{ + MAIN_TYPE, /**< Group cards by their main type (e.g., creature, instant). */ + MANA_COST, /**< Group cards by their total mana cost. */ + COLOR /**< Group cards by their color identity. */ +}; +static inline QString toString(Type t) +{ + switch (t) { + case MAIN_TYPE: + return "Main Type"; + case MANA_COST: + return "Mana Cost"; + case COLOR: + return "Colors"; + } + return {}; +} + +static inline Type fromString(const QString &s) +{ + if (s == "Main Type") + return MAIN_TYPE; + if (s == "Mana Cost") + return MANA_COST; + if (s == "Colors") + return COLOR; + return MAIN_TYPE; // default +} +} // namespace DeckListModelGroupCriteria + +/** + * @class DecklistModelCardNode + * @ingroup DeckModels + * @brief Adapter node that wraps a DecklistCardNode for use in the DeckListModel tree. + * + * This class forwards all property accessors (name, number, provider ID, set info, etc.) + * to the underlying DecklistCardNode. It exists so the model can represent cards + * in the same hierarchy as InnerDecklistNode containers. + */ +class DecklistModelCardNode : public AbstractDecklistCardNode +{ +private: + DecklistCardNode *dataNode; /**< Pointer to the underlying data node. */ + +public: + /** + * @brief Constructs a model node wrapping a DecklistCardNode. + * @param _dataNode The underlying DecklistCardNode to wrap. + * @param _parent The parent InnerDecklistNode in the model tree. + * @param position Optional position to insert in parent (-1 appends at end). + */ + DecklistModelCardNode(DecklistCardNode *_dataNode, InnerDecklistNode *_parent, int position = -1) + : AbstractDecklistCardNode(_parent, position), dataNode(_dataNode) + { + } + [[nodiscard]] int getNumber() const override + { + return dataNode->getNumber(); + } + void setNumber(int _number) override + { + dataNode->setNumber(_number); + } + [[nodiscard]] QString getName() const override + { + return dataNode->getName(); + } + void setName(const QString &_name) override + { + dataNode->setName(_name); + } + [[nodiscard]] QString getCardProviderId() const override + { + return dataNode->getCardProviderId(); + } + void setCardProviderId(const QString &_cardProviderId) override + { + dataNode->setCardProviderId(_cardProviderId); + } + [[nodiscard]] QString getCardSetShortName() const override + { + return dataNode->getCardSetShortName(); + } + void setCardSetShortName(const QString &_cardSetShortName) override + { + dataNode->setCardSetShortName(_cardSetShortName); + } + [[nodiscard]] QString getCardCollectorNumber() const override + { + return dataNode->getCardCollectorNumber(); + } + void setCardCollectorNumber(const QString &_cardSetNumber) override + { + dataNode->setCardCollectorNumber(_cardSetNumber); + } + bool getFormatLegality() const override + { + return dataNode->getFormatLegality(); + } + void setFormatLegality(const bool _formatLegal) override + { + dataNode->setFormatLegality(_formatLegal); + } + + /** + * @brief Returns the underlying data node. + * @return Pointer to the DecklistCardNode wrapped by this node. + */ + [[nodiscard]] DecklistCardNode *getDataNode() const + { + return dataNode; + } + [[nodiscard]] bool isDeckHeader() const override + { + return false; + } +}; + +/** + * @class DeckListModel + * @ingroup DeckModels + * @brief Qt model representing a decklist for use in views (tree/table). + * + * DeckListModel is a QAbstractItemModel that exposes the structure of a deck + * (zones and cards) to Qt views. It organizes cards hierarchically under + * InnerDecklistNode containers and supports grouping, sorting, adding/removing + * cards, and printing decklists. + * + * Outside code should refrain from modifying the model with methods inherited from QAbstractItemModel, such as with + * `setData` or `removeRow`. + * Instead, use the custom methods on this class to modify the model, such as `addCard`, `offsetCountAtIndex`, or + * `removeCardAtIndex`. + * This ensures the custom signals for this class are correctly emitted. + * + * Signals: + * - deckHashChanged(): emitted when the deck contents change in a way that + * affects its hash. + * + * Slots: + * - rebuildTree(): rebuilds the model structure from the underlying node tree. + */ +class DeckListModel : public QAbstractItemModel +{ + Q_OBJECT + +public slots: + /** + * @brief Rebuilds the model tree from the underlying node tree. + * + * This updates all indices and ensures the model reflects the current + * state of the deck. + */ + void rebuildTree(); + + /** + * @brief Sets the criteria used to group cards in the model. + * @param newCriteria The new grouping criteria. + */ + void setActiveGroupCriteria(DeckListModelGroupCriteria::Type newCriteria); + + void setActiveFormat(const QString &_format); + +signals: + /** + * @brief Emitted whenever the deck hash changes due to modifications in the model. + */ + void deckHashChanged(); + + /** + * @brief Emitted whenever the cards in the deck changes. This includes when the deck is replaced. + */ + void cardsChanged(); + + /** + * @brief Emitted whenever a card is added to the deck, regardless of whether it's an entirely new card or an + * existing card that got incremented. + * @param index The index of the card that got added. + */ + void cardAddedAt(const QModelIndex &index); + + /** + * @brief Emitted whenever a card is removed from the deck, regardless of whether a card node was removed or an + * existing card got decremented. + */ + void cardRemoved(); + + /** + * @brief Emitted whenever a card node is added or removed. This includes when the deck is replaced. + */ + void cardNodesChanged(); + + /** + * @brief Emitted whenever a new card node is added. + * @param index The index of the card node that got added. + */ + void cardNodeAddedAt(const QModelIndex &index); + + /** + * @brief Emitted whenever a card node is removed. + */ + void cardNodeRemoved(); + + /** + * @brief Emitted whenever the deck in the model has been replaced with a new one + */ + void deckReplaced(); + +public: + explicit DeckListModel(QObject *parent = nullptr); + explicit DeckListModel(QObject *parent, const QSharedPointer &deckList); + ~DeckListModel() override; + + /** + * @brief Returns the root index of the model. + * @return QModelIndex representing the root node. + */ + [[nodiscard]] QModelIndex getRoot() const + { + return nodeToIndex(root); + } + + /// @name Qt model overrides + ///@{ + [[nodiscard]] int rowCount(const QModelIndex &parent) const override; + [[nodiscard]] int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override; + [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; + [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + [[nodiscard]] QModelIndex index(int row, int column, const QModelIndex &parent) const override; + [[nodiscard]] QModelIndex parent(const QModelIndex &index) const override; + [[nodiscard]] Qt::ItemFlags flags(const QModelIndex &index) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + bool removeRows(int row, int count, const QModelIndex &parent) override; + void sort(int column, Qt::SortOrder order) override; + ///@} + + /** + * @brief Finds a card by name, zone, and optional identifiers. + * @param cardName The card's name. + * @param zoneName The zone to search in (main/side/etc.). + * @param providerId Optional provider-specific ID. + * @param cardNumber Optional collector number. + * @return QModelIndex of the card, or invalid index if not found. + */ + [[nodiscard]] QModelIndex findCard(const QString &cardName, + const QString &zoneName, + const QString &providerId = "", + const QString &cardNumber = "") const; + + /** + * @brief Adds a card using the preferred printing if available. + * + * @param cardName Name of the card to add. + * @param zoneName Zone to insert the card into. + * @param abAddAnyway Whether to add the card even if resolution fails. + * @return QModelIndex pointing to the newly inserted card node. + */ + QModelIndex addPreferredPrintingCard(const QString &cardName, const QString &zoneName, bool abAddAnyway); + + /** + * @brief Adds an ExactCard to the specified zone. + * @param card The card to add. + * @param zoneName The zone to insert the card into. + * @return QModelIndex pointing to the newly inserted card node. + */ + QModelIndex addCard(const ExactCard &card, const QString &zoneName); + + /** + * @brief Changes the `amount` field in the card node at the index by the amount. + * Removes the node if it causes the amount to fall to 0 or below. + * @param idx The index of a card node. No-ops if the index is invalid or not a card node. + * @param offset The amount to change the amount field by. + * @return Whether the operation was successful + */ + bool offsetCountAtIndex(const QModelIndex &idx, int offset); + + /** + * @brief Removes the card node at the index + * @param idx The index of a card node. No-ops if the index is invalid or not a card node. + * @return Whether the node was removed. + */ + bool removeCardAtIndex(const QModelIndex &idx); + + /** + * @brief Removes all cards and resets the model. + */ + void cleanList(); + + [[nodiscard]] QSharedPointer getDeckList() const + { + return deckList; + } + void setDeckList(const QSharedPointer &_deck); + + /** + * @brief Apply a function to every card in the deck tree. + * + * @param func Function taking (zone node, card node). + */ + void forEachCard(const std::function &func); + + /** + * @brief Gets a list of all card nodes in the deck. + */ + [[nodiscard]] QList getCardNodes() const; + [[nodiscard]] QList getCardNodesForZone(const QString &zoneName) const; + + /** + * @brief Gets a deduplicated list of all card names that appear in the model + */ + [[nodiscard]] QList getCardNames() const; + /** + * @brief Gets a deduplicated list of all CardRefs that appear in the model + */ + [[nodiscard]] QList getCardRefs() const; + /** + * @brief Gets a list of all zone names that appear in the model + */ + [[nodiscard]] QList getZones() const; + +private: + QSharedPointer deckList; /**< Pointer to the decklist providing the underlying data. */ + InnerDecklistNode *root; /**< Root node of the model tree. */ + DeckListModelGroupCriteria::Type activeGroupCriteria = DeckListModelGroupCriteria::MAIN_TYPE; + int lastKnownColumn; /**< Last column used for sorting. */ + Qt::SortOrder lastKnownOrder; /**< Last known sort order. */ + + InnerDecklistNode *createNodeIfNeeded(const QString &name, InnerDecklistNode *parent); + QModelIndex nodeToIndex(AbstractDecklistNode *node) const; + [[nodiscard]] DecklistModelCardNode *findCardNode(const QString &cardName, + const QString &zoneName, + const QString &providerId = "", + const QString &cardNumber = "") const; + + /** + * @brief Determines the sorted insertion row for a card. + * @param parent The parent node where the card will be inserted. + * @param cardInfo The card info to insert. + * @return Row index where the card should be inserted to maintain sort order. + */ + int findSortedInsertRow(const InnerDecklistNode *parent, const CardInfoPtr &cardInfo) const; + + /** + * @brief Recursively emits the dataChanged signal with role as Qt::BackgroundRole for all indices that are children + * of the given node. This is used to update the background color when changing formats. + * @param parent The parent node + */ + void emitBackgroundUpdates(const QModelIndex &parent); + + /** + * @brief Recursively emits the dataChanged signal for the given node and all parent nodes. + * @param index The parent node + */ + void emitRecursiveUpdates(const QModelIndex &index); + + void sortHelper(InnerDecklistNode *node, Qt::SortOrder order); + + template T getNode(const QModelIndex &index) const + { + if (!index.isValid()) + return dynamic_cast(root); + return dynamic_cast(static_cast(index.internalPointer())); + } + + void refreshCardFormatLegalities(); +}; + +#endif diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.cpp new file mode 100644 index 000000000..0ec159737 --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.cpp @@ -0,0 +1,55 @@ +#include "deck_list_sort_filter_proxy_model.h" + +#include "deck_list_model.h" + +bool DeckListSortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + auto *src = sourceModel(); + + // Inner nodes? -> sort alphabetically by column 1 + bool leftIsCard = src->data(left, Qt::UserRole + 1).toBool(); + bool rightIsCard = src->data(right, Qt::UserRole + 1).toBool(); + + if (!leftIsCard || !rightIsCard) { + QString lName = src->data(left.siblingAtColumn(DeckListModelColumns::CARD_NAME), Qt::EditRole).toString(); + QString rName = src->data(right.siblingAtColumn(DeckListModelColumns::CARD_NAME), Qt::EditRole).toString(); + return lName.localeAwareCompare(rName) < 0; + } + + // Both are cards -> apply sort criteria + auto *lNode = static_cast(left.internalPointer()); + auto *rNode = static_cast(right.internalPointer()); + + CardInfoPtr lInfo = CardDatabaseManager::query()->guessCard({lNode->getName()}).getCardPtr(); + CardInfoPtr rInfo = CardDatabaseManager::query()->guessCard({rNode->getName()}).getCardPtr(); + + // Example: multiple tie-break criteria (colors > cmc > name) + for (const QString &crit : sortCriteria) { + if (crit == "name") { + QString ln = lNode->getName(); + QString rn = rNode->getName(); + int cmp = ln.localeAwareCompare(rn); + if (cmp != 0) + return cmp < 0; + } else if (crit == "cmc") { + int lc = lInfo ? lInfo->getCmc().toInt() : 0; + int rc = rInfo ? rInfo->getCmc().toInt() : 0; + if (lc != rc) + return lc < rc; + } else if (crit == "colors") { + QString lr = lInfo ? lInfo->getColors() : QString(); + QString rr = rInfo ? rInfo->getColors() : QString(); + int cmp = lr.localeAwareCompare(rr); + if (cmp != 0) + return cmp < 0; + } else if (crit == "maintype") { + QString lr = lInfo ? lInfo->getMainCardType() : QString(); + QString rr = rInfo ? rInfo->getMainCardType() : QString(); + int cmp = lr.localeAwareCompare(rr); + if (cmp != 0) + return cmp < 0; + } + } + + return false; +} diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.h new file mode 100644 index 000000000..94742795d --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.h @@ -0,0 +1,34 @@ +/** + * @file deck_list_sort_filter_proxy_model.h + * @ingroup DeckEditorCardGroupWidgets + * @brief TODO: Document this. + */ + +#ifndef COCKATRICE_DECK_LIST_SORT_FILTER_PROXY_MODEL_H +#define COCKATRICE_DECK_LIST_SORT_FILTER_PROXY_MODEL_H + +#include +#include + +class DeckListSortFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + explicit DeckListSortFilterProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) + { + } + + void setSortCriteria(const QStringList &criteria) + { + sortCriteria = criteria; + invalidate(); // re-sort + } + +protected: + [[nodiscard]] bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + +private: + QStringList sortCriteria; +}; + +#endif // COCKATRICE_DECK_LIST_SORT_FILTER_PROXY_MODEL_H diff --git a/libcockatrice_network/CMakeLists.txt b/libcockatrice_network/CMakeLists.txt new file mode 100644 index 000000000..3069c0db4 --- /dev/null +++ b/libcockatrice_network/CMakeLists.txt @@ -0,0 +1,14 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +add_subdirectory(libcockatrice/network/client) +add_subdirectory(libcockatrice/network/server) + +add_library(libcockatrice_network INTERFACE) + +target_include_directories(libcockatrice_network INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries( + libcockatrice_network INTERFACE ${COCKATRICE_QT_MODULES} libcockatrice_network_client libcockatrice_network_server +) diff --git a/libcockatrice_network/libcockatrice/network/client/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/CMakeLists.txt new file mode 100644 index 000000000..bcf62463c --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/client/CMakeLists.txt @@ -0,0 +1,18 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +add_subdirectory(abstract) +add_subdirectory(local) +add_subdirectory(remote) + +add_library(libcockatrice_network_client INTERFACE) + +target_include_directories(libcockatrice_network_client INTERFACE .) + +target_link_libraries( + libcockatrice_network_client + INTERFACE ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets + libcockatrice_network_client_abstract libcockatrice_network_client_local + libcockatrice_network_client_remote +) diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt new file mode 100644 index 000000000..2753246de --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt @@ -0,0 +1,25 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS abstract_client.h) + +set(SOURCES abstract_client.cpp) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library(libcockatrice_network_client_abstract STATIC ${MOC_SOURCES} ${SOURCES}) + +add_dependencies(libcockatrice_network_client_abstract libcockatrice_protocol libcockatrice_network_server_remote) + +target_include_directories(libcockatrice_network_client_abstract PUBLIC .) + +target_link_libraries( + libcockatrice_network_client_abstract + PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets libcockatrice_protocol + libcockatrice_network_server_remote +) diff --git a/cockatrice/src/abstractclient.cpp b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.cpp similarity index 52% rename from cockatrice/src/abstractclient.cpp rename to libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.cpp index f2e8915bd..11458768d 100644 --- a/cockatrice/src/abstractclient.cpp +++ b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.cpp @@ -1,26 +1,27 @@ -#include "abstractclient.h" +#include "abstract_client.h" -#include "pending_command.h" -#include "pb/commands.pb.h" -#include "pb/server_message.pb.h" -#include "pb/event_server_identification.pb.h" -#include "pb/event_server_message.pb.h" -#include "pb/event_server_shutdown.pb.h" -#include "pb/event_connection_closed.pb.h" -#include "pb/event_user_message.pb.h" -#include "pb/event_list_rooms.pb.h" -#include "pb/event_add_to_list.pb.h" -#include "pb/event_remove_from_list.pb.h" -#include "pb/event_user_joined.pb.h" -#include "pb/event_user_left.pb.h" -#include "pb/event_game_joined.pb.h" -#include "pb/event_replay_added.pb.h" -#include "get_pb_extension.h" #include -#include "client_metatypes.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include AbstractClient::AbstractClient(QObject *parent) - : QObject(parent), nextCmdId(0), status(StatusDisconnected) + : QObject(parent), nextCmdId(0), status(StatusDisconnected), serverSupportsPasswordHash(false) { qRegisterMetaType("QVariant"); qRegisterMetaType("CommandContainer"); @@ -40,11 +41,17 @@ AbstractClient::AbstractClient(QObject *parent) qRegisterMetaType("Event_ListRooms"); qRegisterMetaType("Event_GameJoined"); qRegisterMetaType("Event_UserMessage"); + qRegisterMetaType("Event_NotifyUser"); qRegisterMetaType("ServerInfo_User"); - qRegisterMetaType >("QList"); + qRegisterMetaType>("QList"); qRegisterMetaType("Event_ReplayAdded"); - - connect(this, SIGNAL(sigQueuePendingCommand(PendingCommand *)), this, SLOT(queuePendingCommand(PendingCommand *))); + qRegisterMetaType>("missingFeatures"); + qRegisterMetaType("pendingCommand"); + + FeatureSet features; + features.initalizeFeatureList(clientFeatures); + + connect(this, &AbstractClient::sigQueuePendingCommand, this, &AbstractClient::queuePendingCommand); } AbstractClient::~AbstractClient() @@ -57,32 +64,60 @@ void AbstractClient::processProtocolItem(const ServerMessage &item) case ServerMessage::RESPONSE: { const Response &response = item.response(); const int cmdId = response.cmd_id(); - + PendingCommand *pend = pendingCommands.value(cmdId, 0); if (!pend) return; pendingCommands.remove(cmdId); - + pend->processResponse(response); pend->deleteLater(); break; } case ServerMessage::SESSION_EVENT: { const SessionEvent &event = item.session_event(); - switch ((SessionEvent::SessionEventType) getPbExtension(event)) { - case SessionEvent::SERVER_IDENTIFICATION: emit serverIdentificationEventReceived(event.GetExtension(Event_ServerIdentification::ext)); break; - case SessionEvent::SERVER_MESSAGE: emit serverMessageEventReceived(event.GetExtension(Event_ServerMessage::ext)); break; - case SessionEvent::SERVER_SHUTDOWN: emit serverShutdownEventReceived(event.GetExtension(Event_ServerShutdown::ext)); break; - case SessionEvent::CONNECTION_CLOSED: emit connectionClosedEventReceived(event.GetExtension(Event_ConnectionClosed::ext)); break; - case SessionEvent::USER_MESSAGE: emit userMessageEventReceived(event.GetExtension(Event_UserMessage::ext)); break; - case SessionEvent::LIST_ROOMS: emit listRoomsEventReceived(event.GetExtension(Event_ListRooms::ext)); break; - case SessionEvent::ADD_TO_LIST: emit addToListEventReceived(event.GetExtension(Event_AddToList::ext)); break; - case SessionEvent::REMOVE_FROM_LIST: emit removeFromListEventReceived(event.GetExtension(Event_RemoveFromList::ext)); break; - case SessionEvent::USER_JOINED: emit userJoinedEventReceived(event.GetExtension(Event_UserJoined::ext)); break; - case SessionEvent::USER_LEFT: emit userLeftEventReceived(event.GetExtension(Event_UserLeft::ext)); break; - case SessionEvent::GAME_JOINED: emit gameJoinedEventReceived(event.GetExtension(Event_GameJoined::ext)); break; - case SessionEvent::REPLAY_ADDED: emit replayAddedEventReceived(event.GetExtension(Event_ReplayAdded::ext)); break; - default: break; + switch ((SessionEvent::SessionEventType)getPbExtension(event)) { + case SessionEvent::SERVER_IDENTIFICATION: + emit serverIdentificationEventReceived(event.GetExtension(Event_ServerIdentification::ext)); + break; + case SessionEvent::SERVER_MESSAGE: + emit serverMessageEventReceived(event.GetExtension(Event_ServerMessage::ext)); + break; + case SessionEvent::SERVER_SHUTDOWN: + emit serverShutdownEventReceived(event.GetExtension(Event_ServerShutdown::ext)); + break; + case SessionEvent::CONNECTION_CLOSED: + emit connectionClosedEventReceived(event.GetExtension(Event_ConnectionClosed::ext)); + break; + case SessionEvent::USER_MESSAGE: + emit userMessageEventReceived(event.GetExtension(Event_UserMessage::ext)); + break; + case SessionEvent::NOTIFY_USER: + emit notifyUserEventReceived(event.GetExtension(Event_NotifyUser::ext)); + break; + case SessionEvent::LIST_ROOMS: + emit listRoomsEventReceived(event.GetExtension(Event_ListRooms::ext)); + break; + case SessionEvent::ADD_TO_LIST: + emit addToListEventReceived(event.GetExtension(Event_AddToList::ext)); + break; + case SessionEvent::REMOVE_FROM_LIST: + emit removeFromListEventReceived(event.GetExtension(Event_RemoveFromList::ext)); + break; + case SessionEvent::USER_JOINED: + emit userJoinedEventReceived(event.GetExtension(Event_UserJoined::ext)); + break; + case SessionEvent::USER_LEFT: + emit userLeftEventReceived(event.GetExtension(Event_UserLeft::ext)); + break; + case SessionEvent::GAME_JOINED: + emit gameJoinedEventReceived(event.GetExtension(Event_GameJoined::ext)); + break; + case SessionEvent::REPLAY_ADDED: + emit replayAddedEventReceived(event.GetExtension(Event_ReplayAdded::ext)); + break; + default: + break; } break; } @@ -122,9 +157,9 @@ void AbstractClient::queuePendingCommand(PendingCommand *pend) // This function is always called from the client thread via signal/slot. const int cmdId = getNewCmdId(); pend->getCommandContainer().set_cmd_id(cmdId); - + pendingCommands.insert(cmdId, pend); - + sendCommandContainer(pend->getCommandContainer()); } diff --git a/cockatrice/src/abstractclient.h b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h similarity index 68% rename from cockatrice/src/abstractclient.h rename to libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h index 130c85b52..dc3be5a94 100644 --- a/cockatrice/src/abstractclient.h +++ b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h @@ -1,11 +1,16 @@ +/** + * @file abstract_client.h + * @ingroup Client + * @brief TODO: Document this. + */ + #ifndef ABSTRACTCLIENT_H #define ABSTRACTCLIENT_H -#include -#include #include -#include "pb/response.pb.h" -#include "pb/serverinfo_user.pb.h" +#include +#include +#include class PendingCommand; class CommandContainer; @@ -21,24 +26,34 @@ class Event_ServerMessage; class Event_ListRooms; class Event_GameJoined; class Event_UserMessage; +class Event_NotifyUser; class Event_ConnectionClosed; class Event_ServerShutdown; class Event_ReplayAdded; +class FeatureSet; -enum ClientStatus { +enum ClientStatus +{ StatusDisconnected, StatusDisconnecting, StatusConnecting, - StatusAwaitingWelcome, + StatusRegistering, + StatusActivating, StatusLoggingIn, StatusLoggedIn, + StatusRequestingForgotPassword, + StatusSubmitForgotPasswordReset, + StatusSubmitForgotPasswordChallenge, + StatusGettingPasswordSalt, }; -class AbstractClient : public QObject { +class AbstractClient : public QObject +{ Q_OBJECT signals: void statusChanged(ClientStatus _status); - + void maxPingTime(int seconds, int maxSeconds); + // Room events void roomEventReceived(const RoomEvent &event); // Game events @@ -55,12 +70,17 @@ signals: void listRoomsEventReceived(const Event_ListRooms &event); void gameJoinedEventReceived(const Event_GameJoined &event); void userMessageEventReceived(const Event_UserMessage &event); + void notifyUserEventReceived(const Event_NotifyUser &event); void userInfoChanged(const ServerInfo_User &userInfo); void buddyListReceived(const QList &buddyList); void ignoreListReceived(const QList &ignoreList); void replayAddedEventReceived(const Event_ReplayAdded &event); - + void registerAccepted(); + void registerAcceptedNeedsActivate(); + void activateAccepted(); + void sigQueuePendingCommand(PendingCommand *pend); + private: int nextCmdId; mutable QMutex clientMutex; @@ -69,24 +89,45 @@ private slots: void queuePendingCommand(PendingCommand *pend); protected slots: void processProtocolItem(const ServerMessage &item); + protected: QMap pendingCommands; - QString userName, password; + QString userName, password, email, country, realName, token; + bool serverSupportsPasswordHash; void setStatus(ClientStatus _status); - int getNewCmdId() { return nextCmdId++; } + int getNewCmdId() + { + return nextCmdId++; + } virtual void sendCommandContainer(const CommandContainer &cont) = 0; + public: - AbstractClient(QObject *parent = 0); - ~AbstractClient(); - - ClientStatus getStatus() const { QMutexLocker locker(&clientMutex); return status; } + explicit AbstractClient(QObject *parent = nullptr); + ~AbstractClient() override; + + ClientStatus getStatus() const + { + QMutexLocker locker(&clientMutex); + return status; + } void sendCommand(const CommandContainer &cont); void sendCommand(PendingCommand *pend); - + + bool getServerSupportsPasswordHash() const + { + return serverSupportsPasswordHash; + } + const QString &getUserName() const + { + return userName; + } + static PendingCommand *prepareSessionCommand(const ::google::protobuf::Message &cmd); static PendingCommand *prepareRoomCommand(const ::google::protobuf::Message &cmd, int roomId); static PendingCommand *prepareModeratorCommand(const ::google::protobuf::Message &cmd); static PendingCommand *prepareAdminCommand(const ::google::protobuf::Message &cmd); + + QMap clientFeatures; }; #endif diff --git a/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt new file mode 100644 index 000000000..d12a324dc --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt @@ -0,0 +1,25 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS local_client.h) + +set(SOURCES local_client.cpp) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library(libcockatrice_network_client_local STATIC ${MOC_SOURCES} ${SOURCES}) + +add_dependencies(libcockatrice_network_client_local libcockatrice_network_client_abstract) + +target_include_directories(libcockatrice_network_client_local PUBLIC .) + +target_link_libraries( + libcockatrice_network_client_local + PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets + libcockatrice_network_client_abstract +) diff --git a/libcockatrice_network/libcockatrice/network/client/local/local_client.cpp b/libcockatrice_network/libcockatrice/network/client/local/local_client.cpp new file mode 100644 index 000000000..eefa3a2f3 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/client/local/local_client.cpp @@ -0,0 +1,44 @@ +#include "local_client.h" + +#include "../../server/local/local_server_interface.h" + +#include +#include + +LocalClient::LocalClient(LocalServerInterface *_lsi, + const QString &_playerName, + const QString &_clientId, + QObject *parent) + : AbstractClient(parent), lsi(_lsi) +{ + connect(lsi, &LocalServerInterface::itemToClient, this, &LocalClient::itemFromServer); + + userName = _playerName; + + Command_Login loginCmd; + loginCmd.set_user_name(_playerName.toStdString()); + loginCmd.set_clientid(_clientId.toStdString()); + sendCommand(prepareSessionCommand(loginCmd)); + + Command_JoinRoom joinCmd; + joinCmd.set_room_id(0); + sendCommand(prepareSessionCommand(joinCmd)); +} + +LocalClient::~LocalClient() +{ +} + +void LocalClient::sendCommandContainer(const CommandContainer &cont) +{ + qCDebug(LocalClientLog).noquote() << userName << "OUT" << getSafeDebugString(cont); + + lsi->itemFromClient(cont); +} + +void LocalClient::itemFromServer(const ServerMessage &item) +{ + qCDebug(LocalClientLog).noquote() << userName << "IN" << getSafeDebugString(item); + + processProtocolItem(item); +} diff --git a/libcockatrice_network/libcockatrice/network/client/local/local_client.h b/libcockatrice_network/libcockatrice/network/client/local/local_client.h new file mode 100644 index 000000000..e8c5330ac --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/client/local/local_client.h @@ -0,0 +1,36 @@ +/** + * @file local_client.h + * @ingroup Client + * @brief TODO: Document this. + */ + +#ifndef LOCALCLIENT_H +#define LOCALCLIENT_H + +#include "../abstract/abstract_client.h" + +#include + +inline Q_LOGGING_CATEGORY(LocalClientLog, "local_client"); + +class LocalServerInterface; + +class LocalClient : public AbstractClient +{ + Q_OBJECT +private: + LocalServerInterface *lsi; + +public: + LocalClient(LocalServerInterface *_lsi, + const QString &_playerName, + const QString &_clientId, + QObject *parent = nullptr); + ~LocalClient() override; + + void sendCommandContainer(const CommandContainer &cont) override; +private slots: + void itemFromServer(const ServerMessage &item); +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt new file mode 100644 index 000000000..0548700e4 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt @@ -0,0 +1,25 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS remote_client.h) + +set(SOURCES remote_client.cpp) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library(libcockatrice_network_client_remote STATIC ${MOC_SOURCES} ${SOURCES}) + +add_dependencies(libcockatrice_network_client_remote libcockatrice_network_client_abstract libcockatrice_protocol) + +target_include_directories(libcockatrice_network_client_remote PUBLIC .) + +target_link_libraries( + libcockatrice_network_client_remote + PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets + libcockatrice_network_client_abstract libcockatrice_interfaces libcockatrice_utility libcockatrice_protocol +) diff --git a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp new file mode 100644 index 000000000..3e3ec889d --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp @@ -0,0 +1,739 @@ +#include "remote_client.h" + +#include "version_string.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const unsigned int protocolVersion = 14; + +RemoteClient::RemoteClient(QObject *parent, INetworkSettingsProvider *_networkSettingsProvider) + : AbstractClient(parent), networkSettingsProvider(_networkSettingsProvider), timeRunning(0), lastDataReceived(0), + messageInProgress(false), handshakeStarted(false), usingWebSocket(false), messageLength(0), hashedPassword() +{ + + clearNewClientFeatures(); + maxTimeout = networkSettingsProvider->getTimeOut(); + int keepalive = networkSettingsProvider->getKeepAlive(); + timer = new QTimer(this); + timer->setInterval(keepalive * 1000); + connect(timer, &QTimer::timeout, this, &RemoteClient::ping); + + socket = new QTcpSocket(this); + socket->setSocketOption(QAbstractSocket::LowDelayOption, 1); + connect(socket, &QTcpSocket::connected, this, &RemoteClient::slotConnected); + connect(socket, &QTcpSocket::readyRead, this, &RemoteClient::readData); + + connect(socket, &QTcpSocket::errorOccurred, this, &RemoteClient::slotSocketError); + + websocket = new QWebSocket(QString(), QWebSocketProtocol::VersionLatest, this); + connect(websocket, &QWebSocket::binaryMessageReceived, this, &RemoteClient::websocketMessageReceived); + connect(websocket, &QWebSocket::connected, this, &RemoteClient::slotConnected); + +#if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)) + connect(websocket, &QWebSocket::errorOccurred, this, &RemoteClient::slotWebSocketError); +#else + connect(websocket, qOverload(&QWebSocket::error), this, + &RemoteClient::slotWebSocketError); +#endif + + connect(this, &RemoteClient::serverIdentificationEventReceived, this, + &RemoteClient::processServerIdentificationEvent); + connect(this, &RemoteClient::connectionClosedEventReceived, this, &RemoteClient::processConnectionClosedEvent); + connect(this, &RemoteClient::sigConnectToServer, this, &RemoteClient::doConnectToServer); + connect(this, &RemoteClient::sigDisconnectFromServer, this, &RemoteClient::doDisconnectFromServer); + connect(this, &RemoteClient::sigRegisterToServer, this, &RemoteClient::doRegisterToServer); + connect(this, &RemoteClient::sigActivateToServer, this, &RemoteClient::doActivateToServer); + connect(this, &RemoteClient::sigRequestForgotPasswordToServer, this, + &RemoteClient::doRequestForgotPasswordToServer); + connect(this, &RemoteClient::sigSubmitForgotPasswordResetToServer, this, + &RemoteClient::doSubmitForgotPasswordResetToServer); + connect(this, &RemoteClient::sigSubmitForgotPasswordChallengeToServer, this, + &RemoteClient::doSubmitForgotPasswordChallengeToServer); +} + +RemoteClient::~RemoteClient() +{ + doDisconnectFromServer(); + thread()->quit(); +} + +void RemoteClient::slotSocketError(QAbstractSocket::SocketError /*error*/) +{ + QString errorString = socket->errorString(); + doDisconnectFromServer(); + emit socketError(errorString); +} + +void RemoteClient::slotWebSocketError(QAbstractSocket::SocketError /*error*/) +{ + + QString errorString = websocket->errorString(); + if (getStatus() != ClientStatus::StatusDisconnected) { + doDisconnectFromServer(); + emit socketError(errorString); + } +} + +void RemoteClient::slotConnected() +{ + timeRunning = lastDataReceived = 0; + timer->start(); + + if (!usingWebSocket) { + // dirty hack to be compatible with v14 server + sendCommandContainer(CommandContainer()); + getNewCmdId(); + // end of hack + } +} + +void RemoteClient::processServerIdentificationEvent(const Event_ServerIdentification &event) +{ + if (event.protocol_version() != protocolVersion) { + emit protocolVersionMismatch(protocolVersion, event.protocol_version()); + setStatus(StatusDisconnecting); + return; + } + serverSupportsPasswordHash = event.server_options() & Event_ServerIdentification::SupportsPasswordHash; + + if (getStatus() == StatusRequestingForgotPassword) { + Command_ForgotPasswordRequest cmdForgotPasswordRequest; + cmdForgotPasswordRequest.set_user_name(userName.toStdString()); + cmdForgotPasswordRequest.set_clientid(getSrvClientID(lastHostname).toStdString()); + PendingCommand *pend = prepareSessionCommand(cmdForgotPasswordRequest); + connect(pend, &PendingCommand::finished, this, &RemoteClient::requestForgotPasswordResponse); + sendCommand(pend); + return; + } + + if (getStatus() == StatusSubmitForgotPasswordReset) { + Command_ForgotPasswordReset cmdForgotPasswordReset; + cmdForgotPasswordReset.set_user_name(userName.toStdString()); + cmdForgotPasswordReset.set_clientid(getSrvClientID(lastHostname).toStdString()); + cmdForgotPasswordReset.set_token(token.toStdString()); + if (!password.isEmpty() && serverSupportsPasswordHash) { + auto passwordSalt = PasswordHasher::generateRandomSalt(); + hashedPassword = PasswordHasher::computeHash(password, passwordSalt); + cmdForgotPasswordReset.set_hashed_new_password(hashedPassword.toStdString()); + } else if (!password.isEmpty()) { + qCWarning(RemoteClientLog) << "using plain text password to reset password"; + cmdForgotPasswordReset.set_new_password(password.toStdString()); + } + PendingCommand *pend = prepareSessionCommand(cmdForgotPasswordReset); + connect(pend, &PendingCommand::finished, this, &RemoteClient::submitForgotPasswordResetResponse); + sendCommand(pend); + return; + } + + if (getStatus() == StatusSubmitForgotPasswordChallenge) { + Command_ForgotPasswordChallenge cmdForgotPasswordChallenge; + cmdForgotPasswordChallenge.set_user_name(userName.toStdString()); + cmdForgotPasswordChallenge.set_clientid(getSrvClientID(lastHostname).toStdString()); + cmdForgotPasswordChallenge.set_email(email.toStdString()); + PendingCommand *pend = prepareSessionCommand(cmdForgotPasswordChallenge); + connect(pend, &PendingCommand::finished, this, &RemoteClient::submitForgotPasswordChallengeResponse); + sendCommand(pend); + return; + } + + if (getStatus() == StatusRegistering) { + Command_Register cmdRegister; + cmdRegister.set_user_name(userName.toStdString()); + if (!password.isEmpty() && serverSupportsPasswordHash) { + auto passwordSalt = PasswordHasher::generateRandomSalt(); + hashedPassword = PasswordHasher::computeHash(password, passwordSalt); + cmdRegister.set_hashed_password(hashedPassword.toStdString()); + } else if (!password.isEmpty()) { + qCWarning(RemoteClientLog) << "using plain text password to register"; + cmdRegister.set_password(password.toStdString()); + } + cmdRegister.set_email(email.toStdString()); + cmdRegister.set_country(country.toStdString()); + cmdRegister.set_real_name(realName.toStdString()); + cmdRegister.set_clientid(getSrvClientID(lastHostname).toStdString()); + PendingCommand *pend = prepareSessionCommand(cmdRegister); + connect(pend, &PendingCommand::finished, this, &RemoteClient::registerResponse); + sendCommand(pend); + + return; + } + + if (getStatus() == StatusActivating) { + Command_Activate cmdActivate; + cmdActivate.set_user_name(userName.toStdString()); + cmdActivate.set_token(token.toStdString()); + cmdActivate.set_clientid(getSrvClientID(lastHostname).toStdString()); + + PendingCommand *pend = prepareSessionCommand(cmdActivate); + connect(pend, &PendingCommand::finished, this, &RemoteClient::activateResponse); + sendCommand(pend); + + return; + } + + doLogin(); +} + +void RemoteClient::doRequestPasswordSalt() +{ + setStatus(StatusGettingPasswordSalt); + Command_RequestPasswordSalt cmdRqSalt; + cmdRqSalt.set_user_name(userName.toStdString()); + + PendingCommand *pend = prepareSessionCommand(cmdRqSalt); + connect(pend, &PendingCommand::finished, this, &RemoteClient::passwordSaltResponse); + sendCommand(pend); +} + +Command_Login RemoteClient::generateCommandLogin() +{ + Command_Login cmdLogin; + cmdLogin.set_user_name(userName.toStdString()); + cmdLogin.set_clientid(getSrvClientID(lastHostname).toStdString()); + cmdLogin.set_clientver(VERSION_STRING); + + if (!clientFeatures.isEmpty()) { + QMap::iterator i; + for (i = clientFeatures.begin(); i != clientFeatures.end(); ++i) + cmdLogin.add_clientfeatures(i.key().toStdString().c_str()); + } + + return cmdLogin; +} + +void RemoteClient::doLogin() +{ + if (!password.isEmpty() && serverSupportsPasswordHash) { + // TODO store and log in using stored hashed password + if (hashedPassword.isEmpty()) { + doRequestPasswordSalt(); // ask salt to create hashedPassword, then log in + } else { + doHashedLogin(); // log in using hashed password instead + } + } else { + // TODO add setting for client to reject unhashed logins + setStatus(StatusLoggingIn); + Command_Login cmdLogin = generateCommandLogin(); + if (!password.isEmpty()) { + qCWarning(RemoteClientLog) << "using plain text password to log in"; + cmdLogin.set_password(password.toStdString()); + } + + PendingCommand *pend = prepareSessionCommand(cmdLogin); + connect(pend, &PendingCommand::finished, this, &RemoteClient::loginResponse); + sendCommand(pend); + } +} + +void RemoteClient::doHashedLogin() +{ + setStatus(StatusLoggingIn); + Command_Login cmdLogin = generateCommandLogin(); + + cmdLogin.set_hashed_password(hashedPassword.toStdString()); + + PendingCommand *pend = prepareSessionCommand(cmdLogin); + connect(pend, &PendingCommand::finished, this, &RemoteClient::loginResponse); + sendCommand(pend); +} + +void RemoteClient::processConnectionClosedEvent(const Event_ConnectionClosed & /*event*/) +{ + doDisconnectFromServer(); +} + +void RemoteClient::passwordSaltResponse(const Response &response) +{ + if (response.response_code() == Response::RespOk) { + const Response_PasswordSalt &resp = response.GetExtension(Response_PasswordSalt::ext); + auto passwordSalt = QString::fromStdString(resp.password_salt()); + if (passwordSalt.isEmpty()) { // the server does not recognize the user but allows them to enter unregistered + password.clear(); // the password will not be used + doLogin(); + } else { + hashedPassword = PasswordHasher::computeHash(password, passwordSalt); + doHashedLogin(); + } + } else if (response.response_code() != Response::RespNotConnected) { + emit loginError(response.response_code(), {}, 0, {}); + } +} + +void RemoteClient::loginResponse(const Response &response) +{ + const Response_Login &resp = response.GetExtension(Response_Login::ext); + + QString possibleMissingFeatures; + if (resp.missing_features_size() > 0) { + for (int i = 0; i < resp.missing_features_size(); ++i) + possibleMissingFeatures.append("," + QString::fromStdString(resp.missing_features(i))); + } + + if (response.response_code() == Response::RespOk) { + setStatus(StatusLoggedIn); + emit userInfoChanged(resp.user_info()); + + QList buddyList; + for (int i = resp.buddy_list_size() - 1; i >= 0; --i) + buddyList.append(resp.buddy_list(i)); + emit buddyListReceived(buddyList); + + QList ignoreList; + for (int i = resp.ignore_list_size() - 1; i >= 0; --i) + ignoreList.append(resp.ignore_list(i)); + emit ignoreListReceived(ignoreList); + + if (newMissingFeatureFound(possibleMissingFeatures) && resp.missing_features_size() > 0 && + networkSettingsProvider->getNotifyAboutUpdates()) { + networkSettingsProvider->setKnownMissingFeatures(possibleMissingFeatures); + emit notifyUserAboutUpdate(); + } + + } else if (response.response_code() != Response::RespNotConnected) { + QList missingFeatures; + if (resp.missing_features_size() > 0) { + for (int i = 0; i < resp.missing_features_size(); ++i) + missingFeatures << QString::fromStdString(resp.missing_features(i)); + } + emit loginError(response.response_code(), QString::fromStdString(resp.denied_reason_str()), + static_cast(resp.denied_end_time()), missingFeatures); + setStatus(StatusDisconnecting); + } +} + +void RemoteClient::registerResponse(const Response &response) +{ + const Response_Register &resp = response.GetExtension(Response_Register::ext); + switch (response.response_code()) { + case Response::RespRegistrationAccepted: + emit registerAccepted(); + doLogin(); + break; + case Response::RespRegistrationAcceptedNeedsActivation: + emit registerAcceptedNeedsActivate(); + doLogin(); + break; + case Response::RespNotConnected: + // this response is created by the client from doDisconnectFromServer, do not call it again! + emit registerError(response.response_code(), QString::fromStdString(resp.denied_reason_str()), + static_cast(resp.denied_end_time())); + break; + default: + emit registerError(response.response_code(), QString::fromStdString(resp.denied_reason_str()), + static_cast(resp.denied_end_time())); + setStatus(StatusDisconnecting); + doDisconnectFromServer(); + break; + } +} + +void RemoteClient::activateResponse(const Response &response) +{ + if (response.response_code() == Response::RespActivationAccepted) { + emit activateAccepted(); + + doLogin(); + } else { + emit activateError(); + } +} + +void RemoteClient::readData() +{ + lastDataReceived = timeRunning; + QByteArray data = socket->readAll(); + + inputBuffer.append(data); + + do { + if (!messageInProgress) { + if (inputBuffer.size() >= 4) { + // dirty hack to be compatible with v14 server that sends 60 bytes of garbage at the beginning + if (!handshakeStarted) { + handshakeStarted = true; + if (inputBuffer.startsWith(" 3001000 + auto size = static_cast(cont.ByteSizeLong()); +#else + auto size = static_cast(cont.ByteSize()); +#endif + + qCDebug(RemoteClientLog).noquote() << "OUT" << getSafeDebugString(cont); + + QByteArray buf; + bool ok; + if (usingWebSocket) { + buf.resize(size); + ok = cont.SerializeToArray(buf.data(), size); + if (ok) { + websocket->sendBinaryMessage(buf); + } + } else { + buf.resize(size + 4); + ok = cont.SerializeToArray(buf.data() + 4, size); + if (ok) { + buf.data()[3] = (unsigned char)size; + buf.data()[2] = (unsigned char)(size >> 8); + buf.data()[1] = (unsigned char)(size >> 16); + buf.data()[0] = (unsigned char)(size >> 24); + + socket->write(buf); + } + } + if (!ok) { + qCDebug(RemoteClientLog) << "transmit error!"; + } +} + +void RemoteClient::connectToHost(const QString &hostname, unsigned int port) +{ + usingWebSocket = port == 443 || port == 80 || port == 4748 || port == 8080; + if (usingWebSocket) { + QUrl url(QString("%1://%2:%3/servatrice").arg(port == 443 ? "wss" : "ws").arg(hostname).arg(port)); + websocket->open(url); + } else { + socket->connectToHost(hostname, static_cast(port)); + } +} + +void RemoteClient::doConnectToServer(const QString &hostname, + unsigned int port, + const QString &_userName, + const QString &_password) +{ + doDisconnectFromServer(); + + userName = _userName; + password = _password; + lastHostname = hostname; + lastPort = port; + hashedPassword.clear(); + + connectToHost(hostname, port); + setStatus(StatusConnecting); +} + +void RemoteClient::doRegisterToServer(const QString &hostname, + unsigned int port, + const QString &_userName, + const QString &_password, + const QString &_email, + const QString &_country, + const QString &_realname) +{ + doDisconnectFromServer(); + + userName = _userName; + password = _password; + email = _email; + country = _country; + realName = _realname; + lastHostname = hostname; + lastPort = port; + hashedPassword.clear(); + + connectToHost(hostname, port); + setStatus(StatusRegistering); +} + +void RemoteClient::doActivateToServer(const QString &_token) +{ + doDisconnectFromServer(); + + token = _token.trimmed(); + + connectToHost(lastHostname, lastPort); + setStatus(StatusActivating); +} + +void RemoteClient::doDisconnectFromServer() +{ + timer->stop(); + + messageInProgress = false; + handshakeStarted = false; + messageLength = 0; + + QList pc = pendingCommands.values(); + for (const auto &i : pc) { + Response response; + response.set_response_code(Response::RespNotConnected); + response.set_cmd_id(i->getCommandContainer().cmd_id()); + i->processResponse(response); + + delete i; + } + pendingCommands.clear(); + + setStatus(StatusDisconnected); + if (websocket->isValid()) + websocket->close(); + socket->close(); +} + +void RemoteClient::ping() +{ + QMutableMapIterator i(pendingCommands); + while (i.hasNext()) { + PendingCommand *pend = i.next().value(); + if (pend->tick() > maxTimeout) { + i.remove(); + pend->deleteLater(); + } + } + + int maxTime = timeRunning - lastDataReceived; + emit maxPingTime(maxTime, maxTimeout); + if (maxTime >= maxTimeout) { + disconnectFromServer(); + emit serverTimeout(); + } else { + sendCommand(prepareSessionCommand(Command_Ping())); + ++timeRunning; + } +} + +void RemoteClient::connectToServer(const QString &hostname, + unsigned int port, + const QString &_userName, + const QString &_password) +{ + emit sigConnectToServer(hostname, port, _userName, _password); +} + +void RemoteClient::registerToServer(const QString &hostname, + unsigned int port, + const QString &_userName, + const QString &_password, + const QString &_email, + const QString &_country, + const QString &_realname) +{ + emit sigRegisterToServer(hostname, port, _userName, _password, _email, _country, _realname); +} + +void RemoteClient::activateToServer(const QString &_token) +{ + emit sigActivateToServer(_token.trimmed()); +} + +void RemoteClient::disconnectFromServer() +{ + emit sigDisconnectFromServer(); +} + +QString RemoteClient::getSrvClientID(const QString &_hostname) +{ + QString srvClientID = networkSettingsProvider->getClientID(); + QHostInfo hostInfo = QHostInfo::fromName(_hostname); + if (!hostInfo.error()) { + QHostAddress hostAddress = hostInfo.addresses().first(); + srvClientID += hostAddress.toString(); + } else { + qCWarning(RemoteClientLog) << "ClientID generation host lookup failure [" << hostInfo.errorString() << "]"; + srvClientID += _hostname; + } + QString uniqueServerClientID = + QCryptographicHash::hash(srvClientID.toUtf8(), QCryptographicHash::Sha1).toHex().right(15); + return uniqueServerClientID; +} + +bool RemoteClient::newMissingFeatureFound(const QString &_serversMissingFeatures) +{ + bool newMissingFeature = false; + QStringList serversMissingFeaturesList = _serversMissingFeatures.split(","); + for (const QString &feature : serversMissingFeaturesList) { + if (!feature.isEmpty()) { + if (!networkSettingsProvider->getKnownMissingFeatures().contains(feature)) + return true; + } + } + return newMissingFeature; +} + +void RemoteClient::clearNewClientFeatures() +{ + QString newKnownMissingFeatures; + QStringList existingKnownMissingFeatures = networkSettingsProvider->getKnownMissingFeatures().split(","); + for (const QString &existingKnownFeature : existingKnownMissingFeatures) { + if (!existingKnownFeature.isEmpty()) { + if (!clientFeatures.contains(existingKnownFeature)) + newKnownMissingFeatures.append("," + existingKnownFeature); + } + } + networkSettingsProvider->setKnownMissingFeatures(newKnownMissingFeatures); +} + +void RemoteClient::requestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName) +{ + emit sigRequestForgotPasswordToServer(hostname, port, _userName); +} + +void RemoteClient::submitForgotPasswordResetToServer(const QString &hostname, + unsigned int port, + const QString &_userName, + const QString &_token, + const QString &_newpassword) +{ + emit sigSubmitForgotPasswordResetToServer(hostname, port, _userName, _token.trimmed(), _newpassword); +} + +void RemoteClient::doRequestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName) +{ + doDisconnectFromServer(); + + userName = _userName; + lastHostname = hostname; + lastPort = port; + + connectToHost(lastHostname, lastPort); + setStatus(StatusRequestingForgotPassword); +} + +void RemoteClient::requestForgotPasswordResponse(const Response &response) +{ + const Response_ForgotPasswordRequest &resp = response.GetExtension(Response_ForgotPasswordRequest::ext); + if (response.response_code() == Response::RespOk) { + if (resp.challenge_email()) { + emit sigPromptForForgotPasswordChallenge(); + } else + emit sigPromptForForgotPasswordReset(); + } else + emit sigForgotPasswordError(); + + doDisconnectFromServer(); +} + +void RemoteClient::doSubmitForgotPasswordResetToServer(const QString &hostname, + unsigned int port, + const QString &_userName, + const QString &_token, + const QString &_newpassword) +{ + doDisconnectFromServer(); + + userName = _userName; + lastHostname = hostname; + lastPort = port; + token = _token.trimmed(); + password = _newpassword; + hashedPassword.clear(); + + connectToHost(lastHostname, lastPort); + setStatus(StatusSubmitForgotPasswordReset); +} + +void RemoteClient::submitForgotPasswordResetResponse(const Response &response) +{ + if (response.response_code() == Response::RespOk) { + emit sigForgotPasswordSuccess(); + } else + emit sigForgotPasswordError(); + + doDisconnectFromServer(); +} + +void RemoteClient::submitForgotPasswordChallengeToServer(const QString &hostname, + unsigned int port, + const QString &_userName, + const QString &_email) +{ + emit sigSubmitForgotPasswordChallengeToServer(hostname, port, _userName, _email); +} + +void RemoteClient::doSubmitForgotPasswordChallengeToServer(const QString &hostname, + unsigned int port, + const QString &_userName, + const QString &_email) +{ + doDisconnectFromServer(); + + userName = _userName; + lastHostname = hostname; + lastPort = port; + email = _email; + + connectToHost(lastHostname, lastPort); + setStatus(StatusSubmitForgotPasswordChallenge); +} + +void RemoteClient::submitForgotPasswordChallengeResponse(const Response &response) +{ + if (response.response_code() == Response::RespOk) { + emit sigPromptForForgotPasswordReset(); + } else + emit sigForgotPasswordError(); + + doDisconnectFromServer(); +} diff --git a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h new file mode 100644 index 000000000..15e3e8ef5 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h @@ -0,0 +1,157 @@ +/** + * @file remote_client.h + * @ingroup Client + * @brief TODO: Document this. + */ + +#ifndef REMOTECLIENT_H +#define REMOTECLIENT_H + +#include "../abstract/abstract_client.h" + +#include +#include +#include +#include + +inline Q_LOGGING_CATEGORY(RemoteClientLog, "remote_client"); + +class QTimer; + +class RemoteClient : public AbstractClient +{ + Q_OBJECT +signals: + void serverTimeout(); + void loginError(Response::ResponseCode resp, QString reasonStr, quint32 endTime, QList missingFeatures); + void registerError(Response::ResponseCode resp, QString reasonStr, quint32 endTime); + void activateError(); + void socketError(const QString &errorString); + void protocolVersionMismatch(int clientVersion, int serverVersion); + void + sigConnectToServer(const QString &hostname, unsigned int port, const QString &_userName, const QString &_password); + void sigRegisterToServer(const QString &hostname, + unsigned int port, + const QString &_userName, + const QString &_password, + const QString &_email, + const QString &_country, + const QString &_realname); + void sigActivateToServer(const QString &_token); + void sigDisconnectFromServer(); + void notifyUserAboutUpdate(); + void sigRequestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName); + void sigForgotPasswordSuccess(); + void sigForgotPasswordError(); + void sigPromptForForgotPasswordReset(); + void sigSubmitForgotPasswordResetToServer(const QString &hostname, + unsigned int port, + const QString &_userName, + const QString &_token, + const QString &_newpassword); + void sigPromptForForgotPasswordChallenge(); + void sigSubmitForgotPasswordChallengeToServer(const QString &hostname, + unsigned int port, + const QString &_userName, + const QString &_email); +private slots: + void slotConnected(); + void readData(); + void websocketMessageReceived(const QByteArray &message); + void slotSocketError(QAbstractSocket::SocketError error); + void slotWebSocketError(QAbstractSocket::SocketError error); + void ping(); + void processServerIdentificationEvent(const Event_ServerIdentification &event); + void processConnectionClosedEvent(const Event_ConnectionClosed &event); + void passwordSaltResponse(const Response &response); + void loginResponse(const Response &response); + void registerResponse(const Response &response); + void activateResponse(const Response &response); + void + doConnectToServer(const QString &hostname, unsigned int port, const QString &_userName, const QString &_password); + void doRegisterToServer(const QString &hostname, + unsigned int port, + const QString &_userName, + const QString &_password, + const QString &_email, + const QString &_country, + const QString &_realname); + void doRequestPasswordSalt(); + void doLogin(); + void doHashedLogin(); + Command_Login generateCommandLogin(); + void doDisconnectFromServer(); + void doActivateToServer(const QString &_token); + void doRequestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName); + void requestForgotPasswordResponse(const Response &response); + void doSubmitForgotPasswordResetToServer(const QString &hostname, + unsigned int port, + const QString &_userName, + const QString &_token, + const QString &_newpassword); + void submitForgotPasswordResetResponse(const Response &response); + void doSubmitForgotPasswordChallengeToServer(const QString &hostname, + unsigned int port, + const QString &_userName, + const QString &_email); + void submitForgotPasswordChallengeResponse(const Response &response); + +private: + INetworkSettingsProvider *networkSettingsProvider; + int maxTimeout; + int timeRunning, lastDataReceived; + QByteArray inputBuffer; + bool messageInProgress; + bool handshakeStarted; + bool usingWebSocket; + int messageLength; + QTimer *timer; + QTcpSocket *socket; + QWebSocket *websocket; + QString lastHostname; + unsigned int lastPort; + QString hashedPassword; + + QString getSrvClientID(const QString &_hostname); + bool newMissingFeatureFound(const QString &_serversMissingFeatures); + void clearNewClientFeatures(); + void connectToHost(const QString &hostname, unsigned int port); + +protected slots: + void sendCommandContainer(const CommandContainer &cont) override; + +public: + explicit RemoteClient(QObject *parent = nullptr, INetworkSettingsProvider *networkSettingsProvider = nullptr); + ~RemoteClient() override; + QString peerName() const + { + if (usingWebSocket) { + return websocket->peerName(); + } else { + return socket->peerName(); + } + } + void + connectToServer(const QString &hostname, unsigned int port, const QString &_userName, const QString &_password); + void registerToServer(const QString &hostname, + unsigned int port, + const QString &_userName, + const QString &_password, + const QString &_email, + const QString &_country, + const QString &_realname); + void activateToServer(const QString &_token); + void disconnectFromServer(); + void requestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName); + void submitForgotPasswordResetToServer(const QString &hostname, + unsigned int port, + const QString &_userName, + const QString &_token, + const QString &_newpassword); + void submitForgotPasswordChallengeToServer(const QString &hostname, + unsigned int port, + const QString &_userName, + const QString &_email); +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/CMakeLists.txt new file mode 100644 index 000000000..cbb717ad8 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/CMakeLists.txt @@ -0,0 +1,15 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +add_subdirectory(local) +add_subdirectory(remote) + +add_library(libcockatrice_network_server INTERFACE) + +target_include_directories(libcockatrice_network_server INTERFACE .) + +target_link_libraries( + libcockatrice_network_server INTERFACE ${COCKATRICE_QT_MODULES} libcockatrice_network_server_local + libcockatrice_network_server_remote +) diff --git a/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt new file mode 100644 index 000000000..80fb379a4 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt @@ -0,0 +1,21 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS local_server.h local_server_interface.h) + +set(SOURCES local_server.cpp local_server_interface.cpp) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library(libcockatrice_network_server_local STATIC ${MOC_SOURCES} ${SOURCES}) + +add_dependencies(libcockatrice_network_server_local libcockatrice_protocol) + +target_include_directories(libcockatrice_network_server_local PUBLIC .) + +target_link_libraries(libcockatrice_network_server_local PUBLIC ${COCKATRICE_QT_MODULES} libcockatrice_protocol) diff --git a/libcockatrice_network/libcockatrice/network/server/local/local_server.cpp b/libcockatrice_network/libcockatrice/network/server/local/local_server.cpp new file mode 100644 index 000000000..8f9d82aa4 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/local/local_server.cpp @@ -0,0 +1,51 @@ +#include "local_server.h" + +#include "local_server_interface.h" + +#include <../remote/server_room.h> + +LocalServer::LocalServer(QObject *parent) : Server(parent) +{ + setDatabaseInterface(new LocalServer_DatabaseInterface(this)); + addRoom(new Server_Room(0, 0, QString(), QString(), QString(), QString(), false, QString(), QStringList(), this)); +} + +LocalServer::~LocalServer() +{ + // LocalServer is single threaded so it doesn't need locks on this + for (auto *client : clients) { + client->prepareDestroy(); + } + + prepareDestroy(); +} + +LocalServerInterface *LocalServer::newConnection() +{ + LocalServerInterface *lsi = new LocalServerInterface(this, getDatabaseInterface()); + addClient(lsi); + return lsi; +} + +LocalServer_DatabaseInterface::LocalServer_DatabaseInterface(LocalServer *_localServer) + : Server_DatabaseInterface(_localServer), localServer(_localServer) +{ +} + +ServerInfo_User LocalServer_DatabaseInterface::getUserData(const QString &name, bool /*withId*/) +{ + ServerInfo_User result; + result.set_name(name.toStdString()); + return result; +} + +AuthenticationResult LocalServer_DatabaseInterface::checkUserPassword(Server_ProtocolHandler * /* handler */, + const QString & /* user */, + const QString & /* password */, + const QString & /* clientId */, + QString & /* reasonStr */, + int & /* banSecondsLeft */, + bool /* passwordNeedsHash */) +{ + return UnknownUser; +} diff --git a/libcockatrice_network/libcockatrice/network/server/local/local_server.h b/libcockatrice_network/libcockatrice/network/server/local/local_server.h new file mode 100644 index 000000000..70586f6c1 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/local/local_server.h @@ -0,0 +1,58 @@ +/** + * @file local_server.h + * @ingroup Server + * @brief TODO: Document this. + */ + +#ifndef LOCALSERVER_H +#define LOCALSERVER_H + +#include <../remote/server.h> +#include <../remote/server_database_interface.h> + +class LocalServerInterface; + +class LocalServer : public Server +{ + Q_OBJECT +public: + explicit LocalServer(QObject *parent = nullptr); + ~LocalServer() override; + + LocalServerInterface *newConnection(); +}; + +class LocalServer_DatabaseInterface : public Server_DatabaseInterface +{ + Q_OBJECT +private: + LocalServer *localServer; + +protected: + ServerInfo_User getUserData(const QString &name, bool withId = false) override; + +public: + explicit LocalServer_DatabaseInterface(LocalServer *_localServer); + ~LocalServer_DatabaseInterface() override = default; + AuthenticationResult checkUserPassword(Server_ProtocolHandler *handler, + const QString &user, + const QString &password, + const QString &clientId, + QString &reasonStr, + int &secondsLeft, + bool passwordNeedsHash) override; + int getNextGameId() override + { + return localServer->getNextLocalGameId(); + } + int getNextReplayId() override + { + return -1; + } + int getActiveUserCount(QString /* connectionType */) override + { + return 0; + } +}; + +#endif diff --git a/cockatrice/src/localserverinterface.cpp b/libcockatrice_network/libcockatrice/network/server/local/local_server_interface.cpp similarity index 88% rename from cockatrice/src/localserverinterface.cpp rename to libcockatrice_network/libcockatrice/network/server/local/local_server_interface.cpp index 291da1467..bb86198af 100644 --- a/cockatrice/src/localserverinterface.cpp +++ b/libcockatrice_network/libcockatrice/network/server/local/local_server_interface.cpp @@ -1,5 +1,7 @@ -#include "localserverinterface.h" -#include "localserver.h" +#include "local_server_interface.h" + +#include "local_server.h" + #include LocalServerInterface::LocalServerInterface(LocalServer *_server, Server_DatabaseInterface *_databaseInterface) diff --git a/libcockatrice_network/libcockatrice/network/server/local/local_server_interface.h b/libcockatrice_network/libcockatrice/network/server/local/local_server_interface.h new file mode 100644 index 000000000..4410fd65c --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/local/local_server_interface.h @@ -0,0 +1,36 @@ +/** + * @file local_server_interface.h + * @ingroup Server + * @brief TODO: Document this. + */ + +#ifndef LOCALSERVERINTERFACE_H +#define LOCALSERVERINTERFACE_H + +#include <../remote/server_protocolhandler.h> + +class LocalServer; + +class LocalServerInterface : public Server_ProtocolHandler +{ + Q_OBJECT +public: + LocalServerInterface(LocalServer *_server, Server_DatabaseInterface *_databaseInterface); + ~LocalServerInterface() override; + + QString getAddress() const override + { + return QString(); + } + QString getConnectionType() const override + { + return "local"; + } + void transmitProtocolItem(const ServerMessage &item) override; +signals: + void itemToClient(const ServerMessage &item); +public slots: + void itemFromClient(const CommandContainer &item); +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt new file mode 100644 index 000000000..e883baa0d --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt @@ -0,0 +1,63 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS + game/server_abstract_participant.h + game/server_abstract_player.h + game/server_arrow.h + game/server_arrowtarget.h + game/server_card.h + game/server_cardzone.h + game/server_counter.h + game/server_game.h + game/server_player.h + game/server_spectator.h + server.h + server_abstractuserinterface.h + server_database_interface.h + server_protocolhandler.h + server_remoteuserinterface.h + server_response_containers.h + server_room.h + serverinfo_user_container.h +) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library( + libcockatrice_network_server_remote STATIC + ${MOC_SOURCES} + game/server_abstract_participant.cpp + game/server_abstract_player.cpp + game/server_arrow.cpp + game/server_arrowtarget.cpp + game/server_card.cpp + game/server_cardzone.cpp + game/server_counter.cpp + game/server_game.cpp + game/server_player.cpp + game/server_spectator.cpp + server.cpp + server_abstractuserinterface.cpp + server_database_interface.cpp + server_protocolhandler.cpp + server_remoteuserinterface.cpp + server_response_containers.cpp + server_room.cpp + serverinfo_user_container.cpp +) + +add_dependencies(libcockatrice_network_server_remote libcockatrice_protocol) + +target_include_directories(libcockatrice_network_server_remote PUBLIC .) + +# Make cockatrice_server depend on cockatrice_protocol +target_link_libraries( + libcockatrice_network_server_remote PUBLIC libcockatrice_protocol libcockatrice_utility libcockatrice_rng + libcockatrice_deck_list ${COCKATRICE_QT_MODULES} +) diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp new file mode 100644 index 000000000..493b8e966 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp @@ -0,0 +1,575 @@ +#include "server_abstract_participant.h" + +#include "../server.h" +#include "../server_abstractuserinterface.h" +#include "../server_database_interface.h" +#include "../server_room.h" +#include "server_card.h" +#include "server_game.h" +#include "server_player.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Server_AbstractParticipant::Server_AbstractParticipant(Server_Game *_game, + int _playerId, + const ServerInfo_User &_userInfo, + bool _judge, + Server_AbstractUserInterface *_userInterface) + : ServerInfo_User_Container(_userInfo), game(_game), userInterface(_userInterface), pingTime(0), + playerId(_playerId), judge(_judge) +{ +} + +Server_AbstractParticipant::~Server_AbstractParticipant() = default; + +void Server_AbstractParticipant::removeFromGame() +{ + QMutexLocker locker(&playerMutex); + if (userInterface) { + userInterface->playerRemovedFromGame(game); + } +} + +bool Server_AbstractParticipant::updatePingTime() // returns true if ping time changed +{ + QMutexLocker locker(&playerMutex); + + int oldPingTime = pingTime; + if (userInterface) { + pingTime = userInterface->getLastCommandTime(); + } else { + pingTime = -1; + } + + return pingTime != oldPingTime; +} + +void Server_AbstractParticipant::getProperties(ServerInfo_PlayerProperties &result, bool withUserInfo) +{ + result.set_player_id(playerId); + if (withUserInfo) { + copyUserInfo(*(result.mutable_user_info()), true); + } + result.set_spectator(spectator); + result.set_judge(judge); + result.set_ping_seconds(pingTime); + getPlayerProperties(result); +} + +void Server_AbstractParticipant::getPlayerProperties(ServerInfo_PlayerProperties & /*result*/) +{ +} + +Response::ResponseCode Server_AbstractParticipant::cmdLeaveGame(const Command_LeaveGame & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + game->removeParticipant(this, Event_Leave::USER_LEFT); + return Response::RespOk; +} + +Response::ResponseCode Server_AbstractParticipant::cmdKickFromGame(const Command_KickFromGame &cmd, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + if ((game->getHostId() != playerId) && !(userInfo->user_level() & ServerInfo_User::IsModerator)) { + return Response::RespFunctionNotAllowed; + } + + if (!game->kickParticipant(cmd.player_id())) { + return Response::RespNameNotFound; + } + + return Response::RespOk; +} + +Response::ResponseCode Server_AbstractParticipant::cmdDeckSelect(const Command_DeckSelect & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdSetSideboardPlan(const Command_SetSideboardPlan & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdSetSideboardLock(const Command_SetSideboardLock & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdConcede(const Command_Concede & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdUnconcede(const Command_Unconcede & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode +Server_AbstractParticipant::cmdJudge(const Command_Judge &cmd, ResponseContainer &rc, GameEventStorage &ges) +{ + if (!judge) { + return Response::RespFunctionNotAllowed; + } + + auto *player = this->game->getPlayer(cmd.target_id()); + + ges.setForcedByJudge(playerId); + if (player == nullptr) { + return Response::RespContextError; + } + + for (int i = 0; i < cmd.game_command_size(); ++i) { + player->processGameCommand(cmd.game_command(i), rc, ges); + } + + return Response::RespOk; +} + +Response::ResponseCode Server_AbstractParticipant::cmdReadyStart(const Command_ReadyStart & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode +Server_AbstractParticipant::cmdGameSay(const Command_GameSay &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (spectator) { + /* Spectators can only talk if: + * (a) the game creator allows it + * (b) the spectator is a moderator/administrator + * (c) the spectator is a judge + */ + bool isModOrJudge = (userInfo->user_level() & (ServerInfo_User::IsModerator | ServerInfo_User::IsJudge)); + if (!isModOrJudge && !game->getSpectatorsCanTalk()) { + return Response::RespFunctionNotAllowed; + } + } + + if (!userInterface->addSaidMessageSize(static_cast(cmd.message().size()))) { + return Response::RespChatFlood; + } + Event_GameSay event; + event.set_message(cmd.message()); + ges.enqueueGameEvent(event, playerId); + + game->getRoom()->getServer()->getDatabaseInterface()->logMessage( + userInfo->id(), QString::fromStdString(userInfo->name()), QString::fromStdString(userInfo->address()), + textFromStdString(cmd.message()), Server_DatabaseInterface::MessageTargetGame, game->getGameId(), + game->getDescription()); + + return Response::RespOk; +} + +Response::ResponseCode Server_AbstractParticipant::cmdShuffle(const Command_Shuffle & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdMulligan(const Command_Mulligan & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdRollDie(const Command_RollDie & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) const +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdDrawCards(const Command_DrawCards & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdUndoDraw(const Command_UndoDraw & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdMoveCard(const Command_MoveCard & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdFlipCard(const Command_FlipCard & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdAttachCard(const Command_AttachCard & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdCreateToken(const Command_CreateToken & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdCreateArrow(const Command_CreateArrow & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdDeleteArrow(const Command_DeleteArrow & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdSetCardAttr(const Command_SetCardAttr & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdSetCardCounter(const Command_SetCardCounter & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdIncCardCounter(const Command_IncCardCounter & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdIncCounter(const Command_IncCounter & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdCreateCounter(const Command_CreateCounter & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdSetCounter(const Command_SetCounter & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdDelCounter(const Command_DelCounter & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdNextTurn(const Command_NextTurn & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + + if (!judge) { + return Response::RespFunctionNotAllowed; + } + + game->nextTurn(); + return Response::RespOk; +} + +Response::ResponseCode Server_AbstractParticipant::cmdSetActivePhase(const Command_SetActivePhase &cmd, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + + if (!judge) { + return Response::RespFunctionNotAllowed; + } + + game->setActivePhase(cmd.phase()); + + return Response::RespOk; +} + +Response::ResponseCode Server_AbstractParticipant::cmdDumpZone(const Command_DumpZone & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdRevealCards(const Command_RevealCards & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdChangeZoneProperties(const Command_ChangeZoneProperties & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage &ges) +{ + if (!judge) { + if (spectator) { + return Response::RespFunctionNotAllowed; + } + + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + } + + bool reversedTurn = game->reverseTurnOrder(); + + Event_ReverseTurn event; + event.set_reversed(reversedTurn); + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractParticipant::processGameCommand(const GameCommand &command, ResponseContainer &rc, GameEventStorage &ges) +{ + switch ((GameCommand::GameCommandType)getPbExtension(command)) { + case GameCommand::KICK_FROM_GAME: + return cmdKickFromGame(command.GetExtension(Command_KickFromGame::ext), rc, ges); + break; + case GameCommand::LEAVE_GAME: + return cmdLeaveGame(command.GetExtension(Command_LeaveGame::ext), rc, ges); + break; + case GameCommand::GAME_SAY: + return cmdGameSay(command.GetExtension(Command_GameSay::ext), rc, ges); + break; + case GameCommand::SHUFFLE: + return cmdShuffle(command.GetExtension(Command_Shuffle::ext), rc, ges); + break; + case GameCommand::MULLIGAN: + return cmdMulligan(command.GetExtension(Command_Mulligan::ext), rc, ges); + break; + case GameCommand::ROLL_DIE: + return cmdRollDie(command.GetExtension(Command_RollDie::ext), rc, ges); + break; + case GameCommand::DRAW_CARDS: + return cmdDrawCards(command.GetExtension(Command_DrawCards::ext), rc, ges); + break; + case GameCommand::UNDO_DRAW: + return cmdUndoDraw(command.GetExtension(Command_UndoDraw::ext), rc, ges); + break; + case GameCommand::FLIP_CARD: + return cmdFlipCard(command.GetExtension(Command_FlipCard::ext), rc, ges); + break; + case GameCommand::ATTACH_CARD: + return cmdAttachCard(command.GetExtension(Command_AttachCard::ext), rc, ges); + break; + case GameCommand::CREATE_TOKEN: + return cmdCreateToken(command.GetExtension(Command_CreateToken::ext), rc, ges); + break; + case GameCommand::CREATE_ARROW: + return cmdCreateArrow(command.GetExtension(Command_CreateArrow::ext), rc, ges); + break; + case GameCommand::DELETE_ARROW: + return cmdDeleteArrow(command.GetExtension(Command_DeleteArrow::ext), rc, ges); + break; + case GameCommand::SET_CARD_ATTR: + return cmdSetCardAttr(command.GetExtension(Command_SetCardAttr::ext), rc, ges); + break; + case GameCommand::SET_CARD_COUNTER: + return cmdSetCardCounter(command.GetExtension(Command_SetCardCounter::ext), rc, ges); + break; + case GameCommand::INC_CARD_COUNTER: + return cmdIncCardCounter(command.GetExtension(Command_IncCardCounter::ext), rc, ges); + break; + case GameCommand::READY_START: + return cmdReadyStart(command.GetExtension(Command_ReadyStart::ext), rc, ges); + break; + case GameCommand::CONCEDE: + return cmdConcede(command.GetExtension(Command_Concede::ext), rc, ges); + break; + case GameCommand::INC_COUNTER: + return cmdIncCounter(command.GetExtension(Command_IncCounter::ext), rc, ges); + break; + case GameCommand::CREATE_COUNTER: + return cmdCreateCounter(command.GetExtension(Command_CreateCounter::ext), rc, ges); + break; + case GameCommand::SET_COUNTER: + return cmdSetCounter(command.GetExtension(Command_SetCounter::ext), rc, ges); + break; + case GameCommand::DEL_COUNTER: + return cmdDelCounter(command.GetExtension(Command_DelCounter::ext), rc, ges); + break; + case GameCommand::NEXT_TURN: + return cmdNextTurn(command.GetExtension(Command_NextTurn::ext), rc, ges); + break; + case GameCommand::SET_ACTIVE_PHASE: + return cmdSetActivePhase(command.GetExtension(Command_SetActivePhase::ext), rc, ges); + break; + case GameCommand::DUMP_ZONE: + return cmdDumpZone(command.GetExtension(Command_DumpZone::ext), rc, ges); + break; + case GameCommand::REVEAL_CARDS: + return cmdRevealCards(command.GetExtension(Command_RevealCards::ext), rc, ges); + break; + case GameCommand::MOVE_CARD: + return cmdMoveCard(command.GetExtension(Command_MoveCard::ext), rc, ges); + break; + case GameCommand::SET_SIDEBOARD_PLAN: + return cmdSetSideboardPlan(command.GetExtension(Command_SetSideboardPlan::ext), rc, ges); + break; + case GameCommand::DECK_SELECT: + return cmdDeckSelect(command.GetExtension(Command_DeckSelect::ext), rc, ges); + break; + case GameCommand::SET_SIDEBOARD_LOCK: + return cmdSetSideboardLock(command.GetExtension(Command_SetSideboardLock::ext), rc, ges); + break; + case GameCommand::CHANGE_ZONE_PROPERTIES: + return cmdChangeZoneProperties(command.GetExtension(Command_ChangeZoneProperties::ext), rc, ges); + break; + case GameCommand::UNCONCEDE: + return cmdUnconcede(command.GetExtension(Command_Unconcede::ext), rc, ges); + break; + case GameCommand::JUDGE: + return cmdJudge(command.GetExtension(Command_Judge::ext), rc, ges); + break; + case GameCommand::REVERSE_TURN: + return cmdReverseTurn(command.GetExtension(Command_ReverseTurn::ext), rc, ges); + break; + default: + return Response::RespInvalidCommand; + } +} + +void Server_AbstractParticipant::sendGameEvent(const GameEventContainer &cont) +{ + QMutexLocker locker(&playerMutex); + + if (userInterface) { + userInterface->sendProtocolItem(cont); + } +} + +void Server_AbstractParticipant::setUserInterface(Server_AbstractUserInterface *_userInterface) +{ + playerMutex.lock(); + userInterface = _userInterface; + playerMutex.unlock(); + + pingTime = _userInterface ? 0 : -1; + + Event_PlayerPropertiesChanged event; + event.mutable_player_properties()->set_ping_seconds(pingTime); + + GameEventStorage ges; + ges.setGameEventContext(Context_ConnectionStateChanged()); + ges.enqueueGameEvent(event, playerId); + ges.sendToGame(game); +} + +void Server_AbstractParticipant::disconnectClient() +{ + bool isRegistered = userInfo->user_level() & ServerInfo_User::IsRegistered; + if (!isRegistered || spectator) { + game->removeParticipant(this, Event_Leave::USER_DISCONNECTED); + } else { + setUserInterface(nullptr); + } +} + +void Server_AbstractParticipant::getInfo(ServerInfo_Player *info, + Server_AbstractParticipant * /*recipient*/, + bool /* omniscient */, + bool withUserInfo) +{ + getProperties(*info->mutable_properties(), withUserInfo); +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h new file mode 100644 index 000000000..a24fa5799 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h @@ -0,0 +1,183 @@ +#ifndef ABSTRACT_PARTICIPANT_H +#define ABSTRACT_PARTICIPANT_H + +#include "../serverinfo_user_container.h" +#include "server_arrowtarget.h" + +#include +#include +#include + +class Server_Game; +class Server_AbstractUserInterface; +class ServerInfo_User; +class ServerInfo_Player; +class ServerInfo_PlayerProperties; +class GameEventContainer; +class GameEventStorage; +class ResponseContainer; +class GameCommand; + +class Command_KickFromGame; +class Command_LeaveGame; +class Command_GameSay; +class Command_Shuffle; +class Command_Mulligan; +class Command_RollDie; +class Command_DrawCards; +class Command_UndoDraw; +class Command_FlipCard; +class Command_AttachCard; +class Command_CreateToken; +class Command_CreateArrow; +class Command_DeleteArrow; +class Command_SetCardAttr; +class Command_SetCardCounter; +class Command_IncCardCounter; +class Command_ReadyStart; +class Command_Concede; +class Command_Unconcede; +class Command_Judge; +class Command_IncCounter; +class Command_CreateCounter; +class Command_SetCounter; +class Command_DelCounter; +class Command_NextTurn; +class Command_SetActivePhase; +class Command_DumpZone; +class Command_RevealCards; +class Command_ReverseTurn; +class Command_MoveCard; +class Command_SetSideboardPlan; +class Command_DeckSelect; +class Command_SetSideboardLock; +class Command_ChangeZoneProperties; + +class Server_AbstractParticipant : public Server_ArrowTarget, public ServerInfo_User_Container +{ + Q_OBJECT +protected: + Server_Game *game; + Server_AbstractUserInterface *userInterface; + int pingTime; + int playerId; + bool spectator; + bool judge; + virtual void getPlayerProperties(ServerInfo_PlayerProperties &result); + mutable QMutex playerMutex; + +public: + Server_AbstractParticipant(Server_Game *_game, + int _playerId, + const ServerInfo_User &_userInfo, + bool _judge, + Server_AbstractUserInterface *_handler); + ~Server_AbstractParticipant() override; + virtual void prepareDestroy() + { + removeFromGame(); + } + void removeFromGame(); + Server_AbstractUserInterface *getUserInterface() const + { + return userInterface; + } + void setUserInterface(Server_AbstractUserInterface *_userInterface); + void disconnectClient(); + + int getPlayerId() const + { + return playerId; + } + bool isSpectator() const + { + return spectator; + } + bool isJudge() const + { + return judge; + } + Server_Game *getGame() const + { + return game; + } + int getPingTime() const + { + return pingTime; + } + bool updatePingTime(); + void getProperties(ServerInfo_PlayerProperties &result, bool withUserInfo); + + virtual Response::ResponseCode + cmdLeaveGame(const Command_LeaveGame &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdKickFromGame(const Command_KickFromGame &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode cmdConcede(const Command_Concede &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdUnconcede(const Command_Unconcede &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode cmdJudge(const Command_Judge &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdReadyStart(const Command_ReadyStart &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdDeckSelect(const Command_DeckSelect &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdSetSideboardPlan(const Command_SetSideboardPlan &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdSetSideboardLock(const Command_SetSideboardLock &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode cmdGameSay(const Command_GameSay &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode cmdShuffle(const Command_Shuffle &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdMulligan(const Command_Mulligan &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdRollDie(const Command_RollDie &cmd, ResponseContainer &rc, GameEventStorage &ges) const; + virtual Response::ResponseCode + cmdDrawCards(const Command_DrawCards &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdUndoDraw(const Command_UndoDraw &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdMoveCard(const Command_MoveCard &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdFlipCard(const Command_FlipCard &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdAttachCard(const Command_AttachCard &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdCreateToken(const Command_CreateToken &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdCreateArrow(const Command_CreateArrow &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdDeleteArrow(const Command_DeleteArrow &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdSetCardAttr(const Command_SetCardAttr &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdSetCardCounter(const Command_SetCardCounter &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdIncCardCounter(const Command_IncCardCounter &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdIncCounter(const Command_IncCounter &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdCreateCounter(const Command_CreateCounter &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdSetCounter(const Command_SetCounter &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdNextTurn(const Command_NextTurn &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdSetActivePhase(const Command_SetActivePhase &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdDumpZone(const Command_DumpZone &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdRevealCards(const Command_RevealCards &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges); + virtual Response::ResponseCode + cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd, ResponseContainer &rc, GameEventStorage &ges); + + Response::ResponseCode processGameCommand(const GameCommand &command, ResponseContainer &rc, GameEventStorage &ges); + void sendGameEvent(const GameEventContainer &event); + + virtual void + getInfo(ServerInfo_Player *info, Server_AbstractParticipant *recipient, bool omniscient, bool withUserInfo); +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp new file mode 100644 index 000000000..7c0437bf0 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp @@ -0,0 +1,1652 @@ +#include "server_abstract_player.h" + +#include "server_arrow.h" +#include "server_card.h" +#include "server_cardzone.h" +#include "server_game.h" +#include "server_move_card_struct.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Server_AbstractPlayer::Server_AbstractPlayer(Server_Game *_game, + int _playerId, + const ServerInfo_User &_userInfo, + bool _judge, + Server_AbstractUserInterface *_userInterface) + : Server_AbstractParticipant(_game, _playerId, _userInfo, _judge, _userInterface), conceded(false), deck(nullptr), + sideboardLocked(true), readyStart(false), nextCardId(0) +{ + spectator = false; +} + +Server_AbstractPlayer::~Server_AbstractPlayer() = default; + +void Server_AbstractPlayer::prepareDestroy() +{ + delete deck; + deck = nullptr; + + removeFromGame(); + clearZones(); + + deleteLater(); +} + +int Server_AbstractPlayer::newCardId() +{ + return nextCardId++; +} + +int Server_AbstractPlayer::newArrowId() const +{ + int id = 0; + for (Server_Arrow *a : arrows) { + if (a->getId() > id) { + id = a->getId(); + } + } + return id + 1; +} + +void Server_AbstractPlayer::setupZones() +{ + nextCardId = 0; +} + +void Server_AbstractPlayer::clearZones() +{ + for (Server_CardZone *zone : zones) { + delete zone; + } + zones.clear(); + + for (Server_Arrow *arrow : arrows) { + delete arrow; + } + arrows.clear(); +} + +void Server_AbstractPlayer::addZone(Server_CardZone *zone) +{ + zones.insert(zone->getName(), zone); +} + +void Server_AbstractPlayer::addArrow(Server_Arrow *arrow) +{ + arrows.insert(arrow->getId(), arrow); +} + +void Server_AbstractPlayer::updateArrowId(int id) +{ + auto *arrow = arrows.take(id); + arrows.insert(arrow->getId(), arrow); +} + +bool Server_AbstractPlayer::deleteArrow(int arrowId) +{ + Server_Arrow *arrow = arrows.value(arrowId, 0); + if (!arrow) { + return false; + } + arrows.remove(arrowId); + delete arrow; + return true; +} + +/** + * Creates the create token event. + * By default, will set event's name and color fields to empty if the token is face-down + */ +static Event_CreateToken +makeCreateTokenEvent(Server_CardZone *zone, Server_Card *card, int xCoord, int yCoord, bool revealFacedownInfo = false) +{ + Event_CreateToken event; + event.set_zone_name(zone->getName().toStdString()); + event.set_card_id(card->getId()); + event.set_face_down(card->getFaceDown()); + + if (!card->getFaceDown() || revealFacedownInfo) { + event.set_card_name(card->getName().toStdString()); + event.set_card_provider_id(card->getProviderId().toStdString()); + } + + event.set_color(card->getColor().toStdString()); + event.set_pt(card->getPT().toStdString()); + event.set_annotation(card->getAnnotation().toStdString()); + event.set_destroy_on_zone_change(card->getDestroyOnZoneChange()); + event.set_x(xCoord); + event.set_y(yCoord); + return event; +} + +static Event_AttachCard makeAttachCardEvent(Server_Card *attachedCard, Server_Card *parentCard = nullptr) +{ + Event_AttachCard event; + event.set_start_zone(attachedCard->getZone()->getName().toStdString()); + event.set_card_id(attachedCard->getId()); + + if (parentCard) { + event.set_target_player_id(parentCard->getZone()->getPlayer()->getPlayerId()); + event.set_target_zone(parentCard->getZone()->getName().toStdString()); + event.set_target_card_id(parentCard->getId()); + } + + return event; +} + +/** + * Determines whether moving the card from startZone to targetZone should cause the card to be destroyed. + */ +static bool +shouldDestroyOnMove(const Server_Card *card, const Server_CardZone *startZone, const Server_CardZone *targetZone) +{ + if (!card->getDestroyOnZoneChange()) { + return false; + } + + if (startZone->getName() == targetZone->getName()) { + return false; + } + + // Allow tokens on the stack + if ((startZone->getName() == ZoneNames::TABLE || startZone->getName() == ZoneNames::STACK) && + (targetZone->getName() == ZoneNames::TABLE || targetZone->getName() == ZoneNames::STACK)) { + return false; + } + + return true; +} + +/** + * @brief Determines whether the moved card should be face-down + */ +static bool +shouldBeFaceDown(const MoveCardStruct &cardStruct, const Server_CardZone *startZone, const Server_CardZone *targetZone) +{ + if (!targetZone) { + return false; + } + + // being face-down only makes sense for public zones + if (targetZone->getType() != ServerInfo_Zone::PublicZone) { + return false; + } + + // face-down property in proto takes precedence + if (cardStruct.cardToMove->has_face_down()) { + return cardStruct.cardToMove->face_down(); + } + + // Default to keep face-down the same if zone didn't change. + // Compare using zone names because face-down is maintained when changing controllers. + if (startZone && startZone->getName() == targetZone->getName()) { + return cardStruct.card->getFaceDown(); + } + + return false; +} + +/** + * @brief Determines whether a set of moved cards is from the bottom of the deck + */ +static bool shouldBeFromTheBottom(const Server_CardZone *startZone, const std::set &cardsToMove) +{ + if (!startZone) { + return false; + } + + if (startZone->getName() != ZoneNames::DECK) { + return false; + } + + int movedCount = static_cast(cardsToMove.size()); + int tailStart = startZone->getCards().size() - movedCount; + if (tailStart <= 0) { // if the entire deck is moved it should not be considered from the bottom + return false; + } + + // check if the move is a contiguous block at the end of the deck, fail fast when not + int expectedPosition = tailStart; + for (const auto &card : cardsToMove) { + if (card.position != expectedPosition) { + return false; + } + ++expectedPosition; + } + + return true; +} + +Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges, + Server_CardZone *startzone, + const QList &_cards, + Server_CardZone *targetzone, + int xCoord, + int yCoord, + bool fixFreeSpaces, + bool undoingDraw, + bool isReversed) +{ + // Disallow controller change to other zones than the table. + if (((targetzone->getType() != ServerInfo_Zone::PublicZone) || !targetzone->hasCoords()) && + (startzone->getPlayer() != targetzone->getPlayer()) && !judge) { + return Response::RespContextError; + } + + if (!targetzone->hasCoords()) { + yCoord = 0; + if (xCoord <= -1) { + xCoord = targetzone->getCards().size(); + } + } + + std::set cardsToMove; + QSet cardIdsToMove; + for (auto _card : _cards) { + // The same card being moved twice would lead to undefined behaviour. + if (cardIdsToMove.contains(_card->card_id())) { + continue; + } + cardIdsToMove.insert(_card->card_id()); + + // Consistency checks. In case the command contains illegal moves, try to resolve the legal ones still. + int position; + Server_Card *card = startzone->getCard(_card->card_id(), &position); + if (!card) { + return Response::RespNameNotFound; + } + + // do not allow attached cards to move around on the table + if (card->getParentCard() && targetzone->getName() == ZoneNames::TABLE) { + continue; + } + + // do not allow cards with attachments to stack with other cards + if (!card->getAttachedCards().isEmpty() && !targetzone->isColumnEmpty(xCoord, yCoord)) { + continue; + } + + cardsToMove.insert(MoveCardStruct{card, position, _card}); + } + // In case all moves were filtered out, abort. + if (cardsToMove.empty()) { + return Response::RespContextError; + } + + int xIndex = -1; + bool revealTopStart = false; + bool revealTopTarget = false; + + bool isFromBottom = shouldBeFromTheBottom(startzone, cardsToMove); + + if (isFromBottom) { + std::ranges::reverse_view reversedCardsToMove{cardsToMove}; + for (auto card : reversedCardsToMove) { + processMoveCard(ges, startzone, targetzone, card, xCoord, yCoord, xIndex, revealTopStart, revealTopTarget, + isReversed, undoingDraw); + } + } else { + for (auto card : cardsToMove) { + processMoveCard(ges, startzone, targetzone, card, xCoord, yCoord, xIndex, revealTopStart, revealTopTarget, + isReversed, undoingDraw); + } + } + + if (revealTopStart) { + revealTopCardIfNeeded(startzone, ges); + } + if (targetzone != startzone && revealTopTarget) { + revealTopCardIfNeeded(targetzone, ges); + } + if (undoingDraw) { + ges.setGameEventContext(Context_UndoDraw()); + } else { + ges.setGameEventContext(Context_MoveCard()); + } + + if (startzone->hasCoords() && fixFreeSpaces) { + startzone->fixFreeSpaces(ges); + } + + return Response::RespOk; +} + +void Server_AbstractPlayer::processMoveCard(GameEventStorage &ges, + Server_CardZone *startzone, + Server_CardZone *targetzone, + MoveCardStruct cardStruct, + int xCoord, + int yCoord, + int &xIndex, + bool &revealTopStart, + bool &revealTopTarget, + bool isReversed, + bool undoingDraw) +{ + Server_Card *card = cardStruct.card; + int originalPosition = cardStruct.position; + + bool sourceBeingLookedAt; + int position = startzone->removeCard(card, sourceBeingLookedAt); + + // Attachment relationships can be retained when moving a card onto the opponent's table + if (startzone->getName() != targetzone->getName()) { + // Delete all attachment relationships + if (card->getParentCard()) { + card->setParentCard(nullptr); + } + + // Make a copy of the list because the original one gets modified during the loop + QList attachedCards = card->getAttachedCards(); + for (auto &attachedCard : attachedCards) { + attachedCard->getZone()->getPlayer()->unattachCard(ges, attachedCard); + } + } + + if (startzone != targetzone) { + // Delete all arrows from and to the card + for (auto *player : game->getPlayers().values()) { + QList arrowsToDelete; + for (Server_Arrow *arrow : player->getArrows()) { + if ((arrow->getStartCard() == card) || (arrow->getTargetItem() == card)) + arrowsToDelete.append(arrow->getId()); + } + for (int j : arrowsToDelete) { + player->deleteArrow(j); + } + } + } + + if (shouldDestroyOnMove(card, startzone, targetzone)) { + Event_DestroyCard event; + event.set_zone_name(startzone->getName().toStdString()); + event.set_card_id(static_cast(card->getId())); + ges.enqueueGameEvent(event, playerId); + + if (Server_Card *stashedCard = card->takeStashedCard()) { + stashedCard->setId(newCardId()); + ges.enqueueGameEvent(makeCreateTokenEvent(startzone, stashedCard, card->getX(), card->getY()), playerId); + card->deleteLater(); + card = stashedCard; + } else { + card->deleteLater(); + card = nullptr; + } + } + + if (card) { + ++xIndex; + int newX = isReversed ? targetzone->getCards().size() - xCoord + xIndex : xCoord + xIndex; + + bool faceDown = shouldBeFaceDown(cardStruct, startzone, targetzone); + + if (targetzone->hasCoords()) { + newX = targetzone->getFreeGridColumn(newX, yCoord, card->getName(), faceDown); + } else { + card->resetState(targetzone->getName() == ZoneNames::STACK); + } + + targetzone->insertCard(card, newX, yCoord); + int targetLookedCards = targetzone->getCardsBeingLookedAt(); + bool sourceKnownToPlayer = isReversed || (sourceBeingLookedAt && !card->getFaceDown()); + if (targetzone->getType() == ServerInfo_Zone::HiddenZone && targetLookedCards >= newX) { + if (sourceKnownToPlayer) { + targetLookedCards += 1; + } else { + targetLookedCards = newX; + } + targetzone->setCardsBeingLookedAt(targetLookedCards); + } + + bool targetHiddenToOthers = faceDown || (targetzone->getType() != ServerInfo_Zone::PublicZone); + bool sourceHiddenToOthers = card->getFaceDown() || (startzone->getType() != ServerInfo_Zone::PublicZone); + + int oldCardId = card->getId(); + if ((faceDown && (startzone != targetzone)) || (targetzone->getPlayer() != startzone->getPlayer())) { + card->setId(targetzone->getPlayer()->newCardId()); + } + card->setFaceDown(faceDown); + + Event_MoveCard eventOthers; + eventOthers.set_start_player_id(startzone->getPlayer()->getPlayerId()); + eventOthers.set_start_zone(startzone->getName().toStdString()); + eventOthers.set_target_player_id(targetzone->getPlayer()->getPlayerId()); + if (startzone != targetzone) { + eventOthers.set_target_zone(targetzone->getName().toStdString()); + } + eventOthers.set_y(yCoord); + eventOthers.set_face_down(faceDown); + + Event_MoveCard eventPrivate(eventOthers); + if (sourceBeingLookedAt || targetzone->getType() != ServerInfo_Zone::HiddenZone || + startzone->getType() != ServerInfo_Zone::HiddenZone) { + eventPrivate.set_card_id(oldCardId); + eventPrivate.set_new_card_id(card->getId()); + } else { + eventPrivate.set_card_id(-1); + eventPrivate.set_new_card_id(-1); + } + if (sourceKnownToPlayer || !(faceDown || targetzone->getType() == ServerInfo_Zone::HiddenZone)) { + QString privateCardName = card->getName(); + eventPrivate.set_card_name(privateCardName.toStdString()); + eventPrivate.set_new_card_provider_id(card->getProviderId().toStdString()); + } + if (startzone->getType() == ServerInfo_Zone::HiddenZone) { + eventPrivate.set_position(position); + } else { + eventPrivate.set_position(-1); + } + + eventPrivate.set_x(newX); + + if ( + // cards from public zones have their id known, their previous position is already known, the event does + // not accomodate for previous locations in zones with coordinates (which are always public) + (startzone->getType() != ServerInfo_Zone::PublicZone) && + // other players are not allowed to be able to track which card is which in private zones like the hand + (startzone->getType() != ServerInfo_Zone::PrivateZone)) { + eventOthers.set_position(position); + } + if ( + // other players are not allowed to be able to track which card is which in private zones like the hand + (targetzone->getType() != ServerInfo_Zone::PrivateZone)) { + eventOthers.set_x(newX); + } + + if ((startzone->getType() == ServerInfo_Zone::PublicZone) || + (targetzone->getType() == ServerInfo_Zone::PublicZone)) { + eventOthers.set_card_id(oldCardId); + if (!(sourceHiddenToOthers && targetHiddenToOthers)) { + QString publicCardName = card->getName(); + eventOthers.set_card_name(publicCardName.toStdString()); + eventOthers.set_new_card_provider_id(card->getProviderId().toStdString()); + } + eventOthers.set_new_card_id(card->getId()); + } + + ges.enqueueGameEvent(eventPrivate, playerId, GameEventStorageItem::SendToPrivate, playerId); + ges.enqueueGameEvent(eventOthers, playerId, GameEventStorageItem::SendToOthers); + + if (originalPosition == 0) { + revealTopStart = true; + } + if (newX == 0) { + revealTopTarget = true; + } + + // handle side effects for this card + onCardBeingMoved(ges, cardStruct, startzone, targetzone, undoingDraw); + } +} + +void Server_AbstractPlayer::onCardBeingMoved(GameEventStorage &ges, + const MoveCardStruct &cardStruct, + Server_CardZone *startzone, + Server_CardZone *targetzone, + bool /*undoingDraw*/) +{ + Server_Card *card = cardStruct.card; + const CardToMove *thisCardProperties = cardStruct.cardToMove; + + // set card to be tapped + if (thisCardProperties->tapped()) { + setCardAttrHelper(ges, targetzone->getPlayer()->getPlayerId(), targetzone->getName(), card->getId(), AttrTapped, + "1"); + } + + // set card pt + QString ptString = QString::fromStdString(thisCardProperties->pt()); + if (!ptString.isEmpty()) { + setCardAttrHelper(ges, targetzone->getPlayer()->getPlayerId(), targetzone->getName(), card->getId(), AttrPT, + ptString); + } + + // If card is transferring to a different player, leave an annotation of who actually "owns" the card + const auto &priorAnnotation = card->getAnnotation(); + if (startzone->getPlayer() != targetzone->getPlayer() && !priorAnnotation.contains("Owner:")) { + const auto &ownerAnnotation = "Owner: " + QString::fromStdString(startzone->getPlayer()->getUserInfo()->name()); + const auto &newAnnotation = + priorAnnotation.isEmpty() ? ownerAnnotation : ownerAnnotation + "\n\n" + priorAnnotation; + setCardAttrHelper(ges, targetzone->getPlayer()->getPlayerId(), targetzone->getName(), card->getId(), + AttrAnnotation, newAnnotation, card); + } +} + +void Server_AbstractPlayer::revealTopCardIfNeeded(Server_CardZone *zone, GameEventStorage &ges) +{ + if (zone->getCards().isEmpty()) { + return; + } + if (zone->getAlwaysRevealTopCard()) { + Event_RevealCards revealEvent; + revealEvent.set_zone_name(zone->getName().toStdString()); + revealEvent.add_card_id(0); + zone->getCards().first()->getInfo(revealEvent.add_cards()); + + ges.enqueueGameEvent(revealEvent, playerId); + return; + } + if (zone->getAlwaysLookAtTopCard()) { + Event_DumpZone dumpEvent; + dumpEvent.set_zone_owner_id(playerId); + dumpEvent.set_zone_name(zone->getName().toStdString()); + dumpEvent.set_number_cards(1); + ges.enqueueGameEvent(dumpEvent, playerId, GameEventStorageItem::SendToOthers); + + Event_RevealCards revealEvent; + revealEvent.set_zone_name(zone->getName().toStdString()); + revealEvent.set_number_of_cards(1); + revealEvent.add_card_id(0); + zone->getCards().first()->getInfo(revealEvent.add_cards()); + ges.enqueueGameEvent(revealEvent, playerId, GameEventStorageItem::SendToPrivate, playerId); + } +} + +void Server_AbstractPlayer::unattachCard(GameEventStorage &ges, Server_Card *card) +{ + Server_CardZone *zone = card->getZone(); + Server_Card *parentCard = card->getParentCard(); + card->setParentCard(nullptr); + + ges.enqueueGameEvent(makeAttachCardEvent(card), playerId); + + auto *cardToMove = new CardToMove; + cardToMove->set_card_id(card->getId()); + moveCard(ges, zone, QList() << cardToMove, zone, -1, card->getY(), card->getFaceDown()); + delete cardToMove; + + if (parentCard->getZone()) { + parentCard->getZone()->updateCardCoordinates(parentCard, parentCard->getX(), parentCard->getY()); + } +} + +Response::ResponseCode Server_AbstractPlayer::setCardAttrHelper(GameEventStorage &ges, + int targetPlayerId, + const QString &zoneName, + int cardId, + CardAttribute attribute, + const QString &attrValue, + Server_Card *unzonedCard) +{ + Server_CardZone *zone = getZones().value(zoneName); + if (!zone) { + return Response::RespNameNotFound; + } + if (!zone->hasCoords()) { + return Response::RespContextError; + } + + QString result; + if (cardId == -1) { + QListIterator CardIterator(zone->getCards()); + while (CardIterator.hasNext()) { + result = CardIterator.next()->setAttribute(attribute, attrValue, true); + if (result.isNull()) { + return Response::RespInvalidCommand; + } + } + } else { + Server_Card *card = unzonedCard == nullptr ? zone->getCard(cardId) : unzonedCard; + if (!card) { + return Response::RespNameNotFound; + } + result = card->setAttribute(attribute, attrValue, false); + if (result.isNull()) { + return Response::RespInvalidCommand; + } + } + + Event_SetCardAttr event; + event.set_zone_name(zone->getName().toStdString()); + if (cardId != -1) { + event.set_card_id(cardId); + } + event.set_attribute(attribute); + event.set_attr_value(result.toStdString()); + ges.enqueueGameEvent(event, targetPlayerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdConcede(const Command_Concede & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + setConceded(true); + game->removeArrowsRelatedToPlayer(ges, this); + game->unattachCards(ges, this); + game->returnCardsFromPlayer(ges, this); + + clearZones(); + + Event_PlayerPropertiesChanged event; + event.mutable_player_properties()->set_conceded(true); + ges.enqueueGameEvent(event, playerId); + ges.setGameEventContext(Context_Concede()); + + game->stopGameIfFinished(); + if (game->getGameStarted() && (game->getActivePlayer() == playerId)) { + game->nextTurn(); + } + + return Response::RespOk; +} + +Response::ResponseCode Server_AbstractPlayer::cmdUnconcede(const Command_Unconcede & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (!conceded) { + return Response::RespContextError; + } + + setConceded(false); + + Event_PlayerPropertiesChanged event; + event.mutable_player_properties()->set_conceded(false); + ges.enqueueGameEvent(event, playerId); + ges.setGameEventContext(Context_Unconcede()); + + setupZones(); + + game->sendGameStateToPlayers(); + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdReadyStart(const Command_ReadyStart &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!deck || game->getGameStarted()) { + return Response::RespContextError; + } + + if (readyStart == cmd.ready() && !cmd.force_start()) { + return Response::RespContextError; + } + + setReadyStart(cmd.ready()); + + Event_PlayerPropertiesChanged event; + event.mutable_player_properties()->set_ready_start(cmd.ready()); + ges.enqueueGameEvent(event, playerId); + ges.setGameEventContext(Context_ReadyStart()); + + if (cmd.force_start()) { + if (game->getHostId() != playerId) { + return Response::RespFunctionNotAllowed; + } + game->startGameIfReady(true); + } else if (cmd.ready()) { + game->startGameIfReady(false); + } + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdRollDie(const Command_RollDie &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) const +{ + if (conceded) { + return Response::RespContextError; + } + + const auto validatedSides = static_cast(std::min(std::max(cmd.sides(), MINIMUM_DIE_SIDES), MAXIMUM_DIE_SIDES)); + const auto validatedDiceToRoll = + static_cast(std::min(std::max(cmd.count(), MINIMUM_DICE_TO_ROLL), MAXIMUM_DICE_TO_ROLL)); + + Event_RollDie event; + event.set_sides(validatedSides); + for (auto i = 0; i < validatedDiceToRoll; ++i) { + const auto roll = rng->rand(1, validatedSides); + if (i == 0) { + // Backwards compatibility + event.set_value(roll); + } + event.add_values(roll); + } + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdMoveCard(const Command_MoveCard &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_AbstractPlayer *startPlayer = game->getPlayer(cmd.has_start_player_id() ? cmd.start_player_id() : playerId); + if (!startPlayer) { + return Response::RespNameNotFound; + } + Server_CardZone *startZone = startPlayer->getZones().value(nameFromStdString(cmd.start_zone())); + if (!startZone) { + return Response::RespNameNotFound; + } + + if ((startPlayer != this) && (!startZone->getPlayersWithWritePermission().contains(playerId)) && !judge) { + return Response::RespContextError; + } + + Server_AbstractPlayer *targetPlayer = game->getPlayer(cmd.target_player_id()); + if (!targetPlayer) { + return Response::RespNameNotFound; + } + Server_CardZone *targetZone = targetPlayer->getZones().value(nameFromStdString(cmd.target_zone())); + if (!targetZone) { + return Response::RespNameNotFound; + } + + if ((startPlayer != this) && (targetPlayer != this) && !judge) { + return Response::RespContextError; + } + + QList cardsToMove; + for (int i = 0; i < cmd.cards_to_move().card_size(); ++i) { + cardsToMove.append(&cmd.cards_to_move().card(i)); + } + + return moveCard(ges, startZone, cardsToMove, targetZone, cmd.x(), cmd.y(), true, false, cmd.is_reversed()); +} + +Response::ResponseCode +Server_AbstractPlayer::cmdFlipCard(const Command_FlipCard &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_CardZone *zone = zones.value(nameFromStdString(cmd.zone())); + if (!zone) { + return Response::RespNameNotFound; + } + if (!zone->hasCoords()) { + return Response::RespContextError; + } + + Server_Card *card = zone->getCard(cmd.card_id()); + if (!card) { + return Response::RespNameNotFound; + } + + const bool faceDown = cmd.face_down(); + if (faceDown == card->getFaceDown()) { + return Response::RespContextError; + } + + card->setFaceDown(faceDown); + + Event_FlipCard event; + event.set_zone_name(zone->getName().toStdString()); + event.set_card_id(card->getId()); + if (!faceDown) { + event.set_card_name(card->getName().toStdString()); + event.set_card_provider_id(card->getProviderId().toStdString()); + } + event.set_face_down(faceDown); + ges.enqueueGameEvent(event, playerId); + + QString ptString = nameFromStdString(cmd.pt()); + if (!ptString.isEmpty() && !faceDown) { + setCardAttrHelper(ges, playerId, zone->getName(), card->getId(), AttrPT, ptString); + } + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdAttachCard(const Command_AttachCard &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_CardZone *startzone = zones.value(nameFromStdString(cmd.start_zone())); + if (!startzone) { + return Response::RespNameNotFound; + } + + Server_Card *card = startzone->getCard(cmd.card_id()); + if (!card) { + return Response::RespNameNotFound; + } + + Server_AbstractPlayer *targetPlayer = nullptr; + Server_CardZone *targetzone = nullptr; + Server_Card *targetCard = nullptr; + + if (cmd.has_target_player_id()) { + targetPlayer = game->getPlayer(cmd.target_player_id()); + if (!targetPlayer) { + return Response::RespNameNotFound; + } + } else if (!card->getParentCard()) { + return Response::RespContextError; + } + if (targetPlayer) { + targetzone = targetPlayer->getZones().value(nameFromStdString(cmd.target_zone())); + } + if (targetzone) { + // This is currently enough to make sure cards don't get attached to a card that is not on the table. + // Possibly a flag will have to be introduced for this sometime. + if (!targetzone->hasCoords()) { + return Response::RespContextError; + } + if (cmd.has_target_card_id()) { + targetCard = targetzone->getCard(cmd.target_card_id()); + } + if (targetCard) { + if (targetCard->getParentCard()) { + return Response::RespContextError; + } + } else { + return Response::RespNameNotFound; + } + } + + // prevent attaching from non-table zones + // (attaching from non-table zones is handled client-side by moving the card to table zone first) + if (!startzone->hasCoords()) { + return Response::RespContextError; + } + + for (auto *player : game->getPlayers()) { + QList _arrows = player->getArrows().values(); + QList toDelete; + for (auto a : _arrows) { + auto *tCard = qobject_cast(a->getTargetItem()); + if ((tCard == card) || (a->getStartCard() == card)) { + toDelete.append(a); + } + } + for (auto &i : toDelete) { + Event_DeleteArrow event; + event.set_arrow_id(i->getId()); + ges.enqueueGameEvent(event, player->getPlayerId()); + player->deleteArrow(i->getId()); + } + } + + if (targetCard) { + // Unattach all cards attached to the card being attached. + // Make a copy of the list because its contents change during the loop otherwise. + QList attachedList = card->getAttachedCards(); + for (const auto &i : attachedList) { + i->getZone()->getPlayer()->unattachCard(ges, i); + } + + card->setParentCard(targetCard); + const int oldX = card->getX(); + card->setCoords(-1, card->getY()); + startzone->updateCardCoordinates(card, oldX, card->getY()); + + if (targetzone->isColumnStacked(targetCard->getX(), targetCard->getY())) { + auto *cardToMove = new CardToMove; + cardToMove->set_card_id(targetCard->getId()); + targetPlayer->moveCard(ges, targetzone, QList() << cardToMove, targetzone, + targetzone->getFreeGridColumn(-2, targetCard->getY(), targetCard->getName(), false), + targetCard->getY(), targetCard->getFaceDown()); + delete cardToMove; + } + + ges.enqueueGameEvent(makeAttachCardEvent(card, targetCard), playerId); + + startzone->fixFreeSpaces(ges); + } else { + unattachCard(ges, card); + } + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdCreateToken(const Command_CreateToken &cmd, ResponseContainer &rc, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_CardZone *zone = zones.value(nameFromStdString(cmd.zone())); + if (!zone) { + return Response::RespNameNotFound; + } + + int xCoord = cmd.x(); + int yCoord = cmd.y(); + + Server_Card *targetCard = nullptr; + if (cmd.has_target_card_id()) { + Server_CardZone *targetZone = zones.value(nameFromStdString(cmd.target_zone())); + if (targetZone) { + targetCard = targetZone->getCard(cmd.target_card_id()); + if (targetCard && cmd.target_mode() == Command_CreateToken::TRANSFORM_INTO) { + if (targetCard->getParentCard()) { + ges.enqueueGameEvent(makeAttachCardEvent(targetCard), playerId); + } + + for (Server_Card *attachedCard : targetCard->getAttachedCards()) { + ges.enqueueGameEvent(makeAttachCardEvent(attachedCard), + attachedCard->getZone()->getPlayer()->getPlayerId()); + } + + if (zone->hasCoords() && zone == targetZone) { + xCoord = targetCard->getX(); + yCoord = targetCard->getY(); + } + + targetZone->removeCard(targetCard); + + Event_DestroyCard event; + event.set_zone_name(targetZone->getName().toStdString()); + event.set_card_id(static_cast<::google::protobuf::uint32>(cmd.target_card_id())); + ges.enqueueGameEvent(event, playerId); + } + } + } + + const QString cardName = nameFromStdString(cmd.card_name()); + const QString cardProviderId = nameFromStdString(cmd.card_provider_id()); + if (zone->hasCoords()) { + bool dontStackSameName = cmd.face_down(); + xCoord = zone->getFreeGridColumn(xCoord, yCoord, cardName, dontStackSameName); + } + if (xCoord < 0) { + xCoord = 0; + } + if (yCoord < 0) { + yCoord = 0; + } + + auto *card = new Server_Card({cardName, cardProviderId}, newCardId(), xCoord, yCoord); + card->moveToThread(thread()); + // Client should already prevent face-down tokens from having attributes; this just an extra server-side check + if (!cmd.face_down()) { + card->setColor(nameFromStdString(cmd.color())); + card->setPT(nameFromStdString(cmd.pt())); + } + card->setAnnotation(nameFromStdString(cmd.annotation())); + card->setDestroyOnZoneChange(cmd.destroy_on_zone_change()); + card->setFaceDown(cmd.face_down()); + + zone->insertCard(card, xCoord, yCoord); + sendCreateTokenEvents(zone, card, xCoord, yCoord, ges); + + // check if the token is a replacement for an existing card + if (!targetCard) { + return Response::RespOk; + } + + switch (cmd.target_mode()) { + case Command_CreateToken::ATTACH_TO: { + Command_AttachCard cmd2; + cmd2.set_start_zone(cmd.target_zone()); + cmd2.set_card_id(cmd.target_card_id()); + + cmd2.set_target_player_id(zone->getPlayer()->getPlayerId()); + cmd2.set_target_zone(cmd.zone()); + cmd2.set_target_card_id(card->getId()); + + return cmdAttachCard(cmd2, rc, ges); + } + + case Command_CreateToken::TRANSFORM_INTO: { + // Copy attributes that are not present in the CreateToken event + Event_SetCardAttr event; + event.set_zone_name(card->getZone()->getName().toStdString()); + event.set_card_id(card->getId()); + + if (card->getTapped() != targetCard->getTapped()) { + card->setAttribute(AttrTapped, QVariant(targetCard->getTapped()).toString(), &event); + ges.enqueueGameEvent(event, playerId); + } + + if (card->getAttacking() != targetCard->getAttacking()) { + card->setAttribute(AttrAttacking, QVariant(targetCard->getAttacking()).toString(), &event); + ges.enqueueGameEvent(event, playerId); + } + + if (card->getFaceDown() != targetCard->getFaceDown()) { + card->setAttribute(AttrFaceDown, QVariant(targetCard->getFaceDown()).toString(), &event); + ges.enqueueGameEvent(event, playerId); + } + + if (card->getDoesntUntap() != targetCard->getDoesntUntap()) { + card->setAttribute(AttrDoesntUntap, QVariant(targetCard->getDoesntUntap()).toString(), &event); + ges.enqueueGameEvent(event, playerId); + } + + // Copy counters + QMapIterator i(targetCard->getCounters()); + while (i.hasNext()) { + i.next(); + + Event_SetCardCounter _event; + _event.set_zone_name(card->getZone()->getName().toStdString()); + _event.set_card_id(card->getId()); + + card->setCounter(i.key(), i.value(), &_event); + ges.enqueueGameEvent(_event, playerId); + } + + // Copy parent card + if (Server_Card *parentCard = targetCard->getParentCard()) { + targetCard->setParentCard(nullptr); + card->setParentCard(parentCard); + + ges.enqueueGameEvent(makeAttachCardEvent(card, parentCard), playerId); + } + + // Copy attachments + while (!targetCard->getAttachedCards().isEmpty()) { + Server_Card *attachedCard = targetCard->getAttachedCards().last(); + attachedCard->setParentCard(card); + + ges.enqueueGameEvent(makeAttachCardEvent(attachedCard, card), + attachedCard->getZone()->getPlayer()->getPlayerId()); + } + + // Copy Arrows + for (auto *player : game->getPlayers().values()) { + QList changedArrowIds; + for (Server_Arrow *arrow : player->getArrows()) { + bool sendGameEvent = false; + const auto *startCard = arrow->getStartCard(); + if (startCard == targetCard) { + sendGameEvent = true; + arrow->setStartCard(card); + startCard = card; + } + const auto *targetItem = arrow->getTargetItem(); + if (targetItem == targetCard) { + sendGameEvent = true; + arrow->setTargetItem(card); + targetItem = card; + } + if (sendGameEvent) { + Event_CreateArrow _event; + ServerInfo_Arrow *arrowInfo = _event.mutable_arrow_info(); + changedArrowIds.append(arrow->getId()); + int id = player->newArrowId(); + arrow->setId(id); + arrowInfo->set_id(id); + arrowInfo->set_start_player_id(player->getPlayerId()); + arrowInfo->set_start_zone(startCard->getZone()->getName().toStdString()); + arrowInfo->set_start_card_id(startCard->getId()); + const auto *arrowTargetPlayer = qobject_cast(targetItem); + if (arrowTargetPlayer != nullptr) { + arrowInfo->set_target_player_id(arrowTargetPlayer->getPlayerId()); + } else { + const auto *arrowTargetCard = qobject_cast(targetItem); + arrowInfo->set_target_player_id(arrowTargetCard->getZone()->getPlayer()->getPlayerId()); + arrowInfo->set_target_zone(arrowTargetCard->getZone()->getName().toStdString()); + arrowInfo->set_target_card_id(arrowTargetCard->getId()); + } + arrowInfo->mutable_arrow_color()->CopyFrom(arrow->getColor()); + ges.enqueueGameEvent(_event, player->getPlayerId()); + } + } + for (int id : changedArrowIds) { + player->updateArrowId(id); + } + } + + targetCard->resetState(); + card->setStashedCard(targetCard); + break; + } + } + + return Response::RespOk; +} + +/** + * Creates and sends the events required to properly communicate the given token creation. + * Primarily written to handle creating face-down tokens. + */ +void Server_AbstractPlayer::sendCreateTokenEvents(Server_CardZone *zone, + Server_Card *card, + int xCoord, + int yCoord, + GameEventStorage &ges) +{ + // Token is not face-down; things are easy + if (!card->getFaceDown()) { + ges.enqueueGameEvent(makeCreateTokenEvent(zone, card, xCoord, yCoord), playerId); + return; + } + + // Token is face-down. We have to send different info to each player + auto eventOthers = makeCreateTokenEvent(zone, card, xCoord, yCoord, false); + ges.enqueueGameEvent(eventOthers, playerId, GameEventStorageItem::SendToOthers); + + auto eventPrivate = makeCreateTokenEvent(zone, card, xCoord, yCoord, true); + ges.enqueueGameEvent(eventPrivate, playerId, GameEventStorageItem::SendToPrivate, playerId); + + // Event_CreateToken didn't use to have face_down field; send attribute event afterward for backwards compatibility + Event_SetCardAttr event; + event.set_zone_name(zone->getName().toStdString()); + event.set_card_id(card->getId()); + event.set_attribute(AttrFaceDown); + event.set_attr_value("1"); + ges.enqueueGameEvent(event, playerId); +} + +Response::ResponseCode +Server_AbstractPlayer::cmdCreateArrow(const Command_CreateArrow &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_AbstractPlayer *startPlayer = game->getPlayer(cmd.start_player_id()); + Server_AbstractPlayer *targetPlayer = game->getPlayer(cmd.target_player_id()); + if (!startPlayer || !targetPlayer) { + return Response::RespNameNotFound; + } + QString startZoneName = nameFromStdString(cmd.start_zone()); + Server_CardZone *startZone = startPlayer->getZones().value(startZoneName); + bool playerTarget = !cmd.has_target_zone(); + Server_CardZone *targetZone = nullptr; + if (!playerTarget) { + targetZone = targetPlayer->getZones().value(nameFromStdString(cmd.target_zone())); + } + if (!startZone || (!targetZone && !playerTarget)) { + return Response::RespNameNotFound; + } + if (startZone->getType() != ServerInfo_Zone::PublicZone) { + return Response::RespContextError; + } + Server_Card *startCard = startZone->getCard(cmd.start_card_id()); + if (!startCard) { + return Response::RespNameNotFound; + } + Server_Card *targetCard = nullptr; + if (!playerTarget) { + if (targetZone->getType() != ServerInfo_Zone::PublicZone) { + return Response::RespContextError; + } + targetCard = targetZone->getCard(cmd.target_card_id()); + } + + Server_ArrowTarget *targetItem; + if (playerTarget) { + targetItem = targetPlayer; + } else { + targetItem = targetCard; + } + if (!targetItem) { + return Response::RespNameNotFound; + } + + for (Server_Arrow *temp : arrows) { + if ((temp->getStartCard() == startCard) && (temp->getTargetItem() == targetItem)) { + return Response::RespContextError; + } + } + + int currentPhase = game->getActivePhase(); + int deletionPhase = cmd.has_delete_in_phase() ? cmd.delete_in_phase() : currentPhase; + auto arrow = new Server_Arrow(newArrowId(), startCard, targetItem, cmd.arrow_color(), currentPhase, deletionPhase); + addArrow(arrow); + + Event_CreateArrow event; + ServerInfo_Arrow *arrowInfo = event.mutable_arrow_info(); + arrowInfo->set_id(arrow->getId()); + arrowInfo->set_start_player_id(startPlayer->getPlayerId()); + arrowInfo->set_start_zone(startZoneName.toStdString()); + arrowInfo->set_start_card_id(startCard->getId()); + arrowInfo->set_target_player_id(targetPlayer->getPlayerId()); + if (!playerTarget) { + arrowInfo->set_target_zone(cmd.target_zone()); + arrowInfo->set_target_card_id(cmd.target_card_id()); + } + arrowInfo->mutable_arrow_color()->CopyFrom(cmd.arrow_color()); + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdDeleteArrow(const Command_DeleteArrow &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + if (!deleteArrow(cmd.arrow_id())) { + return Response::RespNameNotFound; + } + + Event_DeleteArrow event; + event.set_arrow_id(cmd.arrow_id()); + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdSetCardAttr(const Command_SetCardAttr &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + return setCardAttrHelper(ges, playerId, nameFromStdString(cmd.zone()), cmd.card_id(), cmd.attribute(), + nameFromStdString(cmd.attr_value())); +} + +Response::ResponseCode Server_AbstractPlayer::cmdSetCardCounter(const Command_SetCardCounter &cmd, + ResponseContainer & /*rc*/, + GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_CardZone *zone = zones.value(nameFromStdString(cmd.zone())); + if (!zone) { + return Response::RespNameNotFound; + } + if (!zone->hasCoords()) { + return Response::RespContextError; + } + + Server_Card *card = zone->getCard(cmd.card_id()); + if (!card) { + return Response::RespNameNotFound; + } + + Event_SetCardCounter event; + event.set_zone_name(zone->getName().toStdString()); + event.set_card_id(card->getId()); + card->setCounter(cmd.counter_id(), cmd.counter_value(), &event); + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode Server_AbstractPlayer::cmdIncCardCounter(const Command_IncCardCounter &cmd, + ResponseContainer & /*rc*/, + GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_CardZone *zone = zones.value(nameFromStdString(cmd.zone())); + if (!zone) { + return Response::RespNameNotFound; + } + if (!zone->hasCoords()) { + return Response::RespContextError; + } + + Server_Card *card = zone->getCard(cmd.card_id()); + if (!card) { + return Response::RespNameNotFound; + } + + int newValue = card->getCounter(cmd.counter_id()) + cmd.counter_delta(); + card->setCounter(cmd.counter_id(), newValue); + + Event_SetCardCounter event; + event.set_zone_name(zone->getName().toStdString()); + event.set_card_id(card->getId()); + event.set_counter_id(cmd.counter_id()); + event.set_counter_value(newValue); + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdDumpZone(const Command_DumpZone &cmd, ResponseContainer &rc, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + + Server_AbstractPlayer *otherPlayer = game->getPlayer(cmd.player_id()); + if (!otherPlayer) { + return Response::RespNameNotFound; + } + Server_CardZone *zone = otherPlayer->getZones().value(nameFromStdString(cmd.zone_name())); + if (!zone) { + return Response::RespNameNotFound; + } + if (!((zone->getType() == ServerInfo_Zone::PublicZone) || (this == otherPlayer))) { + return Response::RespContextError; + } + + int numberCards = cmd.number_cards(); + const QList &cards = zone->getCards(); + + auto *re = new Response_DumpZone; + ServerInfo_Zone *zoneInfo = re->mutable_zone_info(); + zoneInfo->set_name(zone->getName().toStdString()); + zoneInfo->set_type(zone->getType()); + zoneInfo->set_with_coords(zone->hasCoords()); + zoneInfo->set_card_count(numberCards < cards.size() ? cards.size() : numberCards); + + for (int i = 0; (i < cards.size()) && (i < numberCards || numberCards == -1); ++i) { + const auto &findId = cmd.is_reversed() ? cards.size() - numberCards + i : i; + Server_Card *card = cards[findId]; + QString displayedName = card->getFaceDown() ? QString() : card->getName(); + ServerInfo_Card *cardInfo = zoneInfo->add_card_list(); + cardInfo->set_provider_id(card->getProviderId().toStdString()); + cardInfo->set_name(displayedName.toStdString()); + if (zone->getType() == ServerInfo_Zone::HiddenZone) { + cardInfo->set_id(findId); + } else { + cardInfo->set_id(card->getId()); + cardInfo->set_x(card->getX()); + cardInfo->set_y(card->getY()); + cardInfo->set_face_down(card->getFaceDown()); + cardInfo->set_tapped(card->getTapped()); + cardInfo->set_attacking(card->getAttacking()); + cardInfo->set_color(card->getColor().toStdString()); + cardInfo->set_pt(card->getPT().toStdString()); + cardInfo->set_annotation(card->getAnnotation().toStdString()); + cardInfo->set_destroy_on_zone_change(card->getDestroyOnZoneChange()); + cardInfo->set_doesnt_untap(card->getDoesntUntap()); + + QMapIterator cardCounterIterator(card->getCounters()); + while (cardCounterIterator.hasNext()) { + cardCounterIterator.next(); + ServerInfo_CardCounter *counterInfo = cardInfo->add_counter_list(); + counterInfo->set_id(cardCounterIterator.key()); + counterInfo->set_value(cardCounterIterator.value()); + } + + if (card->getParentCard()) { + cardInfo->set_attach_player_id(card->getParentCard()->getZone()->getPlayer()->getPlayerId()); + cardInfo->set_attach_zone(card->getParentCard()->getZone()->getName().toStdString()); + cardInfo->set_attach_card_id(card->getParentCard()->getId()); + } + } + } + if (zone->getType() == ServerInfo_Zone::HiddenZone) { + zone->setCardsBeingLookedAt(numberCards); + + Event_DumpZone event; + event.set_zone_owner_id(otherPlayer->getPlayerId()); + event.set_zone_name(zone->getName().toStdString()); + event.set_number_cards(numberCards); + event.set_is_reversed(cmd.is_reversed()); + ges.enqueueGameEvent(event, playerId); + } + rc.setResponseExtension(re); + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdRevealCards(const Command_RevealCards &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + if (cmd.has_player_id()) { + Server_AbstractPlayer *otherPlayer = game->getPlayer(cmd.player_id()); + if (!otherPlayer) + return Response::RespNameNotFound; + } + Server_CardZone *zone = zones.value(nameFromStdString(cmd.zone_name())); + if (!zone) { + return Response::RespNameNotFound; + } + + QList cardsToReveal; + if (cmd.top_cards() != -1) { + for (int i = 0; i < cmd.top_cards(); i++) { + Server_Card *card = zone->getCard(i); + if (!card) { + return Response::RespNameNotFound; + } + cardsToReveal.append(card); + } + } else if (cmd.card_id_size() == 0) { + cardsToReveal = zone->getCards(); + } else if (cmd.card_id_size() == 1 && cmd.card_id(0) == -2) { + // If there is a single card_id with value -2 (ie + // Player::RANDOM_CARD_FROM_ZONE), pick a random card. + // + // This is to be compatible with clients supporting a single card_id + // value, which send value -2 to request a random card. + if (zone->getCards().isEmpty()) { + return Response::RespContextError; + } + + cardsToReveal.append(zone->getCards().at(rng->rand(0, zone->getCards().size() - 1))); + } else { + for (auto cardId : cmd.card_id()) { + Server_Card *card = zone->getCard(cardId); + if (!card) { + return Response::RespNameNotFound; + } + cardsToReveal.append(card); + } + } + + Event_RevealCards eventOthers; + eventOthers.set_grant_write_access(cmd.grant_write_access()); + eventOthers.set_zone_name(zone->getName().toStdString()); + eventOthers.set_number_of_cards(cardsToReveal.size()); + for (auto cardId : cmd.card_id()) { + eventOthers.add_card_id(cardId); + } + if (cmd.has_player_id()) { + eventOthers.set_other_player_id(cmd.player_id()); + } + + Event_RevealCards eventPrivate(eventOthers); + + for (auto card : cardsToReveal) { + ServerInfo_Card *cardInfo = eventPrivate.add_cards(); + + cardInfo->set_id(card->getId()); + cardInfo->set_provider_id(card->getProviderId().toStdString()); + cardInfo->set_name(card->getName().toStdString()); + cardInfo->set_x(card->getX()); + cardInfo->set_y(card->getY()); + cardInfo->set_face_down(card->getFaceDown()); + cardInfo->set_tapped(card->getTapped()); + cardInfo->set_attacking(card->getAttacking()); + cardInfo->set_color(card->getColor().toStdString()); + cardInfo->set_pt(card->getPT().toStdString()); + cardInfo->set_annotation(card->getAnnotation().toStdString()); + cardInfo->set_destroy_on_zone_change(card->getDestroyOnZoneChange()); + cardInfo->set_doesnt_untap(card->getDoesntUntap()); + + QMapIterator cardCounterIterator(card->getCounters()); + while (cardCounterIterator.hasNext()) { + cardCounterIterator.next(); + ServerInfo_CardCounter *counterInfo = cardInfo->add_counter_list(); + counterInfo->set_id(cardCounterIterator.key()); + counterInfo->set_value(cardCounterIterator.value()); + } + + if (card->getParentCard()) { + cardInfo->set_attach_player_id(card->getParentCard()->getZone()->getPlayer()->getPlayerId()); + cardInfo->set_attach_zone(card->getParentCard()->getZone()->getName().toStdString()); + cardInfo->set_attach_card_id(card->getParentCard()->getId()); + } + } + + if (cmd.has_player_id()) { + if (cmd.grant_write_access()) { + zone->addWritePermission(cmd.player_id()); + } + + if (isJudge()) { + ges.setOverwriteOwnership(true); + } + + ges.enqueueGameEvent(eventPrivate, playerId, GameEventStorageItem::SendToPrivate, cmd.player_id()); + ges.enqueueGameEvent(eventOthers, playerId, GameEventStorageItem::SendToOthers); + } else { + if (cmd.grant_write_access()) { + const QList &participantIds = game->getParticipants().keys(); + for (int anyParticipantId : participantIds) { + zone->addWritePermission(anyParticipantId); + } + } + + ges.enqueueGameEvent(eventPrivate, playerId); + } + + return Response::RespOk; +} + +Response::ResponseCode Server_AbstractPlayer::cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd, + ResponseContainer & /* rc */, + GameEventStorage &ges) +{ + Server_CardZone *zone = zones.value(nameFromStdString(cmd.zone_name())); + if (!zone) { + return Response::RespNameNotFound; + } + + Event_ChangeZoneProperties event; + event.set_zone_name(cmd.zone_name()); + + // Neither value set -> error. + if (!cmd.has_always_look_at_top_card() && !cmd.has_always_reveal_top_card()) { + return Response::RespContextError; + } + + // Neither value changed -> error. + bool alwaysRevealChanged = + cmd.has_always_reveal_top_card() && zone->getAlwaysRevealTopCard() != cmd.always_reveal_top_card(); + bool alwaysLookAtTopChanged = + cmd.has_always_look_at_top_card() && zone->getAlwaysLookAtTopCard() != cmd.always_look_at_top_card(); + if (!alwaysRevealChanged && !alwaysLookAtTopChanged) { + return Response::RespContextError; + } + + if (cmd.has_always_reveal_top_card()) { + zone->setAlwaysRevealTopCard(cmd.always_reveal_top_card()); + event.set_always_reveal_top_card(cmd.always_reveal_top_card()); + } + if (cmd.has_always_look_at_top_card()) { + zone->setAlwaysLookAtTopCard(cmd.always_look_at_top_card()); + event.set_always_look_at_top_card(cmd.always_look_at_top_card()); + } + ges.enqueueGameEvent(event, playerId); + return Response::RespOk; +} + +void Server_AbstractPlayer::getInfo(ServerInfo_Player *info, + Server_AbstractParticipant *recipient, + bool omniscient, + bool withUserInfo) +{ + getProperties(*info->mutable_properties(), withUserInfo); + if (recipient == this) { + if (deck) { + info->set_deck_list(deck->writeToString_Native().toStdString()); + } + } + + for (Server_Arrow *arrow : arrows) { + arrow->getInfo(info->add_arrow_list()); + } + + for (Server_CardZone *zone : zones) { + zone->getInfo(info->add_zone_list(), recipient, omniscient); + } +} + +void Server_AbstractPlayer::getPlayerProperties(ServerInfo_PlayerProperties &result) +{ + result.set_conceded(conceded); + result.set_sideboard_locked(sideboardLocked); + result.set_ready_start(readyStart); + if (deck) { + result.set_deck_hash(deck->getDeckHash().toStdString()); + } +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h new file mode 100644 index 000000000..9d9809298 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h @@ -0,0 +1,164 @@ +#ifndef ABSTRACT_PLAYER_H +#define ABSTRACT_PLAYER_H + +#include "../serverinfo_user_container.h" +#include "server_abstract_participant.h" + +#include +#include + +class CardToMove; +class DeckList; +class Server_Arrow; +class Server_Card; +class Server_CardZone; +class Server_Counter; +struct MoveCardStruct; + +class Server_AbstractPlayer : public Server_AbstractParticipant +{ + Q_OBJECT +private: + class MoveCardCompareFunctor; + QMap arrows; + + void sendCreateTokenEvents(Server_CardZone *zone, Server_Card *card, int xCoord, int yCoord, GameEventStorage &ges); + void getPlayerProperties(ServerInfo_PlayerProperties &result) override; + +protected: + bool conceded; + DeckList *deck; + bool sideboardLocked; + QMap zones; + bool readyStart; + int nextCardId; + + void revealTopCardIfNeeded(Server_CardZone *zone, GameEventStorage &ges); + +public: + Server_AbstractPlayer(Server_Game *_game, + int _playerId, + const ServerInfo_User &_userInfo, + bool _judge, + Server_AbstractUserInterface *_handler); + ~Server_AbstractPlayer() override; + void prepareDestroy() override; + const DeckList *getDeckList() const + { + return deck; + } + bool getReadyStart() const + { + return readyStart; + } + void setReadyStart(bool _readyStart) + { + readyStart = _readyStart; + } + bool getConceded() const + { + return conceded; + } + void setConceded(bool _conceded) + { + conceded = _conceded; + } + + const QMap &getZones() const + { + return zones; + } + const QMap &getArrows() const + { + return arrows; + } + + int newCardId(); + int newArrowId() const; + + void addZone(Server_CardZone *zone); + void addArrow(Server_Arrow *arrow); + void updateArrowId(int id); + bool deleteArrow(int arrowId); + + virtual void setupZones(); + virtual void clearZones(); + + Response::ResponseCode moveCard(GameEventStorage &ges, + Server_CardZone *startzone, + const QList &_cards, + Server_CardZone *targetzone, + int xCoord, + int yCoord, + bool fixFreeSpaces = true, + bool undoingDraw = false, + bool isReversed = false); + + void processMoveCard(GameEventStorage &ges, + Server_CardZone *startzone, + Server_CardZone *targetzone, + MoveCardStruct cardStruct, + int xCoord, + int yCoord, + int &xIndex, + bool &revealTopStart, + bool &revealTopTarget, + bool isReversed, + bool undoingDraw); + + virtual void onCardBeingMoved(GameEventStorage &ges, + const MoveCardStruct &cardStruct, + Server_CardZone *startzone, + Server_CardZone *targetzone, + bool undoingDraw); + + void unattachCard(GameEventStorage &ges, Server_Card *card); + Response::ResponseCode setCardAttrHelper(GameEventStorage &ges, + int targetPlayerId, + const QString &zone, + int cardId, + CardAttribute attribute, + const QString &attrValue, + Server_Card *unzonedCard = nullptr); + + virtual Response::ResponseCode + cmdConcede(const Command_Concede &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdUnconcede(const Command_Unconcede &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdReadyStart(const Command_ReadyStart &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdRollDie(const Command_RollDie &cmd, ResponseContainer &rc, GameEventStorage &ges) const override; + virtual Response::ResponseCode + cmdMoveCard(const Command_MoveCard &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdFlipCard(const Command_FlipCard &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdAttachCard(const Command_AttachCard &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdCreateToken(const Command_CreateToken &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdCreateArrow(const Command_CreateArrow &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdDeleteArrow(const Command_DeleteArrow &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdSetCardAttr(const Command_SetCardAttr &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdSetCardCounter(const Command_SetCardCounter &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdIncCardCounter(const Command_IncCardCounter &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdDumpZone(const Command_DumpZone &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdRevealCards(const Command_RevealCards &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd, + ResponseContainer &rc, + GameEventStorage &ges) override; + + virtual void getInfo(ServerInfo_Player *info, + Server_AbstractParticipant *playerWhosAsking, + bool omniscient, + bool withUserInfo) override; +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_arrow.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_arrow.cpp new file mode 100644 index 000000000..f6787baa2 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_arrow.cpp @@ -0,0 +1,35 @@ +#include "server_arrow.h" + +#include "server_card.h" +#include "server_cardzone.h" +#include "server_player.h" + +#include + +Server_Arrow::Server_Arrow(int _id, + Server_Card *_startCard, + Server_ArrowTarget *_targetItem, + const color &_arrowColor, + int _phaseCreated, + int _phaseDeleted) + : id(_id), startCard(_startCard), targetItem(_targetItem), arrowColor(_arrowColor), phaseCreated(_phaseCreated), + phaseDeleted(_phaseDeleted) +{ +} + +void Server_Arrow::getInfo(ServerInfo_Arrow *info) +{ + info->set_id(id); + info->set_start_player_id(startCard->getZone()->getPlayer()->getPlayerId()); + info->set_start_zone(startCard->getZone()->getName().toStdString()); + info->set_start_card_id(startCard->getId()); + info->mutable_arrow_color()->CopyFrom(arrowColor); + + auto *targetCard = qobject_cast(targetItem); + if (targetCard) { + info->set_target_player_id(targetCard->getZone()->getPlayer()->getPlayerId()); + info->set_target_zone(targetCard->getZone()->getName().toStdString()); + info->set_target_card_id(targetCard->getId()); + } else + info->set_target_player_id(static_cast(targetItem)->getPlayerId()); +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_arrow.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_arrow.h new file mode 100644 index 000000000..1f302358b --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_arrow.h @@ -0,0 +1,62 @@ +#ifndef SERVER_ARROW_H +#define SERVER_ARROW_H + +#include + +class Server_Card; +class Server_ArrowTarget; +class ServerInfo_Arrow; + +class Server_Arrow +{ +private: + int id; + Server_Card *startCard; + Server_ArrowTarget *targetItem; + color arrowColor; + int phaseCreated, phaseDeleted; + +public: + Server_Arrow(int _id, + Server_Card *_startCard, + Server_ArrowTarget *_targetItem, + const color &_arrowColor, + int _phaseCreated, + int _phaseDeleted); + int getId() const + { + return id; + } + void setId(int _id) + { + id = _id; + } + Server_Card *getStartCard() const + { + return startCard; + } + void setStartCard(Server_Card *startCard_) + { + startCard = startCard_; + } + Server_ArrowTarget *getTargetItem() const + { + return targetItem; + } + void setTargetItem(Server_ArrowTarget *targetItem_) + { + targetItem = targetItem_; + } + const color &getColor() const + { + return arrowColor; + } + bool checkPhaseDeletion(int phase) const // returns true if the arrow should be deleted in this phase + { + return phase < phaseCreated || phase >= phaseDeleted; + } + + void getInfo(ServerInfo_Arrow *info); +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_arrowtarget.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_arrowtarget.cpp new file mode 100644 index 000000000..1b3385d58 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_arrowtarget.cpp @@ -0,0 +1,2 @@ + +#include "server_arrowtarget.h" diff --git a/common/server_arrowtarget.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_arrowtarget.h similarity index 61% rename from common/server_arrowtarget.h rename to libcockatrice_network/libcockatrice/network/server/remote/game/server_arrowtarget.h index f5783d5b8..5a080018d 100644 --- a/common/server_arrowtarget.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_arrowtarget.h @@ -3,8 +3,9 @@ #include -class Server_ArrowTarget : public QObject { - Q_OBJECT +class Server_ArrowTarget : public QObject +{ + Q_OBJECT }; #endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp new file mode 100644 index 000000000..86ff2f008 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp @@ -0,0 +1,174 @@ +/*************************************************************************** + * Copyright (C) 2008 by Max-Wilhelm Bruker * + * brukie@laptop * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "server_card.h" + +#include "server_cardzone.h" +#include "server_player.h" + +#include +#include +#include +#include + +Server_Card::Server_Card(const CardRef &cardRef, int _id, int _coord_x, int _coord_y, Server_CardZone *_zone) + : zone(_zone), id(_id), coord_x(_coord_x), coord_y(_coord_y), cardRef(cardRef), tapped(false), attacking(false), + facedown(false), destroyOnZoneChange(false), doesntUntap(false), parentCard(0), stashedCard(nullptr) +{ +} + +Server_Card::~Server_Card() +{ + // setParentCard(0) leads to the item being removed from our list, so we can't iterate properly + while (!attachedCards.isEmpty()) + attachedCards.first()->setParentCard(0); + + if (parentCard) + parentCard->removeAttachedCard(this); + + if (stashedCard) { + stashedCard->deleteLater(); + stashedCard = nullptr; + } +} + +void Server_Card::resetState(bool keepAnnotations) +{ + counters.clear(); + setTapped(false); + setAttacking(false); + setPT(QString()); + if (!keepAnnotations) { + setAnnotation(QString()); + } + setDoesntUntap(false); +} + +QString Server_Card::setAttribute(CardAttribute attribute, const QString &avalue, bool allCards) +{ + if (attribute == AttrTapped && avalue != "1" && allCards && doesntUntap) + return QVariant(tapped).toString(); + + return setAttribute(attribute, avalue); +} + +QString Server_Card::setAttribute(CardAttribute attribute, const QString &avalue, Event_SetCardAttr *event) +{ + if (event) + event->set_attribute(attribute); + + switch (attribute) { + case AttrTapped: { + setTapped(avalue == "1"); + break; + } + case AttrAttacking: + setAttacking(avalue == "1"); + break; + case AttrFaceDown: + setFaceDown(avalue == "1"); + break; + case AttrColor: + setColor(avalue); + break; + case AttrPT: + setPT(avalue); + if (event) + event->set_attr_value(getPT().toStdString()); + return getPT(); + case AttrAnnotation: + setAnnotation(avalue); + break; + case AttrDoesntUntap: + setDoesntUntap(avalue == "1"); + break; + } + if (event) + event->set_attr_value(avalue.toStdString()); + return avalue; +} + +void Server_Card::setCounter(int _id, int value, Event_SetCardCounter *event) +{ + if (value) + counters.insert(_id, value); + else + counters.remove(_id); + + if (event) { + event->set_counter_id(_id); + event->set_counter_value(value); + } +} + +void Server_Card::setParentCard(Server_Card *_parentCard) +{ + if (parentCard) + parentCard->removeAttachedCard(this); + parentCard = _parentCard; + if (parentCard) + parentCard->addAttachedCard(this); +} + +void Server_Card::getInfo(ServerInfo_Card *info) +{ + QString displayedName = facedown ? QString() : cardRef.name; + + info->set_id(id); + info->set_provider_id(cardRef.providerId.toStdString()); + info->set_name(displayedName.toStdString()); + info->set_x(coord_x); + info->set_y(coord_y); + if (facedown) { + info->set_face_down(true); + } + info->set_tapped(tapped); + if (attacking) { + info->set_attacking(true); + } + if (!color.isEmpty()) { + info->set_color(color.toStdString()); + } + if (!ptString.isEmpty()) { + info->set_pt(ptString.toStdString()); + } + if (!annotation.isEmpty()) { + info->set_annotation(annotation.toStdString()); + } + if (destroyOnZoneChange) { + info->set_destroy_on_zone_change(true); + } + if (doesntUntap) { + info->set_doesnt_untap(true); + } + + QMapIterator cardCounterIterator(counters); + while (cardCounterIterator.hasNext()) { + cardCounterIterator.next(); + ServerInfo_CardCounter *counterInfo = info->add_counter_list(); + counterInfo->set_id(cardCounterIterator.key()); + counterInfo->set_value(cardCounterIterator.value()); + } + + if (parentCard) { + info->set_attach_player_id(parentCard->getZone()->getPlayer()->getPlayerId()); + info->set_attach_zone(parentCard->getZone()->getName().toStdString()); + info->set_attach_card_id(parentCard->getId()); + } +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h new file mode 100644 index 000000000..bc326bbc4 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h @@ -0,0 +1,227 @@ +/*************************************************************************** + * Copyright (C) 2008 by Max-Wilhelm Bruker * + * brukie@laptop * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef SERVER_CARD_H +#define SERVER_CARD_H + +#include "server_arrowtarget.h" + +#include +#include +#include +#include +#include + +class Server_CardZone; +class Event_SetCardCounter; +class Event_SetCardAttr; + +class Server_Card : public Server_ArrowTarget +{ + Q_OBJECT +private: + Server_CardZone *zone; + int id; + int coord_x, coord_y; + CardRef cardRef; + QMap counters; + bool tapped; + bool attacking; + bool facedown; + QString color; + QString ptString; + QString annotation; + bool destroyOnZoneChange; + bool doesntUntap; + + Server_Card *parentCard; + QList attachedCards; + Server_Card *stashedCard; + +public: + Server_Card(const CardRef &cardRef, int _id, int _coord_x, int _coord_y, Server_CardZone *_zone = nullptr); + ~Server_Card() override; + + Server_CardZone *getZone() const + { + return zone; + } + void setZone(Server_CardZone *_zone) + { + zone = _zone; + } + + int getId() const + { + return id; + } + CardRef getCardRef() const + { + return cardRef; + } + QString getProviderId() const + { + return cardRef.providerId; + } + int getX() const + { + return coord_x; + } + int getY() const + { + return coord_y; + } + QString getName() const + { + return cardRef.name; + } + const QMap &getCounters() const + { + return counters; + } + int getCounter(int counter_id) const + { + return counters.value(counter_id, 0); + } + bool getTapped() const + { + return tapped; + } + bool getAttacking() const + { + return attacking; + } + bool getFaceDown() const + { + return facedown; + } + QString getColor() const + { + return color; + } + QString getPT() const + { + return ptString; + } + QString getAnnotation() const + { + return annotation; + } + bool getDoesntUntap() const + { + return doesntUntap; + } + bool getDestroyOnZoneChange() const + { + return destroyOnZoneChange; + } + Server_Card *getParentCard() const + { + return parentCard; + } + const QList &getAttachedCards() const + { + return attachedCards; + } + + void setId(int _id) + { + id = _id; + } + void setCoords(int x, int y) + { + coord_x = x; + coord_y = y; + } + void setCardRef(const CardRef &_cardRef) + { + cardRef = _cardRef; + } + void setCounter(int _id, int value, Event_SetCardCounter *event = nullptr); + void setTapped(bool _tapped) + { + tapped = _tapped; + } + void setAttacking(bool _attacking) + { + attacking = _attacking; + } + void setFaceDown(bool _facedown) + { + facedown = _facedown; + } + void setColor(const QString &_color) + { + color = _color; + } + void setPT(const QString &_pt) + { + ptString = _pt; + } + void setAnnotation(const QString &_annotation) + { + annotation = _annotation; + } + void setDestroyOnZoneChange(bool _destroy) + { + destroyOnZoneChange = _destroy; + } + void setDoesntUntap(bool _doesntUntap) + { + doesntUntap = _doesntUntap; + } + void setParentCard(Server_Card *_parentCard); + void addAttachedCard(Server_Card *card) + { + attachedCards.append(card); + } + void removeAttachedCard(Server_Card *card) + { + attachedCards.removeOne(card); + } + void setStashedCard(Server_Card *card) + { + // setStashedCard should only be called on creation of a new card, so + // there should never be an already existing stashed card. + Q_ASSERT(!stashedCard); + + // Stashed cards can't themselves have stashed cards, and tokens can't + // be stashed. + if (card->stashedCard || card->getDestroyOnZoneChange()) { + stashedCard = card->takeStashedCard(); + card->deleteLater(); + } else { + stashedCard = card; + } + } + Server_Card *takeStashedCard() + { + Server_Card *oldStashedCard = stashedCard; + stashedCard = nullptr; + return oldStashedCard; + } + + void resetState(bool keepAnnotations = false); + QString setAttribute(CardAttribute attribute, const QString &avalue, bool allCards); + QString setAttribute(CardAttribute attribute, const QString &avalue, Event_SetCardAttr *event = nullptr); + + void getInfo(ServerInfo_Card *info); +}; + +#endif diff --git a/common/server_cardzone.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_cardzone.cpp similarity index 68% rename from common/server_cardzone.cpp rename to libcockatrice_network/libcockatrice/network/server/remote/game/server_cardzone.cpp index c6456f5fd..f2a35e548 100644 --- a/common/server_cardzone.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_cardzone.cpp @@ -18,20 +18,21 @@ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * ***************************************************************************/ #include "server_cardzone.h" -#include "server_card.h" -#include "server_player.h" -#include "rng_abstract.h" -#include -#include -#include "pb/command_move_card.pb.h" -Server_CardZone::Server_CardZone(Server_Player *_player, const QString &_name, bool _has_coords, ServerInfo_Zone::ZoneType _type) - : player(_player), - name(_name), - has_coords(_has_coords), - type(_type), - cardsBeingLookedAt(0), - alwaysRevealTopCard(false) +#include "server_abstract_player.h" +#include "server_card.h" + +#include +#include +#include +#include + +Server_CardZone::Server_CardZone(Server_AbstractPlayer *_player, + const QString &_name, + bool _has_coords, + ServerInfo_Zone::ZoneType _type) + : player(_player), name(_name), has_coords(_has_coords), type(_type), cardsBeingLookedAt(0), + alwaysRevealTopCard(false), alwaysLookAtTopCard(false) { } @@ -41,36 +42,52 @@ Server_CardZone::~Server_CardZone() clear(); } -void Server_CardZone::shuffle() +void Server_CardZone::shuffle(int start, int end) { + cardsBeingLookedAt = 0; + // Size 0 or 1 decks are sorted - if (cards.size() < 2) return; - for (int i = cards.size() - 1; i > 0; i--){ - int j = rng->rand(0, i); - cards.swap(j,i); + if (cards.size() < 2) + return; + + // Negative numbers signify positions starting at the end of the + // zone convert these to actual indexes. + if (end < 0) + end += cards.size(); + + if (start < 0) + start += cards.size(); + + if (start < 0 || end < 0 || start >= cards.size() || end >= cards.size()) + return; + + for (int i = end; i > start; i--) { + int j = rng->rand(start, i); + cards.swapItemsAt(j, i); } playersWithWritePermission.clear(); } - void Server_CardZone::removeCardFromCoordMap(Server_Card *card, int oldX, int oldY) { if (oldX < 0) return; - + const int baseX = (oldX / 3) * 3; QMap &coordMap = coordinateMap[oldY]; - + if (coordMap.contains(baseX) && coordMap.contains(baseX + 1) && coordMap.contains(baseX + 2)) // If the removal of this card has opened up a previously full pile... freePilesMap[oldY].insert(coordMap.value(baseX)->getName(), baseX); - + coordMap.remove(oldX); - - if (!(coordMap.contains(baseX) && coordMap.value(baseX)->getName() == card->getName()) && !(coordMap.contains(baseX + 1) && coordMap.value(baseX + 1)->getName() == card->getName()) && !(coordMap.contains(baseX + 2) && coordMap.value(baseX + 2)->getName() == card->getName())) + + if (!(coordMap.contains(baseX) && coordMap.value(baseX)->getName() == card->getName()) && + !(coordMap.contains(baseX + 1) && coordMap.value(baseX + 1)->getName() == card->getName()) && + !(coordMap.contains(baseX + 2) && coordMap.value(baseX + 2)->getName() == card->getName())) // If this card was the last one with this name... freePilesMap[oldY].remove(card->getName(), baseX); - + if (!coordMap.contains(baseX) && !coordMap.contains(baseX + 1) && !coordMap.contains(baseX + 2)) { // If the removal of this card has freed a whole pile, i.e. it was the last card in it... if (baseX < freeSpaceMap[oldY]) @@ -82,16 +99,17 @@ void Server_CardZone::insertCardIntoCoordMap(Server_Card *card, int x, int y) { if (x < 0) return; - + coordinateMap[y].insert(x, card); if (!(x % 3)) { - if (!freePilesMap[y].contains(card->getName(), x) && card->getAttachedCards().isEmpty()) + if (!card->getFaceDown() && !freePilesMap[y].contains(card->getName(), x) && card->getAttachedCards().isEmpty()) freePilesMap[y].insert(card->getName(), x); if (freeSpaceMap[y] == x) { int nextFreeX = x; do { nextFreeX += 3; - } while (coordinateMap[y].contains(nextFreeX) || coordinateMap[y].contains(nextFreeX + 1) || coordinateMap[y].contains(nextFreeX + 2)); + } while (coordinateMap[y].contains(nextFreeX) || coordinateMap[y].contains(nextFreeX + 1) || + coordinateMap[y].contains(nextFreeX + 2)); freeSpaceMap[y] = nextFreeX; } } else if (!((x - 2) % 3)) { @@ -101,13 +119,24 @@ void Server_CardZone::insertCardIntoCoordMap(Server_Card *card, int x, int y) } int Server_CardZone::removeCard(Server_Card *card) +{ + bool wasLookedAt; + return removeCard(card, wasLookedAt); +} + +int Server_CardZone::removeCard(Server_Card *card, bool &wasLookedAt) { int index = cards.indexOf(card); + wasLookedAt = isCardAtPosLookedAt(index); + if (wasLookedAt && cardsBeingLookedAt > 0) { + cardsBeingLookedAt -= 1; + } cards.removeAt(index); - if (has_coords) + if (has_coords) { removeCardFromCoordMap(card, card->getX(), card->getY()); - card->setZone(0); - + } + card->setZone(nullptr); + return index; } @@ -121,33 +150,41 @@ Server_Card *Server_CardZone::getCard(int id, int *position, bool remove) *position = i; if (remove) { cards.removeAt(i); - tmp->setZone(0); + tmp->setZone(nullptr); } return tmp; } } - return NULL; + return nullptr; } else { if ((id >= cards.size()) || (id < 0)) - return NULL; + return nullptr; Server_Card *tmp = cards[id]; if (position) *position = id; if (remove) { cards.removeAt(id); - tmp->setZone(0); + tmp->setZone(nullptr); } return tmp; } } -int Server_CardZone::getFreeGridColumn(int x, int y, const QString &cardName) const +bool Server_CardZone::isCardAtPosLookedAt(int pos) const +{ + return type == ServerInfo_Zone::HiddenZone && (cardsBeingLookedAt == -1 || cardsBeingLookedAt > pos); +} + +int Server_CardZone::getFreeGridColumn(int x, int y, const QString &cardName, bool dontStackSameName) const { const QMap &coordMap = coordinateMap.value(y); if (x == -1) { - if (freePilesMap[y].contains(cardName)) { + if (!dontStackSameName && freePilesMap[y].contains(cardName)) { x = (freePilesMap[y].value(cardName) / 3) * 3; - if (!coordMap.contains(x)) + + if (coordMap.contains(x) && (coordMap[x]->getFaceDown() || !coordMap[x]->getAttachedCards().isEmpty())) { + // don't pile up on: 1. facedown cards 2. cards with attached cards + } else if (!coordMap.contains(x)) return x; else if (!coordMap.contains(x + 1)) return x + 1; @@ -176,7 +213,7 @@ int Server_CardZone::getFreeGridColumn(int x, int y, const QString &cardName) co return resultX; } - + return freeSpaceMap[y]; } @@ -184,7 +221,7 @@ bool Server_CardZone::isColumnStacked(int x, int y) const { if (!has_coords) return false; - + return coordinateMap[y].contains((x / 3) * 3 + 1); } @@ -192,13 +229,13 @@ bool Server_CardZone::isColumnEmpty(int x, int y) const { if (!has_coords) return true; - + return !coordinateMap[y].contains((x / 3) * 3); } void Server_CardZone::moveCardInRow(GameEventStorage &ges, Server_Card *card, int x, int y) { - CardToMove *cardToMove = new CardToMove; + auto *cardToMove = new CardToMove; cardToMove->set_card_id(card->getId()); player->moveCard(ges, this, QList() << cardToMove, this, x, y, false, false); delete cardToMove; @@ -208,17 +245,17 @@ void Server_CardZone::fixFreeSpaces(GameEventStorage &ges) { if (!has_coords) return; - - QSet > placesToLook; - for (int i = 0; i < cards.size(); ++i) - placesToLook.insert(QPair((cards[i]->getX() / 3) * 3, cards[i]->getY())); - - QSetIterator > placeIterator(placesToLook); + + QSet> placesToLook; + for (auto &card : cards) + placesToLook.insert(QPair((card->getX() / 3) * 3, card->getY())); + + QSetIterator> placeIterator(placesToLook); while (placeIterator.hasNext()) { const QPair &foo = placeIterator.next(); int baseX = foo.first; int y = foo.second; - + if (!coordinateMap[y].contains(baseX)) { if (coordinateMap[y].contains(baseX + 1)) moveCardInRow(ges, coordinateMap[y].value(baseX + 1), baseX, y); @@ -237,7 +274,7 @@ void Server_CardZone::updateCardCoordinates(Server_Card *card, int oldX, int old { if (!has_coords) return; - + if (oldX != -1) removeCardFromCoordMap(card, oldX, oldY); insertCardIntoCoordMap(card, card->getX(), card->getY()); @@ -251,23 +288,25 @@ void Server_CardZone::insertCard(Server_Card *card, int x, int y) insertCardIntoCoordMap(card, x, y); } else { card->setCoords(0, 0); - if (x == -1) - cards.append(card); - else + if (0 <= x && x < cards.length()) { cards.insert(x, card); + } else { + cards.append(card); + } } card->setZone(this); } void Server_CardZone::clear() { - for (int i = 0; i < cards.size(); i++) - delete cards.at(i); + for (auto card : cards) + delete card; cards.clear(); coordinateMap.clear(); freePilesMap.clear(); freeSpaceMap.clear(); playersWithWritePermission.clear(); + cardsBeingLookedAt = 0; } void Server_CardZone::addWritePermission(int playerId) @@ -275,17 +314,20 @@ void Server_CardZone::addWritePermission(int playerId) playersWithWritePermission.insert(playerId); } -void Server_CardZone::getInfo(ServerInfo_Zone *info, Server_Player *playerWhosAsking, bool omniscient) +void Server_CardZone::getInfo(ServerInfo_Zone *info, Server_AbstractParticipant *recipient, bool omniscient) { info->set_name(name.toStdString()); info->set_type(type); info->set_with_coords(has_coords); - info->set_card_count(cards.size()); + info->set_card_count(static_cast(cards.size())); info->set_always_reveal_top_card(alwaysRevealTopCard); - if ( - (((playerWhosAsking == player) || omniscient) && (type != ServerInfo_Zone::HiddenZone)) - || ((playerWhosAsking != player) && (type == ServerInfo_Zone::PublicZone)) - ) { + info->set_always_look_at_top_card(alwaysLookAtTopCard); + + const bool selfPlayerAsking = recipient == player || omniscient; + const bool zonesSelfCanSee = type != ServerInfo_Zone::HiddenZone; + const bool otherPlayerAsking = recipient != player; + const bool zonesOthersCanSee = type == ServerInfo_Zone::PublicZone; + if ((selfPlayerAsking && zonesSelfCanSee) || (otherPlayerAsking && zonesOthersCanSee)) { QListIterator cardIterator(cards); while (cardIterator.hasNext()) cardIterator.next()->getInfo(info->add_card_list()); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_cardzone.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_cardzone.h new file mode 100644 index 000000000..77fea54ca --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_cardzone.h @@ -0,0 +1,126 @@ +/*************************************************************************** + * Copyright (C) 2008 by Max-Wilhelm Bruker * + * brukie@laptop * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef SERVER_CARDZONE_H +#define SERVER_CARDZONE_H + +#include +#include +#include +#include + +class Server_Card; +class Server_AbstractPlayer; +class Server_AbstractParticipant; +class Server_Game; +class GameEventStorage; + +class Server_CardZone +{ +private: + Server_AbstractPlayer *player; + QString name; + bool has_coords; // having coords means this zone has x and y coordinates + ServerInfo_Zone::ZoneType type; + int cardsBeingLookedAt; + QSet playersWithWritePermission; + bool alwaysRevealTopCard; + bool alwaysLookAtTopCard; + QList cards; + QMap> coordinateMap; // y -> (x -> card) + QMap> freePilesMap; // y -> (cardName -> x) + QMap freeSpaceMap; // y -> x + void removeCardFromCoordMap(Server_Card *card, int oldX, int oldY); + void insertCardIntoCoordMap(Server_Card *card, int x, int y); + +public: + Server_CardZone(Server_AbstractPlayer *_player, + const QString &_name, + bool _has_coords, + ServerInfo_Zone::ZoneType _type); + ~Server_CardZone(); + + [[nodiscard]] const QList &getCards() const + { + return cards; + } + int removeCard(Server_Card *card); + int removeCard(Server_Card *card, bool &wasLookedAt); + Server_Card *getCard(int id, int *position = nullptr, bool remove = false); + + [[nodiscard]] int getCardsBeingLookedAt() const + { + return cardsBeingLookedAt; + } + void setCardsBeingLookedAt(int _cardsBeingLookedAt) + { + cardsBeingLookedAt = qMax(0, _cardsBeingLookedAt); + } + [[nodiscard]] bool isCardAtPosLookedAt(int pos) const; + [[nodiscard]] bool hasCoords() const + { + return has_coords; + } + [[nodiscard]] ServerInfo_Zone::ZoneType getType() const + { + return type; + } + [[nodiscard]] QString getName() const + { + return name; + } + [[nodiscard]] Server_AbstractPlayer *getPlayer() const + { + return player; + } + void getInfo(ServerInfo_Zone *info, Server_AbstractParticipant *recipient, bool omniscient); + + [[nodiscard]] int getFreeGridColumn(int x, int y, const QString &cardName, bool dontStackSameName) const; + [[nodiscard]] bool isColumnEmpty(int x, int y) const; + [[nodiscard]] bool isColumnStacked(int x, int y) const; + void fixFreeSpaces(GameEventStorage &ges); + void moveCardInRow(GameEventStorage &ges, Server_Card *card, int x, int y); + void insertCard(Server_Card *card, int x, int y); + void updateCardCoordinates(Server_Card *card, int oldX, int oldY); + void shuffle(int start = 0, int end = -1); + void clear(); + void addWritePermission(int playerId); + [[nodiscard]] const QSet &getPlayersWithWritePermission() const + { + return playersWithWritePermission; + } + [[nodiscard]] bool getAlwaysRevealTopCard() const + { + return alwaysRevealTopCard; + } + void setAlwaysRevealTopCard(bool _alwaysRevealTopCard) + { + alwaysRevealTopCard = _alwaysRevealTopCard; + } + [[nodiscard]] bool getAlwaysLookAtTopCard() const + { + return alwaysLookAtTopCard; + } + void setAlwaysLookAtTopCard(bool _alwaysLookAtTopCard) + { + alwaysLookAtTopCard = _alwaysLookAtTopCard; + } +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp new file mode 100644 index 000000000..b18e11c2b --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp @@ -0,0 +1,17 @@ +#include "server_counter.h" + +#include + +Server_Counter::Server_Counter(int _id, const QString &_name, const color &_counterColor, int _radius, int _count) + : id(_id), name(_name), counterColor(_counterColor), radius(_radius), count(_count) +{ +} + +void Server_Counter::getInfo(ServerInfo_Counter *info) +{ + info->set_id(id); + info->set_name(name.toStdString()); + info->mutable_counter_color()->CopyFrom(counterColor); + info->set_radius(radius); + info->set_count(count); +} diff --git a/common/server_counter.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h similarity index 67% rename from common/server_counter.h rename to libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h index 42483b9da..55aad991c 100644 --- a/common/server_counter.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h @@ -21,28 +21,50 @@ #define SERVER_COUNTER_H #include -#include "pb/color.pb.h" +#include class ServerInfo_Counter; -class Server_Counter { +class Server_Counter +{ protected: - int id; - QString name; - color counterColor; - int radius; - int count; + int id; + QString name; + color counterColor; + int radius; + int count; + public: - Server_Counter(int _id, const QString &_name, const color &_counterColor, int _radius, int _count = 0); - ~Server_Counter() { } - int getId() const { return id; } - QString getName() const { return name; } - const color &getColor() const { return counterColor; } - int getRadius() const { return radius; } - int getCount() const { return count; } - void setCount(int _count) { count = _count; } - - void getInfo(ServerInfo_Counter *info); + Server_Counter(int _id, const QString &_name, const color &_counterColor, int _radius, int _count = 0); + ~Server_Counter() + { + } + int getId() const + { + return id; + } + QString getName() const + { + return name; + } + const color &getColor() const + { + return counterColor; + } + int getRadius() const + { + return radius; + } + int getCount() const + { + return count; + } + void setCount(int _count) + { + count = _count; + } + + void getInfo(ServerInfo_Counter *info); }; #endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp new file mode 100644 index 000000000..2224ddb13 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -0,0 +1,872 @@ +/*************************************************************************** + * Copyright (C) 2008 by Max-Wilhelm Bruker * + * brukie@laptop * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "server_game.h" + +#include "../server.h" +#include "../server_database_interface.h" +#include "../server_protocolhandler.h" +#include "../server_room.h" +#include "libcockatrice/protocol/pb/command_move_card.pb.h" +#include "server_abstract_player.h" +#include "server_arrow.h" +#include "server_card.h" +#include "server_cardzone.h" +#include "server_player.h" +#include "server_spectator.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Server_Game::Server_Game(const ServerInfo_User &_creatorInfo, + int _gameId, + const QString &_description, + const QString &_password, + int _maxPlayers, + const QList &_gameTypes, + bool _onlyBuddies, + bool _onlyRegistered, + bool _spectatorsAllowed, + bool _spectatorsNeedPassword, + bool _spectatorsCanTalk, + bool _spectatorsSeeEverything, + int _startingLifeTotal, + bool _shareDecklistsOnLoad, + Server_Room *_room) + : QObject(), room(_room), nextPlayerId(0), hostId(0), creatorInfo(new ServerInfo_User(_creatorInfo)), + gameStarted(false), gameClosed(false), gameId(_gameId), password(_password), maxPlayers(_maxPlayers), + gameTypes(_gameTypes), activePlayer(-1), activePhase(-1), onlyBuddies(_onlyBuddies), + onlyRegistered(_onlyRegistered), spectatorsAllowed(_spectatorsAllowed), + spectatorsNeedPassword(_spectatorsNeedPassword), spectatorsCanTalk(_spectatorsCanTalk), + spectatorsSeeEverything(_spectatorsSeeEverything), startingLifeTotal(_startingLifeTotal), + shareDecklistsOnLoad(_shareDecklistsOnLoad), inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0), + firstGameStarted(false), turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr), + gameMutex() +{ + currentReplay = new GameReplay; + currentReplay->set_replay_id(room->getServer()->getDatabaseInterface()->getNextReplayId()); + description = _description.simplified(); + + connect(this, &Server_Game::sigStartGameIfReady, this, &Server_Game::doStartGameIfReady, Qt::QueuedConnection); + + getInfo(*currentReplay->mutable_game_info()); + + if (room->getServer()->getGameShouldPing()) { + pingClock = new QTimer(this); + connect(pingClock, &QTimer::timeout, this, &Server_Game::pingClockTimeout); + pingClock->start(1000); + } +} + +Server_Game::~Server_Game() +{ + room->gamesLock.lockForWrite(); + gameMutex.lock(); + + gameClosed = true; + sendGameEventContainer(prepareGameEvent(Event_GameClosed(), -1)); + for (auto *participant : participants.values()) { + participant->prepareDestroy(); + } + participants.clear(); + + room->removeGame(this); + delete creatorInfo; + creatorInfo = 0; + + gameMutex.unlock(); + room->gamesLock.unlock(); + currentReplay->set_duration_seconds(secondsElapsed - startTimeOfThisGame); + replayList.append(currentReplay); + storeGameInformation(); + + for (auto *replay : replayList) { + delete replay; + } + replayList.clear(); + + room = nullptr; + currentReplay = nullptr; + creatorInfo = nullptr; + + if (pingClock) { + delete pingClock; + pingClock = nullptr; + } + + qDebug() << "Server_Game destructor: gameId=" << gameId; + deleteLater(); +} + +void Server_Game::storeGameInformation() +{ + const ServerInfo_Game &gameInfo = replayList.first()->game_info(); + + Event_ReplayAdded replayEvent; + ServerInfo_ReplayMatch *replayMatchInfo = replayEvent.mutable_match_info(); + replayMatchInfo->set_game_id(gameInfo.game_id()); + replayMatchInfo->set_room_name(room->getName().toStdString()); + replayMatchInfo->set_time_started(QDateTime::currentDateTime().addSecs(-secondsElapsed).toSecsSinceEpoch()); + replayMatchInfo->set_length(secondsElapsed); + replayMatchInfo->set_game_name(gameInfo.description()); + + const QStringList &allGameTypes = room->getGameTypes(); + QStringList _gameTypes; + for (int i = gameInfo.game_types_size() - 1; i >= 0; --i) + _gameTypes.append(allGameTypes[gameInfo.game_types(i)]); + + for (const auto &playerName : allPlayersEver) { + replayMatchInfo->add_player_names(playerName.toStdString()); + } + + for (int i = 0; i < replayList.size(); ++i) { + ServerInfo_Replay *replayInfo = replayMatchInfo->add_replay_list(); + replayInfo->set_replay_id(replayList[i]->replay_id()); + replayInfo->set_replay_name(gameInfo.description()); + replayInfo->set_duration(replayList[i]->duration_seconds()); + } + + SessionEvent *sessionEvent = Server_ProtocolHandler::prepareSessionEvent(replayEvent); + Server *server = room->getServer(); + server->clientsLock.lockForRead(); + for (auto userName : allPlayersEver + allSpectatorsEver) { + Server_AbstractUserInterface *userHandler = server->findUser(userName); + if (userHandler && server->getStoreReplaysEnabled()) + userHandler->sendProtocolItem(*sessionEvent); + } + server->clientsLock.unlock(); + delete sessionEvent; + + if (server->getStoreReplaysEnabled()) { + server->getDatabaseInterface()->storeGameInformation(room->getName(), _gameTypes, gameInfo, allPlayersEver, + allSpectatorsEver, replayList); + } +} + +void Server_Game::pingClockTimeout() +{ + QMutexLocker locker(&gameMutex); + ++secondsElapsed; + + GameEventStorage ges; + ges.setGameEventContext(Context_PingChanged()); + + bool allPlayersInactive = true; + int playerCount = 0; + for (auto *participant : participants) { + if (participant == nullptr) + continue; + + if (!participant->isSpectator()) { + ++playerCount; + } + + if (participant->updatePingTime()) { + Event_PlayerPropertiesChanged event; + event.mutable_player_properties()->set_ping_seconds(participant->getPingTime()); + ges.enqueueGameEvent(event, participant->getPlayerId()); + } + + if ((participant->getPingTime() != -1) && + (!participant->isSpectator() || participant->getPlayerId() == hostId)) { + allPlayersInactive = false; + } + } + ges.sendToGame(this); + + const int maxTime = room->getServer()->getMaxGameInactivityTime(); + if (allPlayersInactive) { + if (((maxTime > 0) && (++inactivityCounter >= maxTime)) || (playerCount < maxPlayers)) { + deleteLater(); + } + } else { + inactivityCounter = 0; + } +} + +QMap Server_Game::getPlayers() const // copies pointers to new map +{ + QMap players; + QMutexLocker locker(&gameMutex); + for (int id : participants.keys()) { + auto *participant = participants[id]; + if (!participant->isSpectator()) { + players[id] = static_cast(participant); + } + } + return players; +} + +Server_AbstractPlayer *Server_Game::getPlayer(int id) const +{ + auto *participant = participants.value(id); + if (participant && !participant->isSpectator()) { + return static_cast(participant); + } else { + return nullptr; + } +} + +int Server_Game::getPlayerCount() const +{ + return participants.size() - getSpectatorCount(); +} + +int Server_Game::getSpectatorCount() const +{ + QMutexLocker locker(&gameMutex); + + int result = 0; + for (Server_AbstractParticipant *participant : participants.values()) { + if (participant->isSpectator()) + ++result; + } + return result; +} + +void Server_Game::createGameStateChangedEvent(Event_GameStateChanged *event, + Server_AbstractParticipant *recipient, + bool omniscient, + bool withUserInfo) +{ + event->set_seconds_elapsed(secondsElapsed); + if (gameStarted) { + event->set_game_started(true); + event->set_active_player_id(0); + event->set_active_phase(0); + } else + event->set_game_started(false); + + for (Server_AbstractParticipant *participant : participants.values()) { + participant->getInfo(event->add_player_list(), recipient, omniscient, withUserInfo); + } +} + +void Server_Game::sendGameStateToPlayers() +{ + // game state information for replay and omniscient spectators + Event_GameStateChanged omniscientEvent; + createGameStateChangedEvent(&omniscientEvent, nullptr, true, false); + + GameEventContainer *replayCont = prepareGameEvent(omniscientEvent, -1); + replayCont->set_seconds_elapsed(secondsElapsed - startTimeOfThisGame); + replayCont->clear_game_id(); + currentReplay->add_event_list()->CopyFrom(*replayCont); + delete replayCont; + + // If spectators are not omniscient, we need an additional createGameStateChangedEvent call, otherwise we can use + // the data we used for the replay. All spectators are equal, so we don't need to make a createGameStateChangedEvent + // call for each one. + Event_GameStateChanged spectatorNormalEvent; + createGameStateChangedEvent(&spectatorNormalEvent, nullptr, false, false); + + // send game state info to clients according to their role in the game + for (auto *participant : participants.values()) { + GameEventContainer *gec; + if (participant->isSpectator()) { + if (spectatorsSeeEverything || participant->isJudge()) { + gec = prepareGameEvent(omniscientEvent, -1); + } else { + gec = prepareGameEvent(spectatorNormalEvent, -1); + } + } else { + Event_GameStateChanged event; + createGameStateChangedEvent(&event, participant, false, false); + + gec = prepareGameEvent(event, -1); + } + participant->sendGameEvent(*gec); + delete gec; + } +} + +void Server_Game::doStartGameIfReady(bool forceStartGame) +{ + Server_DatabaseInterface *databaseInterface = room->getServer()->getDatabaseInterface(); + QMutexLocker locker(&gameMutex); + + if (getPlayerCount() < maxPlayers && !forceStartGame) { + return; + } + + auto players = getPlayers(); + for (auto *player : players.values()) { + if (!player->getReadyStart()) { + if (forceStartGame) { + // Player is not ready to start, so kick them + // TODO: Move them to Spectators instead + kickParticipant(player->getPlayerId()); + } else { + return; + } + } + } + + players = getPlayers(); // players could have been kicked, get new list of players + for (Server_AbstractPlayer *player : players.values()) { + player->setupZones(); + } + + gameStarted = true; + for (auto *player : players.values()) { + player->setConceded(false); + player->setReadyStart(false); + } + + if (firstGameStarted) { + currentReplay->set_duration_seconds(secondsElapsed - startTimeOfThisGame); + replayList.append(currentReplay); + currentReplay = new GameReplay; + currentReplay->set_replay_id(databaseInterface->getNextReplayId()); + ServerInfo_Game *gameInfo = currentReplay->mutable_game_info(); + getInfo(*gameInfo); + gameInfo->set_started(false); + + Event_GameStateChanged omniscientEvent; + createGameStateChangedEvent(&omniscientEvent, nullptr, true, true); + + GameEventContainer *replayCont = prepareGameEvent(omniscientEvent, -1); + replayCont->set_seconds_elapsed(0); + replayCont->clear_game_id(); + currentReplay->add_event_list()->CopyFrom(*replayCont); + delete replayCont; + + startTimeOfThisGame = secondsElapsed; + } else + firstGameStarted = true; + + sendGameStateToPlayers(); + + activePlayer = -1; + nextTurn(); + + locker.unlock(); + + ServerInfo_Game gameInfo; + gameInfo.set_room_id(room->getId()); + gameInfo.set_game_id(gameId); + gameInfo.set_started(true); + emit gameInfoChanged(gameInfo); +} + +void Server_Game::startGameIfReady(bool forceStartGame) +{ + emit sigStartGameIfReady(forceStartGame); +} + +void Server_Game::stopGameIfFinished() +{ + QMutexLocker locker(&gameMutex); + + int playing = 0; + auto players = getPlayers(); + for (auto *player : players.values()) { + if (!player->getConceded()) + ++playing; + } + if (playing > 1) + return; + + gameStarted = false; + + for (auto *player : players.values()) { + player->clearZones(); + player->setConceded(false); + } + + sendGameStateToPlayers(); + + locker.unlock(); + + ServerInfo_Game gameInfo; + gameInfo.set_room_id(room->getId()); + gameInfo.set_game_id(gameId); + gameInfo.set_started(false); + emit gameInfoChanged(gameInfo); +} + +Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user, + const QString &_password, + bool spectator, + bool overrideRestrictions, + bool asJudge) +{ + Server_DatabaseInterface *databaseInterface = room->getServer()->getDatabaseInterface(); + for (auto *participant : participants.values()) { + if (participant->getUserInfo()->name() == user->name()) + return Response::RespContextError; + } + + if (asJudge && !(user->user_level() & ServerInfo_User::IsJudge)) { + return Response::RespUserLevelTooLow; + } + if (!(overrideRestrictions && (user->user_level() & ServerInfo_User::IsModerator))) { + if ((_password != password) && !(spectator && !spectatorsNeedPassword)) + return Response::RespWrongPassword; + if (!(user->user_level() & ServerInfo_User::IsRegistered) && onlyRegistered) + return Response::RespUserLevelTooLow; + if (onlyBuddies && (user->name() != creatorInfo->name())) + if (!databaseInterface->isInBuddyList(QString::fromStdString(creatorInfo->name()), + QString::fromStdString(user->name()))) + return Response::RespOnlyBuddies; + if (databaseInterface->isInIgnoreList(QString::fromStdString(creatorInfo->name()), + QString::fromStdString(user->name()))) + return Response::RespInIgnoreList; + if (spectator) { + if (!spectatorsAllowed) + return Response::RespSpectatorsNotAllowed; + } + } + if (!spectator && (gameStarted || (getPlayerCount() >= getMaxPlayers()))) + return Response::RespGameFull; + + return Response::RespOk; +} + +bool Server_Game::containsUser(const QString &userName) const +{ + QMutexLocker locker(&gameMutex); + + for (auto *participant : participants.values()) { + if (participant->getUserInfo()->name() == userName.toStdString()) + return true; + } + return false; +} + +void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface, + ResponseContainer &rc, + bool spectator, + bool judge, + bool broadcastUpdate) +{ + QMutexLocker locker(&gameMutex); + + Server_AbstractParticipant *newParticipant; + if (spectator) { + newParticipant = new Server_Spectator(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true), + judge, userInterface); + } else { + newParticipant = new Server_Player(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true), judge, + userInterface); + } + + newParticipant->moveToThread(thread()); + + Event_Join joinEvent; + newParticipant->getProperties(*joinEvent.mutable_player_properties(), true); + sendGameEventContainer(prepareGameEvent(joinEvent, -1)); + + const QString playerName = QString::fromStdString(newParticipant->getUserInfo()->name()); + participants.insert(newParticipant->getPlayerId(), newParticipant); + if (spectator) { + allSpectatorsEver.insert(playerName); + } else { + allPlayersEver.insert(playerName); + + // if the original creator of the game joins, give them host status back + //! \todo transferring host to spectators has side effects + if (newParticipant->getUserInfo()->name() == creatorInfo->name()) { + hostId = newParticipant->getPlayerId(); + sendGameEventContainer(prepareGameEvent(Event_GameHostChanged(), hostId)); + } + } + + if (broadcastUpdate) { + ServerInfo_Game gameInfo; + gameInfo.set_room_id(room->getId()); + gameInfo.set_game_id(gameId); + gameInfo.set_player_count(getPlayerCount()); + gameInfo.set_spectators_count(getSpectatorCount()); + emit gameInfoChanged(gameInfo); + } + + if ((newParticipant->getUserInfo()->user_level() & ServerInfo_User::IsRegistered) && !spectator) + room->getServer()->addPersistentPlayer(playerName, room->getId(), gameId, newParticipant->getPlayerId()); + + userInterface->playerAddedToGame(gameId, room->getId(), newParticipant->getPlayerId()); + + createGameJoinedEvent(newParticipant, rc, false); +} + +void Server_Game::removeParticipant(Server_AbstractParticipant *participant, Event_Leave::LeaveReason reason) +{ + room->getServer()->removePersistentPlayer(QString::fromStdString(participant->getUserInfo()->name()), room->getId(), + gameId, participant->getPlayerId()); + participants.remove(participant->getPlayerId()); + + bool spectator = participant->isSpectator(); + GameEventStorage ges; + if (!spectator) { + auto *player = static_cast(participant); + removeArrowsRelatedToPlayer(ges, player); + unattachCards(ges, player); + } + + Event_Leave event; + event.set_reason(reason); + ges.enqueueGameEvent(event, participant->getPlayerId()); + ges.sendToGame(this); + + bool playerActive = activePlayer == participant->getPlayerId(); + bool playerHost = hostId == participant->getPlayerId(); + participant->prepareDestroy(); + + if (playerHost) { + int newHostId = -1; + for (auto *otherPlayer : getPlayers().values()) { + newHostId = otherPlayer->getPlayerId(); + break; + } + if (newHostId != -1) { + hostId = newHostId; + sendGameEventContainer(prepareGameEvent(Event_GameHostChanged(), hostId)); + } else { + gameClosed = true; + deleteLater(); + return; + } + } + if (!spectator) { + stopGameIfFinished(); + if (gameStarted && playerActive) + nextTurn(); + } + + ServerInfo_Game gameInfo; + gameInfo.set_room_id(room->getId()); + gameInfo.set_game_id(gameId); + gameInfo.set_player_count(getPlayerCount()); + gameInfo.set_spectators_count(getSpectatorCount()); + emit gameInfoChanged(gameInfo); +} + +void Server_Game::removeArrowsRelatedToPlayer(GameEventStorage &ges, Server_AbstractPlayer *player) +{ + QMutexLocker locker(&gameMutex); + + // Remove all arrows of other players pointing to the player being removed or to one of his cards. + // Also remove all arrows starting at one of his cards. This is necessary since players can create + // arrows that start at another person's cards. + for (Server_AbstractPlayer *anyPlayer : getPlayers().values()) { + QList toDelete; + for (auto *arrow : anyPlayer->getArrows().values()) { + auto *targetCard = qobject_cast(arrow->getTargetItem()); + if (targetCard) { + if (targetCard->getZone() != nullptr && targetCard->getZone()->getPlayer() == player) + toDelete.append(arrow); + } else if (arrow->getTargetItem() == player) { + toDelete.append(arrow); + } + + // Don't use else here! It has to happen regardless of whether targetCard == 0. + if (arrow->getStartCard()->getZone() != nullptr && arrow->getStartCard()->getZone()->getPlayer() == player) + toDelete.append(arrow); + } + for (auto *arrow : toDelete) { + Event_DeleteArrow event; + event.set_arrow_id(arrow->getId()); + ges.enqueueGameEvent(event, anyPlayer->getPlayerId()); + + anyPlayer->deleteArrow(arrow->getId()); + } + } +} + +void Server_Game::unattachCards(GameEventStorage &ges, Server_AbstractPlayer *player) +{ + QMutexLocker locker(&gameMutex); + + for (auto zone : player->getZones()) { + for (auto card : zone->getCards()) { + // Make a copy of the list because the original one gets modified during the loop + QList attachedCards = card->getAttachedCards(); + for (Server_Card *attachedCard : attachedCards) { + auto otherPlayer = attachedCard->getZone()->getPlayer(); + // do not modify the current player's zone! + // this would cause the current card iterator to be invalidated! + // we only have to return cards owned by other players + // because the current player is leaving the game anyway + if (otherPlayer != player) { + otherPlayer->unattachCard(ges, attachedCard); + } + } + } + } +} + +bool Server_Game::kickParticipant(int playerId) +{ + QMutexLocker locker(&gameMutex); + + auto *participant = participants.value(playerId); + if (!participant) + return false; + + GameEventContainer *gec = prepareGameEvent(Event_Kicked(), -1); + participant->sendGameEvent(*gec); + delete gec; + + removeParticipant(participant, Event_Leave::USER_KICKED); + + return true; +} + +void Server_Game::setActivePlayer(int _activePlayer) +{ + QMutexLocker locker(&gameMutex); + + removeArrows(0, true); + + activePlayer = _activePlayer; + + Event_SetActivePlayer event; + event.set_active_player_id(activePlayer); + sendGameEventContainer(prepareGameEvent(event, -1)); + + setActivePhase(0); +} + +void Server_Game::setActivePhase(int newPhase) +{ + QMutexLocker locker(&gameMutex); + + removeArrows(newPhase); + activePhase = newPhase; + + Event_SetActivePhase event; + event.set_phase(activePhase); + sendGameEventContainer(prepareGameEvent(event, -1)); +} + +void Server_Game::removeArrows(int newPhase, bool force) +{ + QMutexLocker locker(&gameMutex); + + for (auto *anyPlayer : getPlayers().values()) { + for (auto *arrowToDelete : anyPlayer->getArrows().values()) { // values creates a copy + if (force || arrowToDelete->checkPhaseDeletion(newPhase)) { + Event_DeleteArrow event; + event.set_arrow_id(arrowToDelete->getId()); + sendGameEventContainer(prepareGameEvent(event, anyPlayer->getPlayerId())); + + anyPlayer->deleteArrow(arrowToDelete->getId()); + } + } + } +} + +void Server_Game::nextTurn() +{ + QMutexLocker locker(&gameMutex); + + if (participants.isEmpty()) { + qWarning() << "Server_Game::nextTurn was called while players is empty; gameId = " << gameId; + return; + } + + auto players = getPlayers(); + const QList keys = players.keys(); + int listPos = -1; + if (activePlayer != -1) { + listPos = keys.indexOf(activePlayer); + } + do { + if (turnOrderReversed) { + --listPos; + if (listPos < 0) { + listPos = keys.size() - 1; + } + } else { + ++listPos; + if (listPos == keys.size()) { + listPos = 0; + } + } + } while (players.value(keys[listPos])->getConceded()); + + setActivePlayer(keys[listPos]); +} + +void Server_Game::createGameJoinedEvent(Server_AbstractParticipant *joiningParticipant, + ResponseContainer &rc, + bool resuming) +{ + Event_GameJoined event1; + getInfo(*event1.mutable_game_info()); + event1.set_host_id(hostId); + event1.set_player_id(joiningParticipant->getPlayerId()); + event1.set_spectator(joiningParticipant->isSpectator()); + event1.set_judge(joiningParticipant->isJudge()); + event1.set_resuming(resuming); + if (resuming) { + const QStringList &allGameTypes = room->getGameTypes(); + for (int i = 0; i < allGameTypes.size(); ++i) { + ServerInfo_GameType *newGameType = event1.add_game_types(); + newGameType->set_game_type_id(i); + newGameType->set_description(allGameTypes[i].toStdString()); + } + } + rc.enqueuePostResponseItem(ServerMessage::SESSION_EVENT, Server_AbstractUserInterface::prepareSessionEvent(event1)); + + Event_GameStateChanged event2; + event2.set_seconds_elapsed(secondsElapsed); + event2.set_game_started(gameStarted); + event2.set_active_player_id(activePlayer); + event2.set_active_phase(activePhase); + + bool omniscient = joiningParticipant->isSpectator() && (spectatorsSeeEverything || joiningParticipant->isJudge()); + for (auto *participant : participants.values()) { + participant->getInfo(event2.add_player_list(), joiningParticipant, omniscient, true); + } + + rc.enqueuePostResponseItem(ServerMessage::GAME_EVENT_CONTAINER, prepareGameEvent(event2, -1)); +} + +void Server_Game::sendGameEventContainer(GameEventContainer *cont, + GameEventStorageItem::EventRecipients recipients, + int privatePlayerId) +{ + QMutexLocker locker(&gameMutex); + + cont->set_game_id(gameId); + for (auto *participant : participants.values()) { + const bool playerPrivate = (participant->getPlayerId() == privatePlayerId) || + (participant->isSpectator() && (spectatorsSeeEverything || participant->isJudge())); + if ((recipients.testFlag(GameEventStorageItem::SendToPrivate) && playerPrivate) || + (recipients.testFlag(GameEventStorageItem::SendToOthers) && !playerPrivate)) + participant->sendGameEvent(*cont); + } + if (recipients.testFlag(GameEventStorageItem::SendToPrivate)) { + cont->set_seconds_elapsed(secondsElapsed - startTimeOfThisGame); + cont->clear_game_id(); + currentReplay->add_event_list()->CopyFrom(*cont); + } + + delete cont; +} + +GameEventContainer * +Server_Game::prepareGameEvent(const ::google::protobuf::Message &gameEvent, int playerId, GameEventContext *context) +{ + auto *cont = new GameEventContainer; + cont->set_game_id(gameId); + if (context) + cont->mutable_context()->CopyFrom(*context); + GameEvent *event = cont->add_event_list(); + if (playerId != -1) + event->set_player_id(playerId); + event->GetReflection() + ->MutableMessage(event, gameEvent.GetDescriptor()->FindExtensionByName("ext")) + ->CopyFrom(gameEvent); + return cont; +} + +void Server_Game::getInfo(ServerInfo_Game &result) const +{ + QMutexLocker locker(&gameMutex); + + result.set_room_id(room->getId()); + result.set_game_id(gameId); + if (gameClosed) { + result.set_closed(true); + } else { + for (auto type : gameTypes) { + result.add_game_types(type); + } + + result.set_max_players(getMaxPlayers()); + result.set_description(getDescription().toStdString()); + result.set_with_password(!getPassword().isEmpty()); + result.set_player_count(getPlayerCount()); + result.set_started(gameStarted); + result.mutable_creator_info()->CopyFrom(*getCreatorInfo()); + result.set_only_buddies(onlyBuddies); + result.set_only_registered(onlyRegistered); + result.set_spectators_allowed(getSpectatorsAllowed()); + result.set_spectators_need_password(getSpectatorsNeedPassword()); + result.set_spectators_can_chat(spectatorsCanTalk); + result.set_spectators_omniscient(spectatorsSeeEverything); + result.set_share_decklists_on_load(shareDecklistsOnLoad); + result.set_spectators_count(getSpectatorCount()); + result.set_start_time(startTime.toSecsSinceEpoch()); + } +} + +void Server_Game::returnCardsFromPlayer(GameEventStorage &ges, Server_AbstractPlayer *player) +{ + QMutexLocker locker(&gameMutex); + // Return cards to their rightful owners before conceding the game + static const QRegularExpression ownerRegex{"Owner: ?([^\n]+)"}; + const auto &playerTable = player->getZones().value(ZoneNames::TABLE); + for (const auto &card : playerTable->getCards()) { + if (card == nullptr) { + continue; + } + + const auto ®exResult = ownerRegex.match(card->getAnnotation()); + if (!regexResult.hasMatch()) { + continue; + } + + CardToMove cardToMove; + cardToMove.set_card_id(card->getId()); + + for (const auto *otherPlayer : getPlayers()) { + if (otherPlayer == nullptr || otherPlayer->getUserInfo() == nullptr) { + continue; + } + + const auto &ownerToReturnTo = regexResult.captured(1); + const auto &correctOwner = QString::compare(QString::fromStdString(otherPlayer->getUserInfo()->name()), + ownerToReturnTo, Qt::CaseInsensitive) == 0; + if (!correctOwner) { + continue; + } + + const auto &targetZone = otherPlayer->getZones().value(ZoneNames::TABLE); + + if (playerTable == nullptr || targetZone == nullptr) { + continue; + } + + player->moveCard(ges, playerTable, QList() << &cardToMove, targetZone, 0, 0, false); + break; + } + } +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h new file mode 100644 index 000000000..1c658f2ba --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h @@ -0,0 +1,224 @@ +/*************************************************************************** + * Copyright (C) 2008 by Max-Wilhelm Bruker * + * brukie@laptop * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef SERVERGAME_H +#define SERVERGAME_H + +#include "../server_response_containers.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class QTimer; +class GameEventContainer; +class GameReplay; +class Server_Room; +class Server_AbstractPlayer; +class Server_AbstractParticipant; +class ServerInfo_User; +class ServerInfo_Game; +class Server_AbstractUserInterface; +class Event_GameStateChanged; + +class Server_Game : public QObject +{ + Q_OBJECT +private: + Server_Room *room; + int nextPlayerId; + int hostId; + ServerInfo_User *creatorInfo; + QMap participants; + QSet allPlayersEver, allSpectatorsEver; + bool gameStarted; + bool gameClosed; + int gameId; + QString description; + QString password; + int maxPlayers; + QList gameTypes; + int activePlayer, activePhase; + bool onlyBuddies, onlyRegistered; + bool spectatorsAllowed; + bool spectatorsNeedPassword; + bool spectatorsCanTalk; + bool spectatorsSeeEverything; + int startingLifeTotal; + bool shareDecklistsOnLoad; + int inactivityCounter; + int startTimeOfThisGame, secondsElapsed; + bool firstGameStarted; + bool turnOrderReversed; + QDateTime startTime; + QTimer *pingClock; + QList replayList; + GameReplay *currentReplay; + + void createGameStateChangedEvent(Event_GameStateChanged *event, + Server_AbstractParticipant *recipient, + bool omniscient, + bool withUserInfo); + void storeGameInformation(); +signals: + void sigStartGameIfReady(bool override); + void gameInfoChanged(ServerInfo_Game gameInfo); +private slots: + void pingClockTimeout(); + void doStartGameIfReady(bool forceStartGame = false); + +public: + mutable QRecursiveMutex gameMutex; + Server_Game(const ServerInfo_User &_creatorInfo, + int _gameId, + const QString &_description, + const QString &_password, + int _maxPlayers, + const QList &_gameTypes, + bool _onlyBuddies, + bool _onlyRegistered, + bool _spectatorsAllowed, + bool _spectatorsNeedPassword, + bool _spectatorsCanTalk, + bool _spectatorsSeeEverything, + int _startingLifeTotal, + bool _shareDecklistsOnLoad, + Server_Room *parent); + ~Server_Game() override; + Server_Room *getRoom() const + { + return room; + } + void getInfo(ServerInfo_Game &result) const; + int getHostId() const + { + return hostId; + } + ServerInfo_User *getCreatorInfo() const + { + return creatorInfo; + } + bool getGameStarted() const + { + return gameStarted; + } + int getPlayerCount() const; + int getSpectatorCount() const; + QMap getPlayers() const; + Server_AbstractPlayer *getPlayer(int id) const; + const QMap &getParticipants() const + { + return participants; + } + int getGameId() const + { + return gameId; + } + QString getDescription() const + { + return description; + } + QString getPassword() const + { + return password; + } + int getMaxPlayers() const + { + return maxPlayers; + } + bool getSpectatorsAllowed() const + { + return spectatorsAllowed; + } + bool getSpectatorsNeedPassword() const + { + return spectatorsNeedPassword; + } + bool getSpectatorsCanTalk() const + { + return spectatorsCanTalk; + } + bool getSpectatorsSeeEverything() const + { + return spectatorsSeeEverything; + } + int getStartingLifeTotal() const + { + return startingLifeTotal; + } + bool getShareDecklistsOnLoad() const + { + return shareDecklistsOnLoad; + } + Response::ResponseCode + checkJoin(ServerInfo_User *user, const QString &_password, bool spectator, bool overrideRestrictions, bool asJudge); + bool containsUser(const QString &userName) const; + void addPlayer(Server_AbstractUserInterface *userInterface, + ResponseContainer &rc, + bool spectator, + bool judge, + bool broadcastUpdate = true); + void removeParticipant(Server_AbstractParticipant *participant, Event_Leave::LeaveReason reason); + void removeArrowsRelatedToPlayer(GameEventStorage &ges, Server_AbstractPlayer *player); + void unattachCards(GameEventStorage &ges, Server_AbstractPlayer *player); + bool kickParticipant(int playerId); + void startGameIfReady(bool forceStartGame); + void stopGameIfFinished(); + int getActivePlayer() const + { + return activePlayer; + } + int getActivePhase() const + { + return activePhase; + } + void setActivePlayer(int newPlayer); + void setActivePhase(int newPhase); + void removeArrows(int newPhase, bool force = false); + void nextTurn(); + int getSecondsElapsed() const + { + return secondsElapsed; + } + bool reverseTurnOrder() + { + return turnOrderReversed = !turnOrderReversed; + } + + void createGameJoinedEvent(Server_AbstractParticipant *participant, ResponseContainer &rc, bool resuming); + + GameEventContainer * + prepareGameEvent(const ::google::protobuf::Message &gameEvent, int playerId, GameEventContext *context = 0); + GameEventContext prepareGameEventContext(const ::google::protobuf::Message &gameEventContext); + + void sendGameStateToPlayers(); + void sendGameEventContainer(GameEventContainer *cont, + GameEventStorageItem::EventRecipients recipients = GameEventStorageItem::SendToPrivate | + GameEventStorageItem::SendToOthers, + int privatePlayerId = -1); + void returnCardsFromPlayer(GameEventStorage &ges, Server_AbstractPlayer *player); +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_move_card_struct.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_move_card_struct.h new file mode 100644 index 000000000..c5b293b6d --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_move_card_struct.h @@ -0,0 +1,26 @@ +#ifndef MOVE_CARD_STRUCT +#define MOVE_CARD_STRUCT + +#include "server_card.h" +class CardToMove; + +struct MoveCardStruct +{ + Server_Card *card; + int position; + const CardToMove *cardToMove; + int xCoord, yCoord; + MoveCardStruct(Server_Card *_card, int _position, const CardToMove *_cardToMove) + : card(_card), position(_position), cardToMove(_cardToMove), xCoord(_card->getX()), yCoord(_card->getY()) + + { + } + bool operator<(const MoveCardStruct &other) const + { + return (yCoord == other.yCoord && + ((xCoord == other.xCoord && position < other.position) || xCoord < other.xCoord)) || + yCoord < other.yCoord; + } +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp new file mode 100644 index 000000000..1175e4b57 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp @@ -0,0 +1,598 @@ +#include "server_player.h" + +#include "../server.h" +#include "../server_abstractuserinterface.h" +#include "../server_database_interface.h" +#include "../server_room.h" +#include "server_card.h" +#include "server_cardzone.h" +#include "server_counter.h" +#include "server_game.h" +#include "server_move_card_struct.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Server_Player::Server_Player(Server_Game *_game, + int _playerId, + const ServerInfo_User &_userInfo, + bool _judge, + Server_AbstractUserInterface *_userInterface) + : Server_AbstractPlayer(_game, _playerId, _userInfo, _judge, _userInterface) +{ +} + +Server_Player::~Server_Player() = default; + +int Server_Player::newCounterId() const +{ + int id = 0; + QMapIterator i(counters); + while (i.hasNext()) { + Server_Counter *c = i.next().value(); + if (c->getId() > id) { + id = c->getId(); + } + } + return id + 1; +} + +void Server_Player::setupZones() +{ + Server_AbstractPlayer::setupZones(); + + // This may need to be customized according to the game rules. + // ------------------------------------------------------------------ + + // Create zones + auto *deckZone = new Server_CardZone(this, ZoneNames::DECK, false, ServerInfo_Zone::HiddenZone); + addZone(deckZone); + auto *sbZone = new Server_CardZone(this, ZoneNames::SIDEBOARD, false, ServerInfo_Zone::HiddenZone); + addZone(sbZone); + addZone(new Server_CardZone(this, ZoneNames::TABLE, true, ServerInfo_Zone::PublicZone)); + addZone(new Server_CardZone(this, ZoneNames::HAND, false, ServerInfo_Zone::PrivateZone)); + addZone(new Server_CardZone(this, ZoneNames::STACK, false, ServerInfo_Zone::PublicZone)); + addZone(new Server_CardZone(this, ZoneNames::GRAVE, false, ServerInfo_Zone::PublicZone)); + addZone(new Server_CardZone(this, ZoneNames::EXILE, false, ServerInfo_Zone::PublicZone)); + + addCounter(new Server_Counter(0, "life", makeColor(255, 255, 255), 25, game->getStartingLifeTotal())); + addCounter(new Server_Counter(1, "w", makeColor(255, 255, 150), 20, 0)); + addCounter(new Server_Counter(2, "u", makeColor(150, 150, 255), 20, 0)); + addCounter(new Server_Counter(3, "b", makeColor(150, 150, 150), 20, 0)); + addCounter(new Server_Counter(4, "r", makeColor(250, 150, 150), 20, 0)); + addCounter(new Server_Counter(5, "g", makeColor(150, 255, 150), 20, 0)); + addCounter(new Server_Counter(6, "x", makeColor(255, 255, 255), 20, 0)); + addCounter(new Server_Counter(7, "storm", makeColor(255, 150, 30), 20, 0)); + + // ------------------------------------------------------------------ + + // Assign card ids and create deck from deck list + auto insertCardsIntoZone = [this](auto cards, auto *zone) { + for (auto card : cards) { + for (int k = 0; k < card->getNumber(); ++k) { + zone->insertCard(new Server_Card(card->toCardRef(), nextCardId++, 0, 0, zone), -1, 0); + } + } + }; + + insertCardsIntoZone(deck->getCardNodes({DECK_ZONE_MAIN}), deckZone); + insertCardsIntoZone(deck->getCardNodes({DECK_ZONE_SIDE}), sbZone); + + const QList &sideboardPlan = deck->getCurrentSideboardPlan(); + for (const auto &m : sideboardPlan) { + const QString startZone = nameFromStdString(m.start_zone()); + const QString targetZone = nameFromStdString(m.target_zone()); + + Server_CardZone *start, *target; + if (startZone == DECK_ZONE_MAIN) { + start = deckZone; + } else if (startZone == DECK_ZONE_SIDE) { + start = sbZone; + } else { + continue; + } + if (targetZone == DECK_ZONE_MAIN) { + target = deckZone; + } else if (targetZone == DECK_ZONE_SIDE) { + target = sbZone; + } else { + continue; + } + + for (int j = 0; j < start->getCards().size(); ++j) { + if (start->getCards()[j]->getName() == nameFromStdString(m.card_name())) { + Server_Card *card = start->getCard(j, nullptr, true); + target->insertCard(card, -1, 0); + break; + } + } + } + + deckZone->shuffle(); +} + +void Server_Player::clearZones() +{ + Server_AbstractPlayer::clearZones(); + for (Server_Counter *counter : counters) { + delete counter; + } + counters.clear(); + + lastDrawList.clear(); +} + +void Server_Player::addCounter(Server_Counter *counter) +{ + counters.insert(counter->getId(), counter); +} + +Response::ResponseCode Server_Player::drawCards(GameEventStorage &ges, int number) +{ + Server_CardZone *deckZone = zones.value(ZoneNames::DECK); + Server_CardZone *handZone = zones.value(ZoneNames::HAND); + if (deckZone->getCards().size() < number) { + number = deckZone->getCards().size(); + } + + Event_DrawCards eventOthers; + eventOthers.set_number(number); + Event_DrawCards eventPrivate(eventOthers); + + for (int i = 0; i < number; ++i) { + Server_Card *card = deckZone->getCard(0, nullptr, true); + handZone->insertCard(card, -1, 0); + lastDrawList.append(card->getId()); + + ServerInfo_Card *cardInfo = eventPrivate.add_cards(); + cardInfo->set_id(card->getId()); + cardInfo->set_name(card->getName().toStdString()); + cardInfo->set_provider_id(card->getProviderId().toStdString()); + } + + ges.enqueueGameEvent(eventPrivate, playerId, GameEventStorageItem::SendToPrivate, playerId); + ges.enqueueGameEvent(eventOthers, playerId, GameEventStorageItem::SendToOthers); + + if (number > 0) { + revealTopCardIfNeeded(deckZone, ges); + int currentKnownCards = deckZone->getCardsBeingLookedAt(); + deckZone->setCardsBeingLookedAt(currentKnownCards - number); + } + + return Response::RespOk; +} + +void Server_Player::onCardBeingMoved(GameEventStorage &ges, + const MoveCardStruct &cardStruct, + Server_CardZone *startzone, + Server_CardZone *targetzone, + bool undoingDraw) +{ + Server_AbstractPlayer::onCardBeingMoved(ges, cardStruct, startzone, targetzone, undoingDraw); + + Server_Card *card = cardStruct.card; + + // "Undo draw" should only remain valid if the just-drawn card stays within the user's hand (e.g., they only + // reorder their hand). If a just-drawn card leaves the hand then remove cards before it from the list + // (Ignore the case where the card is currently being un-drawn.) + if (startzone->getName() == ZoneNames::HAND && targetzone->getName() != ZoneNames::HAND && !undoingDraw) { + int index = lastDrawList.lastIndexOf(card->getId()); + if (index != -1) { + lastDrawList.erase(lastDrawList.begin(), lastDrawList.begin() + index); + } + } +} + +Response::ResponseCode +Server_Player::cmdDeckSelect(const Command_DeckSelect &cmd, ResponseContainer &rc, GameEventStorage &ges) +{ + if (game->getGameStarted()) { + return Response::RespContextError; + } + + DeckList *newDeck; + if (cmd.has_deck_id()) { + try { + newDeck = game->getRoom()->getServer()->getDatabaseInterface()->getDeckFromDatabase(cmd.deck_id(), + userInfo->id()); + } catch (Response::ResponseCode &r) { + return r; + } + } else { + newDeck = new DeckList(fileFromStdString(cmd.deck())); + } + + if (!newDeck) { + return Response::RespInternalError; + } + + delete deck; + deck = newDeck; + sideboardLocked = true; + + Event_PlayerPropertiesChanged event; + event.mutable_player_properties()->set_sideboard_locked(true); + event.mutable_player_properties()->set_deck_hash(deck->getDeckHash().toStdString()); + ges.enqueueGameEvent(event, playerId); + + Context_DeckSelect context; + context.set_deck_hash(deck->getDeckHash().toStdString()); + context.set_sideboard_size(deck->getSideboardSize()); + if (game->getShareDecklistsOnLoad()) { + context.set_deck_list(deck->writeToString_Native().toStdString()); + } + ges.setGameEventContext(context); + + auto *re = new Response_DeckDownload; + re->set_deck(deck->writeToString_Native().toStdString()); + + rc.setResponseExtension(re); + return Response::RespOk; +} + +Response::ResponseCode Server_Player::cmdSetSideboardPlan(const Command_SetSideboardPlan &cmd, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + if (readyStart) { + return Response::RespContextError; + } + if (!deck) { + return Response::RespContextError; + } + if (sideboardLocked) { + return Response::RespContextError; + } + + QList sideboardPlan; + for (int i = 0; i < cmd.move_list_size(); ++i) { + sideboardPlan.append(cmd.move_list(i)); + } + deck->setCurrentSideboardPlan(sideboardPlan); + + return Response::RespOk; +} + +Response::ResponseCode Server_Player::cmdSetSideboardLock(const Command_SetSideboardLock &cmd, + ResponseContainer & /*rc*/, + GameEventStorage &ges) +{ + if (readyStart) { + return Response::RespContextError; + } + if (!deck) { + return Response::RespContextError; + } + if (sideboardLocked == cmd.locked()) { + return Response::RespContextError; + } + + sideboardLocked = cmd.locked(); + if (sideboardLocked) { + deck->setCurrentSideboardPlan(QList()); + } + + Event_PlayerPropertiesChanged event; + event.mutable_player_properties()->set_sideboard_locked(sideboardLocked); + ges.enqueueGameEvent(event, playerId); + ges.setGameEventContext(Context_SetSideboardLock()); + + return Response::RespOk; +} + +Response::ResponseCode +Server_Player::cmdShuffle(const Command_Shuffle &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + + if (conceded) { + return Response::RespContextError; + } + + if (cmd.has_zone_name() && cmd.zone_name() != ZoneNames::DECK) { + return Response::RespFunctionNotAllowed; + } + + Server_CardZone *zone = zones.value(ZoneNames::DECK); + if (!zone) { + return Response::RespNameNotFound; + } + + zone->shuffle(cmd.start(), cmd.end()); + + Event_Shuffle event; + event.set_zone_name(zone->getName().toStdString()); + event.set_start(cmd.start()); + event.set_end(cmd.end()); + ges.enqueueGameEvent(event, playerId); + revealTopCardIfNeeded(zone, ges); + + return Response::RespOk; +} + +Response::ResponseCode +Server_Player::cmdMulligan(const Command_Mulligan &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_CardZone *hand = zones.value(ZoneNames::HAND); + Server_CardZone *_deck = zones.value(ZoneNames::DECK); + int number = cmd.number(); + + if (!hand->getCards().isEmpty()) { + auto cardsToMove = QList(); + for (auto &card : hand->getCards()) { + auto *cardToMove = new CardToMove; + cardToMove->set_card_id(card->getId()); + cardsToMove.append(cardToMove); + } + moveCard(ges, hand, cardsToMove, _deck, -1, 0, false); + qDeleteAll(cardsToMove); + } + + _deck->shuffle(); + ges.enqueueGameEvent(Event_Shuffle(), playerId); + + drawCards(ges, number); + + Context_Mulligan context; + context.set_number(static_cast(hand->getCards().size())); + ges.setGameEventContext(context); + + return Response::RespOk; +} + +Response::ResponseCode +Server_Player::cmdDrawCards(const Command_DrawCards &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + return drawCards(ges, cmd.number()); +} + +Response::ResponseCode +Server_Player::cmdUndoDraw(const Command_UndoDraw & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + if (lastDrawList.isEmpty()) { + return Response::RespContextError; + } + + Response::ResponseCode retVal; + auto *cardToMove = new CardToMove; + cardToMove->set_card_id(lastDrawList.takeLast()); + retVal = moveCard(ges, zones.value(ZoneNames::HAND), QList() << cardToMove, + zones.value(ZoneNames::DECK), 0, 0, false, true); + delete cardToMove; + + return retVal; +} + +Response::ResponseCode +Server_Player::cmdIncCounter(const Command_IncCounter &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_Counter *c = counters.value(cmd.counter_id(), 0); + if (!c) { + return Response::RespNameNotFound; + } + + c->setCount(c->getCount() + cmd.delta()); + + Event_SetCounter event; + event.set_counter_id(c->getId()); + event.set_value(c->getCount()); + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_Player::cmdCreateCounter(const Command_CreateCounter &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + auto *c = new Server_Counter(newCounterId(), nameFromStdString(cmd.counter_name()), cmd.counter_color(), + cmd.radius(), cmd.value()); + addCounter(c); + + Event_CreateCounter event; + ServerInfo_Counter *counterInfo = event.mutable_counter_info(); + counterInfo->set_id(c->getId()); + counterInfo->set_name(c->getName().toStdString()); + counterInfo->mutable_counter_color()->CopyFrom(cmd.counter_color()); + counterInfo->set_radius(c->getRadius()); + counterInfo->set_count(c->getCount()); + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_Player::cmdSetCounter(const Command_SetCounter &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_Counter *c = counters.value(cmd.counter_id(), 0); + if (!c) { + return Response::RespNameNotFound; + } + + c->setCount(cmd.value()); + + Event_SetCounter event; + event.set_counter_id(c->getId()); + event.set_value(c->getCount()); + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_Player::cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_Counter *counter = counters.value(cmd.counter_id(), 0); + if (!counter) { + return Response::RespNameNotFound; + } + counters.remove(cmd.counter_id()); + delete counter; + + Event_DelCounter event; + event.set_counter_id(cmd.counter_id()); + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_Player::cmdNextTurn(const Command_NextTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage & /*ges*/) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + + if (conceded && !judge) { + return Response::RespContextError; + } + + game->nextTurn(); + return Response::RespOk; +} + +Response::ResponseCode Server_Player::cmdSetActivePhase(const Command_SetActivePhase &cmd, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + + if (!judge) { + if (conceded) { + return Response::RespContextError; + } + + if (game->getActivePlayer() != playerId) { + return Response::RespContextError; + } + } + + game->setActivePhase(cmd.phase()); + + return Response::RespOk; +} + +Response::ResponseCode Server_Player::cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd, + ResponseContainer &rc, + GameEventStorage &ges) +{ + auto ret = Server_AbstractPlayer::cmdChangeZoneProperties(cmd, rc, ges); + + Server_CardZone *zone = zones.value(nameFromStdString(cmd.zone_name())); + if (!zone) { + return Response::RespNameNotFound; + } + + revealTopCardIfNeeded(zone, ges); + return ret; +} + +Response::ResponseCode +Server_Player::cmdReverseTurn(const Command_ReverseTurn &cmd, ResponseContainer &rc, GameEventStorage &ges) +{ + + if (!judge && conceded) { + return Response::RespContextError; + } + return Server_AbstractParticipant::cmdReverseTurn(cmd, rc, ges); +} + +void Server_Player::getInfo(ServerInfo_Player *info, + Server_AbstractParticipant *recipient, + bool omniscient, + bool withUserInfo) +{ + Server_AbstractPlayer::getInfo(info, recipient, omniscient, withUserInfo); + + for (Server_Counter *counter : counters) { + counter->getInfo(info->add_counter_list()); + } +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.h new file mode 100644 index 000000000..5925ed3c2 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.h @@ -0,0 +1,75 @@ +#ifndef PLAYER_H +#define PLAYER_H + +#include "server_abstract_player.h" + +class Server_Player : public Server_AbstractPlayer +{ + Q_OBJECT +private: + QMap counters; + QList lastDrawList; + +public: + Server_Player(Server_Game *_game, + int _playerId, + const ServerInfo_User &_userInfo, + bool _judge, + Server_AbstractUserInterface *_handler); + ~Server_Player() override; + const QMap &getCounters() const + { + return counters; + } + int newCounterId() const; + void addCounter(Server_Counter *counter); + + void setupZones() override; + void clearZones() override; + + Response::ResponseCode drawCards(GameEventStorage &ges, int number); + void onCardBeingMoved(GameEventStorage &ges, + const MoveCardStruct &cardStruct, + Server_CardZone *startzone, + Server_CardZone *targetzone, + bool undoingDraw) override; + + Response::ResponseCode + cmdDeckSelect(const Command_DeckSelect &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdSetSideboardPlan(const Command_SetSideboardPlan &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdSetSideboardLock(const Command_SetSideboardLock &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdShuffle(const Command_Shuffle &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdMulligan(const Command_Mulligan &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdDrawCards(const Command_DrawCards &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdUndoDraw(const Command_UndoDraw &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdIncCounter(const Command_IncCounter &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdCreateCounter(const Command_CreateCounter &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdSetCounter(const Command_SetCounter &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdNextTurn(const Command_NextTurn &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdSetActivePhase(const Command_SetActivePhase &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges) override; + Response::ResponseCode cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd, + ResponseContainer &rc, + GameEventStorage &ges) override; + + void getInfo(ServerInfo_Player *info, + Server_AbstractParticipant *playerWhosAsking, + bool omniscient, + bool withUserInfo) override; +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.cpp new file mode 100644 index 000000000..bc873e386 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.cpp @@ -0,0 +1,11 @@ +#include "server_spectator.h" + +Server_Spectator::Server_Spectator(Server_Game *_game, + int _playerId, + const ServerInfo_User &_userInfo, + bool _judge, + Server_AbstractUserInterface *_userInterface) + : Server_AbstractParticipant(_game, _playerId, _userInfo, _judge, _userInterface) +{ + spectator = true; +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.h new file mode 100644 index 000000000..4d3928a63 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.h @@ -0,0 +1,17 @@ +#ifndef SPECTATOR_H +#define SPECTATOR_H + +#include "server_abstract_participant.h" + +class Server_Spectator : public Server_AbstractParticipant +{ + Q_OBJECT +public: + Server_Spectator(Server_Game *_game, + int _playerId, + const ServerInfo_User &_userInfo, + bool _judge, + Server_AbstractUserInterface *_handler); +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/room_message_type.h b/libcockatrice_network/libcockatrice/network/server/remote/room_message_type.h new file mode 100644 index 000000000..5559db965 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/room_message_type.h @@ -0,0 +1,15 @@ +#ifndef ROOM_MESSAGE_TYPE_H +#define ROOM_MESSAGE_TYPE_H + +#ifdef Q_OS_MACOS +// avoid collision from Mac OS X's ConditionalMacros.h +// https://github.com/protocolbuffers/protobuf/issues/119 +#undef TYPE_BOOL +#endif +#include +#include + +Q_DECLARE_FLAGS(RoomMessageTypeFlags, Event_RoomSay::RoomMessageType) +Q_DECLARE_OPERATORS_FOR_FLAGS(RoomMessageTypeFlags) + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server.cpp new file mode 100644 index 000000000..a5a74c54c --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/server.cpp @@ -0,0 +1,659 @@ +/*************************************************************************** + * Copyright (C) 2008 by Max-Wilhelm Bruker * + * brukie@laptop * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "server.h" + +#include "game/server_game.h" +#include "game/server_player.h" +#include "server_database_interface.h" +#include "server_protocolhandler.h" +#include "server_remoteuserinterface.h" +#include "server_room.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Server::Server(QObject *parent) : QObject(parent), nextLocalGameId(0), tcpUserCount(0), webSocketUserCount(0) +{ + qRegisterMetaType("ServerInfo_Ban"); + qRegisterMetaType("ServerInfo_Game"); + qRegisterMetaType("ServerInfo_Room"); + qRegisterMetaType("ServerInfo_User"); + qRegisterMetaType("CommandContainer"); + qRegisterMetaType("Response"); + qRegisterMetaType("GameEventContainer"); + qRegisterMetaType("IslMessage"); + qRegisterMetaType("Command_JoinGame"); + + connect(this, &Server::sigSendIslMessage, this, &Server::doSendIslMessage, Qt::QueuedConnection); +} + +void Server::prepareDestroy() +{ + roomsLock.lockForWrite(); + QMapIterator roomIterator(rooms); + while (roomIterator.hasNext()) + delete roomIterator.next().value(); + rooms.clear(); + roomsLock.unlock(); +} + +void Server::setDatabaseInterface(Server_DatabaseInterface *_databaseInterface) +{ + connect(this, &Server::endSession, _databaseInterface, &Server_DatabaseInterface::endSession); + databaseInterfaces.insert(QThread::currentThread(), _databaseInterface); +} + +Server_DatabaseInterface *Server::getDatabaseInterface() const +{ + return databaseInterfaces.value(QThread::currentThread()); +} + +AuthenticationResult Server::loginUser(Server_ProtocolHandler *session, + QString &name, + const QString &password, + bool passwordNeedsHash, + QString &reasonStr, + int &secondsLeft, + QString &clientid, + QString &clientVersion, + QString & /* connectionType */) +{ + bool hasClientId = false; + if (clientid.isEmpty()) { + // client id is empty, either out dated client or client has been modified + if (getClientIDRequiredEnabled()) + return ClientIdRequired; + } else { + hasClientId = true; + } + + if (name.size() > 35) + name = name.left(35); + + Server_DatabaseInterface *databaseInterface = getDatabaseInterface(); + + AuthenticationResult authState = databaseInterface->checkUserPassword(session, name, password, clientid, reasonStr, + secondsLeft, passwordNeedsHash); + if (authState == NotLoggedIn || authState == UserIsBanned || authState == UsernameInvalid || + authState == UserIsInactive) + return authState; + + ServerInfo_User data = databaseInterface->getUserData(name, true); + data.set_address(session->getAddress().toStdString()); + name = QString::fromStdString(data.name()); // Compensate for case indifference + + if (authState == PasswordRight) { + if (users.contains(name) || databaseInterface->userSessionExists(name)) { + if (users.contains(name)) { + qDebug("Session already logged in, logging old session out"); + Event_ConnectionClosed event; + event.set_reason(Event_ConnectionClosed::LOGGEDINELSEWERE); + event.set_reason_str("You have been logged out due to logging in at another location."); + event.set_end_time(QDateTime::currentDateTime().toSecsSinceEpoch()); + + SessionEvent *se = users.value(name)->prepareSessionEvent(event); + users.value(name)->sendProtocolItem(*se); + delete se; + + users.value(name)->prepareDestroy(); + } else { + qDebug() << "Active session and sessions table inconsistent, please validate session table information " + "for user " + << name; + } + } + + } else if (authState == UnknownUser) { + // Change user name so that no two users have the same names, + // don't interfere with registered user names though. + if (getRegOnlyServerEnabled()) { + qDebug("Login denied: registration required"); + databaseInterface->unlockSessionTables(); + return RegistrationRequired; + } + + QString tempName = name; + int i = 0; + while (users.contains(tempName) || databaseInterface->activeUserExists(tempName) || + databaseInterface->userSessionExists(tempName)) + tempName = name + "_" + QString::number(++i); + name = tempName; + data.set_name(name.toStdString()); + } + + QWriteLocker locker(&clientsLock); + databaseInterface->lockSessionTables(); + users.insert(name, session); + qDebug() << "Server::loginUser:" << session << "name=" << name; + + data.set_session_id(static_cast( + databaseInterface->startSession(name, session->getAddress(), clientid, session->getConnectionType()))); + databaseInterface->unlockSessionTables(); + + usersBySessionId.insert(data.session_id(), session); + + qDebug() << "session id:" << data.session_id(); + session->setUserInfo(data); + + Event_UserJoined event; + event.mutable_user_info()->CopyFrom(session->copyUserInfo(false)); + SessionEvent *se = Server_ProtocolHandler::prepareSessionEvent(event); + for (auto &client : clients) + if (client->getAcceptsUserListChanges()) + client->sendProtocolItem(*se); + delete se; + + event.mutable_user_info()->CopyFrom(session->copyUserInfo(true, true, true)); + locker.unlock(); + + if (hasClientId) { + // update users database table with client id + databaseInterface->updateUsersClientID(name, clientid); + } + databaseInterface->updateUsersLastLoginData(name, clientVersion); + se = Server_ProtocolHandler::prepareSessionEvent(event); + sendIsl_SessionEvent(*se); + delete se; + + return authState; +} + +void Server::addPersistentPlayer(const QString &userName, int roomId, int gameId, int playerId) +{ + QWriteLocker locker(&persistentPlayersLock); + persistentPlayers.insert(userName, PlayerReference(roomId, gameId, playerId)); +} + +void Server::removePersistentPlayer(const QString &userName, int roomId, int gameId, int playerId) +{ + QWriteLocker locker(&persistentPlayersLock); + persistentPlayers.remove(userName, PlayerReference(roomId, gameId, playerId)); +} + +QList Server::getPersistentPlayerReferences(const QString &userName) const +{ + QReadLocker locker(&persistentPlayersLock); + return persistentPlayers.values(userName); +} + +Server_AbstractUserInterface *Server::findUser(const QString &userName) const +{ + // Call this only with clientsLock set. + + Server_AbstractUserInterface *userHandler = users.value(userName); + if (userHandler) + return userHandler; + else + return externalUsers.value(userName); +} + +void Server::addClient(Server_ProtocolHandler *client) +{ + if (client->getConnectionType() == "tcp") + tcpUserCount++; + + if (client->getConnectionType() == "websocket") + webSocketUserCount++; + + QWriteLocker locker(&clientsLock); + clients << client; +} + +void Server::removeClient(Server_ProtocolHandler *client) +{ + int clientIndex = clients.indexOf(client); + if (clientIndex == -1) { + qWarning() << "tried to remove non existing client"; + return; + } + + if (client->getConnectionType() == "tcp") + tcpUserCount--; + + if (client->getConnectionType() == "websocket") + webSocketUserCount--; + + QWriteLocker locker(&clientsLock); + clients.removeAt(clientIndex); + ServerInfo_User *data = client->getUserInfo(); + if (data) { + Event_UserLeft event; + event.set_name(data->name()); + SessionEvent *se = Server_ProtocolHandler::prepareSessionEvent(event); + for (auto &_client : clients) + if (_client->getAcceptsUserListChanges()) + _client->sendProtocolItem(*se); + sendIsl_SessionEvent(*se); + delete se; + + users.remove(QString::fromStdString(data->name())); + qDebug() << "Server::removeClient: name=" << QString::fromStdString(data->name()); + + if (data->has_session_id()) { + const qint64 sessionId = data->session_id(); + usersBySessionId.remove(sessionId); + emit endSession(sessionId); + qDebug() << "closed session id:" << sessionId; + } + } + qDebug() << "Server::removeClient: removed" << (void *)client << ";" << clients.size() << "clients; " + << users.size() << "users left"; +} + +QList Server::getOnlineModeratorList() const +{ + // clients list should be locked by calling function prior to iteration otherwise sigfaults may occur + QList results; + for (auto &client : clients) { + ServerInfo_User *data = client->getUserInfo(); + + // TODO: this line should be updated in the event there is any type of new user level created + if (data && + (data->user_level() & ServerInfo_User::IsModerator || data->user_level() & ServerInfo_User::IsAdmin)) + results << QString::fromStdString(data->name()).simplified(); + } + return results; +} + +void Server::externalUserJoined(const ServerInfo_User &userInfo) +{ + // This function is always called from the main thread via signal/slot. + clientsLock.lockForWrite(); + + Server_RemoteUserInterface *newUser = new Server_RemoteUserInterface(this, ServerInfo_User_Container(userInfo)); + externalUsers.insert(QString::fromStdString(userInfo.name()), newUser); + externalUsersBySessionId.insert(userInfo.session_id(), newUser); + + Event_UserJoined event; + event.mutable_user_info()->CopyFrom(userInfo); + + SessionEvent *se = Server_ProtocolHandler::prepareSessionEvent(event); + for (auto &client : clients) + if (client->getAcceptsUserListChanges()) + client->sendProtocolItem(*se); + delete se; + clientsLock.unlock(); + + ResponseContainer rc(-1); + newUser->joinPersistentGames(rc); + newUser->sendResponseContainer(rc, Response::RespNothing); +} + +void Server::externalUserLeft(const QString &userName) +{ + // This function is always called from the main thread via signal/slot. + + clientsLock.lockForWrite(); + Server_AbstractUserInterface *user = externalUsers.take(userName); + externalUsersBySessionId.remove(user->getUserInfo()->session_id()); + clientsLock.unlock(); + + QMap> userGames(user->getGames()); + QMapIterator> userGamesIterator(userGames); + roomsLock.lockForRead(); + while (userGamesIterator.hasNext()) { + userGamesIterator.next(); + Server_Room *room = rooms.value(userGamesIterator.value().first); + if (!room) + continue; + + QReadLocker roomGamesLocker(&room->gamesLock); + Server_Game *game = room->getGames().value(userGamesIterator.key()); + if (!game) + continue; + + QMutexLocker gameLocker(&game->gameMutex); + auto *participant = game->getParticipants().value(userGamesIterator.value().second); + if (!participant) + continue; + + participant->disconnectClient(); + } + roomsLock.unlock(); + + delete user; + + Event_UserLeft event; + event.set_name(userName.toStdString()); + + SessionEvent *se = Server_ProtocolHandler::prepareSessionEvent(event); + clientsLock.lockForRead(); + for (auto &client : clients) + if (client->getAcceptsUserListChanges()) + client->sendProtocolItem(*se); + clientsLock.unlock(); + delete se; +} + +void Server::externalRoomUserJoined(int roomId, const ServerInfo_User &userInfo) +{ + // This function is always called from the main thread via signal/slot. + QReadLocker locker(&roomsLock); + + Server_Room *room = rooms.value(roomId); + if (!room) { + qDebug() << "externalRoomUserJoined: room id=" << roomId << "not found"; + return; + } + room->addExternalUser(userInfo); +} + +void Server::externalRoomUserLeft(int roomId, const QString &userName) +{ + // This function is always called from the main thread via signal/slot. + QReadLocker locker(&roomsLock); + + Server_Room *room = rooms.value(roomId); + if (!room) { + qDebug() << "externalRoomUserLeft: room id=" << roomId << "not found"; + return; + } + room->removeExternalUser(userName); +} + +void Server::externalRoomSay(int roomId, const QString &userName, const QString &message) +{ + // This function is always called from the main thread via signal/slot. + QReadLocker locker(&roomsLock); + + Server_Room *room = rooms.value(roomId); + if (!room) { + qDebug() << "externalRoomSay: room id=" << roomId << "not found"; + return; + } + room->say(userName, message, false); + + getDatabaseInterface()->logMessage(0, userName, "ISL", message, Server_DatabaseInterface::MessageTargetIslRoom, + room->getId(), room->getName()); +} + +void Server::externalRoomRemoveMessages(int roomId, const QString &userName, int amount) +{ + // This function is always called from the main thread via signal/slot. + QReadLocker locker(&roomsLock); + + Server_Room *room = rooms.value(roomId); + if (room == nullptr) { + qDebug() << "externalRoomRemoveMessages: room id=" << roomId << "not found"; + return; + } + room->removeSaidMessages(userName, amount); +} + +void Server::externalRoomGameListChanged(int roomId, const ServerInfo_Game &gameInfo) +{ + // This function is always called from the main thread via signal/slot. + QReadLocker locker(&roomsLock); + + Server_Room *room = rooms.value(roomId); + if (!room) { + qDebug() << "externalRoomGameListChanged: room id=" << roomId << "not found"; + return; + } + room->updateExternalGameList(gameInfo); +} + +void Server::externalJoinGameCommandReceived(const Command_JoinGame &cmd, + int cmdId, + int roomId, + int serverId, + qint64 sessionId) +{ + // This function is always called from the main thread via signal/slot. + + try { + QReadLocker roomsLocker(&roomsLock); + QReadLocker clientsLocker(&clientsLock); + + Server_Room *room = rooms.value(roomId); + if (!room) { + qDebug() << "externalJoinGameCommandReceived: room id=" << roomId << "not found"; + throw Response::RespNotInRoom; + } + Server_AbstractUserInterface *userInterface = externalUsersBySessionId.value(sessionId); + if (!userInterface) { + qDebug() << "externalJoinGameCommandReceived: session id=" << sessionId << "not found"; + throw Response::RespNotInRoom; + } + + ResponseContainer responseContainer(cmdId); + Response::ResponseCode responseCode = room->processJoinGameCommand(cmd, responseContainer, userInterface); + userInterface->sendResponseContainer(responseContainer, responseCode); + } catch (Response::ResponseCode &code) { + Response response; + response.set_cmd_id(static_cast(cmdId)); + response.set_response_code(code); + + sendIsl_Response(response, serverId, sessionId); + } +} + +void Server::externalGameCommandContainerReceived(const CommandContainer &cont, + int playerId, + int serverId, + qint64 sessionId) +{ + // This function is always called from the main thread via signal/slot. + + try { + ResponseContainer responseContainer(static_cast(cont.cmd_id())); + Response::ResponseCode finalResponseCode = Response::RespOk; + + QReadLocker roomsLocker(&roomsLock); + Server_Room *room = rooms.value(cont.room_id()); + if (!room) { + qDebug() << "externalGameCommandContainerReceived: room id=" << cont.room_id() << "not found"; + throw Response::RespNotInRoom; + } + + QReadLocker roomGamesLocker(&room->gamesLock); + Server_Game *game = room->getGames().value(cont.game_id()); + if (!game) { + qDebug() << "externalGameCommandContainerReceived: game id=" << cont.game_id() << "not found"; + throw Response::RespNotInRoom; + } + + QMutexLocker gameLocker(&game->gameMutex); + auto *participant = game->getParticipants().value(playerId); + if (!participant) { + qDebug() << "externalGameCommandContainerReceived: player id=" << playerId << "not found"; + throw Response::RespNotInRoom; + } + + GameEventStorage ges; + for (int i = cont.game_command_size() - 1; i >= 0; --i) { + const GameCommand &sc = cont.game_command(i); + qDebug() << "[ISL]" << getSafeDebugString(sc); + + Response::ResponseCode resp = participant->processGameCommand(sc, responseContainer, ges); + + if (resp != Response::RespOk) + finalResponseCode = resp; + } + ges.sendToGame(game); + + if (finalResponseCode != Response::RespNothing) { + participant->getUserInterface()->sendResponseContainer(responseContainer, finalResponseCode); + } + } catch (Response::ResponseCode code) { + Response response; + response.set_cmd_id(cont.cmd_id()); + response.set_response_code(code); + + sendIsl_Response(response, serverId, sessionId); + } +} + +void Server::externalGameEventContainerReceived(const GameEventContainer &cont, qint64 sessionId) +{ + // This function is always called from the main thread via signal/slot. + + QReadLocker usersLocker(&clientsLock); + + Server_ProtocolHandler *client = usersBySessionId.value(sessionId); + if (!client) { + qDebug() << "externalGameEventContainerReceived: session" << sessionId << "not found"; + return; + } + client->sendProtocolItem(cont); +} + +void Server::externalResponseReceived(const Response &resp, qint64 sessionId) +{ + // This function is always called from the main thread via signal/slot. + + QReadLocker usersLocker(&clientsLock); + + Server_ProtocolHandler *client = usersBySessionId.value(sessionId); + if (!client) { + qDebug() << "externalResponseReceived: session" << sessionId << "not found"; + return; + } + client->sendProtocolItem(resp); +} + +void Server::broadcastRoomUpdate(const ServerInfo_Room &roomInfo, bool sendToIsl) +{ + // This function is always called from the main thread via signal/slot. + + Event_ListRooms event; + event.add_room_list()->CopyFrom(roomInfo); + + SessionEvent *se = Server_ProtocolHandler::prepareSessionEvent(event); + + clientsLock.lockForRead(); + for (auto &client : clients) + if (client->getAcceptsRoomListChanges()) + client->sendProtocolItem(*se); + clientsLock.unlock(); + + if (sendToIsl) + sendIsl_SessionEvent(*se); + + delete se; +} + +void Server::addRoom(Server_Room *newRoom) +{ + QWriteLocker locker(&roomsLock); + qDebug() << "Adding room: ID=" << newRoom->getId() << "name=" << newRoom->getName(); + rooms.insert(newRoom->getId(), newRoom); + connect( + newRoom, &Server_Room::roomInfoChanged, this, [this](auto roomInfo) { broadcastRoomUpdate(roomInfo); }, + Qt::QueuedConnection); +} + +int Server::getUsersCount() const +{ + QReadLocker locker(&clientsLock); + return users.size(); +} + +int Server::getGamesCount() const +{ + int result = 0; + QReadLocker locker(&roomsLock); + QMapIterator roomIterator(rooms); + while (roomIterator.hasNext()) { + Server_Room *room = roomIterator.next().value(); + QReadLocker roomLocker(&room->gamesLock); + result += room->getGames().size(); + } + return result; +} + +void Server::sendIsl_Response(const Response &item, int serverId, qint64 sessionId) +{ + IslMessage msg; + msg.set_message_type(IslMessage::RESPONSE); + if (sessionId != -1) + msg.set_session_id(static_cast(sessionId)); + msg.mutable_response()->CopyFrom(item); + + emit sigSendIslMessage(msg, serverId); +} + +void Server::sendIsl_SessionEvent(const SessionEvent &item, int serverId, qint64 sessionId) +{ + IslMessage msg; + msg.set_message_type(IslMessage::SESSION_EVENT); + if (sessionId != -1) + msg.set_session_id(static_cast(sessionId)); + msg.mutable_session_event()->CopyFrom(item); + + emit sigSendIslMessage(msg, serverId); +} + +void Server::sendIsl_GameEventContainer(const GameEventContainer &item, int serverId, qint64 sessionId) +{ + IslMessage msg; + msg.set_message_type(IslMessage::GAME_EVENT_CONTAINER); + if (sessionId != -1) + msg.set_session_id(static_cast(sessionId)); + msg.mutable_game_event_container()->CopyFrom(item); + + emit sigSendIslMessage(msg, serverId); +} + +void Server::sendIsl_RoomEvent(const RoomEvent &item, int serverId, qint64 sessionId) +{ + IslMessage msg; + msg.set_message_type(IslMessage::ROOM_EVENT); + if (sessionId != -1) + msg.set_session_id(static_cast(sessionId)); + msg.mutable_room_event()->CopyFrom(item); + + emit sigSendIslMessage(msg, serverId); +} + +void Server::sendIsl_GameCommand(const CommandContainer &item, int serverId, qint64 sessionId, int roomId, int playerId) +{ + IslMessage msg; + msg.set_message_type(IslMessage::GAME_COMMAND_CONTAINER); + msg.set_session_id(static_cast(sessionId)); + msg.set_player_id(playerId); + + CommandContainer *cont = msg.mutable_game_command(); + cont->CopyFrom(item); + cont->set_room_id(static_cast(roomId)); + + emit sigSendIslMessage(msg, serverId); +} + +void Server::sendIsl_RoomCommand(const CommandContainer &item, int serverId, qint64 sessionId, int roomId) +{ + IslMessage msg; + msg.set_message_type(IslMessage::ROOM_COMMAND_CONTAINER); + msg.set_session_id(static_cast(sessionId)); + + CommandContainer *cont = msg.mutable_room_command(); + cont->CopyFrom(item); + cont->set_room_id(static_cast(roomId)); + + emit sigSendIslMessage(msg, serverId); +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server.h b/libcockatrice_network/libcockatrice/network/server/remote/server.h new file mode 100644 index 000000000..ab57fac4e --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/server.h @@ -0,0 +1,249 @@ +#ifndef SERVER_H +#define SERVER_H + +#include "server_player_reference.h" + +#include +#include +#include +#include +#include +#include +#include + +class Server_DatabaseInterface; +class Server_Game; +class Server_Room; +class Server_ProtocolHandler; +class Server_AbstractUserInterface; +class GameReplay; +class IslMessage; +class SessionEvent; +class RoomEvent; +class DeckList; +class ServerInfo_Game; +class ServerInfo_Room; +class Response; +class GameEventContainer; +class CommandContainer; +class Command_JoinGame; + +enum AuthenticationResult +{ + NotLoggedIn, + PasswordRight, + UnknownUser, + WouldOverwriteOldSession, + UserIsBanned, + UsernameInvalid, + RegistrationRequired, + UserIsInactive, + ClientIdRequired +}; + +class Server : public QObject +{ + Q_OBJECT +signals: + void pingClockTimeout(); + void sigSendIslMessage(const IslMessage &message, int serverId); + void endSession(qint64 sessionId); +private slots: + void broadcastRoomUpdate(const ServerInfo_Room &roomInfo, bool sendToIsl = false); + +public: + mutable QReadWriteLock clientsLock, roomsLock; // locking order: roomsLock before clientsLock + explicit Server(QObject *parent = nullptr); + ~Server() override = default; + AuthenticationResult loginUser(Server_ProtocolHandler *session, + QString &name, + const QString &password, + bool passwordNeedsHash, + QString &reason, + int &secondsLeft, + QString &clientid, + QString &clientVersion, + QString &connectionType); + + const QMap &getRooms() + { + return rooms; + } + + Server_AbstractUserInterface *findUser(const QString &userName) const; + const QMap &getUsers() const + { + return users; + } + const QMap &getUsersBySessionId() const + { + return usersBySessionId; + } + virtual QMap getServerRequiredFeatureList() const + { + return QMap(); + } + void addClient(Server_ProtocolHandler *player); + void removeClient(Server_ProtocolHandler *player); + QList getOnlineModeratorList() const; + virtual QString getLoginMessage() const + { + return QString(); + } + virtual QString getRequiredFeatures() const + { + return QString(); + } + virtual bool permitUnregisteredUsers() const + { + return true; + } + virtual bool getGameShouldPing() const + { + return false; + } + virtual bool getClientIDRequiredEnabled() const + { + return false; + } + virtual bool getRegOnlyServerEnabled() const + { + return false; + } + virtual bool getMaxUserLimitEnabled() const + { + return false; + } + virtual bool getEnableLogQuery() const + { + return false; + } + virtual bool getStoreReplaysEnabled() const + { + return true; + } + virtual int getIdleClientTimeout() const + { + return 0; + } + virtual int getClientKeepAlive() const + { + return 0; + } + virtual int getMaxGameInactivityTime() const + { + return 9999999; + } + virtual int getMaxPlayerInactivityTime() const + { + return 9999999; + } + virtual int getMessageCountingInterval() const + { + return 0; + } + virtual int getMaxMessageCountPerInterval() const + { + return 0; + } + virtual int getMaxMessageSizePerInterval() const + { + return 0; + } + virtual int getMaxGamesPerUser() const + { + return -1; + } + virtual int getCommandCountingInterval() const + { + return 0; + } + virtual int getMaxCommandCountPerInterval() const + { + return 0; + } + virtual int getMaxUserTotal() const + { + return 9999999; + } + virtual int getServerID() const + { + return 0; + } + virtual bool permitCreateGameAsJudge() const + { + return false; + } + + Server_DatabaseInterface *getDatabaseInterface() const; + int getNextLocalGameId() + { + QMutexLocker locker(&nextLocalGameIdMutex); + return ++nextLocalGameId; + } + + void sendIsl_Response(const Response &item, int serverId = -1, qint64 sessionId = -1); + void sendIsl_SessionEvent(const SessionEvent &item, int serverId = -1, qint64 sessionId = -1); + void sendIsl_GameEventContainer(const GameEventContainer &item, int serverId = -1, qint64 sessionId = -1); + void sendIsl_RoomEvent(const RoomEvent &item, int serverId = -1, qint64 sessionId = -1); + void sendIsl_GameCommand(const CommandContainer &item, int serverId, qint64 sessionId, int roomId, int playerId); + void sendIsl_RoomCommand(const CommandContainer &item, int serverId, qint64 sessionId, int roomId); + + const QMap &getExternalUsers() const + { + return externalUsers; + } + + void addPersistentPlayer(const QString &userName, int roomId, int gameId, int playerId); + void removePersistentPlayer(const QString &userName, int roomId, int gameId, int playerId); + QList getPersistentPlayerReferences(const QString &userName) const; + int getUsersCount() const; + int getGamesCount() const; + int getTCPUserCount() const + { + return tcpUserCount; + } + int getWebSocketUserCount() const + { + return webSocketUserCount; + } + +private: + QMultiMap persistentPlayers; + mutable QReadWriteLock persistentPlayersLock; + int nextLocalGameId, tcpUserCount, webSocketUserCount; + QMutex nextLocalGameIdMutex; + +protected slots: + void externalUserJoined(const ServerInfo_User &userInfo); + void externalUserLeft(const QString &userName); + void externalRoomUserJoined(int roomId, const ServerInfo_User &userInfo); + void externalRoomUserLeft(int roomId, const QString &userName); + void externalRoomSay(int roomId, const QString &userName, const QString &message); + void externalRoomRemoveMessages(int roomId, const QString &userName, int amount); + void externalRoomGameListChanged(int roomId, const ServerInfo_Game &gameInfo); + void + externalJoinGameCommandReceived(const Command_JoinGame &cmd, int cmdId, int roomId, int serverId, qint64 sessionId); + void + externalGameCommandContainerReceived(const CommandContainer &cont, int playerId, int serverId, qint64 sessionId); + void externalGameEventContainerReceived(const GameEventContainer &cont, qint64 sessionId); + void externalResponseReceived(const Response &resp, qint64 sessionId); + + virtual void doSendIslMessage(const IslMessage & /* msg */, int /* serverId */) + { + } + +protected: + void prepareDestroy(); + void setDatabaseInterface(Server_DatabaseInterface *_databaseInterface); + QList clients; + QMap usersBySessionId; + QMap users; + QMap externalUsersBySessionId; + QMap externalUsers; + QMap rooms; + QMap databaseInterfaces; + void addRoom(Server_Room *newRoom); +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.cpp new file mode 100644 index 000000000..f9b61ab48 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.cpp @@ -0,0 +1,114 @@ +#include "server_abstractuserinterface.h" + +#include "game/server_game.h" +#include "game/server_player.h" +#include "server.h" +#include "server_player_reference.h" +#include "server_response_containers.h" +#include "server_room.h" + +#include +#include +#include +#include + +void Server_AbstractUserInterface::sendProtocolItemByType(ServerMessage::MessageType type, + const ::google::protobuf::Message &item) +{ + switch (type) { + case ServerMessage::RESPONSE: + sendProtocolItem(static_cast(item)); + break; + case ServerMessage::SESSION_EVENT: + sendProtocolItem(static_cast(item)); + break; + case ServerMessage::GAME_EVENT_CONTAINER: + sendProtocolItem(static_cast(item)); + break; + case ServerMessage::ROOM_EVENT: + sendProtocolItem(static_cast(item)); + break; + } +} + +SessionEvent *Server_AbstractUserInterface::prepareSessionEvent(const ::google::protobuf::Message &sessionEvent) +{ + auto *event = new SessionEvent; + event->GetReflection() + ->MutableMessage(event, sessionEvent.GetDescriptor()->FindExtensionByName("ext")) + ->CopyFrom(sessionEvent); + return event; +} + +void Server_AbstractUserInterface::sendResponseContainer(const ResponseContainer &responseContainer, + Response::ResponseCode responseCode) +{ + const QList> &preResponseQueue = + responseContainer.getPreResponseQueue(); + for (int i = 0; i < preResponseQueue.size(); ++i) + sendProtocolItemByType(preResponseQueue[i].first, *preResponseQueue[i].second); + + if (responseCode != Response::RespNothing) { + Response response; + response.set_cmd_id(responseContainer.getCmdId()); + response.set_response_code(responseCode); + ::google::protobuf::Message *responseExtension = responseContainer.getResponseExtension(); + if (responseExtension) + response.GetReflection() + ->MutableMessage(&response, responseExtension->GetDescriptor()->FindExtensionByName("ext")) + ->CopyFrom(*responseExtension); + sendProtocolItem(response); + } + + const QList> &postResponseQueue = + responseContainer.getPostResponseQueue(); + for (int i = 0; i < postResponseQueue.size(); ++i) + sendProtocolItemByType(postResponseQueue[i].first, *postResponseQueue[i].second); +} + +void Server_AbstractUserInterface::playerRemovedFromGame(Server_Game *game) +{ + qDebug() << "Server_AbstractUserInterface::playerRemovedFromGame(): gameId =" << game->getGameId(); + + QMutexLocker locker(&gameListMutex); + games.remove(game->getGameId()); +} + +void Server_AbstractUserInterface::playerAddedToGame(int gameId, int roomId, int playerId) +{ + qDebug() << "Server_AbstractUserInterface::playerAddedToGame(): gameId =" << gameId; + + QMutexLocker locker(&gameListMutex); + games.insert(gameId, QPair(roomId, playerId)); +} + +void Server_AbstractUserInterface::joinPersistentGames(ResponseContainer &rc) +{ + QList gamesToJoin = + server->getPersistentPlayerReferences(QString::fromStdString(userInfo->name())); + + server->roomsLock.lockForRead(); + for (int i = 0; i < gamesToJoin.size(); ++i) { + const PlayerReference &pr = gamesToJoin.at(i); + + Server_Room *room = server->getRooms().value(pr.getRoomId()); + if (!room) + continue; + QReadLocker roomGamesLocker(&room->gamesLock); + + Server_Game *game = room->getGames().value(pr.getGameId()); + if (!game) + continue; + QMutexLocker gameLocker(&game->gameMutex); + + auto *participant = game->getParticipants().value(pr.getPlayerId()); + if (!participant) + continue; + + participant->setUserInterface(this); + playerAddedToGame(game->getGameId(), room->getId(), participant->getPlayerId()); + + game->createGameJoinedEvent(participant, rc, true); + } + server->roomsLock.unlock(); +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.h b/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.h new file mode 100644 index 000000000..b11260003 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.h @@ -0,0 +1,62 @@ +#ifndef SERVER_ABSTRACTUSERINTERFACE +#define SERVER_ABSTRACTUSERINTERFACE + +#include "serverinfo_user_container.h" + +#include +#include +#include +#include + +class SessionEvent; +class GameEventContainer; +class RoomEvent; +class ResponseContainer; + +class Server; +class Server_Game; + +class Server_AbstractUserInterface : public ServerInfo_User_Container +{ +private: + mutable QMutex gameListMutex; + QMap> games; // gameId -> (roomId, playerId) +protected: + Server *server; + +public: + explicit Server_AbstractUserInterface(Server *_server) : server(_server) + { + } + Server_AbstractUserInterface(Server *_server, const ServerInfo_User_Container &other) + : ServerInfo_User_Container(other), server(_server) + { + } + ~Server_AbstractUserInterface() override + { + } + + virtual int getLastCommandTime() const = 0; + virtual bool addSaidMessageSize(int size) = 0; + + void playerRemovedFromGame(Server_Game *game); + void playerAddedToGame(int gameId, int roomId, int playerId); + void joinPersistentGames(ResponseContainer &rc); + + QMap> getGames() const + { + QMutexLocker locker(&gameListMutex); + return games; + } + + virtual void sendProtocolItem(const Response &item) = 0; + virtual void sendProtocolItem(const SessionEvent &item) = 0; + virtual void sendProtocolItem(const GameEventContainer &item) = 0; + virtual void sendProtocolItem(const RoomEvent &item) = 0; + void sendProtocolItemByType(ServerMessage::MessageType type, const ::google::protobuf::Message &item); + + static SessionEvent *prepareSessionEvent(const ::google::protobuf::Message &sessionEvent); + void sendResponseContainer(const ResponseContainer &responseContainer, Response::ResponseCode responseCode); +}; + +#endif diff --git a/common/server_database_interface.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.cpp similarity index 100% rename from common/server_database_interface.cpp rename to libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.cpp diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h b/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h new file mode 100644 index 000000000..b43dbde42 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h @@ -0,0 +1,177 @@ +#ifndef SERVER_DATABASE_INTERFACE_H +#define SERVER_DATABASE_INTERFACE_H + +#include "server.h" + +#include + +class Server_DatabaseInterface : public QObject +{ + Q_OBJECT +public: + explicit Server_DatabaseInterface(QObject *parent = nullptr) : QObject(parent) + { + } + + virtual AuthenticationResult checkUserPassword(Server_ProtocolHandler *handler, + const QString &user, + const QString &password, + const QString &clientId, + QString &reasonStr, + int &secondsLeft, + bool passwordNeedsHash) = 0; + virtual bool checkUserIsBanned(const QString & /* ipAddress */, + const QString & /* userName */, + const QString & /* clientId */, + QString & /* banReason */, + int & /* banSecondsRemaining */) + { + return false; + } + virtual bool activeUserExists(const QString & /* user */) + { + return false; + } + virtual bool userExists(const QString & /* user */) + { + return false; + } + virtual QString getUserSalt(const QString & /* user */) + { + return {}; + } + virtual QMap getBuddyList(const QString & /* name */) + { + return QMap(); + } + virtual QMap getIgnoreList(const QString & /* name */) + { + return QMap(); + } + virtual bool isInBuddyList(const QString & /* whoseList */, const QString & /* who */) + { + return false; + } + virtual bool isInIgnoreList(const QString & /* whoseList */, const QString & /* who */) + { + return false; + } + virtual ServerInfo_User getUserData(const QString &name, bool withId = false) = 0; + virtual void storeGameInformation(const QString & /* roomName */, + const QStringList & /* roomGameTypes */, + const ServerInfo_Game & /* gameInfo */, + const QSet & /* allPlayersEver */, + const QSet & /* allSpectatorsEver */, + const QList & /* replayList */) + { + } + virtual DeckList *getDeckFromDatabase(int /* deckId */, int /* userId */) + { + return 0; + } + virtual bool removeForgotPassword(const QString & /* user */) + { + return false; + } + virtual qint64 startSession(const QString & /* userName */, + const QString & /* address */, + const QString & /* clientId */, + const QString & /* connectionType */) + { + return 0; + } + virtual bool usernameIsValid(const QString & /*userName */, QString & /* error */) + { + return true; + } +public slots: + virtual void endSession(qint64 /* sessionId */) + { + } + +public: + virtual int getNextGameId() = 0; + virtual int getNextReplayId() = 0; + virtual int getActiveUserCount(QString connectionType = QString()) = 0; + + virtual void clearSessionTables() + { + } + virtual void lockSessionTables() + { + } + virtual void unlockSessionTables() + { + } + virtual bool userSessionExists(const QString & /* userName */) + { + return false; + } + + virtual bool getRequireRegistration() + { + return false; + } + virtual bool registerUser(const QString & /* userName */, + const QString & /* realName */, + const QString & /* password */, + bool /* passwordNeedsHash */, + const QString & /* emailAddress */, + const QString & /* country */, + bool /* active = false */) + { + return false; + } + virtual bool activateUser(const QString & /* userName */, const QString & /* token */) + { + return false; + } + virtual void updateUsersClientID(const QString & /* userName */, const QString & /* userClientID */) + { + } + virtual void updateUsersLastLoginData(const QString & /* userName */, const QString & /* clientVersion */) + { + } + + enum LogMessage_TargetType + { + MessageTargetRoom, + MessageTargetGame, + MessageTargetChat, + MessageTargetIslRoom + }; + virtual void logMessage(const int /* senderId */, + const QString & /* senderName */, + const QString & /* senderIp */, + const QString & /* logMessage */, + LogMessage_TargetType /* targetType */, + const int /* targetId */, + const QString & /* targetName */) + { + } + virtual bool checkUserIsBanned(Server_ProtocolHandler * /* session */, + QString & /* banReason */, + int & /* banSecondsRemaining */) + { + return false; + } + virtual int checkNumberOfUserAccounts(const QString & /* email */) + { + return 0; + } + virtual bool + changeUserPassword(const QString & /* user */, const QString & /* password */, bool /* passwordNeedsHash */) + { + return false; + } + virtual bool changeUserPassword(const QString & /* user */, + const QString & /* oldPassword */, + bool /* oldPasswordNeedsHash */, + const QString & /* newPassword */, + bool /* newPasswordNeedsHash */) + { + return false; + } +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_player_reference.h b/libcockatrice_network/libcockatrice/network/server/remote/server_player_reference.h new file mode 100644 index 000000000..07b2d3d2b --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_player_reference.h @@ -0,0 +1,33 @@ +#ifndef SERVER_PLAYER_REFERENCE_H +#define SERVER_PLAYER_REFERENCE_H + +class PlayerReference +{ +private: + int roomId; + int gameId; + int playerId; + +public: + PlayerReference(int _roomId, int _gameId, int _playerId) : roomId(_roomId), gameId(_gameId), playerId(_playerId) + { + } + int getRoomId() const + { + return roomId; + } + int getGameId() const + { + return gameId; + } + int getPlayerId() const + { + return playerId; + } + bool operator==(const PlayerReference &other) + { + return ((roomId == other.roomId) && (gameId == other.gameId) && (playerId == other.playerId)); + } +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp new file mode 100644 index 000000000..bfd8d113c --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -0,0 +1,865 @@ +#include "server_protocolhandler.h" + +#include "game/server_game.h" +#include "game/server_player.h" +#include "server_database_interface.h" +#include "server_room.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Server_ProtocolHandler::Server_ProtocolHandler(Server *_server, + Server_DatabaseInterface *_databaseInterface, + QObject *parent) + : QObject(parent), Server_AbstractUserInterface(_server), deleted(false), databaseInterface(_databaseInterface), + authState(NotLoggedIn), usingRealPassword(false), acceptsUserListChanges(false), acceptsRoomListChanges(false), + idleClientWarningSent(false), timeRunning(0), lastDataReceived(0), lastActionReceived(0) + +{ + connect(server, &Server::pingClockTimeout, this, &Server_ProtocolHandler::pingClockTimeout); +} + +Server_ProtocolHandler::~Server_ProtocolHandler() +{ +} + +// This function must only be called from the thread this object lives in. +// Except when the server is shutting down. +// The thread must not hold any server locks when calling this (e.g. clientsLock, roomsLock). +void Server_ProtocolHandler::prepareDestroy() +{ + if (deleted) + return; + deleted = true; + + for (auto *room : rooms.values()) { + room->removeClient(this); + } + + QMap> tempGames(getGames()); + + server->roomsLock.lockForRead(); + QMapIterator> gameIterator(tempGames); + while (gameIterator.hasNext()) { + gameIterator.next(); + + Server_Room *room = server->getRooms().value(gameIterator.value().first); + if (!room) + continue; + room->gamesLock.lockForRead(); + Server_Game *game = room->getGames().value(gameIterator.key()); + if (!game) { + room->gamesLock.unlock(); + continue; + } + game->gameMutex.lock(); + auto *participant = game->getParticipants().value(gameIterator.value().second); + if (!participant) { + game->gameMutex.unlock(); + room->gamesLock.unlock(); + continue; + } + + participant->disconnectClient(); + + game->gameMutex.unlock(); + room->gamesLock.unlock(); + } + server->roomsLock.unlock(); + + server->removeClient(this); + + deleteLater(); +} + +void Server_ProtocolHandler::sendProtocolItem(const Response &item) +{ + ServerMessage msg; + msg.mutable_response()->CopyFrom(item); + msg.set_message_type(ServerMessage::RESPONSE); + + transmitProtocolItem(msg); +} + +void Server_ProtocolHandler::sendProtocolItem(const SessionEvent &item) +{ + ServerMessage msg; + msg.mutable_session_event()->CopyFrom(item); + msg.set_message_type(ServerMessage::SESSION_EVENT); + + transmitProtocolItem(msg); +} + +void Server_ProtocolHandler::sendProtocolItem(const GameEventContainer &item) +{ + ServerMessage msg; + msg.mutable_game_event_container()->CopyFrom(item); + msg.set_message_type(ServerMessage::GAME_EVENT_CONTAINER); + + transmitProtocolItem(msg); +} + +void Server_ProtocolHandler::sendProtocolItem(const RoomEvent &item) +{ + ServerMessage msg; + msg.mutable_room_event()->CopyFrom(item); + msg.set_message_type(ServerMessage::ROOM_EVENT); + + transmitProtocolItem(msg); +} + +Response::ResponseCode Server_ProtocolHandler::processSessionCommandContainer(const CommandContainer &cont, + ResponseContainer &rc) +{ + Response::ResponseCode finalResponseCode = Response::RespOk; + for (int i = cont.session_command_size() - 1; i >= 0; --i) { + Response::ResponseCode resp = Response::RespInvalidCommand; + const SessionCommand &sc = cont.session_command(i); + const int num = getPbExtension(sc); + if (num != SessionCommand::PING) { // don't log ping commands + logDebugMessage(getSafeDebugString(sc)); + } + switch ((SessionCommand::SessionCommandType)num) { + case SessionCommand::PING: + resp = cmdPing(sc.GetExtension(Command_Ping::ext), rc); + break; + case SessionCommand::LOGIN: + resp = cmdLogin(sc.GetExtension(Command_Login::ext), rc); + break; + case SessionCommand::MESSAGE: + resp = cmdMessage(sc.GetExtension(Command_Message::ext), rc); + break; + case SessionCommand::GET_GAMES_OF_USER: + resp = cmdGetGamesOfUser(sc.GetExtension(Command_GetGamesOfUser::ext), rc); + break; + case SessionCommand::GET_USER_INFO: + resp = cmdGetUserInfo(sc.GetExtension(Command_GetUserInfo::ext), rc); + break; + case SessionCommand::LIST_ROOMS: + resp = cmdListRooms(sc.GetExtension(Command_ListRooms::ext), rc); + break; + case SessionCommand::JOIN_ROOM: + resp = cmdJoinRoom(sc.GetExtension(Command_JoinRoom::ext), rc); + break; + case SessionCommand::LIST_USERS: + resp = cmdListUsers(sc.GetExtension(Command_ListUsers::ext), rc); + break; + default: + resp = processExtendedSessionCommand(num, sc, rc); + } + if (resp != Response::RespOk) + finalResponseCode = resp; + } + return finalResponseCode; +} + +Response::ResponseCode Server_ProtocolHandler::processRoomCommandContainer(const CommandContainer &cont, + ResponseContainer &rc) +{ + if (authState == NotLoggedIn) + return Response::RespLoginNeeded; + + QReadLocker locker(&server->roomsLock); + Server_Room *room = rooms.value(cont.room_id(), 0); + if (!room) + return Response::RespNotInRoom; + + resetIdleTimer(); + + Response::ResponseCode finalResponseCode = Response::RespOk; + for (int i = cont.room_command_size() - 1; i >= 0; --i) { + Response::ResponseCode resp = Response::RespInvalidCommand; + const RoomCommand &sc = cont.room_command(i); + const int num = getPbExtension(sc); + logDebugMessage(getSafeDebugString(sc)); + switch ((RoomCommand::RoomCommandType)num) { + case RoomCommand::LEAVE_ROOM: + resp = cmdLeaveRoom(sc.GetExtension(Command_LeaveRoom::ext), room, rc); + break; + case RoomCommand::ROOM_SAY: + resp = cmdRoomSay(sc.GetExtension(Command_RoomSay::ext), room, rc); + break; + case RoomCommand::CREATE_GAME: + resp = cmdCreateGame(sc.GetExtension(Command_CreateGame::ext), room, rc); + break; + case RoomCommand::JOIN_GAME: + resp = cmdJoinGame(sc.GetExtension(Command_JoinGame::ext), room, rc); + break; + } + if (resp != Response::RespOk) + finalResponseCode = resp; + } + return finalResponseCode; +} + +Response::ResponseCode Server_ProtocolHandler::processGameCommandContainer(const CommandContainer &cont, + ResponseContainer &rc) +{ + static QList antifloodCommandsWhiteList = + QList() + // draw/undo card draw (example: drawing 10 cards one by one from the deck) + << GameCommand::DRAW_CARDS + << GameCommand::UNDO_DRAW + // create, delete arrows (example: targeting with 10 cards during an attack) + << GameCommand::CREATE_ARROW + << GameCommand::DELETE_ARROW + // set card attributes (example: tapping 10 cards at once) + << GameCommand::SET_CARD_ATTR + // increment / decrement counter (example: -10 life points one by one) + << GameCommand::INC_COUNTER + // mulling lots of hands in a row + << GameCommand::MULLIGAN + // allows a user to sideboard without receiving flooding message + << GameCommand::MOVE_CARD; + + if (authState == NotLoggedIn) + return Response::RespLoginNeeded; + + QMap> gameMap = getGames(); + if (!gameMap.contains(cont.game_id())) + return Response::RespNotInRoom; + const QPair roomIdAndPlayerId = gameMap.value(cont.game_id()); + + QReadLocker roomsLocker(&server->roomsLock); + Server_Room *room = server->getRooms().value(roomIdAndPlayerId.first); + if (!room) + return Response::RespNotInRoom; + + QReadLocker roomGamesLocker(&room->gamesLock); + Server_Game *game = room->getGames().value(cont.game_id()); + if (!game) { + if (room->getExternalGames().contains(cont.game_id())) { + server->sendIsl_GameCommand(cont, room->getExternalGames().value(cont.game_id()).server_id(), + userInfo->session_id(), roomIdAndPlayerId.first, roomIdAndPlayerId.second); + return Response::RespNothing; + } + return Response::RespNotInRoom; + } + + QMutexLocker gameLocker(&game->gameMutex); + auto *participant = game->getParticipants().value(roomIdAndPlayerId.second); + if (!participant) + return Response::RespNotInRoom; + + resetIdleTimer(); + + int commandCountingInterval = server->getCommandCountingInterval(); + int maxCommandCountPerInterval = server->getMaxCommandCountPerInterval(); + GameEventStorage ges; + Response::ResponseCode finalResponseCode = Response::RespOk; + for (int i = cont.game_command_size() - 1; i >= 0; --i) { + const GameCommand &sc = cont.game_command(i); + logDebugMessage(QString("game %1 player %2: ").arg(cont.game_id()).arg(roomIdAndPlayerId.second) + + getSafeDebugString(sc)); + + if (commandCountingInterval > 0) { + int totalCount = 0; + if (commandCountOverTime.isEmpty()) + commandCountOverTime.prepend(0); + + if (!antifloodCommandsWhiteList.contains((GameCommand::GameCommandType)getPbExtension(sc))) + ++commandCountOverTime[0]; + + for (int count : commandCountOverTime) { + totalCount += count; + } + + if (maxCommandCountPerInterval > 0 && totalCount > maxCommandCountPerInterval) { + return Response::RespChatFlood; + } + } + + Response::ResponseCode resp = participant->processGameCommand(sc, rc, ges); + + if (resp != Response::RespOk) + finalResponseCode = resp; + } + ges.sendToGame(game); + + return finalResponseCode; +} + +Response::ResponseCode Server_ProtocolHandler::processModeratorCommandContainer(const CommandContainer &cont, + ResponseContainer &rc) +{ + if (!userInfo) + return Response::RespLoginNeeded; + if (!(userInfo->user_level() & ServerInfo_User::IsModerator)) + return Response::RespLoginNeeded; + + resetIdleTimer(); + + Response::ResponseCode finalResponseCode = Response::RespOk; + for (int i = cont.moderator_command_size() - 1; i >= 0; --i) { + Response::ResponseCode resp = Response::RespInvalidCommand; + const ModeratorCommand &sc = cont.moderator_command(i); + const int num = getPbExtension(sc); + logDebugMessage(getSafeDebugString(sc)); + + resp = processExtendedModeratorCommand(num, sc, rc); + if (resp != Response::RespOk) + finalResponseCode = resp; + } + return finalResponseCode; +} + +Response::ResponseCode Server_ProtocolHandler::processAdminCommandContainer(const CommandContainer &cont, + ResponseContainer &rc) +{ + if (!userInfo) + return Response::RespLoginNeeded; + if (!(userInfo->user_level() & ServerInfo_User::IsAdmin)) + return Response::RespLoginNeeded; + + resetIdleTimer(); + + Response::ResponseCode finalResponseCode = Response::RespOk; + for (int i = cont.admin_command_size() - 1; i >= 0; --i) { + Response::ResponseCode resp = Response::RespInvalidCommand; + const AdminCommand &sc = cont.admin_command(i); + const int num = getPbExtension(sc); + logDebugMessage(getSafeDebugString(sc)); + + resp = processExtendedAdminCommand(num, sc, rc); + if (resp != Response::RespOk) + finalResponseCode = resp; + } + return finalResponseCode; +} + +void Server_ProtocolHandler::processCommandContainer(const CommandContainer &cont) +{ + // Command processing must be disabled after prepareDestroy() has been called. + if (deleted) + return; + + lastDataReceived = timeRunning; + + ResponseContainer responseContainer(cont.has_cmd_id() ? cont.cmd_id() : -1); + Response::ResponseCode finalResponseCode; + + if (cont.game_command_size()) + finalResponseCode = processGameCommandContainer(cont, responseContainer); + else if (cont.room_command_size()) + finalResponseCode = processRoomCommandContainer(cont, responseContainer); + else if (cont.session_command_size()) + finalResponseCode = processSessionCommandContainer(cont, responseContainer); + else if (cont.moderator_command_size()) + finalResponseCode = processModeratorCommandContainer(cont, responseContainer); + else if (cont.admin_command_size()) + finalResponseCode = processAdminCommandContainer(cont, responseContainer); + else + finalResponseCode = Response::RespInvalidCommand; + + if ((finalResponseCode != Response::RespNothing)) + sendResponseContainer(responseContainer, finalResponseCode); +} + +void Server_ProtocolHandler::pingClockTimeout() +{ + + int cmdcountinterval = server->getCommandCountingInterval(); + int msgcountinterval = server->getMessageCountingInterval(); + int pingclockinterval = server->getClientKeepAlive(); + + int interval = server->getMessageCountingInterval(); + if (interval > 0) { + if (pingclockinterval > 0) { + messageSizeOverTime.prepend(0); + if (messageSizeOverTime.size() > (msgcountinterval / pingclockinterval)) + messageSizeOverTime.removeLast(); + messageCountOverTime.prepend(0); + if (messageCountOverTime.size() > (msgcountinterval / pingclockinterval)) + messageCountOverTime.removeLast(); + } + } + + interval = server->getCommandCountingInterval(); + if (interval > 0) { + if (pingclockinterval > 0) { + commandCountOverTime.prepend(0); + if (commandCountOverTime.size() > (cmdcountinterval / pingclockinterval)) + commandCountOverTime.removeLast(); + } + } + + if (timeRunning - lastDataReceived > server->getMaxPlayerInactivityTime()) + prepareDestroy(); + + // PrivLevel users, Moderators, and Admins are not subject to the server idle timeout policy + const bool hasPrivLevel = userInfo && QString::fromStdString(userInfo->privlevel()).toLower() != "none"; + const bool isModOrAdmin = + userInfo && (userInfo->user_level() & (ServerInfo_User::IsModerator | ServerInfo_User::IsAdmin)); + if (!hasPrivLevel && !isModOrAdmin) { + if ((server->getIdleClientTimeout() > 0) && (idleClientWarningSent)) { + if (timeRunning - lastActionReceived > server->getIdleClientTimeout()) { + prepareDestroy(); + } + } + + if (((timeRunning - lastActionReceived) >= qCeil(server->getIdleClientTimeout() * .9)) && + (!idleClientWarningSent) && (server->getIdleClientTimeout() > 0)) { + Event_NotifyUser event; + event.set_type(Event_NotifyUser::IDLEWARNING); + SessionEvent *se = prepareSessionEvent(event); + sendProtocolItem(*se); + delete se; + idleClientWarningSent = true; + } + } + + ++timeRunning; +} + +Response::ResponseCode Server_ProtocolHandler::cmdPing(const Command_Ping & /*cmd*/, ResponseContainer & /*rc*/) +{ + return Response::RespOk; +} + +Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd, ResponseContainer &rc) +{ + QString userName = nameFromStdString(cmd.user_name()).simplified(); + QString clientId = nameFromStdString(cmd.clientid()).simplified(); + QString clientVersion = nameFromStdString(cmd.clientver()).simplified(); + QString password; + bool needsHash = false; + if (cmd.has_password()) { + if (cmd.password().length() > MAX_NAME_LENGTH) + return Response::RespWrongPassword; + password = QString::fromStdString(cmd.password()); + needsHash = true; + } else if (cmd.hashed_password().length() > MAX_NAME_LENGTH) { + return Response::RespContextError; + } else { + password = nameFromStdString(cmd.hashed_password()); + } + + if (userInfo != 0) { + return Response::RespContextError; + } + + // check client feature set against server feature set + FeatureSet features; + QMap receivedClientFeatures; + QMap missingClientFeatures; + + int featureCount = qMin(cmd.clientfeatures().size(), MAX_NAME_LENGTH); + for (int i = 0; i < featureCount; ++i) { + receivedClientFeatures.insert(nameFromStdString(cmd.clientfeatures(i)).simplified(), false); + } + + missingClientFeatures = + features.identifyMissingFeatures(receivedClientFeatures, server->getServerRequiredFeatureList()); + + if (!missingClientFeatures.isEmpty()) { + if (features.isRequiredFeaturesMissing(missingClientFeatures, server->getServerRequiredFeatureList())) { + auto *re = new Response_Login; + re->set_denied_reason_str("Client upgrade required"); + QMap::iterator i; + for (i = missingClientFeatures.begin(); i != missingClientFeatures.end(); ++i) { + re->add_missing_features(i.key().toStdString().c_str()); + } + rc.setResponseExtension(re); + return Response::RespClientUpdateRequired; + } + } + + QString reasonStr; + int banSecondsLeft = 0; + QString connectionType = getConnectionType(); + AuthenticationResult res = server->loginUser(this, userName, password, needsHash, reasonStr, banSecondsLeft, + clientId, clientVersion, connectionType); + switch (res) { + case UserIsBanned: { + auto *re = new Response_Login; + re->set_denied_reason_str(reasonStr.toStdString()); + if (banSecondsLeft != 0) + re->set_denied_end_time(QDateTime::currentDateTime().addSecs(banSecondsLeft).toSecsSinceEpoch()); + rc.setResponseExtension(re); + return Response::RespUserIsBanned; + } + case NotLoggedIn: + return Response::RespWrongPassword; + case WouldOverwriteOldSession: + return Response::RespWouldOverwriteOldSession; + case UsernameInvalid: { + auto *re = new Response_Login; + re->set_denied_reason_str(reasonStr.toStdString()); + rc.setResponseExtension(re); + return Response::RespUsernameInvalid; + } + case RegistrationRequired: + return Response::RespRegistrationRequired; + case ClientIdRequired: + return Response::RespClientIdRequired; + case UserIsInactive: + return Response::RespAccountNotActivated; + default: + authState = res; + usingRealPassword = needsHash; + } + + // limit the number of non-privileged users that can connect to the server based on configuration settings + if (!userInfo || QString::fromStdString(userInfo->privlevel()).toLower() == "none") { + if (server->getMaxUserLimitEnabled()) { + if (server->getUsersCount() > server->getMaxUserTotal()) { + qDebug() << "Max Users Total Limit Reached, please increase the max_users_total setting."; + return Response::RespServerFull; + } + } + } + + userName = QString::fromStdString(userInfo->name()); + Event_ServerMessage event; + event.set_message(server->getLoginMessage().toStdString()); + rc.enqueuePostResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); + + auto *re = new Response_Login; + re->mutable_user_info()->CopyFrom(copyUserInfo(true)); + + if (authState == PasswordRight) { + QMapIterator buddyIterator(databaseInterface->getBuddyList(userName)); + while (buddyIterator.hasNext()) + re->add_buddy_list()->CopyFrom(buddyIterator.next().value()); + + QMapIterator ignoreIterator(databaseInterface->getIgnoreList(userName)); + while (ignoreIterator.hasNext()) + re->add_ignore_list()->CopyFrom(ignoreIterator.next().value()); + } + + // return to client any missing features the server has that the client does not + if (!missingClientFeatures.isEmpty()) { + QMap::iterator i; + for (i = missingClientFeatures.begin(); i != missingClientFeatures.end(); ++i) + re->add_missing_features(i.key().toStdString().c_str()); + } + + joinPersistentGames(rc); + databaseInterface->removeForgotPassword(userName); + rc.setResponseExtension(re); + return Response::RespOk; +} + +Response::ResponseCode Server_ProtocolHandler::cmdMessage(const Command_Message &cmd, ResponseContainer &rc) +{ + if (authState == NotLoggedIn) + return Response::RespLoginNeeded; + + QReadLocker locker(&server->clientsLock); + + QString receiver = nameFromStdString(cmd.user_name()); + Server_AbstractUserInterface *userInterface = server->findUser(receiver); + if (!userInterface) { + return Response::RespNameNotFound; + } + if (databaseInterface->isInIgnoreList(receiver, QString::fromStdString(userInfo->name()))) { + return Response::RespInIgnoreList; + } + if (!addSaidMessageSize(static_cast(cmd.message().size()))) { + return Response::RespChatFlood; + } + + Event_UserMessage event; + event.set_sender_name(userInfo->name()); + event.set_receiver_name(receiver.toStdString()); + event.set_message(cmd.message()); + + SessionEvent *se = prepareSessionEvent(event); + userInterface->sendProtocolItem(*se); + rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, se); + + databaseInterface->logMessage(userInfo->id(), QString::fromStdString(userInfo->name()), + QString::fromStdString(userInfo->address()), QString::fromStdString(cmd.message()), + Server_DatabaseInterface::MessageTargetChat, userInterface->getUserInfo()->id(), + receiver); + resetIdleTimer(); + return Response::RespOk; +} + +Response::ResponseCode Server_ProtocolHandler::cmdGetGamesOfUser(const Command_GetGamesOfUser &cmd, + ResponseContainer &rc) +{ + if (authState == NotLoggedIn) + return Response::RespLoginNeeded; + + // Do not show games to someone on the ignore list of that user, except for mods + QString target_user = nameFromStdString(cmd.user_name()); + Server_AbstractUserInterface *userInterface = server->findUser(target_user); + if (!userInterface) { + return Response::RespNameNotFound; + } + if (!(userInfo->user_level() & (ServerInfo_User::IsModerator | ServerInfo_User::IsAdmin)) && + databaseInterface->isInIgnoreList(target_user, QString::fromStdString(userInfo->name()))) { + return Response::RespInIgnoreList; + } + + // We don't need to check whether the user is logged in; persistent games should also work. + // The client needs to deal with an empty result list. + + auto *re = new Response_GetGamesOfUser; + server->roomsLock.lockForRead(); + QMapIterator roomIterator(server->getRooms()); + while (roomIterator.hasNext()) { + Server_Room *room = roomIterator.next().value(); + room->gamesLock.lockForRead(); + room->getInfo(*re->add_room_list(), false, true); + QListIterator gameIterator(room->getGamesOfUser(nameFromStdString(cmd.user_name()))); + while (gameIterator.hasNext()) + re->add_game_list()->CopyFrom(gameIterator.next()); + room->gamesLock.unlock(); + } + server->roomsLock.unlock(); + + rc.setResponseExtension(re); + return Response::RespOk; +} + +Response::ResponseCode Server_ProtocolHandler::cmdGetUserInfo(const Command_GetUserInfo &cmd, ResponseContainer &rc) +{ + if (authState == NotLoggedIn) + return Response::RespLoginNeeded; + + QString userName = nameFromStdString(cmd.user_name()); + auto *re = new Response_GetUserInfo; + if (userName.isEmpty()) + re->mutable_user_info()->CopyFrom(*userInfo); + else { + + QReadLocker locker(&server->clientsLock); + + ServerInfo_User_Container *infoSource = server->findUser(userName); + if (!infoSource) { + re->mutable_user_info()->CopyFrom(databaseInterface->getUserData(userName, true)); + } else { + re->mutable_user_info()->CopyFrom( + infoSource->copyUserInfo(true, false, userInfo->user_level() & ServerInfo_User::IsModerator)); + } + } + + rc.setResponseExtension(re); + return Response::RespOk; +} + +Response::ResponseCode Server_ProtocolHandler::cmdListRooms(const Command_ListRooms & /*cmd*/, ResponseContainer &rc) +{ + if (authState == NotLoggedIn) + return Response::RespLoginNeeded; + + Event_ListRooms event; + QMapIterator roomIterator(server->getRooms()); + while (roomIterator.hasNext()) + roomIterator.next().value()->getInfo(*event.add_room_list(), false); + rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); + + acceptsRoomListChanges = true; + return Response::RespOk; +} + +Response::ResponseCode Server_ProtocolHandler::cmdJoinRoom(const Command_JoinRoom &cmd, ResponseContainer &rc) +{ + if (authState == NotLoggedIn) + return Response::RespLoginNeeded; + + if (rooms.contains(cmd.room_id())) + return Response::RespContextError; + + QReadLocker serverLocker(&server->roomsLock); + Server_Room *room = server->getRooms().value(cmd.room_id(), 0); + if (!room) + return Response::RespNameNotFound; + + if (!(userInfo->user_level() & ServerInfo_User::IsModerator)) + if (!(room->userMayJoin(*userInfo))) + return Response::RespUserLevelTooLow; + + room->addClient(this); + rooms.insert(room->getId(), room); + + QReadLocker chatHistoryLocker(&room->historyLock); + QList chatHistory = room->getChatHistory(); + ServerInfo_ChatMessage chatMessage; + for (int i = 0; i < chatHistory.size(); ++i) { + chatMessage = chatHistory.at(i); + Event_RoomSay roomChatHistory; + roomChatHistory.set_message(chatMessage.sender_name() + ": " + chatMessage.message()); + roomChatHistory.set_message_type(Event_RoomSay::ChatHistory); + roomChatHistory.set_time_of( + QDateTime::fromString(QString::fromStdString(chatMessage.time())).toMSecsSinceEpoch()); + rc.enqueuePostResponseItem(ServerMessage::ROOM_EVENT, room->prepareRoomEvent(roomChatHistory)); + } + + Event_RoomSay joinMessageEvent; + joinMessageEvent.set_message(room->getJoinMessage().toStdString()); + joinMessageEvent.set_message_type(Event_RoomSay::Welcome); + rc.enqueuePostResponseItem(ServerMessage::ROOM_EVENT, room->prepareRoomEvent(joinMessageEvent)); + + auto *re = new Response_JoinRoom; + room->getInfo(*re->mutable_room_info(), true); + + rc.setResponseExtension(re); + return Response::RespOk; +} + +Response::ResponseCode Server_ProtocolHandler::cmdListUsers(const Command_ListUsers & /*cmd*/, ResponseContainer &rc) +{ + if (authState == NotLoggedIn) + return Response::RespLoginNeeded; + + auto *re = new Response_ListUsers; + server->clientsLock.lockForRead(); + QMapIterator userIterator = server->getUsers(); + while (userIterator.hasNext()) + re->add_user_list()->CopyFrom(userIterator.next().value()->copyUserInfo(false)); + QMapIterator extIterator = server->getExternalUsers(); + while (extIterator.hasNext()) + re->add_user_list()->CopyFrom(extIterator.next().value()->copyUserInfo(false)); + + acceptsUserListChanges = true; + server->clientsLock.unlock(); + + rc.setResponseExtension(re); + return Response::RespOk; +} + +Response::ResponseCode +Server_ProtocolHandler::cmdLeaveRoom(const Command_LeaveRoom & /*cmd*/, Server_Room *room, ResponseContainer & /*rc*/) +{ + rooms.remove(room->getId()); + room->removeClient(this); + return Response::RespOk; +} + +bool Server_ProtocolHandler::addSaidMessageSize(int size) +{ + if (server->getMessageCountingInterval() <= 0) { + return true; + } + + int totalSize = 0, totalCount = 0; + if (messageSizeOverTime.isEmpty()) { + messageSizeOverTime.prepend(0); + } + + messageSizeOverTime[0] += size; + for (int messageSize : messageSizeOverTime) { + totalSize += messageSize; + } + + if (messageCountOverTime.isEmpty()) { + messageCountOverTime.prepend(0); + } + + messageCountOverTime[0] += 1; + for (int messageCount : messageCountOverTime) { + totalCount += messageCount; + } + + return totalSize <= server->getMaxMessageSizePerInterval() && totalCount <= server->getMaxMessageCountPerInterval(); +} + +Response::ResponseCode +Server_ProtocolHandler::cmdRoomSay(const Command_RoomSay &cmd, Server_Room *room, ResponseContainer & /*rc*/) +{ + if (!addSaidMessageSize(static_cast(cmd.message().size()))) { + return Response::RespChatFlood; + } + QString msg = QString::fromStdString(cmd.message()); + + msg.replace(QChar('\n'), QChar(' ')); + + room->say(QString::fromStdString(userInfo->name()), msg); + + databaseInterface->logMessage(userInfo->id(), QString::fromStdString(userInfo->name()), + QString::fromStdString(userInfo->address()), msg, + Server_DatabaseInterface::MessageTargetRoom, room->getId(), room->getName()); + + return Response::RespOk; +} + +Response::ResponseCode +Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room *room, ResponseContainer &rc) +{ + if (authState == NotLoggedIn) + return Response::RespLoginNeeded; + if (cmd.password().length() > MAX_NAME_LENGTH) + return Response::RespContextError; + + auto level = userInfo->user_level(); + bool isJudge = level & ServerInfo_User::IsJudge; + int maxGames = server->getMaxGamesPerUser(); + bool asJudge = cmd.join_as_judge(); + bool asSpectator = cmd.join_as_spectator(); + // allow judges to open games as spectator without limit to facilitate bots etc, -1 means no limit + if (!(isJudge && asJudge && asSpectator) && maxGames >= 0 && + room->getGamesCreatedByUser(QString::fromStdString(userInfo->name())) >= maxGames) { + return Response::RespContextError; + } + + // if a non judge user tries to create a game as judge while not permitted, instead create a normal game + if (asJudge && !(server->permitCreateGameAsJudge() || isJudge)) { + asJudge = false; + } + + QList gameTypes; + int gameTypeCount = qMin(cmd.game_type_ids().size(), MAX_NAME_LENGTH); + for (int i = 0; i < gameTypeCount; ++i) { // the client actually only sends one of these + gameTypes.append(cmd.game_type_ids(i)); + } + + QString description = nameFromStdString(cmd.description()); + int startingLifeTotal = cmd.has_starting_life_total() ? cmd.starting_life_total() : 20; + + bool shareDecklistsOnLoad = cmd.has_share_decklists_on_load() ? cmd.share_decklists_on_load() : false; + + const int gameId = databaseInterface->getNextGameId(); + if (gameId == -1) { + return Response::RespInternalError; + } + + // When server doesn't permit registered users to exist, do not honor only-reg setting + bool onlyRegisteredUsers = cmd.only_registered() && (server->permitUnregisteredUsers()); + auto *game = new Server_Game(copyUserInfo(false), gameId, description, QString::fromStdString(cmd.password()), + cmd.max_players(), gameTypes, cmd.only_buddies(), onlyRegisteredUsers, + cmd.spectators_allowed(), cmd.spectators_need_password(), cmd.spectators_can_talk(), + cmd.spectators_see_everything(), startingLifeTotal, shareDecklistsOnLoad, room); + + game->addPlayer(this, rc, asSpectator, asJudge, false); + room->addGame(game); + + return Response::RespOk; +} + +Response::ResponseCode +Server_ProtocolHandler::cmdJoinGame(const Command_JoinGame &cmd, Server_Room *room, ResponseContainer &rc) +{ + if (authState == NotLoggedIn) + return Response::RespLoginNeeded; + + return room->processJoinGameCommand(cmd, rc, this); +} + +void Server_ProtocolHandler::resetIdleTimer() +{ + lastActionReceived = timeRunning; + idleClientWarningSent = false; +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h new file mode 100644 index 000000000..0d05b91c8 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h @@ -0,0 +1,140 @@ +#ifndef SERVER_PROTOCOLHANDLER_H +#define SERVER_PROTOCOLHANDLER_H + +#include "server.h" +#include "server_abstractuserinterface.h" + +#include +#include +#include + +class Features; +class Server_DatabaseInterface; +class Server_Player; +class ServerInfo_User; +class Server_Room; +class QTimer; +class FeatureSet; + +class ServerMessage; +class Response; +class SessionEvent; +class GameEventContainer; +class RoomEvent; +class ResponseContainer; + +class CommandContainer; +class SessionCommand; +class ModeratorCommand; +class AdminCommand; + +class Command_Ping; +class Command_Login; +class Command_Register; +class Command_Message; +class Command_ListUsers; +class Command_GetGamesOfUser; +class Command_GetUserInfo; +class Command_ListRooms; +class Command_JoinRoom; +class Command_LeaveRoom; +class Command_RoomSay; +class Command_CreateGame; +class Command_JoinGame; + +class Server_ProtocolHandler : public QObject, public Server_AbstractUserInterface +{ + Q_OBJECT +protected: + QMap rooms; + + bool deleted; + Server_DatabaseInterface *databaseInterface; + AuthenticationResult authState; + bool usingRealPassword; + bool acceptsUserListChanges; + bool acceptsRoomListChanges; + bool idleClientWarningSent; + virtual void logDebugMessage(const QString & /* message */) + { + } + +private: + QList messageSizeOverTime, messageCountOverTime, commandCountOverTime; + int timeRunning, lastDataReceived, lastActionReceived; + + virtual void transmitProtocolItem(const ServerMessage &item) = 0; + + Response::ResponseCode cmdPing(const Command_Ping &cmd, ResponseContainer &rc); + Response::ResponseCode cmdLogin(const Command_Login &cmd, ResponseContainer &rc); + Response::ResponseCode cmdMessage(const Command_Message &cmd, ResponseContainer &rc); + Response::ResponseCode cmdGetGamesOfUser(const Command_GetGamesOfUser &cmd, ResponseContainer &rc); + Response::ResponseCode cmdGetUserInfo(const Command_GetUserInfo &cmd, ResponseContainer &rc); + Response::ResponseCode cmdListRooms(const Command_ListRooms &cmd, ResponseContainer &rc); + Response::ResponseCode cmdJoinRoom(const Command_JoinRoom &cmd, ResponseContainer &rc); + Response::ResponseCode cmdListUsers(const Command_ListUsers &cmd, ResponseContainer &rc); + Response::ResponseCode cmdLeaveRoom(const Command_LeaveRoom &cmd, Server_Room *room, ResponseContainer &rc); + Response::ResponseCode cmdRoomSay(const Command_RoomSay &cmd, Server_Room *room, ResponseContainer &rc); + Response::ResponseCode cmdCreateGame(const Command_CreateGame &cmd, Server_Room *room, ResponseContainer &rc); + Response::ResponseCode cmdJoinGame(const Command_JoinGame &cmd, Server_Room *room, ResponseContainer &rc); + + Response::ResponseCode processSessionCommandContainer(const CommandContainer &cont, ResponseContainer &rc); + virtual Response::ResponseCode + processExtendedSessionCommand(int /* cmdType */, const SessionCommand & /* cmd */, ResponseContainer & /* rc */) + { + return Response::RespFunctionNotAllowed; + } + Response::ResponseCode processRoomCommandContainer(const CommandContainer &cont, ResponseContainer &rc); + Response::ResponseCode processGameCommandContainer(const CommandContainer &cont, ResponseContainer &rc); + Response::ResponseCode processModeratorCommandContainer(const CommandContainer &cont, ResponseContainer &rc); + virtual Response::ResponseCode + processExtendedModeratorCommand(int /* cmdType */, const ModeratorCommand & /* cmd */, ResponseContainer & /* rc */) + { + return Response::RespFunctionNotAllowed; + } + Response::ResponseCode processAdminCommandContainer(const CommandContainer &cont, ResponseContainer &rc); + virtual Response::ResponseCode + processExtendedAdminCommand(int /* cmdType */, const AdminCommand & /* cmd */, ResponseContainer & /* rc */) + { + return Response::RespFunctionNotAllowed; + } + + void resetIdleTimer(); +private slots: + void pingClockTimeout(); +public slots: + void prepareDestroy(); + +public: + Server_ProtocolHandler(Server *_server, Server_DatabaseInterface *_databaseInterface, QObject *parent = 0); + ~Server_ProtocolHandler(); + + bool getAcceptsUserListChanges() const + { + return acceptsUserListChanges; + } + bool getAcceptsRoomListChanges() const + { + return acceptsRoomListChanges; + } + virtual QString getAddress() const = 0; + virtual QString getConnectionType() const = 0; + Server_DatabaseInterface *getDatabaseInterface() const + { + return databaseInterface; + } + + int getLastCommandTime() const + { + return timeRunning - lastDataReceived; + } + bool addSaidMessageSize(int size); + void processCommandContainer(const CommandContainer &cont); + + void sendProtocolItem(const Response &item); + void sendProtocolItem(const SessionEvent &item); + void sendProtocolItem(const GameEventContainer &item); + void sendProtocolItem(const RoomEvent &item); +}; + +#endif diff --git a/common/server_remoteuserinterface.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_remoteuserinterface.cpp similarity index 51% rename from common/server_remoteuserinterface.cpp rename to libcockatrice_network/libcockatrice/network/server/remote/server_remoteuserinterface.cpp index cd0e30312..54af5a265 100644 --- a/common/server_remoteuserinterface.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_remoteuserinterface.cpp @@ -1,23 +1,25 @@ #include "server_remoteuserinterface.h" + #include "server.h" -#include "pb/serverinfo_user.pb.h" + +#include void Server_RemoteUserInterface::sendProtocolItem(const Response &item) { - server->sendIsl_Response(item, userInfo->server_id(), userInfo->session_id()); + server->sendIsl_Response(item, userInfo->server_id(), userInfo->session_id()); } void Server_RemoteUserInterface::sendProtocolItem(const SessionEvent &item) { - server->sendIsl_SessionEvent(item, userInfo->server_id(), userInfo->session_id()); + server->sendIsl_SessionEvent(item, userInfo->server_id(), userInfo->session_id()); } void Server_RemoteUserInterface::sendProtocolItem(const GameEventContainer &item) { - server->sendIsl_GameEventContainer(item, userInfo->server_id(), userInfo->session_id()); + server->sendIsl_GameEventContainer(item, userInfo->server_id(), userInfo->session_id()); } void Server_RemoteUserInterface::sendProtocolItem(const RoomEvent &item) { - server->sendIsl_RoomEvent(item, userInfo->server_id(), userInfo->session_id()); + server->sendIsl_RoomEvent(item, userInfo->server_id(), userInfo->session_id()); } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_remoteuserinterface.h b/libcockatrice_network/libcockatrice/network/server/remote/server_remoteuserinterface.h new file mode 100644 index 000000000..d68656b9d --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_remoteuserinterface.h @@ -0,0 +1,29 @@ +#ifndef SERVER_REMOTEUSERINTERFACE_H +#define SERVER_REMOTEUSERINTERFACE_H + +#include "server_abstractuserinterface.h" + +class Server_RemoteUserInterface : public Server_AbstractUserInterface +{ +public: + Server_RemoteUserInterface(Server *_server, const ServerInfo_User_Container &_userInfoContainer) + : Server_AbstractUserInterface(_server, _userInfoContainer) + { + } + + int getLastCommandTime() const + { + return 0; + } + bool addSaidMessageSize(int /*size*/) + { + return true; + } + + void sendProtocolItem(const Response &item); + void sendProtocolItem(const SessionEvent &item); + void sendProtocolItem(const GameEventContainer &item); + void sendProtocolItem(const RoomEvent &item); +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_response_containers.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_response_containers.cpp new file mode 100644 index 000000000..9b07bdb91 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_response_containers.cpp @@ -0,0 +1,95 @@ +#include "server_response_containers.h" + +#include "game/server_game.h" + +#include + +GameEventStorageItem::GameEventStorageItem(const ::google::protobuf::Message &_event, + int _playerId, + EventRecipients _recipients) + : event(new GameEvent), recipients(_recipients) +{ + event->GetReflection()->MutableMessage(event, _event.GetDescriptor()->FindExtensionByName("ext"))->CopyFrom(_event); + event->set_player_id(_playerId); +} + +GameEventStorageItem::~GameEventStorageItem() +{ + delete event; +} + +GameEventStorage::GameEventStorage() : gameEventContext(0), privatePlayerId(0) +{ +} + +GameEventStorage::~GameEventStorage() +{ + delete gameEventContext; + for (int i = 0; i < gameEventList.size(); ++i) + delete gameEventList[i]; +} + +void GameEventStorage::setGameEventContext(const ::google::protobuf::Message &_gameEventContext) +{ + delete gameEventContext; + gameEventContext = new GameEventContext; + gameEventContext->GetReflection() + ->MutableMessage(gameEventContext, _gameEventContext.GetDescriptor()->FindExtensionByName("ext")) + ->CopyFrom(_gameEventContext); +} + +void GameEventStorage::enqueueGameEvent(const ::google::protobuf::Message &event, + int playerId, + GameEventStorageItem::EventRecipients recipients, + int _privatePlayerId) +{ + gameEventList.append(new GameEventStorageItem(event, playerId, recipients)); + if (_privatePlayerId != -1) + privatePlayerId = _privatePlayerId; +} + +void GameEventStorage::sendToGame(Server_Game *game) +{ + if (gameEventList.isEmpty()) + return; + + auto *contPrivate = new GameEventContainer; + auto *contOthers = new GameEventContainer; + int id = privatePlayerId; + if (forcedByJudge != -1) { + contPrivate->set_forced_by_judge(forcedByJudge); + contOthers->set_forced_by_judge(forcedByJudge); + if (overwriteOwnership) { + id = forcedByJudge; + setOverwriteOwnership(false); + } + } + + for (const auto &i : gameEventList) { + const GameEvent &event = i->getGameEvent(); + const GameEventStorageItem::EventRecipients recipients = i->getRecipients(); + if (recipients.testFlag(GameEventStorageItem::SendToPrivate)) + contPrivate->add_event_list()->CopyFrom(event); + if (recipients.testFlag(GameEventStorageItem::SendToOthers)) + contOthers->add_event_list()->CopyFrom(event); + } + if (gameEventContext) { + contPrivate->mutable_context()->CopyFrom(*gameEventContext); + contOthers->mutable_context()->CopyFrom(*gameEventContext); + } + game->sendGameEventContainer(contPrivate, GameEventStorageItem::SendToPrivate, id); + game->sendGameEventContainer(contOthers, GameEventStorageItem::SendToOthers, id); +} + +ResponseContainer::ResponseContainer(int _cmdId) : cmdId(_cmdId), responseExtension(0) +{ +} + +ResponseContainer::~ResponseContainer() +{ + delete responseExtension; + for (int i = 0; i < preResponseQueue.size(); ++i) + delete preResponseQueue[i].second; + for (int i = 0; i < postResponseQueue.size(); ++i) + delete postResponseQueue[i].second; +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_response_containers.h b/libcockatrice_network/libcockatrice/network/server/remote/server_response_containers.h new file mode 100644 index 000000000..118b32d38 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_response_containers.h @@ -0,0 +1,131 @@ +#ifndef SERVER_RESPONSE_CONTAINERS_H +#define SERVER_RESPONSE_CONTAINERS_H + +#include +#include +#include + +namespace google +{ +namespace protobuf +{ +class Message; +} +} // namespace google +class Server_Game; + +class GameEventStorageItem +{ +public: + enum EventRecipient + { + SendToPrivate = 0x01, + SendToOthers = 0x02 + }; + Q_DECLARE_FLAGS(EventRecipients, EventRecipient) +private: + GameEvent *event; + EventRecipients recipients; + +public: + GameEventStorageItem(const ::google::protobuf::Message &_event, int _playerId, EventRecipients _recipients); + ~GameEventStorageItem(); + + [[nodiscard]] const GameEvent &getGameEvent() const + { + return *event; + } + [[nodiscard]] EventRecipients getRecipients() const + { + return recipients; + } +}; +Q_DECLARE_OPERATORS_FOR_FLAGS(GameEventStorageItem::EventRecipients) + +class GameEventStorage +{ +private: + ::google::protobuf::Message *gameEventContext; + QList gameEventList; + int privatePlayerId; + int forcedByJudge = -1; + bool overwriteOwnership = false; + +public: + GameEventStorage(); + ~GameEventStorage(); + + void setGameEventContext(const ::google::protobuf::Message &_gameEventContext); + [[nodiscard]] ::google::protobuf::Message *getGameEventContext() const + { + return gameEventContext; + } + [[nodiscard]] const QList &getGameEventList() const + { + return gameEventList; + } + [[nodiscard]] int getPrivatePlayerId() const + { + return privatePlayerId; + } + void setForcedByJudge(int playerId) + { + forcedByJudge = playerId; + } + void setOverwriteOwnership(bool shouldOverwriteOwnership) + { + overwriteOwnership = shouldOverwriteOwnership; + } + + void enqueueGameEvent(const ::google::protobuf::Message &event, + int playerId, + GameEventStorageItem::EventRecipients recipients = GameEventStorageItem::SendToPrivate | + GameEventStorageItem::SendToOthers, + int _privatePlayerId = -1); + void sendToGame(Server_Game *game); +}; + +class ResponseContainer +{ +private: + int cmdId; + ::google::protobuf::Message *responseExtension; + QList> preResponseQueue, postResponseQueue; + +public: + ResponseContainer(int _cmdId); + ~ResponseContainer(); + + [[nodiscard]] int getCmdId() const + { + return cmdId; + } + void setResponseExtension(::google::protobuf::Message *_responseExtension) + { + responseExtension = _responseExtension; + } + [[nodiscard]] ::google::protobuf::Message *getResponseExtension() const + { + return responseExtension; + } + void enqueuePreResponseItem(ServerMessage::MessageType type, ::google::protobuf::Message *item) + { + preResponseQueue.append(qMakePair(type, item)); + } + void enqueuePostResponseItem(ServerMessage::MessageType type, ::google::protobuf::Message *item) + { + postResponseQueue.append(qMakePair(type, item)); + } + [[nodiscard]] const QList> & + getPreResponseQueue() const + { + return preResponseQueue; + } + [[nodiscard]] const QList> & + getPostResponseQueue() const + { + return postResponseQueue; + } +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp new file mode 100644 index 000000000..bfa8912b1 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp @@ -0,0 +1,429 @@ +#include "server_room.h" + +#include "game/server_game.h" +#include "server_protocolhandler.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Server_Room::Server_Room(int _id, + int _chatHistorySize, + const QString &_name, + const QString &_description, + const QString &_permissionLevel, + const QString &_privilegeLevel, + bool _autoJoin, + const QString &_joinMessage, + const QStringList &_gameTypes, + Server *parent) + : QObject(parent), id(_id), chatHistorySize(_chatHistorySize), name(_name), description(_description), + permissionLevel(_permissionLevel), privilegeLevel(_privilegeLevel), autoJoin(_autoJoin), + joinMessage(_joinMessage), gameTypes(_gameTypes), gamesLock(QReadWriteLock::Recursive) +{ + connect( + this, &Server_Room::gameListChanged, this, [this](auto gameInfo) { broadcastGameListUpdate(gameInfo); }, + Qt::QueuedConnection); +} + +Server_Room::~Server_Room() +{ + qDebug("Server_Room destructor"); + + gamesLock.lockForWrite(); + const QList gameList = games.values(); + for (int i = 0; i < gameList.size(); ++i) + delete gameList[i]; + games.clear(); + gamesLock.unlock(); + + usersLock.lockForWrite(); + users.clear(); + usersLock.unlock(); +} + +bool Server_Room::userMayJoin(const ServerInfo_User &userInfo) +{ + + if (permissionLevel.toLower() == "administrator" || permissionLevel.toLower() == "moderator") + return false; + + if (permissionLevel.toLower() == "registered" && !(userInfo.user_level() & ServerInfo_User::IsRegistered)) + return false; + + if (privilegeLevel.toLower() != "none") { + if (privilegeLevel.toLower() == "privileged") { + if (privilegeLevel.toLower() == "none") + return false; + } else { + if (privilegeLevel.toLower() != QString::fromStdString(userInfo.privlevel()).toLower()) + return false; + } + } + return true; +} + +Server *Server_Room::getServer() const +{ + return static_cast(parent()); +} + +const ServerInfo_Room & +Server_Room::getInfo(ServerInfo_Room &result, bool complete, bool showGameTypes, bool includeExternalData) const +{ + result.set_room_id(id); + result.set_name(name.toStdString()); + result.set_description(description.toStdString()); + result.set_auto_join(autoJoin); + result.set_permissionlevel(permissionLevel.toStdString()); + result.set_privilegelevel(privilegeLevel.toStdString()); + + gamesLock.lockForRead(); + result.set_game_count(games.size() + externalGames.size()); + if (complete) { + QMapIterator gameIterator(games); + while (gameIterator.hasNext()) + gameIterator.next().value()->getInfo(*result.add_game_list()); + if (includeExternalData) { + QMapIterator externalGameIterator(externalGames); + while (externalGameIterator.hasNext()) + result.add_game_list()->CopyFrom(externalGameIterator.next().value()); + } + } + gamesLock.unlock(); + + usersLock.lockForRead(); + result.set_player_count(users.size() + externalUsers.size()); + if (complete) { + QMapIterator userIterator(users); + while (userIterator.hasNext()) + result.add_user_list()->CopyFrom(userIterator.next().value()->copyUserInfo(false)); + if (includeExternalData) { + QMapIterator externalUserIterator(externalUsers); + while (externalUserIterator.hasNext()) + result.add_user_list()->CopyFrom(externalUserIterator.next().value().copyUserInfo(false)); + } + } + usersLock.unlock(); + + if (complete || showGameTypes) + for (int i = 0; i < gameTypes.size(); ++i) { + ServerInfo_GameType *gameTypeInfo = result.add_gametype_list(); + gameTypeInfo->set_game_type_id(i); + gameTypeInfo->set_description(gameTypes[i].toStdString()); + } + + return result; +} + +RoomEvent *Server_Room::prepareRoomEvent(const ::google::protobuf::Message &roomEvent) +{ + auto *event = new RoomEvent; + event->set_room_id(id); + event->GetReflection() + ->MutableMessage(event, roomEvent.GetDescriptor()->FindExtensionByName("ext")) + ->CopyFrom(roomEvent); + return event; +} + +void Server_Room::addClient(Server_ProtocolHandler *client) +{ + Event_JoinRoom event; + event.mutable_user_info()->CopyFrom(client->copyUserInfo(false)); + sendRoomEvent(prepareRoomEvent(event)); + + ServerInfo_Room roomInfo; + roomInfo.set_room_id(id); + + usersLock.lockForWrite(); + users.insert(QString::fromStdString(client->getUserInfo()->name()), client); + roomInfo.set_player_count(users.size() + externalUsers.size()); + usersLock.unlock(); + + // XXX This can be removed during the next client update. + gamesLock.lockForRead(); + roomInfo.set_game_count(games.size() + externalGames.size()); + gamesLock.unlock(); + // ----------- + + emit roomInfoChanged(roomInfo); +} + +void Server_Room::removeClient(Server_ProtocolHandler *client) +{ + usersLock.lockForWrite(); + users.remove(QString::fromStdString(client->getUserInfo()->name())); + + ServerInfo_Room roomInfo; + roomInfo.set_room_id(id); + roomInfo.set_player_count(users.size() + externalUsers.size()); + usersLock.unlock(); + + Event_LeaveRoom event; + event.set_name(client->getUserInfo()->name()); + sendRoomEvent(prepareRoomEvent(event)); + + // XXX This can be removed during the next client update. + gamesLock.lockForRead(); + roomInfo.set_game_count(games.size() + externalGames.size()); + gamesLock.unlock(); + // ----------- + + emit roomInfoChanged(roomInfo); +} + +void Server_Room::addExternalUser(const ServerInfo_User &userInfo) +{ + // This function is always called from the Server thread with server->roomsMutex locked. + ServerInfo_User_Container userInfoContainer(userInfo); + Event_JoinRoom event; + event.mutable_user_info()->CopyFrom(userInfoContainer.copyUserInfo(false)); + sendRoomEvent(prepareRoomEvent(event), false); + + ServerInfo_Room roomInfo; + roomInfo.set_room_id(id); + + usersLock.lockForWrite(); + externalUsers.insert(QString::fromStdString(userInfo.name()), userInfoContainer); + roomInfo.set_player_count(users.size() + externalUsers.size()); + usersLock.unlock(); + + emit roomInfoChanged(roomInfo); +} + +void Server_Room::removeExternalUser(const QString &_name) +{ + // This function is always called from the Server thread with server->roomsMutex locked. + ServerInfo_Room roomInfo; + roomInfo.set_room_id(id); + + usersLock.lockForWrite(); + if (externalUsers.contains(_name)) + externalUsers.remove(_name); + roomInfo.set_player_count(users.size() + externalUsers.size()); + usersLock.unlock(); + + Event_LeaveRoom event; + event.set_name(_name.toStdString()); + sendRoomEvent(prepareRoomEvent(event), false); + + emit roomInfoChanged(roomInfo); +} + +void Server_Room::updateExternalGameList(const ServerInfo_Game &gameInfo) +{ + // This function is always called from the Server thread with server->roomsMutex locked. + ServerInfo_Room roomInfo; + roomInfo.set_room_id(id); + + gamesLock.lockForWrite(); + if (!gameInfo.has_player_count() && externalGames.contains(gameInfo.game_id())) + externalGames.remove(gameInfo.game_id()); + else + externalGames.insert(gameInfo.game_id(), gameInfo); + roomInfo.set_game_count(games.size() + externalGames.size()); + gamesLock.unlock(); + + broadcastGameListUpdate(gameInfo, false); + emit roomInfoChanged(roomInfo); +} + +Response::ResponseCode Server_Room::processJoinGameCommand(const Command_JoinGame &cmd, + ResponseContainer &rc, + Server_AbstractUserInterface *userInterface) +{ + if (cmd.password().length() > MAX_NAME_LENGTH) + return Response::RespWrongPassword; + // This function is called from the Server thread and from the S_PH thread. + // server->roomsMutex is always locked. + + QReadLocker roomGamesLocker(&gamesLock); + Server_Game *game = games.value(cmd.game_id()); + if (!game) { + if (externalGames.contains(cmd.game_id())) { + CommandContainer cont; + cont.set_cmd_id(rc.getCmdId()); + RoomCommand *roomCommand = cont.add_room_command(); + roomCommand->GetReflection() + ->MutableMessage(roomCommand, cmd.GetDescriptor()->FindExtensionByName("ext")) + ->CopyFrom(cmd); + getServer()->sendIsl_RoomCommand(cont, externalGames.value(cmd.game_id()).server_id(), + userInterface->getUserInfo()->session_id(), id); + + return Response::RespNothing; + } else { + return Response::RespNameNotFound; + } + } + + QMutexLocker gameLocker(&game->gameMutex); + + Response::ResponseCode result = + game->checkJoin(userInterface->getUserInfo(), QString::fromStdString(cmd.password()), cmd.spectator(), + cmd.override_restrictions(), cmd.join_as_judge()); + if (result == Response::RespOk) + game->addPlayer(userInterface, rc, cmd.spectator(), cmd.join_as_judge()); + + return result; +} + +void Server_Room::say(const QString &userName, const QString &userMessage, bool sendToIsl) +{ + Event_RoomSay event; + event.set_name(userName.toStdString()); + event.set_message(userMessage.toStdString()); + sendRoomEvent(prepareRoomEvent(event), sendToIsl); + + if (chatHistorySize != 0) { + ServerInfo_ChatMessage chatMessage; + QDateTime dateTime = dateTime.currentDateTimeUtc(); + QString dateTimeString = dateTime.toString(); + chatMessage.set_time(dateTimeString.toStdString()); + chatMessage.set_sender_name(userName.toStdString()); + chatMessage.set_message(userMessage.simplified().toStdString()); + + historyLock.lockForWrite(); + if (chatHistory.size() >= chatHistorySize) { + chatHistory.removeAt(0); + } + + chatHistory.push_back(std::move(chatMessage)); + historyLock.unlock(); + } +} + +void Server_Room::removeSaidMessages(const QString &userName, int amount, bool sendToIsl) +{ + Event_RemoveMessages event; + auto stdStringUserName = userName.toStdString(); + event.set_name(stdStringUserName); + event.set_amount(amount); + sendRoomEvent(prepareRoomEvent(event), sendToIsl); + + if (chatHistorySize != 0) { + int removed = 0; + historyLock.lockForWrite(); + // redact [amount] of the most recent messages from this user from history + for (auto message = chatHistory.rbegin(); message != chatHistory.rend() && removed != amount; ++message) { + if (message->sender_name() == stdStringUserName) { + message->clear_message(); + ++removed; + } + } + historyLock.unlock(); + } +} + +void Server_Room::sendRoomEvent(RoomEvent *event, bool sendToIsl) +{ + usersLock.lockForRead(); + { + QMapIterator userIterator(users); + while (userIterator.hasNext()) + userIterator.next().value()->sendProtocolItem(*event); + } + usersLock.unlock(); + + if (sendToIsl) + static_cast(parent())->sendIsl_RoomEvent(*event); + + delete event; +} + +void Server_Room::broadcastGameListUpdate(const ServerInfo_Game &gameInfo, bool sendToIsl) +{ + Event_ListGames event; + event.add_game_list()->CopyFrom(gameInfo); + sendRoomEvent(prepareRoomEvent(event), sendToIsl); +} + +void Server_Room::addGame(Server_Game *game) +{ + ServerInfo_Room roomInfo; + roomInfo.set_room_id(id); + + gamesLock.lockForWrite(); + connect(game, &Server_Game::gameInfoChanged, this, [this](auto gameInfo) { broadcastGameListUpdate(gameInfo); }); + + game->gameMutex.lock(); + games.insert(game->getGameId(), game); + ServerInfo_Game gameInfo; + game->getInfo(gameInfo); + roomInfo.set_game_count(games.size() + externalGames.size()); + game->gameMutex.unlock(); + gamesLock.unlock(); + + // XXX This can be removed during the next client update. + usersLock.lockForRead(); + roomInfo.set_player_count(users.size() + externalUsers.size()); + usersLock.unlock(); + // ----------- + + emit gameListChanged(gameInfo); + emit roomInfoChanged(roomInfo); +} + +void Server_Room::removeGame(Server_Game *game) +{ + // No need to lock gamesLock or gameMutex. This method is only + // called from ~Server_Game, which locks both mutexes anyway beforehand. + + disconnect(game, 0, this, 0); + + ServerInfo_Game gameInfo; + game->getInfo(gameInfo); + emit gameListChanged(gameInfo); + + games.remove(game->getGameId()); + + ServerInfo_Room roomInfo; + roomInfo.set_room_id(id); + roomInfo.set_game_count(games.size() + externalGames.size()); + + // XXX This can be removed during the next client update. + usersLock.lockForRead(); + roomInfo.set_player_count(users.size() + externalUsers.size()); + usersLock.unlock(); + // ----------- + + emit roomInfoChanged(roomInfo); +} + +int Server_Room::getGamesCreatedByUser(const QString &userName) const +{ + QReadLocker locker(&gamesLock); + + QMapIterator gamesIterator(games); + int result = 0; + while (gamesIterator.hasNext()) + if (gamesIterator.next().value()->getCreatorInfo()->name() == userName.toStdString()) + ++result; + return result; +} + +QList Server_Room::getGamesOfUser(const QString &userName) const +{ + QReadLocker locker(&gamesLock); + + QList result; + QMapIterator gamesIterator(games); + while (gamesIterator.hasNext()) { + Server_Game *game = gamesIterator.next().value(); + if (game->containsUser(userName)) { + ServerInfo_Game gameInfo; + game->getInfo(gameInfo); + result.append(gameInfo); + } + } + return result; +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_room.h b/libcockatrice_network/libcockatrice/network/server/remote/server_room.h new file mode 100644 index 000000000..3d9988f20 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_room.h @@ -0,0 +1,142 @@ +#ifndef SERVER_ROOM_H +#define SERVER_ROOM_H + +#include "serverinfo_user_container.h" + +#include +#include +#include +#include +#include +#include + +class Server_DatabaseInterface; +class Server_ProtocolHandler; +class RoomEvent; +class ServerInfo_User; +class ServerInfo_Room; +class ServerInfo_Game; +class Server_Game; +class Server; + +class Command_JoinGame; +class ResponseContainer; +class Server_AbstractUserInterface; + +class Server_Room : public QObject +{ + Q_OBJECT +signals: + void roomInfoChanged(const ServerInfo_Room &roomInfo); + void gameListChanged(const ServerInfo_Game &gameInfo); + +private: + int id; + int chatHistorySize; + QString name; + QString description; + QString permissionLevel; + QString privilegeLevel; + bool autoJoin; + QString joinMessage; + QStringList gameTypes; + QMap games; + QMap externalGames; + QMap users; + QMap externalUsers; + QList chatHistory; +private slots: + void broadcastGameListUpdate(const ServerInfo_Game &gameInfo, bool sendToIsl = true); + +public: + mutable QReadWriteLock usersLock; + mutable QReadWriteLock gamesLock; + mutable QReadWriteLock historyLock; + Server_Room(int _id, + int _chatHistorySize, + const QString &_name, + const QString &_description, + const QString &_permissionLevel, + const QString &_privilegeLevel, + bool _autoJoin, + const QString &_joinMessage, + const QStringList &_gameTypes, + Server *parent); + ~Server_Room() override; + int getId() const + { + return id; + } + QString getName() const + { + return name; + } + QString getDescription() const + { + return description; + } + QString getRoomPermission() const + { + return permissionLevel; + } + QString getRoomPrivilege() const + { + return privilegeLevel; + } + bool getAutoJoin() const + { + return autoJoin; + } + bool userMayJoin(const ServerInfo_User &userInfo); + QString getJoinMessage() const + { + return joinMessage; + } + const QStringList &getGameTypes() const + { + return gameTypes; + } + const QMap &getGames() const + { + return games; + } + const QMap &getExternalGames() const + { + return externalGames; + } + Server *getServer() const; + const ServerInfo_Room & + getInfo(ServerInfo_Room &result, bool complete, bool showGameTypes = false, bool includeExternalData = true) const; + int getGamesCreatedByUser(const QString &name) const; + QList getGamesOfUser(const QString &name) const; + QList &getChatHistory() + { + return chatHistory; + } + + void addClient(Server_ProtocolHandler *client); + void removeClient(Server_ProtocolHandler *client); + + void addExternalUser(const ServerInfo_User &userInfo); + void removeExternalUser(const QString &_name); + const QMap &getExternalUsers() const + { + return externalUsers; + } + void updateExternalGameList(const ServerInfo_Game &gameInfo); + + Response::ResponseCode processJoinGameCommand(const Command_JoinGame &cmd, + ResponseContainer &rc, + Server_AbstractUserInterface *userInterface); + + void say(const QString &userName, const QString &s, bool sendToIsl = true); + void removeSaidMessages(const QString &userName, int amount, bool sendToIsl = true); + + void addGame(Server_Game *game); + void removeGame(Server_Game *game); + + void sendRoomEvent(RoomEvent *event, bool sendToIsl = true); + RoomEvent *prepareRoomEvent(const ::google::protobuf::Message &roomEvent); +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/serverinfo_user_container.cpp b/libcockatrice_network/libcockatrice/network/server/remote/serverinfo_user_container.cpp new file mode 100644 index 000000000..77ff38906 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/serverinfo_user_container.cpp @@ -0,0 +1,58 @@ +#include "serverinfo_user_container.h" + +#include + +ServerInfo_User_Container::ServerInfo_User_Container(ServerInfo_User *_userInfo) : userInfo(_userInfo) +{ +} + +ServerInfo_User_Container::ServerInfo_User_Container(const ServerInfo_User &_userInfo) + : userInfo(new ServerInfo_User(_userInfo)) +{ +} + +ServerInfo_User_Container::ServerInfo_User_Container(const ServerInfo_User_Container &other) +{ + if (other.userInfo) + userInfo = new ServerInfo_User(*other.userInfo); + else + userInfo = nullptr; +} + +ServerInfo_User_Container::~ServerInfo_User_Container() +{ + delete userInfo; +} + +void ServerInfo_User_Container::setUserInfo(const ServerInfo_User &_userInfo) +{ + userInfo = new ServerInfo_User(_userInfo); +} + +ServerInfo_User &ServerInfo_User_Container::copyUserInfo(ServerInfo_User &result, + bool complete, + bool internalInfo, + bool sessionInfo) const +{ + if (userInfo) { + result.CopyFrom(*userInfo); + if (!sessionInfo) { + result.clear_session_id(); + result.clear_address(); + result.clear_clientid(); + } + if (!internalInfo) { + result.clear_id(); + result.clear_email(); + } + if (!complete) + result.clear_avatar_bmp(); + } + return result; +} + +ServerInfo_User ServerInfo_User_Container::copyUserInfo(bool complete, bool internalInfo, bool sessionInfo) const +{ + ServerInfo_User result; + return copyUserInfo(result, complete, internalInfo, sessionInfo); +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/serverinfo_user_container.h b/libcockatrice_network/libcockatrice/network/server/remote/serverinfo_user_container.h new file mode 100644 index 000000000..a959f4535 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/serverinfo_user_container.h @@ -0,0 +1,28 @@ +#ifndef SERVERINFO_USER_CONTAINER +#define SERVERINFO_USER_CONTAINER + +class ServerInfo_User; + +class ServerInfo_User_Container +{ +protected: + ServerInfo_User *userInfo; + +public: + explicit ServerInfo_User_Container(ServerInfo_User *_userInfo = nullptr); + explicit ServerInfo_User_Container(const ServerInfo_User &_userInfo); + ServerInfo_User_Container(const ServerInfo_User_Container &other); + ServerInfo_User_Container &operator=(const ServerInfo_User_Container &other) = default; + virtual ~ServerInfo_User_Container(); + [[nodiscard]] ServerInfo_User *getUserInfo() const + { + return userInfo; + } + void setUserInfo(const ServerInfo_User &_userInfo); + ServerInfo_User & + copyUserInfo(ServerInfo_User &result, bool complete, bool internalInfo = false, bool sessionInfo = false) const; + [[nodiscard]] ServerInfo_User + copyUserInfo(bool complete, bool internalInfo = false, bool sessionInfo = false) const; +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/user_level.h b/libcockatrice_network/libcockatrice/network/server/remote/user_level.h new file mode 100644 index 000000000..9b7a0ca88 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/user_level.h @@ -0,0 +1,15 @@ +#ifndef USER_LEVEL_H +#define USER_LEVEL_H + +#ifdef Q_OS_MACOS +// avoid collision from Mac OS X's ConditionalMacros.h +// https://github.com/protocolbuffers/protobuf/issues/119 +#undef TYPE_BOOL +#endif +#include +#include + +Q_DECLARE_FLAGS(UserLevelFlags, ServerInfo_User::UserLevelFlag) +Q_DECLARE_OPERATORS_FOR_FLAGS(UserLevelFlags) + +#endif diff --git a/libcockatrice_protocol/CMakeLists.txt b/libcockatrice_protocol/CMakeLists.txt new file mode 100644 index 000000000..7dc0a0360 --- /dev/null +++ b/libcockatrice_protocol/CMakeLists.txt @@ -0,0 +1,26 @@ +# Top-level wrapper for the protobuf library + +add_subdirectory(libcockatrice/protocol/pb) + +add_library(libcockatrice_protocol STATIC) + +set(SOURCES libcockatrice/protocol/debug_pb_message.cpp libcockatrice/protocol/featureset.cpp + libcockatrice/protocol/get_pb_extension.cpp libcockatrice/protocol/pending_command.cpp +) + +set(HEADERS libcockatrice/protocol/debug_pb_message.h libcockatrice/protocol/featureset.h + libcockatrice/protocol/get_pb_extension.h libcockatrice/protocol/pending_command.h +) + +target_sources(libcockatrice_protocol PRIVATE ${SOURCES} ${HEADERS}) + +add_dependencies(libcockatrice_protocol libcockatrice_protocol_pb) + +# Link the actual generated protobuf library +target_link_libraries(libcockatrice_protocol PUBLIC ${QT_CORE_MODULE} libcockatrice_protocol_pb libcockatrice_utility) + +# Expose include paths +target_include_directories( + libcockatrice_protocol PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} # points to the generated headers +) diff --git a/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp b/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp new file mode 100644 index 000000000..718487c18 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp @@ -0,0 +1,110 @@ +#include "debug_pb_message.h" + +#include +#include +#include +#include +#include +#include + +// FastFieldValuePrinter is added in protobuf 3.4, going out of our way to add the old FieldValuePrinter is not worth it +#if GOOGLE_PROTOBUF_VERSION > 3004000 + +// value printer to use for all values, will snip too long contents +class LimitedPrinter : public ::google::protobuf::TextFormat::FastFieldValuePrinter +{ +public: + void PrintString(const std::string &val, + ::google::protobuf::TextFormat::BaseTextGenerator *generator) const override; +}; + +// value printer to use for specifc values, will expunge sensitive info +class SafePrinter : public ::google::protobuf::TextFormat::FastFieldValuePrinter +{ +public: + void PrintString(const std::string &val, + ::google::protobuf::TextFormat::BaseTextGenerator *generator) const override; + + static void applySafePrinter(const ::google::protobuf::Message &message, + ::google::protobuf::TextFormat::Printer &printer); +}; + +void LimitedPrinter::PrintString(const std::string &val, + ::google::protobuf::TextFormat::BaseTextGenerator *generator) const +{ + auto length = val.length(); + if (length > MAX_TEXT_LENGTH) { + ::google::protobuf::TextFormat::FastFieldValuePrinter::PrintString( + val.substr(0, MAX_NAME_LENGTH) + "... ---snip--- (" + std::to_string(length) + " bytes total", generator); + } else { + ::google::protobuf::TextFormat::FastFieldValuePrinter::PrintString(val, generator); + } +} + +void SafePrinter::PrintString(const std::string & /*val*/, + ::google::protobuf::TextFormat::BaseTextGenerator *generator) const +{ + generator->PrintLiteral("\" ---value expunged--- \""); +} + +void SafePrinter::applySafePrinter(const ::google::protobuf::Message &message, + ::google::protobuf::TextFormat::Printer &printer) +{ + const auto *reflection = message.GetReflection(); + std::vector fields; + reflection->ListFields(message, &fields); + for (const auto *field : fields) { + switch (field->cpp_type()) { + case ::google::protobuf::FieldDescriptor::CPPTYPE_STRING: + if (field->name().find("password") != std::string::npos) { // name contains password + auto *safePrinter = new SafePrinter(); + if (!printer.RegisterFieldValuePrinter(field, safePrinter)) + delete safePrinter; // in case safePrinter has not been taken ownership of + } + break; + case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: + if (field->is_repeated()) { + for (int i = 0; i < reflection->FieldSize(message, field); ++i) { + applySafePrinter(reflection->GetRepeatedMessage(message, field, i), printer); + } + } else { + applySafePrinter(reflection->GetMessage(message, field), printer); + } + break; + default: + break; + } + } +} +#endif // GOOGLE_PROTOBUF_VERSION > 3004000 + +QString getSafeDebugString(const ::google::protobuf::Message &message) +{ +#if GOOGLE_PROTOBUF_VERSION > 3001000 + auto size = message.ByteSizeLong(); +#else + auto size = message.ByteSize(); +#endif + + ::google::protobuf::TextFormat::Printer printer; + printer.SetSingleLineMode(true); // compact mode + printer.SetExpandAny(true); // prints all fields + +#if GOOGLE_PROTOBUF_VERSION > 3004000 + // printer takes ownership of the LimitedPrinter and will delete it + printer.SetDefaultFieldValuePrinter(new LimitedPrinter()); + // check field names an create SafePrinters for necessary fields + SafePrinter::applySafePrinter(message, printer); +#else + // removing passwords from debug output will only be supported on newer protobuf versions + printer.SetTruncateStringFieldLongerThan(MAX_TEXT_LENGTH); +#endif // GOOGLE_PROTOBUF_VERSION > 3004000 + + std::string debug_string; + bool ok = printer.PrintToString(message, &debug_string); + if (ok) { + return QString::number(size) + " bytes " + QString::fromStdString(debug_string); + } else { + return "[could not convert message to string]"; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.h b/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.h new file mode 100644 index 000000000..34e7845c5 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.h @@ -0,0 +1,15 @@ +#ifndef DEBUG_PB_MESSAGE_H +#define DEBUG_PB_MESSAGE_H + +class QString; +namespace google +{ +namespace protobuf +{ +class Message; +} +} // namespace google + +QString getSafeDebugString(const ::google::protobuf::Message &message); + +#endif // DEBUG_PB_MESSAGE_H diff --git a/libcockatrice_protocol/libcockatrice/protocol/featureset.cpp b/libcockatrice_protocol/libcockatrice/protocol/featureset.cpp new file mode 100644 index 000000000..1b08c4040 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/featureset.cpp @@ -0,0 +1,76 @@ +#include "featureset.h" + +#include + +FeatureSet::FeatureSet() +{ +} + +QMap FeatureSet::getDefaultFeatureList() +{ + initalizeFeatureList(featureList); + return featureList; +} + +void FeatureSet::initalizeFeatureList(QMap &_featureList) +{ + // default features [name], [is required to connect] + _featureList.insert("client_id", false); + _featureList.insert("client_ver", false); + _featureList.insert("feature_set", false); + _featureList.insert("user_ban_history", false); + _featureList.insert("room_chat_history", false); + _featureList.insert("client_warnings", false); + _featureList.insert("mod_log_lookup", false); + _featureList.insert("idle_client", false); + _featureList.insert("forgot_password", false); + _featureList.insert("websocket", false); + // featureList.insert("hashed_password_login", false); + // These are temp to force users onto a newer client + _featureList.insert("2.7.0_min_version", false); + _featureList.insert("2.8.0_min_version", false); +} + +void FeatureSet::enableRequiredFeature(QMap &_featureList, const QString &featureName) +{ + if (_featureList.contains(featureName)) + _featureList.insert(featureName, true); +} + +void FeatureSet::disableRequiredFeature(QMap &_featureList, const QString &featureName) +{ + if (_featureList.contains(featureName)) + _featureList.insert(featureName, false); +} + +QMap +FeatureSet::addFeature(QMap &_featureList, const QString &featureName, bool isFeatureRequired) +{ + _featureList.insert(featureName, isFeatureRequired); + return _featureList; +} + +QMap FeatureSet::identifyMissingFeatures(const QMap &suppliedFeatures, + QMap requiredFeatures) +{ + QMap missingList; + QMap::iterator i; + for (i = requiredFeatures.begin(); i != requiredFeatures.end(); ++i) { + if (!suppliedFeatures.contains(i.key())) { + missingList.insert(i.key(), i.value()); + } + } + return missingList; +} + +bool FeatureSet::isRequiredFeaturesMissing(const QMap &suppliedFeatures, + QMap requiredFeatures) +{ + QMap::iterator i; + for (i = requiredFeatures.begin(); i != requiredFeatures.end(); ++i) { + if (i.value() && suppliedFeatures.contains(i.key())) { + return true; + } + } + return false; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/featureset.h b/libcockatrice_protocol/libcockatrice/protocol/featureset.h new file mode 100644 index 000000000..32625d5f8 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/featureset.h @@ -0,0 +1,27 @@ +#ifndef FEATURESET_H +#define FEATURESET_H + +#include +#include +#include + +class FeatureSet : public QObject +{ +public: + FeatureSet(); + QMap getDefaultFeatureList(); + void initalizeFeatureList(QMap &_featureList); + void enableRequiredFeature(QMap &_featureList, const QString &featureName); + void disableRequiredFeature(QMap &_featureList, const QString &featureName); + QMap + addFeature(QMap &_featureList, const QString &featureName, bool isFeatureRequired); + QMap identifyMissingFeatures(const QMap &featureListToCheck, + QMap featureListToCompareTo); + bool isRequiredFeaturesMissing(const QMap &featureListToCheck, + QMap featureListToCompareTo); + +private: + QMap featureList; +}; + +#endif // FEEATURESET_H diff --git a/libcockatrice_protocol/libcockatrice/protocol/get_pb_extension.cpp b/libcockatrice_protocol/libcockatrice/protocol/get_pb_extension.cpp new file mode 100644 index 000000000..d6235858a --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/get_pb_extension.cpp @@ -0,0 +1,14 @@ +#include "get_pb_extension.h" + +#include +#include + +int getPbExtension(const ::google::protobuf::Message &message) +{ + std::vector fieldList; + message.GetReflection()->ListFields(message, &fieldList); + for (unsigned int j = 0; j < fieldList.size(); ++j) + if (fieldList[j]->is_extension()) + return fieldList[j]->number(); + return -1; +} diff --git a/common/get_pb_extension.h b/libcockatrice_protocol/libcockatrice/protocol/get_pb_extension.h similarity index 62% rename from common/get_pb_extension.h rename to libcockatrice_protocol/libcockatrice/protocol/get_pb_extension.h index c55aa937f..4a89e1707 100644 --- a/common/get_pb_extension.h +++ b/libcockatrice_protocol/libcockatrice/protocol/get_pb_extension.h @@ -1,11 +1,13 @@ #ifndef GET_PB_EXTENSION_H #define GET_PB_EXTENSION_H -namespace google { - namespace protobuf { - class Message; - } +namespace google +{ +namespace protobuf +{ +class Message; } +} // namespace google int getPbExtension(const ::google::protobuf::Message &message); diff --git a/common/pb/CMakeLists.txt b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt similarity index 62% rename from common/pb/CMakeLists.txt rename to libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt index 7b0051573..212ab69dd 100644 --- a/common/pb/CMakeLists.txt +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt @@ -2,9 +2,7 @@ # # provides the protobuf interfaces -CMAKE_MINIMUM_REQUIRED(VERSION 2.6) - -SET(PROTO_FILES +set(PROTO_FILES admin_commands.proto card_attributes.proto color.proto @@ -14,8 +12,8 @@ SET(PROTO_FILES command_create_arrow.proto command_create_counter.proto command_create_token.proto - command_deck_del_dir.proto command_deck_del.proto + command_deck_del_dir.proto command_deck_download.proto command_deck_list.proto command_deck_new_dir.proto @@ -36,21 +34,23 @@ SET(PROTO_FILES command_next_turn.proto command_ready_start.proto command_replay_delete_match.proto - command_replay_list.proto command_replay_download.proto + command_replay_get_code.proto + command_replay_list.proto command_replay_modify_match.proto + command_replay_submit_code.proto command_reveal_cards.proto + command_reverse_turn.proto command_roll_die.proto command_set_active_phase.proto command_set_card_attr.proto command_set_card_counter.proto command_set_counter.proto - command_set_sideboard_plan.proto command_set_sideboard_lock.proto + command_set_sideboard_plan.proto command_shuffle.proto - commands.proto - command_stop_dump_zone.proto command_undo_draw.proto + commands.proto context_concede.proto context_connection_state_changed.proto context_deck_select.proto @@ -78,6 +78,7 @@ SET(PROTO_FILES event_game_joined.proto event_game_say.proto event_game_state_changed.proto + event_game_state_changed.proto event_join.proto event_join_room.proto event_kicked.proto @@ -86,10 +87,13 @@ SET(PROTO_FILES event_list_games.proto event_list_rooms.proto event_move_card.proto + event_notify_user.proto event_player_properties_changed.proto event_remove_from_list.proto + event_remove_messages.proto event_replay_added.proto event_reveal_cards.proto + event_reverse_turn.proto event_roll_die.proto event_room_say.proto event_server_complete_list.proto @@ -102,62 +106,100 @@ SET(PROTO_FILES event_set_card_counter.proto event_set_counter.proto event_shuffle.proto - event_stop_dump_zone.proto event_user_joined.proto event_user_left.proto event_user_message.proto game_commands.proto + game_event.proto game_event_container.proto game_event_context.proto - game_event.proto game_replay.proto isl_message.proto moderator_commands.proto move_card_to_zone.proto + response.proto + response_activate.proto + response_adjust_mod.proto + response_ban_history.proto response_deck_download.proto response_deck_list.proto response_deck_upload.proto response_dump_zone.proto + response_forgotpasswordrequest.proto + response_get_admin_notes.proto response_get_games_of_user.proto response_get_user_info.proto response_join_room.proto response_list_users.proto response_login.proto + response_password_salt.proto + response_register.proto response_replay_download.proto + response_replay_get_code.proto response_replay_list.proto - response.proto + response_viewlog_history.proto + response_warn_history.proto + response_warn_list.proto room_commands.proto room_event.proto + server_message.proto serverinfo_arrow.proto - serverinfo_cardcounter.proto + serverinfo_ban.proto serverinfo_card.proto + serverinfo_cardcounter.proto + serverinfo_chat_message.proto serverinfo_counter.proto serverinfo_deckstorage.proto serverinfo_game.proto serverinfo_gametype.proto + serverinfo_player.proto serverinfo_playerping.proto serverinfo_playerproperties.proto - serverinfo_player.proto serverinfo_replay.proto serverinfo_replay_match.proto serverinfo_room.proto serverinfo_user.proto + serverinfo_warning.proto serverinfo_zone.proto - server_message.proto session_commands.proto session_event.proto ) -include_directories(${PROTOBUF_INCLUDE_DIRS}) -include_directories(${CMAKE_CURRENT_BINARY_DIR}) -PROTOBUF_GENERATE_CPP(PROTO_SRCS PROTO_HDRS ${PROTO_FILES}) +if(MSVC) + set(unused_warning /wd4100) +else() + set(unused_warning -Wno-unused-parameter) +endif() -add_library(cockatrice_protocol ${PROTO_SRCS} ${PROTO_HDRS}) -set(cockatrice_protocol_LIBS ${PROTOBUF_LIBRARIES}) -if (MSVC) - set(cockatrice_protocol_LIBS ${cockatrice_protocol_LIBS} -lprotobuf) -endif (MSVC) -if (UNIX) - set(cockatrice_protocol_LIBS ${cockatrice_protocol_LIBS} -lpthread) -endif (UNIX) -target_link_libraries(cockatrice_protocol ${cockatrice_protocol_LIBS}) +if(${Protobuf_VERSION} VERSION_LESS "3.21.0.0") + message(STATUS "Using Protobuf Legacy Mode") + include_directories(${PROTOBUF_INCLUDE_DIRS}) + include_directories(${CMAKE_CURRENT_BINARY_DIR}) + protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${PROTO_FILES}) + + add_library(libcockatrice_protocol_pb ${PROTO_SRCS} ${PROTO_HDRS}) + target_compile_options(libcockatrice_protocol_pb PRIVATE ${unused_warning}) + set(libcockatrice_protocol_pb_LIBS ${PROTOBUF_LIBRARIES}) + if(UNIX) + set(libcockatrice_protocol_pb_LIBS ${libcockatrice_protocol_pb_LIBS} -lpthread) + endif(UNIX) + target_link_libraries(libcockatrice_protocol_pb ${libcockatrice_protocol_pb_LIBS}) + + # ubuntu uses an outdated package for protobuf, 3.1.0 is required + if(${Protobuf_VERSION} VERSION_LESS "3.1.0") + # remove unused parameter and misleading indentation warnings when compiling to avoid errors + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wno-unused-parameter -Wno-misleading-indentation") + message(WARNING "Older protobuf version found (${Protobuf_VERSION} < 3.1.0), " + "disabled the warnings 'unused-parameter' and 'misleading-indentation' for protobuf generated code " + "to avoid compilation errors." + ) + endif() +else() + add_library(libcockatrice_protocol_pb ${PROTO_FILES}) + target_compile_options(libcockatrice_protocol_pb PRIVATE ${unused_warning}) + target_link_libraries(libcockatrice_protocol_pb PUBLIC protobuf::libprotobuf) + set(PROTO_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}") + target_include_directories(libcockatrice_protocol_pb PUBLIC "${PROTOBUF_INCLUDE_DIRS}") + + protobuf_generate(TARGET libcockatrice_protocol_pb IMPORT_DIRS "." PROTOC_OUT_DIR "${PROTO_BINARY_DIR}") +endif() diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/admin_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/admin_commands.proto new file mode 100644 index 000000000..8faaec2d2 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/admin_commands.proto @@ -0,0 +1,39 @@ +syntax = "proto2"; +message AdminCommand { + enum AdminCommandType { + UPDATE_SERVER_MESSAGE = 1000; + SHUTDOWN_SERVER = 1001; + RELOAD_CONFIG = 1002; + ADJUST_MOD = 1003; + } + extensions 100 to max; +} + +message Command_UpdateServerMessage { + extend AdminCommand { + optional Command_UpdateServerMessage ext = 1000; + } +} + +message Command_ShutdownServer { + extend AdminCommand { + optional Command_ShutdownServer ext = 1001; + } + optional string reason = 1; + optional uint32 minutes = 2; +} + +message Command_ReloadConfig { + extend AdminCommand { + optional Command_ReloadConfig ext = 1002; + } +} + +message Command_AdjustMod { + extend AdminCommand { + optional Command_AdjustMod ext = 1003; + } + required string user_name = 1; + optional bool should_be_mod = 2; + optional bool should_be_judge = 3; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/card_attributes.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/card_attributes.proto new file mode 100644 index 000000000..ac23ca0d3 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/card_attributes.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +enum CardAttribute { + AttrTapped = 1; + AttrAttacking = 2; + AttrFaceDown = 3; + AttrColor = 4; + AttrPT = 5; + AttrAnnotation = 6; + AttrDoesntUntap = 7; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/color.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/color.proto new file mode 100644 index 000000000..3daae6ce2 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/color.proto @@ -0,0 +1,16 @@ +syntax = "proto2"; + +// Container for a 4 component color code +message color { + // the red component of the color, limited to 256 values + optional uint32 r = 1; + + // the green component of the color, limited to 256 values + optional uint32 g = 2; + + // the blue component of the color, limited to 256 values + optional uint32 b = 3; + + // the opacity component of the color, limited to 256 values + optional uint32 a = 4; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_attach_card.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_attach_card.proto new file mode 100644 index 000000000..9e13edc7b --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_attach_card.proto @@ -0,0 +1,12 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_AttachCard { + extend GameCommand { + optional Command_AttachCard ext = 1009; + } + optional string start_zone = 1; + optional sint32 card_id = 2 [default = -1]; + optional sint32 target_player_id = 3 [default = -1]; + optional string target_zone = 4; + optional sint32 target_card_id = 5 [default = -1]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_change_zone_properties.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_change_zone_properties.proto new file mode 100644 index 000000000..027cc5720 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_change_zone_properties.proto @@ -0,0 +1,14 @@ +syntax = "proto2"; +import "game_commands.proto"; + +message Command_ChangeZoneProperties { + extend GameCommand { + optional Command_ChangeZoneProperties ext = 1031; + } + optional string zone_name = 1; + + // Reveal top card to all players. + optional bool always_reveal_top_card = 10; + // reveal top card to the owner. + optional bool always_look_at_top_card = 11; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_concede.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_concede.proto new file mode 100644 index 000000000..9bec8e48c --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_concede.proto @@ -0,0 +1,13 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_Concede { + extend GameCommand { + optional Command_Concede ext = 1017; + } +} + +message Command_Unconcede { + extend GameCommand { + optional Command_Unconcede ext = 1032; + } +} \ No newline at end of file diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_create_arrow.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_create_arrow.proto new file mode 100644 index 000000000..cb3871b60 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_create_arrow.proto @@ -0,0 +1,35 @@ +syntax = "proto2"; +import "game_commands.proto"; +import "color.proto"; + +// Command to draw an arrow from cards to either other cards or a player +message Command_CreateArrow { + extend GameCommand { + optional Command_CreateArrow ext = 1011; + } + // the player that has the card the arrow is drawn from + optional sint32 start_player_id = 1 [default = -1]; + + // the zone that the card the arrow is drawn from is in + optional string start_zone = 2; + + // the id of the card that the arrow is drawn from + optional sint32 start_card_id = 3 [default = -1]; + + // the player that has the card the arrow is drawn to, or that the arrow is drawn to if not a card + optional sint32 target_player_id = 4 [default = -1]; + + // the zone that the card the arrow is drawn to is in, the player will be targeted if this is absent + optional string target_zone = 5; + + // the id of the card that the arrow is drawn to, the player will be targeted if this is absent + optional sint32 target_card_id = 6 [default = -1]; + + // the color of the arrow + optional color arrow_color = 7; + + // the phase that this arrow is deleted in, arrows are deleted when this or a later phase is activated and also + // when the phase moves back before the current phase or the turn is passed, when not set the arrow is deleted + // immediately when the phase is changed + optional sint32 delete_in_phase = 8; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_create_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_create_counter.proto new file mode 100644 index 000000000..d60fbdc21 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_create_counter.proto @@ -0,0 +1,13 @@ +syntax = "proto2"; +import "game_commands.proto"; +import "color.proto"; + +message Command_CreateCounter { + extend GameCommand { + optional Command_CreateCounter ext = 1019; + } + optional string counter_name = 1; + optional color counter_color = 2; + optional uint32 radius = 3; + optional sint32 value = 4; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_create_token.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_create_token.proto new file mode 100644 index 000000000..4b7d11098 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_create_token.proto @@ -0,0 +1,31 @@ +syntax = "proto2"; +import "game_commands.proto"; + +message Command_CreateToken { + enum TargetMode { + // Attach the target to the token + ATTACH_TO = 0; + // Transform the target into the token + TRANSFORM_INTO = 1; + } + + extend GameCommand { + optional Command_CreateToken ext = 1010; + } + optional string zone = 1; + optional string card_name = 2; + optional string color = 3; + optional string pt = 4; + optional string annotation = 5; + optional bool destroy_on_zone_change = 6; + optional sint32 x = 7; + optional sint32 y = 8; + optional string target_zone = 9; + optional sint32 target_card_id = 10 [default = -1]; + + // What to do with the target card. Ignored if there is no target card. + optional TargetMode target_mode = 11; + + optional string card_provider_id = 12; + optional bool face_down = 13; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_del.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_del.proto new file mode 100644 index 000000000..a231ca087 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_del.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "session_commands.proto"; + +message Command_DeckDel { + extend SessionCommand { + optional Command_DeckDel ext = 1011; + } + optional sint32 deck_id = 1 [default = -1]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_del_dir.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_del_dir.proto new file mode 100644 index 000000000..10790749f --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_del_dir.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "session_commands.proto"; + +message Command_DeckDelDir { + extend SessionCommand { + optional Command_DeckDelDir ext = 1010; + } + optional string path = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_download.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_download.proto new file mode 100644 index 000000000..610a2e785 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_download.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "session_commands.proto"; + +message Command_DeckDownload { + extend SessionCommand { + optional Command_DeckDownload ext = 1012; + } + optional sint32 deck_id = 1 [default = -1]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_list.proto new file mode 100644 index 000000000..43f1b0e01 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_list.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "session_commands.proto"; + +message Command_DeckList { + extend SessionCommand { + optional Command_DeckList ext = 1008; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_new_dir.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_new_dir.proto new file mode 100644 index 000000000..7611fa531 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_new_dir.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "session_commands.proto"; + +message Command_DeckNewDir { + extend SessionCommand { + optional Command_DeckNewDir ext = 1009; + } + optional string path = 1; + optional string dir_name = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_select.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_select.proto new file mode 100644 index 000000000..d79bf37e4 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_select.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_DeckSelect { + extend GameCommand { + optional Command_DeckSelect ext = 1029; + } + optional string deck = 1; + optional sint32 deck_id = 2 [default = -1]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_upload.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_upload.proto new file mode 100644 index 000000000..63d9c80ef --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_upload.proto @@ -0,0 +1,11 @@ +syntax = "proto2"; +import "session_commands.proto"; + +message Command_DeckUpload { + extend SessionCommand { + optional Command_DeckUpload ext = 1013; + } + optional string path = 1; // to upload a new deck + optional uint32 deck_id = 2; // to replace an existing deck + optional string deck_list = 3; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_del_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_del_counter.proto new file mode 100644 index 000000000..8d2689a21 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_del_counter.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_DelCounter { + extend GameCommand { + optional Command_DelCounter ext = 1021; + } + optional sint32 counter_id = 1 [default = -1]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_delete_arrow.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_delete_arrow.proto new file mode 100644 index 000000000..ba204989b --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_delete_arrow.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_DeleteArrow { + extend GameCommand { + optional Command_DeleteArrow ext = 1012; + } + optional sint32 arrow_id = 1 [default = -1]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_draw_cards.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_draw_cards.proto new file mode 100644 index 000000000..d95e05787 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_draw_cards.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_DrawCards { + extend GameCommand { + optional Command_DrawCards ext = 1006; + } + optional uint32 number = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_dump_zone.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_dump_zone.proto new file mode 100644 index 000000000..21bac21c2 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_dump_zone.proto @@ -0,0 +1,11 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_DumpZone { + extend GameCommand { + optional Command_DumpZone ext = 1024; + } + optional sint32 player_id = 1 [default = -1]; + optional string zone_name = 2; + optional sint32 number_cards = 3; + optional bool is_reversed = 4 [default = false]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_flip_card.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_flip_card.proto new file mode 100644 index 000000000..fe5047199 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_flip_card.proto @@ -0,0 +1,11 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_FlipCard { + extend GameCommand { + optional Command_FlipCard ext = 1008; + } + optional string zone = 1; + optional sint32 card_id = 2 [default = -1]; + optional bool face_down = 3; + optional string pt = 4; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_game_say.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_game_say.proto new file mode 100644 index 000000000..2011ee096 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_game_say.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_GameSay { + extend GameCommand { + optional Command_GameSay ext = 1002; + } + optional string message = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_inc_card_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_inc_card_counter.proto new file mode 100644 index 000000000..8676fc89d --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_inc_card_counter.proto @@ -0,0 +1,11 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_IncCardCounter { + extend GameCommand { + optional Command_IncCardCounter ext = 1015; + } + optional string zone = 1; + optional sint32 card_id = 2 [default = -1]; + optional sint32 counter_id = 3 [default = -1]; + optional sint32 counter_delta = 4; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_inc_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_inc_counter.proto new file mode 100644 index 000000000..d99521b7e --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_inc_counter.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_IncCounter { + extend GameCommand { + optional Command_IncCounter ext = 1018; + } + optional sint32 counter_id = 1 [default = -1]; + optional sint32 delta = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_kick_from_game.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_kick_from_game.proto new file mode 100644 index 000000000..331ec2547 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_kick_from_game.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_KickFromGame { + extend GameCommand { + optional Command_KickFromGame ext = 1000; + } + optional sint32 player_id = 1 [default = -1]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_leave_game.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_leave_game.proto new file mode 100644 index 000000000..8518cf2fc --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_leave_game.proto @@ -0,0 +1,7 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_LeaveGame { + extend GameCommand { + optional Command_LeaveGame ext = 1001; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_move_card.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_move_card.proto new file mode 100644 index 000000000..a5b96da2e --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_move_card.proto @@ -0,0 +1,55 @@ +syntax = "proto2"; +import "game_commands.proto"; + +// Container describing a single card to move +message CardToMove { + // Id of the card in its current zone + optional sint32 card_id = 1 [default = -1]; + + // If true, places the card face down, hiding its name. + // If false, forcibly turns the card face up. + // If not set, defers the resulting face down state to the server. + optional bool face_down = 2; + + // When moving add this value to the power/toughness field of the card + optional string pt = 3; + + // When moving sets the card to be tapped + optional bool tapped = 4; +} + +// Container of multiple cards to move +message ListOfCardsToMove { + repeated CardToMove card = 1; +} + +// Command to move an amount of cards from one zone to another index/coordinate or another zone +message Command_MoveCard { + extend GameCommand { + optional Command_MoveCard ext = 1027; + } + + // The player the zone the cards are in belongs to + optional sint32 start_player_id = 1 [default = -1]; + + // The zone the cards start in + optional string start_zone = 2; + + // List of the cards and their new properties + optional ListOfCardsToMove cards_to_move = 3; + + // The player the zone the cards will be moved to belongs to + optional sint32 target_player_id = 4 [default = -1]; + + // The zone the cards will be moved to + optional string target_zone = 5; + + // New x coordinate of the first card in the list + optional sint32 x = 6 [default = -1]; + + // New y coordinate of the first card in the list + optional sint32 y = 7 [default = -1]; + + // Inverts the x coordinate to apply from the end of the target zone instead of the start + optional bool is_reversed = 8 [default = false]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_mulligan.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_mulligan.proto new file mode 100644 index 000000000..a48b1a9ca --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_mulligan.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_Mulligan { + extend GameCommand { + optional Command_Mulligan ext = 1004; + } + optional uint32 number = 7; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_next_turn.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_next_turn.proto new file mode 100644 index 000000000..802d63cfc --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_next_turn.proto @@ -0,0 +1,7 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_NextTurn { + extend GameCommand { + optional Command_NextTurn ext = 1022; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_ready_start.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_ready_start.proto new file mode 100644 index 000000000..912ed55af --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_ready_start.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_ReadyStart { + extend GameCommand { + optional Command_ReadyStart ext = 1016; + } + optional bool ready = 1; + optional bool force_start = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_delete_match.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_delete_match.proto new file mode 100644 index 000000000..33d6d44b0 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_delete_match.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "session_commands.proto"; + +message Command_ReplayDeleteMatch { + extend SessionCommand { + optional Command_ReplayDeleteMatch ext = 1103; + } + optional sint32 game_id = 1 [default = -1]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_download.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_download.proto new file mode 100644 index 000000000..17724e5bb --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_download.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "session_commands.proto"; + +message Command_ReplayDownload { + extend SessionCommand { + optional Command_ReplayDownload ext = 1101; + } + optional sint32 replay_id = 1 [default = -1]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_get_code.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_get_code.proto new file mode 100644 index 000000000..e4573153c --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_get_code.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "session_commands.proto"; + +message Command_ReplayGetCode { + extend SessionCommand { + optional Command_ReplayGetCode ext = 1104; + } + optional sint32 game_id = 1 [default = -1]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_list.proto new file mode 100644 index 000000000..8fbcdc5a4 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_list.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "session_commands.proto"; + +message Command_ReplayList { + extend SessionCommand { + optional Command_ReplayList ext = 1100; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_modify_match.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_modify_match.proto new file mode 100644 index 000000000..94e35db20 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_modify_match.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "session_commands.proto"; + +message Command_ReplayModifyMatch { + extend SessionCommand { + optional Command_ReplayModifyMatch ext = 1102; + } + optional sint32 game_id = 1 [default = -1]; + optional bool do_not_hide = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_submit_code.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_submit_code.proto new file mode 100644 index 000000000..73c5d0ba0 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_submit_code.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "session_commands.proto"; + +message Command_ReplaySubmitCode { + extend SessionCommand { + optional Command_ReplaySubmitCode ext = 1105; + } + optional string replay_code = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_reveal_cards.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_reveal_cards.proto new file mode 100644 index 000000000..82216911c --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_reveal_cards.proto @@ -0,0 +1,12 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_RevealCards { + extend GameCommand { + optional Command_RevealCards ext = 1026; + } + optional string zone_name = 1; + repeated sint32 card_id = 2 [packed = false]; + optional sint32 player_id = 3 [default = -1]; + optional bool grant_write_access = 4; + optional sint32 top_cards = 5 [default = -1]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_reverse_turn.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_reverse_turn.proto new file mode 100644 index 000000000..c5cc1c4d9 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_reverse_turn.proto @@ -0,0 +1,7 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_ReverseTurn { + extend GameCommand { + optional Command_ReverseTurn ext = 1034; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_roll_die.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_roll_die.proto new file mode 100644 index 000000000..6639241e1 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_roll_die.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_RollDie { + extend GameCommand { + optional Command_RollDie ext = 1005; + } + optional uint32 sides = 1; + optional uint32 count = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_active_phase.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_active_phase.proto new file mode 100644 index 000000000..ded7d8197 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_active_phase.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_SetActivePhase { + extend GameCommand { + optional Command_SetActivePhase ext = 1023; + } + optional uint32 phase = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_card_attr.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_card_attr.proto new file mode 100644 index 000000000..1e1d8664a --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_card_attr.proto @@ -0,0 +1,13 @@ +syntax = "proto2"; +import "game_commands.proto"; +import "card_attributes.proto"; + +message Command_SetCardAttr { + extend GameCommand { + optional Command_SetCardAttr ext = 1013; + } + optional string zone = 1; + optional sint32 card_id = 2 [default = -1]; + optional CardAttribute attribute = 3; + optional string attr_value = 4; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_card_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_card_counter.proto new file mode 100644 index 000000000..5fa9c3ee3 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_card_counter.proto @@ -0,0 +1,11 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_SetCardCounter { + extend GameCommand { + optional Command_SetCardCounter ext = 1014; + } + optional string zone = 1; + optional sint32 card_id = 2 [default = -1]; + optional sint32 counter_id = 3 [default = -1]; + optional sint32 counter_value = 4; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_counter.proto new file mode 100644 index 000000000..33cae56a5 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_counter.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_SetCounter { + extend GameCommand { + optional Command_SetCounter ext = 1020; + } + optional sint32 counter_id = 1 [default = -1]; + optional sint32 value = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_sideboard_lock.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_sideboard_lock.proto new file mode 100644 index 000000000..5aa6a64ba --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_sideboard_lock.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_SetSideboardLock { + extend GameCommand { + optional Command_SetSideboardLock ext = 1030; + } + optional bool locked = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_sideboard_plan.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_sideboard_plan.proto new file mode 100644 index 000000000..89c676d2d --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_sideboard_plan.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "game_commands.proto"; +import "move_card_to_zone.proto"; + +message Command_SetSideboardPlan { + extend GameCommand { + optional Command_SetSideboardPlan ext = 1028; + } + repeated MoveCard_ToZone move_list = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_shuffle.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_shuffle.proto new file mode 100644 index 000000000..a99f90896 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_shuffle.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_Shuffle { + extend GameCommand { + optional Command_Shuffle ext = 1003; + } + optional string zone_name = 1; + optional sint32 start = 2 [default = 0]; + optional sint32 end = 3 [default = -1]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_undo_draw.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_undo_draw.proto new file mode 100644 index 000000000..094289333 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_undo_draw.proto @@ -0,0 +1,7 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_UndoDraw { + extend GameCommand { + optional Command_UndoDraw ext = 1007; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/commands.proto new file mode 100644 index 000000000..b6eaf6733 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/commands.proto @@ -0,0 +1,19 @@ +syntax = "proto2"; +import "session_commands.proto"; +import "game_commands.proto"; +import "room_commands.proto"; +import "moderator_commands.proto"; +import "admin_commands.proto"; + +message CommandContainer { + optional uint64 cmd_id = 1; + + optional uint32 game_id = 10; + optional uint32 room_id = 20; + + repeated SessionCommand session_command = 100; + repeated GameCommand game_command = 101; + repeated RoomCommand room_command = 102; + repeated ModeratorCommand moderator_command = 103; + repeated AdminCommand admin_command = 104; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/context_concede.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/context_concede.proto new file mode 100644 index 000000000..a266ed2b7 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/context_concede.proto @@ -0,0 +1,14 @@ +syntax = "proto2"; +import "game_event_context.proto"; + +message Context_Concede { + extend GameEventContext { + optional Context_Concede ext = 1001; + } +} + +message Context_Unconcede { + extend GameEventContext { + optional Context_Unconcede ext = 1009; + } +} \ No newline at end of file diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/context_connection_state_changed.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/context_connection_state_changed.proto new file mode 100644 index 000000000..c87f8beda --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/context_connection_state_changed.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "game_event_context.proto"; + +message Context_ConnectionStateChanged { + extend GameEventContext { + optional Context_ConnectionStateChanged ext = 1007; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/context_deck_select.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/context_deck_select.proto new file mode 100644 index 000000000..44abd8583 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/context_deck_select.proto @@ -0,0 +1,11 @@ +syntax = "proto2"; +import "game_event_context.proto"; + +message Context_DeckSelect { + extend GameEventContext { + optional Context_DeckSelect ext = 1002; + } + optional string deck_hash = 1; + optional int32 sideboard_size = 2 [default = -1]; + optional string deck_list = 3; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/context_move_card.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/context_move_card.proto new file mode 100644 index 000000000..49bcb77cd --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/context_move_card.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "game_event_context.proto"; + +message Context_MoveCard { + extend GameEventContext { + optional Context_MoveCard ext = 1004; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/context_mulligan.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/context_mulligan.proto new file mode 100644 index 000000000..edcaf9003 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/context_mulligan.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "game_event_context.proto"; + +message Context_Mulligan { + extend GameEventContext { + optional Context_Mulligan ext = 1005; + } + optional uint32 number = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/context_ping_changed.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/context_ping_changed.proto new file mode 100644 index 000000000..e8f1b0bdb --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/context_ping_changed.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "game_event_context.proto"; + +message Context_PingChanged { + extend GameEventContext { + optional Context_PingChanged ext = 1006; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/context_ready_start.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/context_ready_start.proto new file mode 100644 index 000000000..7a4e60899 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/context_ready_start.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "game_event_context.proto"; + +message Context_ReadyStart { + extend GameEventContext { + optional Context_ReadyStart ext = 1000; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/context_set_sideboard_lock.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/context_set_sideboard_lock.proto new file mode 100644 index 000000000..a9a36f8b2 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/context_set_sideboard_lock.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "game_event_context.proto"; + +message Context_SetSideboardLock { + extend GameEventContext { + optional Context_SetSideboardLock ext = 1008; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/context_undo_draw.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/context_undo_draw.proto new file mode 100644 index 000000000..8b934079f --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/context_undo_draw.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "game_event_context.proto"; + +message Context_UndoDraw { + extend GameEventContext { + optional Context_UndoDraw ext = 1003; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_add_to_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_add_to_list.proto new file mode 100644 index 000000000..4a1072e81 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_add_to_list.proto @@ -0,0 +1,11 @@ +syntax = "proto2"; +import "session_event.proto"; +import "serverinfo_user.proto"; + +message Event_AddToList { + extend SessionEvent { + optional Event_AddToList ext = 1005; + } + optional string list_name = 1; + optional ServerInfo_User user_info = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_attach_card.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_attach_card.proto new file mode 100644 index 000000000..a71610f9f --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_attach_card.proto @@ -0,0 +1,13 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_AttachCard { + extend GameEvent { + optional Event_AttachCard ext = 2012; + } + optional string start_zone = 1; + optional sint32 card_id = 2; + optional sint32 target_player_id = 3; + optional string target_zone = 4; + optional sint32 target_card_id = 5; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_change_zone_properties.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_change_zone_properties.proto new file mode 100644 index 000000000..35b077b38 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_change_zone_properties.proto @@ -0,0 +1,14 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_ChangeZoneProperties { + extend GameEvent { + optional Event_ChangeZoneProperties ext = 2020; + } + optional string zone_name = 1; + + // Reveal top card to all players. + optional bool always_reveal_top_card = 10; + // reveal top card to the owner. + optional bool always_look_at_top_card = 11; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_connection_closed.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_connection_closed.proto new file mode 100644 index 000000000..03018b800 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_connection_closed.proto @@ -0,0 +1,21 @@ +syntax = "proto2"; +import "session_event.proto"; + +message Event_ConnectionClosed { + extend SessionEvent { + optional Event_ConnectionClosed ext = 1002; + } + enum CloseReason { + OTHER = 1; + SERVER_SHUTDOWN = 2; + TOO_MANY_CONNECTIONS = 3; + BANNED = 4; + USERNAMEINVALID = 5; + USER_LIMIT_REACHED = 6; + DEMOTED = 7; + LOGGEDINELSEWERE = 8; + } + optional CloseReason reason = 1; + optional string reason_str = 2; + optional uint32 end_time = 3; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_create_arrow.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_create_arrow.proto new file mode 100644 index 000000000..820d3cea9 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_create_arrow.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "game_event.proto"; +import "serverinfo_arrow.proto"; + +message Event_CreateArrow { + extend GameEvent { + optional Event_CreateArrow ext = 2000; + } + optional ServerInfo_Arrow arrow_info = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_create_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_create_counter.proto new file mode 100644 index 000000000..5dfca01e1 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_create_counter.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "game_event.proto"; +import "serverinfo_counter.proto"; + +message Event_CreateCounter { + extend GameEvent { + optional Event_CreateCounter ext = 2002; + } + optional ServerInfo_Counter counter_info = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_create_token.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_create_token.proto new file mode 100644 index 000000000..6947b6048 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_create_token.proto @@ -0,0 +1,19 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_CreateToken { + extend GameEvent { + optional Event_CreateToken ext = 2013; + } + optional string zone_name = 1; + optional sint32 card_id = 2; + optional string card_name = 3; + optional string color = 4; + optional string pt = 5; + optional string annotation = 6; + optional bool destroy_on_zone_change = 7; + optional sint32 x = 8; + optional sint32 y = 9; + optional string card_provider_id = 10; + optional bool face_down = 11; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_del_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_del_counter.proto new file mode 100644 index 000000000..c693fc869 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_del_counter.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_DelCounter { + extend GameEvent { + optional Event_DelCounter ext = 2004; + } + optional sint32 counter_id = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_delete_arrow.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_delete_arrow.proto new file mode 100644 index 000000000..bd165c97f --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_delete_arrow.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_DeleteArrow { + extend GameEvent { + optional Event_DeleteArrow ext = 2001; + } + optional sint32 arrow_id = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_destroy_card.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_destroy_card.proto new file mode 100644 index 000000000..8bef22421 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_destroy_card.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_DestroyCard { + extend GameEvent { + optional Event_DestroyCard ext = 2011; + } + optional string zone_name = 1; + optional uint32 card_id = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_draw_cards.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_draw_cards.proto new file mode 100644 index 000000000..cea343a12 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_draw_cards.proto @@ -0,0 +1,11 @@ +syntax = "proto2"; +import "game_event.proto"; +import "serverinfo_card.proto"; + +message Event_DrawCards { + extend GameEvent { + optional Event_DrawCards ext = 2005; + } + optional sint32 number = 1; + repeated ServerInfo_Card cards = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_dump_zone.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_dump_zone.proto new file mode 100644 index 000000000..06c7a99d0 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_dump_zone.proto @@ -0,0 +1,12 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_DumpZone { + extend GameEvent { + optional Event_DumpZone ext = 2018; + } + optional sint32 zone_owner_id = 1; + optional string zone_name = 2; + optional sint32 number_cards = 3; + optional bool is_reversed = 4 [default = false]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_flip_card.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_flip_card.proto new file mode 100644 index 000000000..06f7c7cf7 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_flip_card.proto @@ -0,0 +1,13 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_FlipCard { + extend GameEvent { + optional Event_FlipCard ext = 2010; + } + optional string zone_name = 1; + optional sint32 card_id = 2; + optional string card_name = 3; + optional bool face_down = 4; + optional string card_provider_id = 5; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_closed.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_closed.proto new file mode 100644 index 000000000..e400926c1 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_closed.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_GameClosed { + extend GameEvent { + optional Event_GameClosed ext = 1002; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_host_changed.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_host_changed.proto new file mode 100644 index 000000000..0afe2effb --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_host_changed.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_GameHostChanged { + extend GameEvent { + optional Event_GameHostChanged ext = 1003; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_joined.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_joined.proto new file mode 100644 index 000000000..5d90b0b76 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_joined.proto @@ -0,0 +1,17 @@ +syntax = "proto2"; +import "session_event.proto"; +import "serverinfo_game.proto"; +import "serverinfo_gametype.proto"; + +message Event_GameJoined { + extend SessionEvent { + optional Event_GameJoined ext = 1009; + } + optional ServerInfo_Game game_info = 1; + repeated ServerInfo_GameType game_types = 2; + optional sint32 host_id = 3; + optional sint32 player_id = 4; + optional bool spectator = 5; + optional bool resuming = 6; + optional bool judge = 7; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_say.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_say.proto new file mode 100644 index 000000000..8aa42eca1 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_say.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_GameSay { + extend GameEvent { + optional Event_GameSay ext = 1009; + } + optional string message = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_state_changed.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_state_changed.proto new file mode 100644 index 000000000..5c8aee3fe --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_state_changed.proto @@ -0,0 +1,27 @@ +syntax = "proto2"; +import "game_event.proto"; +import "serverinfo_player.proto"; + +// Signals that the game state has changed. +// If a field is present in this message, it will overwrite the client's game state. +// Also used to provide the entire game state when joining a game. +message Event_GameStateChanged { + extend GameEvent { + optional Event_GameStateChanged ext = 1005; + } + + // the list of players. Players contain their zones which contain all cards in the game + repeated ServerInfo_Player player_list = 1; + + // if the game has started + optional bool game_started = 2; + + // the player who is currently holding the turn + optional sint32 active_player_id = 3; + + // the current phase + optional sint32 active_phase = 4; + + // the amount of seconds since the game started + optional uint32 seconds_elapsed = 5; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_join.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_join.proto new file mode 100644 index 000000000..1c91dc099 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_join.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "game_event.proto"; +import "serverinfo_playerproperties.proto"; + +message Event_Join { + extend GameEvent { + optional Event_Join ext = 1000; + } + optional ServerInfo_PlayerProperties player_properties = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_join_room.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_join_room.proto new file mode 100644 index 000000000..e575edc41 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_join_room.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "room_event.proto"; +import "serverinfo_user.proto"; + +message Event_JoinRoom { + extend RoomEvent { + optional Event_JoinRoom ext = 1001; + } + optional ServerInfo_User user_info = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_kicked.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_kicked.proto new file mode 100644 index 000000000..02036cee7 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_kicked.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_Kicked { + extend GameEvent { + optional Event_Kicked ext = 1004; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_leave.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_leave.proto new file mode 100644 index 000000000..6e1ed871d --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_leave.proto @@ -0,0 +1,15 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_Leave { + extend GameEvent { + optional Event_Leave ext = 1001; + } + enum LeaveReason { + OTHER = 1; + USER_KICKED = 2; + USER_LEFT = 3; + USER_DISCONNECTED = 4; + } + optional LeaveReason reason = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_leave_room.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_leave_room.proto new file mode 100644 index 000000000..dc9f3e866 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_leave_room.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "room_event.proto"; + +message Event_LeaveRoom { + extend RoomEvent { + optional Event_LeaveRoom ext = 1000; + } + optional string name = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_list_games.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_list_games.proto new file mode 100644 index 000000000..b69c7b5c9 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_list_games.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "room_event.proto"; +import "serverinfo_game.proto"; + +message Event_ListGames { + extend RoomEvent { + optional Event_ListGames ext = 1003; + } + repeated ServerInfo_Game game_list = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_list_rooms.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_list_rooms.proto new file mode 100644 index 000000000..e72d2ba19 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_list_rooms.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "session_event.proto"; +import "serverinfo_room.proto"; + +message Event_ListRooms { + extend SessionEvent { + optional Event_ListRooms ext = 1004; + } + repeated ServerInfo_Room room_list = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_move_card.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_move_card.proto new file mode 100644 index 000000000..e8e60066a --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_move_card.proto @@ -0,0 +1,48 @@ +syntax = "proto2"; +import "game_event.proto"; + +// Sent by the server to signal a single card was moved to update the client state +message Event_MoveCard { + extend GameEvent { + optional Event_MoveCard ext = 2009; + } + + // The card id in the original zone + optional sint32 card_id = 1 [default = -1]; + + // The name of the card in case it was not known yet + optional string card_name = 2; + + // The player whose zone the card started in + optional sint32 start_player_id = 3 [default = -1]; + + // The original zone + optional string start_zone = 4; + + // The original position that the card was at. + // In zones without y coordinate, this corresponds with the previous x coordinate of the card. + // In zones with y coordinate, this value is not used. + optional sint32 position = 5 [default = -1]; + + // The player who owns the new zone the card is in + optional sint32 target_player_id = 6 [default = -1]; + + // The new zone the card is in + optional string target_zone = 7; + + // The new x coordinate (or new position for zones with no y coordinate) + optional sint32 x = 8 [default = -1]; + + // The new y coordinate + optional sint32 y = 9 [default = -1]; + + // The new id of the card if the card moved zone + optional sint32 new_card_id = 10 [default = -1]; + + // If the card is face down, face down cards will not show their name + optional bool face_down = 11; + + // The provider id of the card in case it was not known yet. + // Extends the name to supply a specific printing of that type of card. + optional string new_card_provider_id = 12; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_notify_user.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_notify_user.proto new file mode 100644 index 000000000..3a90d278b --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_notify_user.proto @@ -0,0 +1,21 @@ +syntax = "proto2"; +import "session_event.proto"; + +message Event_NotifyUser { + + enum NotificationType { + UNKNOWN = 0; // Default enum value if no "type" is defined when used + PROMOTED = 1; + WARNING = 2; + IDLEWARNING = 3; + CUSTOM = 4; + } + + extend SessionEvent { + optional Event_NotifyUser ext = 1010; + } + optional NotificationType type = 1; + optional string warning_reason = 2; + optional string custom_title = 3; + optional string custom_content = 4; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_player_properties_changed.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_player_properties_changed.proto new file mode 100644 index 000000000..c0feaf532 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_player_properties_changed.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "game_event.proto"; +import "serverinfo_playerproperties.proto"; + +message Event_PlayerPropertiesChanged { + extend GameEvent { + optional Event_PlayerPropertiesChanged ext = 1007; + } + optional ServerInfo_PlayerProperties player_properties = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_remove_from_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_remove_from_list.proto new file mode 100644 index 000000000..81bb64ede --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_remove_from_list.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "session_event.proto"; + +message Event_RemoveFromList { + extend SessionEvent { + optional Event_RemoveFromList ext = 1006; + } + optional string list_name = 1; + optional string user_name = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_remove_messages.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_remove_messages.proto new file mode 100644 index 000000000..bb1023464 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_remove_messages.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "room_event.proto"; + +message Event_RemoveMessages { + extend RoomEvent { + optional Event_RemoveMessages ext = 1004; + } + optional string name = 1; + optional uint32 amount = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_replay_added.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_replay_added.proto new file mode 100644 index 000000000..2efb5f37a --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_replay_added.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "session_event.proto"; +import "serverinfo_replay_match.proto"; + +message Event_ReplayAdded { + extend SessionEvent { + optional Event_ReplayAdded ext = 1100; + } + optional ServerInfo_ReplayMatch match_info = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_reveal_cards.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_reveal_cards.proto new file mode 100644 index 000000000..a64276c3a --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_reveal_cards.proto @@ -0,0 +1,15 @@ +syntax = "proto2"; +import "game_event.proto"; +import "serverinfo_card.proto"; + +message Event_RevealCards { + extend GameEvent { + optional Event_RevealCards ext = 2006; + } + optional string zone_name = 1; + repeated sint32 card_id = 2 [packed = false]; + optional sint32 other_player_id = 3 [default = -1]; + repeated ServerInfo_Card cards = 4; + optional bool grant_write_access = 5; + optional uint32 number_of_cards = 6; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_reverse_turn.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_reverse_turn.proto new file mode 100644 index 000000000..0357d5c0a --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_reverse_turn.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_ReverseTurn { + extend GameEvent { + optional Event_ReverseTurn ext = 2021; + } + optional bool reversed = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_roll_die.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_roll_die.proto new file mode 100644 index 000000000..26014f218 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_roll_die.proto @@ -0,0 +1,11 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_RollDie { + extend GameEvent { + optional Event_RollDie ext = 2008; + } + optional uint32 sides = 1; + optional uint32 value = 2; + repeated uint32 values = 3; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_room_say.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_room_say.proto new file mode 100644 index 000000000..2c6a990ea --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_room_say.proto @@ -0,0 +1,17 @@ +syntax = "proto2"; +import "room_event.proto"; + +message Event_RoomSay { + extend RoomEvent { + optional Event_RoomSay ext = 1002; + } + enum RoomMessageType { + UserMessage = 0; // user message + Welcome = 1; // rooms welcome message + ChatHistory = 2; // rooms chat history message + } + optional string name = 1; + optional string message = 2; + optional RoomMessageType message_type = 3; + optional uint64 time_of = 4; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_server_complete_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_server_complete_list.proto new file mode 100644 index 000000000..8adba1fd7 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_server_complete_list.proto @@ -0,0 +1,13 @@ +syntax = "proto2"; +import "session_event.proto"; +import "serverinfo_user.proto"; +import "serverinfo_room.proto"; + +message Event_ServerCompleteList { + extend SessionEvent { + optional Event_ServerCompleteList ext = 600; + } + optional uint32 server_id = 1; + repeated ServerInfo_User user_list = 2; + repeated ServerInfo_Room room_list = 3; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_server_identification.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_server_identification.proto new file mode 100644 index 000000000..987ab20d1 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_server_identification.proto @@ -0,0 +1,16 @@ +syntax = "proto2"; +import "session_event.proto"; + +message Event_ServerIdentification { + extend SessionEvent { + optional Event_ServerIdentification ext = 500; + } + enum ServerOptions { + NoOptions = 0; + SupportsPasswordHash = 1; + } + optional string server_name = 1; + optional string server_version = 2; + optional uint32 protocol_version = 3; + optional ServerOptions server_options = 4 [default = NoOptions]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_server_message.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_server_message.proto new file mode 100644 index 000000000..a47d4eb7d --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_server_message.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "session_event.proto"; + +message Event_ServerMessage { + extend SessionEvent { + optional Event_ServerMessage ext = 1000; + } + optional string message = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_server_shutdown.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_server_shutdown.proto new file mode 100644 index 000000000..879ec1ea4 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_server_shutdown.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "session_event.proto"; + +message Event_ServerShutdown { + extend SessionEvent { + optional Event_ServerShutdown ext = 1001; + } + optional string reason = 1; + optional uint32 minutes = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_active_phase.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_active_phase.proto new file mode 100644 index 000000000..a19dd4b98 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_active_phase.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_SetActivePhase { + extend GameEvent { + optional Event_SetActivePhase ext = 2017; + } + optional sint32 phase = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_active_player.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_active_player.proto new file mode 100644 index 000000000..7962ac15e --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_active_player.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_SetActivePlayer { + extend GameEvent { + optional Event_SetActivePlayer ext = 2016; + } + optional sint32 active_player_id = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_card_attr.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_card_attr.proto new file mode 100644 index 000000000..aa902fdd4 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_card_attr.proto @@ -0,0 +1,13 @@ +syntax = "proto2"; +import "game_event.proto"; +import "card_attributes.proto"; + +message Event_SetCardAttr { + extend GameEvent { + optional Event_SetCardAttr ext = 2014; + } + optional string zone_name = 1; + optional sint32 card_id = 2; + optional CardAttribute attribute = 3; + optional string attr_value = 4; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_card_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_card_counter.proto new file mode 100644 index 000000000..e04262715 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_card_counter.proto @@ -0,0 +1,12 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_SetCardCounter { + extend GameEvent { + optional Event_SetCardCounter ext = 2015; + } + optional string zone_name = 1; + optional sint32 card_id = 2; + optional sint32 counter_id = 3; + optional sint32 counter_value = 4; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_counter.proto new file mode 100644 index 000000000..55541a027 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_counter.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_SetCounter { + extend GameEvent { + optional Event_SetCounter ext = 2003; + } + optional sint32 counter_id = 1; + optional sint32 value = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_shuffle.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_shuffle.proto new file mode 100644 index 000000000..8285a1d84 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_shuffle.proto @@ -0,0 +1,11 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_Shuffle { + extend GameEvent { + optional Event_Shuffle ext = 2007; + } + optional string zone_name = 1; + optional sint32 start = 2 [default = 0]; + optional sint32 end = 3 [default = -1]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_user_joined.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_user_joined.proto new file mode 100644 index 000000000..15ae93c3f --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_user_joined.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "session_event.proto"; +import "serverinfo_user.proto"; + +message Event_UserJoined { + extend SessionEvent { + optional Event_UserJoined ext = 1007; + } + optional ServerInfo_User user_info = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_user_left.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_user_left.proto new file mode 100644 index 000000000..c857640df --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_user_left.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "session_event.proto"; + +message Event_UserLeft { + extend SessionEvent { + optional Event_UserLeft ext = 1008; + } + optional string name = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_user_message.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_user_message.proto new file mode 100644 index 000000000..9cf6003d9 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_user_message.proto @@ -0,0 +1,11 @@ +syntax = "proto2"; +import "session_event.proto"; + +message Event_UserMessage { + extend SessionEvent { + optional Event_UserMessage ext = 1003; + } + optional string sender_name = 1; + optional string receiver_name = 2; + optional string message = 3; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto new file mode 100644 index 000000000..796f4fc68 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto @@ -0,0 +1,56 @@ +syntax = "proto2"; + +// Commands that are sent during a game to change the game state +message GameCommand { + enum GameCommandType { + KICK_FROM_GAME = 1000; + LEAVE_GAME = 1001; + GAME_SAY = 1002; + SHUFFLE = 1003; + MULLIGAN = 1004; + ROLL_DIE = 1005; + DRAW_CARDS = 1006; + UNDO_DRAW = 1007; + FLIP_CARD = 1008; + ATTACH_CARD = 1009; + CREATE_TOKEN = 1010; + CREATE_ARROW = 1011; + DELETE_ARROW = 1012; + SET_CARD_ATTR = 1013; + SET_CARD_COUNTER = 1014; + INC_CARD_COUNTER = 1015; + READY_START = 1016; + CONCEDE = 1017; + INC_COUNTER = 1018; + CREATE_COUNTER = 1019; + SET_COUNTER = 1020; + DEL_COUNTER = 1021; + NEXT_TURN = 1022; + SET_ACTIVE_PHASE = 1023; + DUMP_ZONE = 1024; + // STOP_DUMP_ZONE = 1025; // obsolete + REVEAL_CARDS = 1026; + MOVE_CARD = 1027; + SET_SIDEBOARD_PLAN = 1028; + DECK_SELECT = 1029; + SET_SIDEBOARD_LOCK = 1030; + CHANGE_ZONE_PROPERTIES = 1031; + UNCONCEDE = 1032; + JUDGE = 1033; + REVERSE_TURN = 1034; + } + extensions 100 to max; +} + +// A wrapper around a normal game command that allows a privileged user to send a command on behalf of another player +message Command_Judge { + extend GameCommand { + optional Command_Judge ext = 1033; + } + + // The player on whose behalf this command is sent + optional sint32 target_id = 1 [default = -1]; + + // The wrapped game command + repeated GameCommand game_command = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto new file mode 100644 index 000000000..8682128af --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto @@ -0,0 +1,39 @@ +syntax = "proto2"; + +// Sent every time something happens in the game to update the client's state +message GameEvent { + enum GameEventType { + JOIN = 1000; + LEAVE = 1001; + GAME_CLOSED = 1002; + GAME_HOST_CHANGED = 1003; + KICKED = 1004; + GAME_STATE_CHANGED = 1005; + PLAYER_PROPERTIES_CHANGED = 1007; + GAME_SAY = 1009; + CREATE_ARROW = 2000; + DELETE_ARROW = 2001; + CREATE_COUNTER = 2002; + SET_COUNTER = 2003; + DEL_COUNTER = 2004; + DRAW_CARDS = 2005; + REVEAL_CARDS = 2006; + SHUFFLE = 2007; + ROLL_DIE = 2008; + MOVE_CARD = 2009; + FLIP_CARD = 2010; + DESTROY_CARD = 2011; + ATTACH_CARD = 2012; + CREATE_TOKEN = 2013; + SET_CARD_ATTR = 2014; + SET_CARD_COUNTER = 2015; + SET_ACTIVE_PLAYER = 2016; + SET_ACTIVE_PHASE = 2017; + DUMP_ZONE = 2018; + // STOP_DUMP_ZONE = 2019; // obsolete + CHANGE_ZONE_PROPERTIES = 2020; + REVERSE_TURN = 2021; + } + optional sint32 player_id = 1 [default = -1]; + extensions 100 to max; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/game_event_container.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_event_container.proto new file mode 100644 index 000000000..239da4fd8 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/game_event_container.proto @@ -0,0 +1,11 @@ +syntax = "proto2"; +import "game_event.proto"; +import "game_event_context.proto"; + +message GameEventContainer { + optional uint32 game_id = 1; + repeated GameEvent event_list = 2; + optional GameEventContext context = 3; + optional uint32 seconds_elapsed = 4; + optional uint32 forced_by_judge = 5; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/game_event_context.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_event_context.proto new file mode 100644 index 000000000..76dccad0d --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/game_event_context.proto @@ -0,0 +1,16 @@ +syntax = "proto2"; +message GameEventContext { + enum ContextType { + READY_START = 1000; + CONCEDE = 1001; + DECK_SELECT = 1002; + UNDO_DRAW = 1003; + MOVE_CARD = 1004; + MULLIGAN = 1005; + PING_CHANGED = 1006; + CONNECTION_STATE_CHANGED = 1007; + SET_SIDEBOARD_LOCK = 1008; + UNCONCEDE = 1009; + } + extensions 100 to max; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/game_replay.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_replay.proto new file mode 100644 index 000000000..e8a0aa0bc --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/game_replay.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "serverinfo_game.proto"; +import "game_event_container.proto"; + +message GameReplay { + optional uint64 replay_id = 1; + optional ServerInfo_Game game_info = 2; + repeated GameEventContainer event_list = 3; + optional uint32 duration_seconds = 4; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/isl_message.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/isl_message.proto new file mode 100644 index 000000000..d4bb8d785 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/isl_message.proto @@ -0,0 +1,30 @@ +syntax = "proto2"; +import "response.proto"; +import "session_event.proto"; +import "commands.proto"; +import "game_event_container.proto"; +import "room_event.proto"; + +message IslMessage { + enum MessageType { + GAME_COMMAND_CONTAINER = 0; + ROOM_COMMAND_CONTAINER = 1; + + RESPONSE = 10; + SESSION_EVENT = 11; + GAME_EVENT_CONTAINER = 12; + ROOM_EVENT = 13; + } + optional MessageType message_type = 1; + + optional uint64 session_id = 9; + optional sint32 player_id = 10 [default = -1]; + + optional CommandContainer game_command = 100; + optional CommandContainer room_command = 101; + + optional Response response = 200; + optional SessionEvent session_event = 201; + optional GameEventContainer game_event_container = 202; + optional RoomEvent room_event = 203; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto new file mode 100644 index 000000000..9d01b51d2 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto @@ -0,0 +1,108 @@ +syntax = "proto2"; +message ModeratorCommand { + enum ModeratorCommandType { + BAN_FROM_SERVER = 1000; + BAN_HISTORY = 1001; + WARN_USER = 1002; + WARN_HISTORY = 1003; + WARN_LIST = 1004; + VIEWLOG_HISTORY = 1005; + GRANT_REPLAY_ACCESS = 1006; + FORCE_ACTIVATE_USER = 1007; + GET_ADMIN_NOTES = 1008; + UPDATE_ADMIN_NOTES = 1009; + } + extensions 100 to max; +} + +message Command_BanFromServer { + extend ModeratorCommand { + optional Command_BanFromServer ext = 1000; + } + optional string user_name = 1; + optional string address = 2; + optional uint32 minutes = 3; + optional string reason = 4; + optional string visible_reason = 5; + optional string clientid = 6; + optional uint32 remove_messages = 7; +} + +message Command_GetBanHistory { + extend ModeratorCommand { + optional Command_GetBanHistory ext = 1001; + } + optional string user_name = 1; +} + +message Command_WarnUser { + extend ModeratorCommand { + optional Command_WarnUser ext = 1002; + } + + optional string user_name = 1; + optional string reason = 2; + optional string clientid = 3; + optional uint32 remove_messages = 4; +} + +message Command_GetWarnHistory { + extend ModeratorCommand { + optional Command_GetWarnHistory ext = 1003; + } + optional string user_name = 1; +} + +message Command_GetWarnList { + extend ModeratorCommand { + optional Command_GetWarnList ext = 1004; + } + optional string mod_name = 1; + optional string user_name = 2; + optional string user_clientid = 3; +} + +message Command_ViewLogHistory { + extend ModeratorCommand { + optional Command_ViewLogHistory ext = 1005; + } + optional string user_name = 1; // user that created message + optional string ip_address = 2; // ip address of user that created message + optional string game_name = 3; // client id of user that created the message + optional string game_id = 4; // game number the message was sent to + optional string message = 5; // raw message that was sent + repeated string log_location = 6; // destination of message (ex: main room, game room, private chat) + required uint32 date_range = 7; // the length of time (in minutes) to look back for + optional uint32 maximum_results = 8; // the maximum number of query results +} + +message Command_GrantReplayAccess { + extend ModeratorCommand { + optional Command_GrantReplayAccess ext = 1006; + } + optional uint32 replay_id = 1; + optional string moderator_name = 2; +} + +message Command_ForceActivateUser { + extend ModeratorCommand { + optional Command_ForceActivateUser ext = 1007; + } + optional string username_to_activate = 1; + optional string moderator_name = 2; +} + +message Command_GetAdminNotes { + extend ModeratorCommand { + optional Command_GetAdminNotes ext = 1008; + } + optional string user_name = 1; +} + +message Command_UpdateAdminNotes { + extend ModeratorCommand { + optional Command_UpdateAdminNotes ext = 1009; + } + optional string user_name = 1; + optional string notes = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/move_card_to_zone.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/move_card_to_zone.proto new file mode 100644 index 000000000..10ce87c3e --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/move_card_to_zone.proto @@ -0,0 +1,6 @@ +syntax = "proto2"; +message MoveCard_ToZone { + optional string card_name = 1; + optional string start_zone = 2; + optional string target_zone = 3; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto new file mode 100644 index 000000000..dece8ae17 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto @@ -0,0 +1,76 @@ +syntax = "proto2"; + +// Sent immediately after a command with the same cmd_id, connecting it to the command sent to the server +message Response { + enum ResponseCode { + RespNotConnected = -1; + RespNothing = 0; + RespOk = 1; + RespNotInRoom = 2; + RespInternalError = 3; + RespInvalidCommand = 4; + RespInvalidData = 5; + RespNameNotFound = 6; + RespLoginNeeded = 7; + RespFunctionNotAllowed = 8; + RespGameNotStarted = 9; + RespGameFull = 10; + RespContextError = 11; + RespWrongPassword = 12; + RespSpectatorsNotAllowed = 13; + RespOnlyBuddies = 14; + RespUserLevelTooLow = 15; + RespInIgnoreList = 16; + RespWouldOverwriteOldSession = 17; + RespChatFlood = 18; + RespUserIsBanned = 19; + RespAccessDenied = 20; + RespUsernameInvalid = 21; + RespRegistrationRequired = 22; + RespRegistrationAccepted = 23; // Server agrees to process client's registration request + RespUserAlreadyExists = 24; // Client attempted to register a name which is already registered + RespEmailRequiredToRegister = 25; // Server requires email to register accounts but client did not provide one + RespTooManyRequests = 26; // Server refused to complete command because client has sent too many too quickly + RespPasswordTooShort = 27; // Server requires a decent password + RespAccountNotActivated = + 28; // Client attempted to log into a registered username but the account hasn't been activated + RespRegistrationDisabled = 29; // Server does not allow clients to register + RespRegistrationFailed = 30; // Server accepted a reg request but failed to perform the registration + RespActivationAccepted = 31; // Server accepted a reg user activation token + RespActivationFailed = 32; // Server didn't accept a reg user activation token + RespRegistrationAcceptedNeedsActivation = + 33; // Server accepted client registration, but it will need token activation + RespClientIdRequired = 34; // Server requires client to generate and send its client id before allowing access + RespClientUpdateRequired = 35; // Client is missing features that the server is requiring + RespServerFull = 36; // Server user limit reached + RespEmailBlackListed = 37; // Server has blocked the email address provided for registration for some reason + } + enum ResponseType { + JOIN_ROOM = 1000; + LIST_USERS = 1001; + GET_GAMES_OF_USER = 1002; + GET_USER_INFO = 1003; + DUMP_ZONE = 1004; + LOGIN = 1005; + DECK_LIST = 1006; + DECK_DOWNLOAD = 1007; + DECK_UPLOAD = 1008; + REGISTER = 1009; + ACTIVATE = 1010; + ADJUST_MOD = 1011; + BAN_HISTORY = 1012; + WARN_HISTORY = 1013; + WARN_LIST = 1014; + VIEW_LOG = 1015; + FORGOT_PASSWORD_REQUEST = 1016; + PASSWORD_SALT = 1017; + GET_ADMIN_NOTES = 1018; + REPLAY_LIST = 1100; + REPLAY_DOWNLOAD = 1101; + REPLAY_GET_CODE = 1102; + } + required uint64 cmd_id = 1; + optional ResponseCode response_code = 2; + + extensions 100 to max; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_activate.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_activate.proto new file mode 100644 index 000000000..744023314 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_activate.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "response.proto"; + +message Response_Activate { + extend Response { + optional Response_Activate ext = 1010; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_adjust_mod.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_adjust_mod.proto new file mode 100644 index 000000000..948eec1ba --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_adjust_mod.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +import "response.proto"; + +message Response_AdjustMod { + extend Response { + optional Response_AdjustMod ext = 1011; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_ban_history.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_ban_history.proto new file mode 100644 index 000000000..696004152 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_ban_history.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "response.proto"; +import "serverinfo_ban.proto"; + +message Response_BanHistory { + extend Response { + optional Response_BanHistory ext = 1012; + } + repeated ServerInfo_Ban ban_list = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_download.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_download.proto new file mode 100644 index 000000000..f1839a9e2 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_download.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "response.proto"; + +message Response_DeckDownload { + extend Response { + optional Response_DeckDownload ext = 1007; + } + optional string deck = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_list.proto new file mode 100644 index 000000000..bc516a25c --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_list.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "response.proto"; +import "serverinfo_deckstorage.proto"; + +message Response_DeckList { + extend Response { + optional Response_DeckList ext = 1006; + } + optional ServerInfo_DeckStorage_Folder root = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_upload.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_upload.proto new file mode 100644 index 000000000..3648d8777 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_upload.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "response.proto"; +import "serverinfo_deckstorage.proto"; + +message Response_DeckUpload { + extend Response { + optional Response_DeckUpload ext = 1008; + } + optional ServerInfo_DeckStorage_TreeItem new_file = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_dump_zone.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_dump_zone.proto new file mode 100644 index 000000000..e37288dfb --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_dump_zone.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "response.proto"; +import "serverinfo_zone.proto"; + +message Response_DumpZone { + extend Response { + optional Response_DumpZone ext = 1004; + } + optional ServerInfo_Zone zone_info = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_forgotpasswordrequest.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_forgotpasswordrequest.proto new file mode 100644 index 000000000..5205e0fdc --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_forgotpasswordrequest.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "response.proto"; + +message Response_ForgotPasswordRequest { + extend Response { + optional Response_ForgotPasswordRequest ext = 1016; + } + optional bool challenge_email = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_get_admin_notes.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_get_admin_notes.proto new file mode 100644 index 000000000..f08bce5d5 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_get_admin_notes.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "response.proto"; + +message Response_GetAdminNotes { + extend Response { + optional Response_GetAdminNotes ext = 1018; + } + optional string user_name = 1; + optional string notes = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_get_games_of_user.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_get_games_of_user.proto new file mode 100644 index 000000000..dd0ddd160 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_get_games_of_user.proto @@ -0,0 +1,12 @@ +syntax = "proto2"; +import "response.proto"; +import "serverinfo_game.proto"; +import "serverinfo_room.proto"; + +message Response_GetGamesOfUser { + extend Response { + optional Response_GetGamesOfUser ext = 1002; + } + repeated ServerInfo_Room room_list = 1; + repeated ServerInfo_Game game_list = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_get_user_info.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_get_user_info.proto new file mode 100644 index 000000000..0332a07ad --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_get_user_info.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "response.proto"; +import "serverinfo_user.proto"; + +message Response_GetUserInfo { + extend Response { + optional Response_GetUserInfo ext = 1003; + } + optional ServerInfo_User user_info = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_join_room.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_join_room.proto new file mode 100644 index 000000000..7023c648d --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_join_room.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "response.proto"; +import "serverinfo_room.proto"; + +message Response_JoinRoom { + extend Response { + optional Response_JoinRoom ext = 1000; + } + optional ServerInfo_Room room_info = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_list_users.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_list_users.proto new file mode 100644 index 000000000..d653521dd --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_list_users.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "response.proto"; +import "serverinfo_user.proto"; + +message Response_ListUsers { + extend Response { + optional Response_ListUsers ext = 1001; + } + repeated ServerInfo_User user_list = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_login.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_login.proto new file mode 100644 index 000000000..673eaa46f --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_login.proto @@ -0,0 +1,15 @@ +syntax = "proto2"; +import "response.proto"; +import "serverinfo_user.proto"; + +message Response_Login { + extend Response { + optional Response_Login ext = 1005; + } + optional ServerInfo_User user_info = 1; + repeated ServerInfo_User buddy_list = 2; + repeated ServerInfo_User ignore_list = 3; + optional string denied_reason_str = 4; + optional uint64 denied_end_time = 5; + repeated string missing_features = 6; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_password_salt.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_password_salt.proto new file mode 100644 index 000000000..3fc228530 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_password_salt.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "response.proto"; + +message Response_PasswordSalt { + extend Response { + optional Response_PasswordSalt ext = 1017; + } + optional string password_salt = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_register.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_register.proto new file mode 100644 index 000000000..1137b0b54 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_register.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "response.proto"; + +message Response_Register { + extend Response { + optional Response_Register ext = 1009; + } + optional string denied_reason_str = 1; + optional uint64 denied_end_time = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_replay_download.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_replay_download.proto new file mode 100644 index 000000000..1805280c1 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_replay_download.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "response.proto"; + +message Response_ReplayDownload { + extend Response { + optional Response_ReplayDownload ext = 1101; + } + optional bytes replay_data = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_replay_get_code.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_replay_get_code.proto new file mode 100644 index 000000000..5bb1b511a --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_replay_get_code.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "response.proto"; + +message Response_ReplayGetCode { + extend Response { + optional Response_ReplayGetCode ext = 1102; + } + optional string replay_code = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_replay_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_replay_list.proto new file mode 100644 index 000000000..68e7d41bc --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_replay_list.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "response.proto"; +import "serverinfo_replay_match.proto"; + +message Response_ReplayList { + extend Response { + optional Response_ReplayList ext = 1100; + } + repeated ServerInfo_ReplayMatch match_list = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_viewlog_history.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_viewlog_history.proto new file mode 100644 index 000000000..2ec0aff28 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_viewlog_history.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "response.proto"; +import "serverinfo_chat_message.proto"; + +message Response_ViewLogHistory { + extend Response { + optional Response_ViewLogHistory ext = 1015; + } + repeated ServerInfo_ChatMessage log_message = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_warn_history.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_warn_history.proto new file mode 100644 index 000000000..8108ececf --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_warn_history.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "response.proto"; +import "serverinfo_warning.proto"; + +message Response_WarnHistory { + extend Response { + optional Response_WarnHistory ext = 1013; + } + repeated ServerInfo_Warning warn_list = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_warn_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_warn_list.proto new file mode 100644 index 000000000..d48352529 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_warn_list.proto @@ -0,0 +1,11 @@ +syntax = "proto2"; +import "response.proto"; + +message Response_WarnList { + extend Response { + optional Response_WarnList ext = 1014; + } + repeated string warning = 1; + optional string user_name = 2; + optional string user_clientid = 3; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto new file mode 100644 index 000000000..a8c90ec6c --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto @@ -0,0 +1,83 @@ +syntax = "proto2"; +message RoomCommand { + enum RoomCommandType { + LEAVE_ROOM = 1000; + ROOM_SAY = 1001; + CREATE_GAME = 1002; + JOIN_GAME = 1003; + } + extensions 100 to max; +} + +message Command_LeaveRoom { + extend RoomCommand { + optional Command_LeaveRoom ext = 1000; + } +} + +message Command_RoomSay { + extend RoomCommand { + optional Command_RoomSay ext = 1001; + } + optional string message = 1; +} + +// Create a new game in the room +message Command_CreateGame { + extend RoomCommand { + optional Command_CreateGame ext = 1002; + } + + // game description shown in game list + optional string description = 1; + + // password users will have to provide to join the game + optional string password = 2; + + // amount of players needed to play + optional uint32 max_players = 3; + + // limits the game to only allowing users on the creator's buddy list to join + optional bool only_buddies = 4; + + // limits the game to only allowing registered users + optional bool only_registered = 5; + + // allows non players to view the game as spectator + optional bool spectators_allowed = 6; + + // allows spectators to join without password if false + optional bool spectators_need_password = 7; + + // allows spectators to use game say commands + optional bool spectators_can_talk = 8; + + // allows spectators to see hands and hidden information + optional bool spectators_see_everything = 9; + + // selection of game types as presented in the server's room configuration + repeated uint32 game_type_ids = 10; + + // the creator of the game will join it as a judge + optional bool join_as_judge = 11; + + // the creator of the game will join it as a spectator + optional bool join_as_spectator = 12; + + // set the starting life total + optional uint32 starting_life_total = 13; + + // share decklists with all players when selected + optional bool share_decklists_on_load = 14; +} + +message Command_JoinGame { + extend RoomCommand { + optional Command_JoinGame ext = 1003; + } + optional sint32 game_id = 1 [default = -1]; + optional string password = 2; + optional bool spectator = 3; + optional bool override_restrictions = 4; + optional bool join_as_judge = 5; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/room_event.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/room_event.proto new file mode 100644 index 000000000..9fa70e7e1 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/room_event.proto @@ -0,0 +1,12 @@ +syntax = "proto2"; +message RoomEvent { + enum RoomEventType { + LEAVE_ROOM = 1000; + JOIN_ROOM = 1001; + ROOM_SAY = 1002; + LIST_GAMES = 1003; + REMOVE_MESSAGES = 1004; + } + optional sint32 room_id = 1; + extensions 100 to max; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/server_message.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/server_message.proto new file mode 100644 index 000000000..50bbfb0fd --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/server_message.proto @@ -0,0 +1,20 @@ +syntax = "proto2"; +import "response.proto"; +import "session_event.proto"; +import "game_event_container.proto"; +import "room_event.proto"; + +message ServerMessage { + enum MessageType { + RESPONSE = 0; + SESSION_EVENT = 1; + GAME_EVENT_CONTAINER = 2; + ROOM_EVENT = 3; + } + optional MessageType message_type = 1; + + optional Response response = 2; + optional SessionEvent session_event = 3; + optional GameEventContainer game_event_container = 4; + optional RoomEvent room_event = 5; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_arrow.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_arrow.proto new file mode 100644 index 000000000..cd99bd798 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_arrow.proto @@ -0,0 +1,13 @@ +syntax = "proto2"; +import "color.proto"; + +message ServerInfo_Arrow { + optional sint32 id = 1; + optional sint32 start_player_id = 2; + optional string start_zone = 3; + optional sint32 start_card_id = 4; + optional sint32 target_player_id = 5; + optional string target_zone = 6; + optional sint32 target_card_id = 7; + optional color arrow_color = 8; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_ban.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_ban.proto new file mode 100644 index 000000000..241fc3b50 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_ban.proto @@ -0,0 +1,12 @@ +syntax = "proto2"; +/* + * Historical ban information stored in the ban table + */ +message ServerInfo_Ban { + required string admin_id = 1; // id of the staff member placing the ban + required string admin_name = 2; // name of the staff member placing the ban + required string ban_time = 3; // start time of the ban + required string ban_length = 4; // amount of time in minutes the ban is for + optional string ban_reason = 5; // reason seen only by moderation staff + optional string visible_reason = 6; // reason shown to the user +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_card.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_card.proto new file mode 100644 index 000000000..a9a8b5c9d --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_card.proto @@ -0,0 +1,56 @@ +syntax = "proto2"; +import "serverinfo_cardcounter.proto"; + +// Container for all the properties of a single card +message ServerInfo_Card { + // unique card id in this zone + optional sint32 id = 1 [default = -1]; + + // name of this kind of card + optional string name = 2; + + // x coordinate in zone + optional sint32 x = 3 [default = -1]; + + // y coordinate in zone + optional sint32 y = 4 [default = -1]; + + // if the card is face_down, hiding its information + optional bool face_down = 5; + + // if the card is tapped, turned sideways + optional bool tapped = 6; + + // if the card is marked as attacking + optional bool attacking = 7; + + // the card's color + optional string color = 8; + + // the power/toughness field, displayed on the bottom right + optional string pt = 9; + + // an optional string placed over the card + optional string annotation = 10; + + // whether the card should be deleted when removed from the table, like a token + optional bool destroy_on_zone_change = 11; + + // whether the card should not be untapped when the untap command is sent + optional bool doesnt_untap = 12; + + // list of counters on the card + repeated ServerInfo_CardCounter counter_list = 13; + + // the player that owns the card this is attached to + optional sint32 attach_player_id = 14 [default = -1]; + + // the zone of the card this is attached to + optional string attach_zone = 15; + + // the unique id of the card in that zone + optional sint32 attach_card_id = 16 [default = -1]; + + // unique id of this kind of card, extends the name to specify a specific printing of a card + optional string provider_id = 17; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_cardcounter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_cardcounter.proto new file mode 100644 index 000000000..d1799a65b --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_cardcounter.proto @@ -0,0 +1,5 @@ +syntax = "proto2"; +message ServerInfo_CardCounter { + optional sint32 id = 1; + optional sint32 value = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_chat_message.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_chat_message.proto new file mode 100644 index 000000000..d47ec4fc7 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_chat_message.proto @@ -0,0 +1,16 @@ +syntax = "proto2"; +/* + * Chat communication of a user to a target. + * Targets can be users or rooms. + * These communications are also stored in the DB log table. + */ +message ServerInfo_ChatMessage { + optional string time = 1; // time chat was sent + optional string sender_id = 2; // id of sender + optional string sender_name = 3; // name of sender + optional string sender_ip = 4; // ip of sender + optional string message = 5; // message + optional string target_type = 6; // target type (room,game,chat) + optional string target_id = 7; // id of target + optional string target_name = 8; // name of target +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_counter.proto new file mode 100644 index 000000000..849e3b4e9 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_counter.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "color.proto"; + +message ServerInfo_Counter { + optional sint32 id = 1; + optional string name = 2; + optional color counter_color = 3; + optional sint32 radius = 4; + optional sint32 count = 5; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deckstorage.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deckstorage.proto new file mode 100644 index 000000000..16e3f28e3 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deckstorage.proto @@ -0,0 +1,15 @@ +syntax = "proto2"; +message ServerInfo_DeckStorage_File { + optional uint32 creation_time = 1; +} + +message ServerInfo_DeckStorage_Folder { + repeated ServerInfo_DeckStorage_TreeItem items = 1; +} + +message ServerInfo_DeckStorage_TreeItem { + optional uint32 id = 1; + optional string name = 2; + optional ServerInfo_DeckStorage_File file = 10; + optional ServerInfo_DeckStorage_Folder folder = 11; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto new file mode 100644 index 000000000..9a56e034c --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto @@ -0,0 +1,65 @@ +syntax = "proto2"; +import "serverinfo_user.proto"; + +// Container for information about a game in the room's game list +message ServerInfo_Game { + // id of server the game is on + optional sint32 server_id = 1 [default = -1]; + + // id of room the game is in + optional sint32 room_id = 2 [default = -1]; + + // unique id of the game inside the room + optional sint32 game_id = 3 [default = -1]; + + // user provided game description + optional string description = 4; + + // password required to join game + optional bool with_password = 5; + + // players required to play + optional uint32 max_players = 6; + + // mask of server defined game types + repeated sint32 game_types = 7; + + // user that created the game + optional ServerInfo_User creator_info = 8; + + // only buddies of the creator can join this game + optional bool only_buddies = 9; + + // only registered users can join this game + optional bool only_registered = 10; + + // if spectators are allowed to join + optional bool spectators_allowed = 11; + + // spectators need to enter the game + optional bool spectators_need_password = 12; + + // spectators can use cmdGameSay + optional bool spectators_can_chat = 13; + + // spectators receive private events for all players + optional bool spectators_omniscient = 14; + + // decklists are sent to all players when loaded + optional bool share_decklists_on_load = 15; + + // the current player count + optional uint32 player_count = 30; + + // the current spectator count + optional uint32 spectators_count = 31; + + // whether the game is currently ongoing + optional bool started = 50; + + // time that the game started at + optional uint32 start_time = 51; + + // whether the game is closed. Closed games are finished and can't be interacted with + optional bool closed = 52; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_gametype.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_gametype.proto new file mode 100644 index 000000000..b73841be4 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_gametype.proto @@ -0,0 +1,5 @@ +syntax = "proto2"; +message ServerInfo_GameType { + optional sint32 game_type_id = 1; + optional string description = 2; +}; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_player.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_player.proto new file mode 100644 index 000000000..69cd4498a --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_player.proto @@ -0,0 +1,13 @@ +syntax = "proto2"; +import "serverinfo_zone.proto"; +import "serverinfo_counter.proto"; +import "serverinfo_arrow.proto"; +import "serverinfo_playerproperties.proto"; + +message ServerInfo_Player { + optional ServerInfo_PlayerProperties properties = 1; + optional string deck_list = 2; + repeated ServerInfo_Zone zone_list = 3; + repeated ServerInfo_Counter counter_list = 4; + repeated ServerInfo_Arrow arrow_list = 5; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_playerping.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_playerping.proto new file mode 100644 index 000000000..cb27e0e9b --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_playerping.proto @@ -0,0 +1,5 @@ +syntax = "proto2"; +message ServerInfo_PlayerPing { + optional sint32 player_id = 1; + optional sint32 ping_time = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_playerproperties.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_playerproperties.proto new file mode 100644 index 000000000..cdd0ee42c --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_playerproperties.proto @@ -0,0 +1,14 @@ +syntax = "proto2"; +import "serverinfo_user.proto"; + +message ServerInfo_PlayerProperties { + optional sint32 player_id = 1; + optional ServerInfo_User user_info = 2; + optional bool spectator = 3; + optional bool conceded = 4; + optional bool ready_start = 5; + optional string deck_hash = 6; + optional sint32 ping_seconds = 7; + optional bool sideboard_locked = 8; + optional bool judge = 9; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_replay.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_replay.proto new file mode 100644 index 000000000..4b15db18a --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_replay.proto @@ -0,0 +1,6 @@ +syntax = "proto2"; +message ServerInfo_Replay { + optional sint32 replay_id = 1 [default = -1]; + optional string replay_name = 2; + optional uint32 duration = 3; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_replay_match.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_replay_match.proto new file mode 100644 index 000000000..7fdc6471b --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_replay_match.proto @@ -0,0 +1,14 @@ +syntax = "proto2"; +import "serverinfo_replay.proto"; + +message ServerInfo_ReplayMatch { + repeated ServerInfo_Replay replay_list = 1; + + optional sint32 game_id = 2 [default = -1]; + optional string room_name = 3; + optional uint32 time_started = 4; + optional uint32 length = 5; + optional string game_name = 6; + repeated string player_names = 7; + optional bool do_not_hide = 8; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_room.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_room.proto new file mode 100644 index 000000000..c1c2e363b --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_room.proto @@ -0,0 +1,18 @@ +syntax = "proto2"; +import "serverinfo_game.proto"; +import "serverinfo_user.proto"; +import "serverinfo_gametype.proto"; + +message ServerInfo_Room { + optional sint32 room_id = 1; + optional string name = 2; + optional string description = 3; + optional uint32 game_count = 4; + optional uint32 player_count = 5; + optional bool auto_join = 6; + repeated ServerInfo_Game game_list = 7; + repeated ServerInfo_User user_list = 8; + repeated ServerInfo_GameType gametype_list = 9; + optional string permissionlevel = 10; + optional string privilegelevel = 11; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto new file mode 100644 index 000000000..10add611f --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto @@ -0,0 +1,31 @@ +syntax = "proto2"; +message ServerInfo_User { + enum UserLevelFlag { + IsNothing = 0; + IsUser = 1; + IsRegistered = 2; + IsModerator = 4; + IsAdmin = 8; + IsJudge = 16; + }; + message PawnColorsOverride { + optional string left_side = 1; + optional string right_side = 2; + }; + + optional string name = 1; + optional uint32 user_level = 2; + optional string address = 3; + optional string real_name = 4; + // gender = 5; // obsolete + optional string country = 6; + optional bytes avatar_bmp = 7; + optional sint32 id = 8 [default = -1]; + optional sint32 server_id = 9 [default = -1]; + optional uint64 session_id = 10; + optional uint64 accountage_secs = 11; + optional string email = 12; + optional string clientid = 13; + optional string privlevel = 14; + optional PawnColorsOverride pawn_colors = 15; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_warning.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_warning.proto new file mode 100644 index 000000000..20287be06 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_warning.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +/* + * Historical warning information stored in the warnings table + */ +message ServerInfo_Warning { + optional string user_name = 1; // name of user being warned + optional string admin_name = 2; // name of the moderator making the warning + optional string reason = 3; // type of warning being placed + optional string time_of = 4; // time of warning +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_zone.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_zone.proto new file mode 100644 index 000000000..f0ad5d709 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_zone.proto @@ -0,0 +1,32 @@ +syntax = "proto2"; +import "serverinfo_card.proto"; + +message ServerInfo_Zone { + enum ZoneType { + // PrivateZone: Contents of the zone are always visible to the owner, + // but not to anyone else. + // PublicZone: Contents of the zone are always visible to anyone. + // HiddenZone: Contents of the zone are never visible to anyone. + // However, the owner of the zone can issue a dump_zone command, + // setting beingLookedAt to true. + // Cards in a zone with the type HiddenZone are referenced by their + // list index, whereas cards in any other zone are referenced by their ids. + // + // WARNING: Adding new zone types will break compatibility with older + // clients. Older clients will read new zone types as PrivateZone, which + // is likely *NOT* what you want. + + PrivateZone = 0; + PublicZone = 1; + HiddenZone = 2; + } + optional string name = 1; + optional ZoneType type = 2; + optional bool with_coords = 3; + optional sint32 card_count = 4; + repeated ServerInfo_Card card_list = 5; + // Reveal top card to all players. + optional bool always_reveal_top_card = 10; + // reveal top card to the owner. + optional bool always_look_at_top_card = 11; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto new file mode 100644 index 000000000..cecf87370 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto @@ -0,0 +1,207 @@ +syntax = "proto2"; + +message SessionCommand { + enum SessionCommandType { + PING = 1000; + LOGIN = 1001; + MESSAGE = 1002; + LIST_USERS = 1003; + GET_GAMES_OF_USER = 1004; + GET_USER_INFO = 1005; + ADD_TO_LIST = 1006; + REMOVE_FROM_LIST = 1007; + DECK_LIST = 1008; + DECK_NEW_DIR = 1009; + DECK_DEL_DIR = 1010; + DECK_DEL = 1011; + DECK_DOWNLOAD = 1012; + DECK_UPLOAD = 1013; + LIST_ROOMS = 1014; + JOIN_ROOM = 1015; + REGISTER = 1016; + ACTIVATE = 1017; + ACCOUNT_EDIT = 1018; + ACCOUNT_IMAGE = 1019; + ACCOUNT_PASSWORD = 1020; + FORGOT_PASSWORD_REQUEST = 1021; + FORGOT_PASSWORD_RESET = 1022; + FORGOT_PASSWORD_CHALLENGE = 1023; + REQUEST_PASSWORD_SALT = 1024; + REPLAY_LIST = 1100; + REPLAY_DOWNLOAD = 1101; + REPLAY_MODIFY_MATCH = 1102; + REPLAY_DELETE_MATCH = 1103; + REPLAY_GET_CODE = 1104; + REPLAY_SUBMIT_CODE = 1105; + } + extensions 100 to max; +} + +message Command_Ping { + extend SessionCommand { + optional Command_Ping ext = 1000; + } +} + +message Command_Login { + extend SessionCommand { + optional Command_Login ext = 1001; + } + optional string user_name = 1; + optional string password = 2; + optional string clientid = 3; + optional string clientver = 4; + repeated string clientfeatures = 5; + optional string hashed_password = 6; +} + +message Command_Message { + extend SessionCommand { + optional Command_Message ext = 1002; + } + optional string user_name = 1; + optional string message = 2; +} + +message Command_ListUsers { + extend SessionCommand { + optional Command_ListUsers ext = 1003; + } +} + +message Command_GetGamesOfUser { + extend SessionCommand { + optional Command_GetGamesOfUser ext = 1004; + } + optional string user_name = 1; +} + +message Command_GetUserInfo { + extend SessionCommand { + optional Command_GetUserInfo ext = 1005; + } + optional string user_name = 1; +} + +message Command_AddToList { + extend SessionCommand { + optional Command_AddToList ext = 1006; + } + optional string list = 1; + optional string user_name = 2; +} + +message Command_RemoveFromList { + extend SessionCommand { + optional Command_RemoveFromList ext = 1007; + } + optional string list = 1; + optional string user_name = 2; +} + +message Command_ListRooms { + extend SessionCommand { + optional Command_ListRooms ext = 1014; + } +} + +message Command_JoinRoom { + extend SessionCommand { + optional Command_JoinRoom ext = 1015; + } + optional uint32 room_id = 1; +} + +// User wants to register a new account +message Command_Register { + extend SessionCommand { + optional Command_Register ext = 1016; + } + // User name client wants to register + required string user_name = 1; + // Hashed password to be inserted into database + optional string password = 2; + // Email address of the client for user validation + optional string email = 3; + // gender = 4; // obsolete + // Country code of the user. 2 letter ISO format + optional string country = 5; + optional string real_name = 6; + optional string clientid = 7; + optional string hashed_password = 8; +} + +// User wants to activate an account +message Command_Activate { + extend SessionCommand { + optional Command_Activate ext = 1017; + } + // User name client wants to activate + required string user_name = 1; + // Activation token + required string token = 2; + optional string clientid = 3; +} + +message Command_AccountEdit { + extend SessionCommand { + optional Command_AccountEdit ext = 1018; + } + optional string real_name = 1; + optional string email = 2; + // gender = 3; // obsolete + optional string country = 4; + optional string password_check = 100; // password is required to change sensitive information +} + +message Command_AccountImage { + extend SessionCommand { + optional Command_AccountImage ext = 1019; + } + optional bytes image = 1; +} + +message Command_AccountPassword { + extend SessionCommand { + optional Command_AccountPassword ext = 1020; + } + optional string old_password = 1; + optional string new_password = 2; + // optional string hashed_old_password = 3; // we don't want users to steal hashed passwords and change them + optional string hashed_new_password = 4; +} + +message Command_ForgotPasswordRequest { + extend SessionCommand { + optional Command_ForgotPasswordRequest ext = 1021; + } + required string user_name = 1; + optional string clientid = 2; +} + +message Command_ForgotPasswordReset { + extend SessionCommand { + optional Command_ForgotPasswordReset ext = 1022; + } + required string user_name = 1; + optional string clientid = 2; + optional string token = 3; + optional string new_password = 4; + optional string hashed_new_password = 5; +} + +message Command_ForgotPasswordChallenge { + extend SessionCommand { + optional Command_ForgotPasswordChallenge ext = 1023; + } + required string user_name = 1; + optional string clientid = 2; + optional string email = 3; +} + +message Command_RequestPasswordSalt { + extend SessionCommand { + optional Command_RequestPasswordSalt ext = 1024; + } + required string user_name = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/session_event.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/session_event.proto new file mode 100644 index 000000000..3cce332c0 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/session_event.proto @@ -0,0 +1,20 @@ +syntax = "proto2"; +message SessionEvent { + enum SessionEventType { + SERVER_IDENTIFICATION = 500; + SERVER_COMPLETE_LIST = 600; + SERVER_MESSAGE = 1000; + SERVER_SHUTDOWN = 1001; + CONNECTION_CLOSED = 1002; + USER_MESSAGE = 1003; + LIST_ROOMS = 1004; + ADD_TO_LIST = 1005; + REMOVE_FROM_LIST = 1006; + USER_JOINED = 1007; + USER_LEFT = 1008; + GAME_JOINED = 1009; + NOTIFY_USER = 1010; + REPLAY_ADDED = 1100; + } + extensions 100 to max; +} diff --git a/cockatrice/src/pending_command.cpp b/libcockatrice_protocol/libcockatrice/protocol/pending_command.cpp similarity index 68% rename from cockatrice/src/pending_command.cpp rename to libcockatrice_protocol/libcockatrice/protocol/pending_command.cpp index 0ebb1d82a..4a9943d33 100644 --- a/cockatrice/src/pending_command.cpp +++ b/libcockatrice_protocol/libcockatrice/protocol/pending_command.cpp @@ -3,26 +3,26 @@ PendingCommand::PendingCommand(const CommandContainer &_commandContainer, QVariant _extraData) : commandContainer(_commandContainer), extraData(_extraData), ticks(0) { - } -CommandContainer & PendingCommand::getCommandContainer() +CommandContainer &PendingCommand::getCommandContainer() { return commandContainer; } -void PendingCommand::setExtraData(const QVariant &_extraData) { +void PendingCommand::setExtraData(const QVariant &_extraData) +{ extraData = _extraData; } -QVariant PendingCommand::getExtraData() const { +QVariant PendingCommand::getExtraData() const +{ return extraData; } void PendingCommand::processResponse(const Response &response) { emit finished(response, commandContainer, extraData); - emit finished(response.response_code()); } int PendingCommand::tick() diff --git a/cockatrice/src/pending_command.h b/libcockatrice_protocol/libcockatrice/protocol/pending_command.h similarity index 59% rename from cockatrice/src/pending_command.h rename to libcockatrice_protocol/libcockatrice/protocol/pending_command.h index 4ca9035ba..1d2d9ff17 100644 --- a/cockatrice/src/pending_command.h +++ b/libcockatrice_protocol/libcockatrice/protocol/pending_command.h @@ -1,21 +1,29 @@ +/** + * @file pending_command.h + * @ingroup Messages + * @brief TODO: Document this. + */ + #ifndef PENDING_COMMAND_H #define PENDING_COMMAND_H -#include "pb/commands.pb.h" -#include "pb/response.pb.h" #include +#include +#include -class PendingCommand : public QObject { +class PendingCommand : public QObject +{ Q_OBJECT signals: void finished(const Response &response, const CommandContainer &commandContainer, const QVariant &extraData); - void finished(Response::ResponseCode respCode); + private: CommandContainer commandContainer; QVariant extraData; int ticks; + public: - PendingCommand(const CommandContainer &_commandContainer, QVariant _extraData = QVariant()); + explicit PendingCommand(const CommandContainer &_commandContainer, QVariant _extraData = QVariant()); CommandContainer &getCommandContainer(); void setExtraData(const QVariant &_extraData); QVariant getExtraData() const; @@ -23,4 +31,4 @@ public: int tick(); }; -#endif +#endif diff --git a/libcockatrice_rng/CMakeLists.txt b/libcockatrice_rng/CMakeLists.txt new file mode 100644 index 000000000..6ff2a4537 --- /dev/null +++ b/libcockatrice_rng/CMakeLists.txt @@ -0,0 +1,20 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS libcockatrice/rng/rng_abstract.h libcockatrice/rng/rng_sfmt.h libcockatrice/rng/sfmt/SFMT.h) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library( + libcockatrice_rng STATIC ${MOC_SOURCES} libcockatrice/rng/rng_abstract.cpp libcockatrice/rng/rng_sfmt.cpp + libcockatrice/rng/sfmt/SFMT.c +) + +target_include_directories(libcockatrice_rng PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(libcockatrice_rng PUBLIC ${QT_CORE_MODULE}) diff --git a/libcockatrice_rng/libcockatrice/rng/rng_abstract.cpp b/libcockatrice_rng/libcockatrice/rng/rng_abstract.cpp new file mode 100644 index 000000000..63072b988 --- /dev/null +++ b/libcockatrice_rng/libcockatrice/rng/rng_abstract.cpp @@ -0,0 +1,30 @@ +#include "rng_abstract.h" + +#include + +QVector RNG_Abstract::makeNumbersVector(int n, int min, int max) +{ + const int bins = max - min + 1; + QVector result(bins); + for (int i = 0; i < n; ++i) { + int number = rand(min, max); + if ((number < min) || (number > max)) + qDebug() << "rand(" << min << "," << max << ") returned " << number; + else + result[number - min]++; + } + return result; +} + +double RNG_Abstract::testRandom(const QVector &numbers) const +{ + int n = 0; + for (int i = 0; i < numbers.size(); ++i) + n += numbers[i]; + double expected = (double)n / (double)numbers.size(); + double chisq = 0; + for (int i = 0; i < numbers.size(); ++i) + chisq += ((double)numbers[i] - expected) * ((double)numbers[i] - expected) / expected; + + return chisq; +} diff --git a/libcockatrice_rng/libcockatrice/rng/rng_abstract.h b/libcockatrice_rng/libcockatrice/rng/rng_abstract.h new file mode 100644 index 000000000..903e6ef1a --- /dev/null +++ b/libcockatrice_rng/libcockatrice/rng/rng_abstract.h @@ -0,0 +1,21 @@ +#ifndef RNG_ABSTRACT_H +#define RNG_ABSTRACT_H + +#include +#include + +class RNG_Abstract : public QObject +{ + Q_OBJECT +public: + explicit RNG_Abstract(QObject *parent = nullptr) : QObject(parent) + { + } + virtual unsigned int rand(int min, int max) = 0; + QVector makeNumbersVector(int n, int min, int max); + [[nodiscard]] double testRandom(const QVector &numbers) const; +}; + +extern RNG_Abstract *rng; + +#endif diff --git a/libcockatrice_rng/libcockatrice/rng/rng_sfmt.cpp b/libcockatrice_rng/libcockatrice/rng/rng_sfmt.cpp new file mode 100644 index 000000000..5a6d8c862 --- /dev/null +++ b/libcockatrice_rng/libcockatrice/rng/rng_sfmt.cpp @@ -0,0 +1,137 @@ +#include "rng_sfmt.h" + +#include +#include +#include +#include + +// This is from gcc sources, namely from fixincludes/inclhack.def +// On C++11 systems, could be included instead. +#ifndef UINT64_MAX +#define UINT64_MAX (~(uint64_t)0) +#endif + +RNG_SFMT::RNG_SFMT(QObject *parent) : RNG_Abstract(parent) +{ + // initialize the random number generator with a 32bit integer seed (timestamp) + sfmt_init_gen_rand(&sfmt, QDateTime::currentDateTime().toSecsSinceEpoch()); +} + +/** + * This method is the rand() equivalent which calls the cdf with proper bounds. + * + * It is possible to generate random numbers from [-min, +/-max] though the RNG uses + * unsigned numbers only, so this wrapper handles some special cases for min and max. + * + * It is only necessary that the upper bound is larger or equal to the lower bound - with the exception + * that someone wants something like rand() % -foo. + */ +unsigned int RNG_SFMT::rand(int min, int max) +{ + /* If min is negative, it would be possible to calculate + * cdf(0, max - min) + min + * There has been no use for negative random numbers with rand() though, so it's treated as error. + */ + if (min < 0) { + throw std::invalid_argument( + QString("Invalid bounds for RNG: Got min " + QString::number(min) + " < 0!\n").toStdString()); + // at this point, the method exits. No return value is needed, because + // basically the exception itself is returned. + } + + // For complete fairness and equal timing, this should be a roll, but let's skip it anyway + if (min == max) + return max; + + // This is actually not used in Cockatrice: + // Someone wants rand() % -foo, so we should compute -rand(0, +foo) + // But this method returns an unsigned int, so it doesn't really make + // a difference. + // This is the only time when min > max is (sort of) legal. + // Not handling this will cause the application to crash. + if (min == 0 && max < 0) { + return cdf(0, -max); + } + + // No special cases are left, except !(min > max) which is caught in the cdf itself. + return cdf(min, max); +} + +/** + * Much thought went into this, please read this comment before you modify the code. + * Let SFMT() be an alias for sfmt_genrand_uint64() aka SFMT's rand() function. + * + * SMFT() returns a uniformly distributed pseudorandom number from 0 to UINT64_MAX. + * As SFMT() operates on a limited integer range, it is a _discrete_ function. + * + * We want a random number from a given interval [min, max] though, so we need to + * implement the (discrete) cumulative distribution function SFMT(min, max), which + * returns a random number X from [min, max]. + * + * This CDF is by formal definition: + * SFMT(X; min, max) = (floor(X) - min + 1) / (max - min + 1) + * + * To get out the random variable, solve for X: + * floor(X) = SFMT(X; min, max) * (max - min + 1) + min - 1 + * So this is, what rand(min, max) should look like. + * Problem: SFMT(X; min, max) * (max - min + 1) could produce an integer overflow, + * so it is not safe. + * + * One solution is to divide the universe into buckets of equal size depending on the + * range [min, max] and assign X to the bucket that contains the number generated + * by SFMT(). This equals to modulo computation and is not satisfying: + * If the buckets don't divide the universe equally, because the bucket size is not + * a divisor of 2, there will be a range in the universe that is biased because one + * bucket is too small thus will be chosen less equally! + * + * This is solved by rejection sampling: + * As SFMT() is assumed to be unbiased, we are allowed to ignore those random numbers + * from SFMT() that would force us to have an unequal bucket and generate new random + * numbers until one number fits into one of the other buckets. + * This can be compared to an ideal six sided die that is rolled until only sides + * 1-5 show up, while 6 represents something that you don't want. So you basically roll + * a five sided die. + * + * Note: If you replace the SFMT RNG with some other rand() function in the future, + * then you _need_ to change the UINT64_MAX constant to the largest possible random + * number which can be created by the new rand() function. This value is often defined + * in a RAND_MAX constant. + * Otherwise you will probably skew the outcome of the rand() method or worsen the + * performance of the application. + */ +unsigned int RNG_SFMT::cdf(unsigned int min, unsigned int max) +{ + // This all makes no sense if min > max, which should never happen. + if (min > max) { + throw std::invalid_argument(QString("Invalid bounds for RNG: min > max! Values were: min = " + + QString::number(min) + ", max = " + QString::number(max)) + .toStdString()); + // at this point, the method exits. No return value is needed, because + // basically the exception itself is returned. + } + + // First compute the diameter (aka size, length) of the [min, max] interval + const unsigned int diameter = max - min + 1; + + // Compute how many buckets (each in size of the diameter) will fit into the + // universe. + // If the division has a remainder, the result is floored automatically. + const uint64_t buckets = UINT64_MAX / diameter; + + // Compute the last valid random number. All numbers beyond have to be ignored. + // If there was no remainder in the previous step, limit is equal to UINT64_MAX. + const uint64_t limit = diameter * buckets; + + uint64_t rand; + // To make the random number generation thread-safe, a mutex is created around + // the generation. Outside of the loop of course, to avoid lock/unlock overhead. + mutex.lock(); + do { + rand = sfmt_genrand_uint64(&sfmt); + } while (rand >= limit); + mutex.unlock(); + + // Now determine the bucket containing the SFMT() random number and after adding + // the lower bound, a random number from [min, max] can be returned. + return (unsigned int)(rand / buckets + min); +} diff --git a/common/rng_sfmt.h b/libcockatrice_rng/libcockatrice/rng/rng_sfmt.h similarity index 77% rename from common/rng_sfmt.h rename to libcockatrice_rng/libcockatrice/rng/rng_sfmt.h index d35ce97df..7e9f53df3 100644 --- a/common/rng_sfmt.h +++ b/libcockatrice_rng/libcockatrice/rng/rng_sfmt.h @@ -1,10 +1,11 @@ #ifndef RNG_SFMT_H #define RNG_SFMT_H -#include -#include -#include "sfmt/SFMT.h" #include "rng_abstract.h" +#include "sfmt/SFMT.h" + +#include +#include /** * This class encapsulates a state of the art PRNG and can be used @@ -25,17 +26,18 @@ * Edition Volume 2 / Seminumerical Algorithms". */ -class RNG_SFMT : public RNG_Abstract { - Q_OBJECT +class RNG_SFMT : public RNG_Abstract +{ + Q_OBJECT private: - QMutex mutex; - sfmt_t sfmt; - // The discrete cumulative distribution function for the RNG - unsigned int cdf(unsigned int min, unsigned int max); + QMutex mutex; + sfmt_t sfmt; + // The discrete cumulative distribution function for the RNG + unsigned int cdf(unsigned int min, unsigned int max); + public: - RNG_SFMT(QObject *parent = 0); - unsigned int rand(int min, int max); + explicit RNG_SFMT(QObject *parent = nullptr); + unsigned int rand(int min, int max) override; }; #endif - diff --git a/common/sfmt/LICENSE.txt b/libcockatrice_rng/libcockatrice/rng/sfmt/LICENSE.txt similarity index 100% rename from common/sfmt/LICENSE.txt rename to libcockatrice_rng/libcockatrice/rng/sfmt/LICENSE.txt diff --git a/common/sfmt/SFMT-common.h b/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT-common.h similarity index 90% rename from common/sfmt/SFMT-common.h rename to libcockatrice_rng/libcockatrice/rng/sfmt/SFMT-common.h index c7d8aa9fb..a5a9b0504 100644 --- a/common/sfmt/SFMT-common.h +++ b/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT-common.h @@ -28,7 +28,7 @@ extern "C" { #include "SFMT.h" inline static void do_recursion(w128_t * r, w128_t * a, w128_t * b, - w128_t * c, w128_t * d); + w128_t * c, w128_t * d); inline static void rshift128(w128_t *out, w128_t const *in, int shift); inline static void lshift128(w128_t *out, w128_t const *in, int shift); @@ -123,24 +123,24 @@ inline static void lshift128(w128_t *out, w128_t const *in, int shift) */ #ifdef ONLY64 inline static void do_recursion(w128_t *r, w128_t *a, w128_t *b, w128_t *c, - w128_t *d) { + w128_t *d) { w128_t x; w128_t y; lshift128(&x, a, SFMT_SL2); rshift128(&y, c, SFMT_SR2); r->u[0] = a->u[0] ^ x.u[0] ^ ((b->u[0] >> SFMT_SR1) & SFMT_MSK2) ^ y.u[0] - ^ (d->u[0] << SFMT_SL1); + ^ (d->u[0] << SFMT_SL1); r->u[1] = a->u[1] ^ x.u[1] ^ ((b->u[1] >> SFMT_SR1) & SFMT_MSK1) ^ y.u[1] - ^ (d->u[1] << SFMT_SL1); + ^ (d->u[1] << SFMT_SL1); r->u[2] = a->u[2] ^ x.u[2] ^ ((b->u[2] >> SFMT_SR1) & SFMT_MSK4) ^ y.u[2] - ^ (d->u[2] << SFMT_SL1); + ^ (d->u[2] << SFMT_SL1); r->u[3] = a->u[3] ^ x.u[3] ^ ((b->u[3] >> SFMT_SR1) & SFMT_MSK3) ^ y.u[3] - ^ (d->u[3] << SFMT_SL1); + ^ (d->u[3] << SFMT_SL1); } #else inline static void do_recursion(w128_t *r, w128_t *a, w128_t *b, - w128_t *c, w128_t *d) + w128_t *c, w128_t *d) { w128_t x; w128_t y; @@ -148,17 +148,17 @@ inline static void do_recursion(w128_t *r, w128_t *a, w128_t *b, lshift128(&x, a, SFMT_SL2); rshift128(&y, c, SFMT_SR2); r->u[0] = a->u[0] ^ x.u[0] ^ ((b->u[0] >> SFMT_SR1) & SFMT_MSK1) - ^ y.u[0] ^ (d->u[0] << SFMT_SL1); + ^ y.u[0] ^ (d->u[0] << SFMT_SL1); r->u[1] = a->u[1] ^ x.u[1] ^ ((b->u[1] >> SFMT_SR1) & SFMT_MSK2) - ^ y.u[1] ^ (d->u[1] << SFMT_SL1); + ^ y.u[1] ^ (d->u[1] << SFMT_SL1); r->u[2] = a->u[2] ^ x.u[2] ^ ((b->u[2] >> SFMT_SR1) & SFMT_MSK3) - ^ y.u[2] ^ (d->u[2] << SFMT_SL1); + ^ y.u[2] ^ (d->u[2] << SFMT_SL1); r->u[3] = a->u[3] ^ x.u[3] ^ ((b->u[3] >> SFMT_SR1) & SFMT_MSK4) - ^ y.u[3] ^ (d->u[3] << SFMT_SL1); + ^ y.u[3] ^ (d->u[3] << SFMT_SL1); } #endif -#endif - #if defined(__cplusplus) } #endif + +#endif // SFMT_COMMON_H diff --git a/common/sfmt/SFMT-params.h b/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT-params.h similarity index 91% rename from common/sfmt/SFMT-params.h rename to libcockatrice_rng/libcockatrice/rng/sfmt/SFMT-params.h index 372e6f11a..2fe663ab6 100644 --- a/common/sfmt/SFMT-params.h +++ b/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT-params.h @@ -46,8 +46,8 @@ */ /** the parameter of shift right as one 128-bit register. - * The 128-bit integer is shifted by (SFMT_SL2 * 8) bits. -#define SFMT_SR21 1 + * The 128-bit integer is shifted by (SFMT_SR2 * 8) bits. +#define SFMT_SR2 1 */ /** A bitmask, used in the recursion. These parameters are introduced @@ -59,10 +59,10 @@ */ /** These definitions are part of a 128-bit period certification vector. -#define SFMT_PARITY1 0x00000001U -#define SFMT_PARITY2 0x00000000U -#define SFMT_PARITY3 0x00000000U -#define SFMT_PARITY4 0xc98e126aU +#define SFMT_PARITY1 0x00000001U +#define SFMT_PARITY2 0x00000000U +#define SFMT_PARITY3 0x00000000U +#define SFMT_PARITY4 0xc98e126aU */ #if SFMT_MEXP == 607 diff --git a/common/sfmt/SFMT-params19937.h b/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT-params19937.h similarity index 100% rename from common/sfmt/SFMT-params19937.h rename to libcockatrice_rng/libcockatrice/rng/sfmt/SFMT-params19937.h diff --git a/common/sfmt/SFMT.c b/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT.c similarity index 96% rename from common/sfmt/SFMT.c rename to libcockatrice_rng/libcockatrice/rng/sfmt/SFMT.c index 2652df7de..b4ac9308b 100644 --- a/common/sfmt/SFMT.c +++ b/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT.c @@ -40,11 +40,6 @@ extern "C" { #undef ONLY64 #endif -/** - * parameters used by sse2. - */ -static const w128_t sse2_param_mask = {{SFMT_MSK1, SFMT_MSK2, - SFMT_MSK3, SFMT_MSK4}}; /*---------------- STATIC FUNCTIONS ----------------*/ @@ -60,11 +55,18 @@ inline static void swap(w128_t *array, int size); #if defined(HAVE_ALTIVEC) #include "SFMT-alti.h" #elif defined(HAVE_SSE2) +/** + * parameters used by sse2. + */ + static const w128_t sse2_param_mask = {{SFMT_MSK1, SFMT_MSK2, + SFMT_MSK3, SFMT_MSK4}}; #if defined(_MSC_VER) #include "SFMT-sse2-msc.h" #else #include "SFMT-sse2.h" #endif +#elif defined(HAVE_NEON) + #include "SFMT-neon.h" #endif /** @@ -81,7 +83,7 @@ inline static int idxof(int i) { } #endif -#if (!defined(HAVE_ALTIVEC)) && (!defined(HAVE_SSE2)) +#if (!defined(HAVE_ALTIVEC)) && (!defined(HAVE_SSE2)) && (!defined(HAVE_NEON)) /** * This function fills the user-specified array with pseudorandom * integers. @@ -166,17 +168,19 @@ static uint32_t func2(uint32_t x) { * @param sfmt SFMT internal state */ static void period_certification(sfmt_t * sfmt) { - int inner = 0; + uint32_t inner = 0; int i, j; uint32_t work; uint32_t *psfmt32 = &sfmt->state[0].u[0]; const uint32_t parity[4] = {SFMT_PARITY1, SFMT_PARITY2, SFMT_PARITY3, SFMT_PARITY4}; - for (i = 0; i < 4; i++) + for (i = 0; i < 4; i++) { inner ^= psfmt32[idxof(i)] & parity[i]; - for (i = 16; i > 0; i >>= 1) + } + for (i = 16; i > 0; i >>= 1) { inner ^= inner >> i; + } inner &= 1; /* check OK */ if (inner == 1) { @@ -232,7 +236,7 @@ int sfmt_get_min_array_size64(sfmt_t * sfmt) { return SFMT_N64; } -#if !defined(HAVE_SSE2) && !defined(HAVE_ALTIVEC) +#if !defined(HAVE_SSE2) && !defined(HAVE_ALTIVEC) && !defined(HAVE_NEON) /** * This function fills the internal state array with pseudorandom * integers. diff --git a/common/sfmt/SFMT.h b/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT.h similarity index 97% rename from common/sfmt/SFMT.h rename to libcockatrice_rng/libcockatrice/rng/sfmt/SFMT.h index dca308a00..79e012d63 100644 --- a/common/sfmt/SFMT.h +++ b/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT.h @@ -79,6 +79,15 @@ union W128_T { uint32_t u[4]; uint64_t u64[2]; }; +#elif defined(HAVE_NEON) + #include + +/** 128-bit data structure */ +union W128_T { + uint32_t u[4]; + uint64_t u64[2]; + uint32x4_t si; +}; #elif defined(HAVE_SSE2) #include @@ -247,7 +256,7 @@ inline static double sfmt_genrand_real3(sfmt_t * sfmt) */ inline static double sfmt_to_res53(uint64_t v) { - return v * (1.0/18446744073709551616.0); + return (v >> 11) * (1.0/9007199254740992.0); } /** diff --git a/libcockatrice_settings/CMakeLists.txt b/libcockatrice_settings/CMakeLists.txt new file mode 100644 index 000000000..3afe6e00a --- /dev/null +++ b/libcockatrice_settings/CMakeLists.txt @@ -0,0 +1,45 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS + libcockatrice/settings/card_database_settings.h + libcockatrice/settings/card_override_settings.h + libcockatrice/settings/debug_settings.h + libcockatrice/settings/download_settings.h + libcockatrice/settings/game_filters_settings.h + libcockatrice/settings/layouts_settings.h + libcockatrice/settings/message_settings.h + libcockatrice/settings/recents_settings.h + libcockatrice/settings/servers_settings.h + libcockatrice/settings/settings_manager.h +) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library( + libcockatrice_settings STATIC + ${MOC_SOURCES} + libcockatrice/settings/card_database_settings.cpp + libcockatrice/settings/card_override_settings.cpp + libcockatrice/settings/debug_settings.cpp + libcockatrice/settings/download_settings.cpp + libcockatrice/settings/game_filters_settings.cpp + libcockatrice/settings/layouts_settings.cpp + libcockatrice/settings/message_settings.cpp + libcockatrice/settings/recents_settings.cpp + libcockatrice/settings/servers_settings.cpp + libcockatrice/settings/settings_manager.cpp +) + +target_include_directories( + libcockatrice_settings + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} + PUBLIC ${CMAKE_SOURCE_DIR}/cockatrice/src/client/network +) + +target_link_libraries(libcockatrice_settings PUBLIC libcockatrice_card libcockatrice_utility ${QT_CORE_MODULE}) diff --git a/libcockatrice_settings/libcockatrice/settings/card_database_settings.cpp b/libcockatrice_settings/libcockatrice/settings/card_database_settings.cpp new file mode 100644 index 000000000..26a91a4dd --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/card_database_settings.cpp @@ -0,0 +1,36 @@ +#include "card_database_settings.h" + +CardDatabaseSettings::CardDatabaseSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "cardDatabase.ini", QString(), QString(), parent) +{ +} + +void CardDatabaseSettings::setSortKey(QString shortName, unsigned int sortKey) +{ + setValue(sortKey, "sortkey", "sets", std::move(shortName)); +} + +void CardDatabaseSettings::setEnabled(QString shortName, bool enabled) +{ + setValue(enabled, "enabled", "sets", std::move(shortName)); +} + +void CardDatabaseSettings::setIsKnown(QString shortName, bool isknown) +{ + setValue(isknown, "isknown", "sets", std::move(shortName)); +} + +unsigned int CardDatabaseSettings::getSortKey(QString shortName) const +{ + return getValue("sortkey", "sets", std::move(shortName)).toUInt(); +} + +bool CardDatabaseSettings::isEnabled(QString shortName) const +{ + return getValue("enabled", "sets", std::move(shortName)).toBool(); +} + +bool CardDatabaseSettings::isKnown(QString shortName) const +{ + return getValue("isknown", "sets", std::move(shortName)).toBool(); +} diff --git a/libcockatrice_settings/libcockatrice/settings/card_database_settings.h b/libcockatrice_settings/libcockatrice/settings/card_database_settings.h new file mode 100644 index 000000000..bb946ea80 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/card_database_settings.h @@ -0,0 +1,33 @@ +/** + * @file card_database_settings.h + * @ingroup CardDatabase + * @ingroup CardSettings + * @brief TODO: Document this. + */ + +#ifndef CARDDATABASESETTINGS_H +#define CARDDATABASESETTINGS_H + +#include "settings_manager.h" + +#include + +class CardDatabaseSettings : public SettingsManager, public ICardSetPriorityController +{ + Q_OBJECT + friend class SettingsCache; + +public: + void setSortKey(QString shortName, unsigned int sortKey) override; + void setEnabled(QString shortName, bool enabled) override; + void setIsKnown(QString shortName, bool isknown) override; + + unsigned int getSortKey(QString shortName) const override; + bool isEnabled(QString shortName) const override; + bool isKnown(QString shortName) const override; + +private: + explicit CardDatabaseSettings(const QString &settingPath, QObject *parent = nullptr); +}; + +#endif // CARDDATABASESETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/card_override_settings.cpp b/libcockatrice_settings/libcockatrice/settings/card_override_settings.cpp new file mode 100644 index 000000000..a61a4693b --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/card_override_settings.cpp @@ -0,0 +1,21 @@ +#include "card_override_settings.h" + +CardOverrideSettings::CardOverrideSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "cardPreferenceOverrides.ini", "cards", QString(), parent) +{ +} + +void CardOverrideSettings::setCardPreferenceOverride(const CardRef &cardRef) +{ + setValue(cardRef.providerId, cardRef.name); +} + +void CardOverrideSettings::deleteCardPreferenceOverride(const QString &cardName) +{ + deleteValue(cardName); +} + +QString CardOverrideSettings::getCardPreferenceOverride(const QString &cardName) const +{ + return getValue(cardName).toString(); +} \ No newline at end of file diff --git a/libcockatrice_settings/libcockatrice/settings/card_override_settings.h b/libcockatrice_settings/libcockatrice/settings/card_override_settings.h new file mode 100644 index 000000000..3d9db4e65 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/card_override_settings.h @@ -0,0 +1,32 @@ +/** + * @file card_override_settings.h + * @ingroup CardSettings + * @brief TODO: Document this. + */ + +#ifndef COCKATRICE_CARD_OVERRIDE_SETTINGS_H +#define COCKATRICE_CARD_OVERRIDE_SETTINGS_H + +#include "settings_manager.h" + +#include +#include + +class CardOverrideSettings : public SettingsManager +{ + Q_OBJECT + friend class SettingsCache; + +public: + void setCardPreferenceOverride(const CardRef &cardRef); + + void deleteCardPreferenceOverride(const QString &cardName); + + QString getCardPreferenceOverride(const QString &cardName) const; + +private: + explicit CardOverrideSettings(const QString &settingPath, QObject *parent = nullptr); + CardOverrideSettings(const CardOverrideSettings & /*other*/); +}; + +#endif // COCKATRICE_CARD_OVERRIDE_SETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/debug_settings.cpp b/libcockatrice_settings/libcockatrice/settings/debug_settings.cpp new file mode 100644 index 000000000..5bf6eca30 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/debug_settings.cpp @@ -0,0 +1,32 @@ +#include "debug_settings.h" + +#include + +DebugSettings::DebugSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "debug.ini", "debug", QString(), parent) +{ + // Create the default debug.ini if it doesn't exist yet + if (!QFile(settingPath + "debug.ini").exists()) { + QFile::copy(":/resources/config/debug.ini", settingPath + "debug.ini"); + } +} + +bool DebugSettings::getShowCardId() const +{ + return getValue("showCardId").toBool(); +} + +bool DebugSettings::getLocalGameOnStartup() const +{ + return getValue("onStartup", "localgame").toBool(); +} + +int DebugSettings::getLocalGamePlayerCount() const +{ + return getValue("playerCount", "localgame").toInt(); +} + +QString DebugSettings::getDeckPathForPlayer(const QString &playerName) const +{ + return getValue(playerName, "localgame", "deck").toString(); +} \ No newline at end of file diff --git a/libcockatrice_settings/libcockatrice/settings/debug_settings.h b/libcockatrice_settings/libcockatrice/settings/debug_settings.h new file mode 100644 index 000000000..30cdd5fa5 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/debug_settings.h @@ -0,0 +1,28 @@ +/** + * @file debug_settings.h + * @ingroup CoreSettings + * @brief TODO: Document this. + */ + +#ifndef DEBUG_SETTINGS_H +#define DEBUG_SETTINGS_H +#include "settings_manager.h" + +class DebugSettings : public SettingsManager +{ + Q_OBJECT + friend class SettingsCache; + + explicit DebugSettings(const QString &settingPath, QObject *parent = nullptr); + DebugSettings(const DebugSettings & /*other*/); + +public: + bool getShowCardId() const; + + bool getLocalGameOnStartup() const; + int getLocalGamePlayerCount() const; + + QString getDeckPathForPlayer(const QString &playerName) const; +}; + +#endif // DEBUG_SETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/download_settings.cpp b/libcockatrice_settings/libcockatrice/settings/download_settings.cpp new file mode 100644 index 000000000..66525a598 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/download_settings.cpp @@ -0,0 +1,29 @@ +#include "download_settings.h" + +#include "settings_manager.h" + +const QStringList DownloadSettings::DEFAULT_DOWNLOAD_URLS = { + "https://api.scryfall.com/cards/!set:uuid!?format=image&face=!prop:side!", + "https://api.scryfall.com/cards/multiverse/!set:muid!?format=image", + "https://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!set:muid!&type=card", + "https://gatherer.wizards.com/Handlers/Image.ashx?name=!name!&type=card"}; + +DownloadSettings::DownloadSettings(const QString &settingPath, QObject *parent = nullptr) + : SettingsManager(settingPath + "downloads.ini", "downloads", QString(), parent) +{ +} + +void DownloadSettings::setDownloadUrls(const QStringList &downloadURLs) +{ + setValue(QVariant::fromValue(downloadURLs), "urls"); +} + +QStringList DownloadSettings::getAllURLs() const +{ + return getValue("urls").toStringList(); +} + +void DownloadSettings::resetToDefaultURLs() +{ + setValue(QVariant::fromValue(DEFAULT_DOWNLOAD_URLS), "urls"); +} diff --git a/libcockatrice_settings/libcockatrice/settings/download_settings.h b/libcockatrice_settings/libcockatrice/settings/download_settings.h new file mode 100644 index 000000000..b7442301e --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/download_settings.h @@ -0,0 +1,27 @@ +/** + * @file download_settings.h + * @ingroup NetworkSettings + * @brief TODO: Document this. + */ + +#ifndef COCKATRICE_DOWNLOADSETTINGS_H +#define COCKATRICE_DOWNLOADSETTINGS_H + +#include "settings_manager.h" + +class DownloadSettings : public SettingsManager +{ + Q_OBJECT + friend class SettingsCache; + + static const QStringList DEFAULT_DOWNLOAD_URLS; + +public: + explicit DownloadSettings(const QString &, QObject *); + + QStringList getAllURLs() const; + void setDownloadUrls(const QStringList &downloadURLs); + void resetToDefaultURLs(); +}; + +#endif // COCKATRICE_DOWNLOADSETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp new file mode 100644 index 000000000..4f5bf52ee --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp @@ -0,0 +1,208 @@ +#include "game_filters_settings.h" + +#include +#include + +GameFiltersSettings::GameFiltersSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "gamefilters.ini", "filter_games", QString(), parent) +{ +} + +/** + * The game type might contain special characters, so to use it in + * QSettings we just hash it. + */ +static QString hashGameType(const QString &gameType) +{ + return QCryptographicHash::hash(gameType.toUtf8(), QCryptographicHash::Md5).toHex(); +} + +void GameFiltersSettings::setHideBuddiesOnlyGames(bool hide) +{ + setValue(hide, "hide_buddies_only_games"); +} + +bool GameFiltersSettings::isHideBuddiesOnlyGames() const +{ + QVariant previous = getValue("hide_buddies_only_games"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setHideFullGames(bool hide) +{ + setValue(hide, "hide_full_games"); +} + +bool GameFiltersSettings::isHideFullGames() const +{ + QVariant previous = getValue("hide_full_games"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setHideGamesThatStarted(bool hide) +{ + setValue(hide, "hide_games_that_started"); +} + +bool GameFiltersSettings::isHideGamesThatStarted() const +{ + QVariant previous = getValue("hide_games_that_started"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setHidePasswordProtectedGames(bool hide) +{ + setValue(hide, "hide_password_protected_games"); +} + +bool GameFiltersSettings::isHidePasswordProtectedGames() const +{ + QVariant previous = getValue("hide_password_protected_games"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setHideIgnoredUserGames(bool hide) +{ + setValue(hide, "hide_ignored_user_games"); +} + +bool GameFiltersSettings::isHideIgnoredUserGames() const +{ + QVariant previous = getValue("hide_ignored_user_games"); + return previous == QVariant() ? true : previous.toBool(); +} + +void GameFiltersSettings::setHideNotBuddyCreatedGames(bool hide) +{ + setValue(hide, "hide_not_buddy_created_games"); +} + +bool GameFiltersSettings::isHideNotBuddyCreatedGames() const +{ + QVariant previous = getValue("hide_not_buddy_created_games"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setHideOpenDecklistGames(bool hide) +{ + setValue(hide, "hide_open_decklist_games"); +} + +bool GameFiltersSettings::isHideOpenDecklistGames() const +{ + QVariant previous = getValue("hide_open_decklist_games"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setGameNameFilter(QString gameName) +{ + setValue(gameName, "game_name_filter"); +} + +QString GameFiltersSettings::getGameNameFilter() const +{ + return getValue("game_name_filter").toString(); +} + +void GameFiltersSettings::setCreatorNameFilters(QStringList creatorName) +{ + setValue(creatorName, "creator_name_filter"); +} + +QStringList GameFiltersSettings::getCreatorNameFilters() const +{ + return getValue("creator_name_filter").toStringList(); +} + +void GameFiltersSettings::setMinPlayers(int min) +{ + setValue(min, "min_players"); +} + +int GameFiltersSettings::getMinPlayers() const +{ + QVariant previous = getValue("min_players"); + return previous == QVariant() ? 1 : previous.toInt(); +} + +void GameFiltersSettings::setMaxPlayers(int max) +{ + setValue(max, "max_players"); +} + +int GameFiltersSettings::getMaxPlayers() const +{ + QVariant previous = getValue("max_players"); + return previous == QVariant() ? 99 : previous.toInt(); +} + +void GameFiltersSettings::setMaxGameAge(const QTime &maxGameAge) +{ + setValue(maxGameAge, "max_game_age_time"); +} + +QTime GameFiltersSettings::getMaxGameAge() const +{ + QVariant previous = getValue("max_game_age_time"); + return previous.toTime(); +} + +void GameFiltersSettings::setGameTypeEnabled(QString gametype, bool enabled) +{ + setValue(enabled, "game_type/" + hashGameType(gametype)); +} + +void GameFiltersSettings::setGameHashedTypeEnabled(QString gametypeHASHED, bool enabled) +{ + setValue(enabled, gametypeHASHED); +} + +bool GameFiltersSettings::isGameTypeEnabled(QString gametype) const +{ + QVariant previous = getValue("game_type/" + hashGameType(gametype)); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setShowOnlyIfSpectatorsCanWatch(bool show) +{ + setValue(show, "show_only_if_spectators_can_watch"); +} + +bool GameFiltersSettings::isShowOnlyIfSpectatorsCanWatch() const +{ + QVariant previous = getValue("show_only_if_spectators_can_watch"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setShowSpectatorPasswordProtected(bool show) +{ + setValue(show, "show_spectator_password_protected"); +} + +bool GameFiltersSettings::isShowSpectatorPasswordProtected() const +{ + QVariant previous = getValue("show_spectator_password_protected"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setShowOnlyIfSpectatorsCanChat(bool show) +{ + setValue(show, "show_only_if_spectators_can_chat"); +} + +bool GameFiltersSettings::isShowOnlyIfSpectatorsCanChat() const +{ + QVariant previous = getValue("show_only_if_spectators_can_chat"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setShowOnlyIfSpectatorsCanSeeHands(bool show) +{ + setValue(show, "show_only_if_spectators_can_see_hands"); +} + +bool GameFiltersSettings::isShowOnlyIfSpectatorsCanSeeHands() const +{ + QVariant previous = getValue("show_only_if_spectators_can_see_hands"); + return previous == QVariant() ? false : previous.toBool(); +} \ No newline at end of file diff --git a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h new file mode 100644 index 000000000..c0e60551a --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h @@ -0,0 +1,61 @@ +/** + * @file game_filters_settings.h + * @ingroup Lobby + * @ingroup GameSettings + * @brief TODO: Document this. + */ + +#ifndef GAMEFILTERSSETTINGS_H +#define GAMEFILTERSSETTINGS_H + +#include "settings_manager.h" + +class GameFiltersSettings : public SettingsManager +{ + Q_OBJECT + friend class SettingsCache; + +public: + bool isHideBuddiesOnlyGames() const; + bool isHideFullGames() const; + bool isHideGamesThatStarted() const; + bool isHidePasswordProtectedGames() const; + bool isHideIgnoredUserGames() const; + bool isHideNotBuddyCreatedGames() const; + bool isHideOpenDecklistGames() const; + QString getGameNameFilter() const; + QStringList getCreatorNameFilters() const; + int getMinPlayers() const; + int getMaxPlayers() const; + QTime getMaxGameAge() const; + bool isGameTypeEnabled(QString gametype) const; + bool isShowOnlyIfSpectatorsCanWatch() const; + bool isShowSpectatorPasswordProtected() const; + bool isShowOnlyIfSpectatorsCanChat() const; + bool isShowOnlyIfSpectatorsCanSeeHands() const; + + void setHideBuddiesOnlyGames(bool hide); + void setHideIgnoredUserGames(bool hide); + void setHideOpenDecklistGames(bool hide); + void setHideFullGames(bool hide); + void setHideGamesThatStarted(bool hide); + void setHidePasswordProtectedGames(bool hide); + void setHideNotBuddyCreatedGames(bool hide); + void setGameNameFilter(QString gameName); + void setCreatorNameFilters(QStringList creatorName); + void setMinPlayers(int min); + void setMaxPlayers(int max); + void setMaxGameAge(const QTime &maxGameAge); + void setGameTypeEnabled(QString gametype, bool enabled); + void setGameHashedTypeEnabled(QString gametypeHASHED, bool enabled); + void setShowOnlyIfSpectatorsCanWatch(bool show); + void setShowSpectatorPasswordProtected(bool show); + void setShowOnlyIfSpectatorsCanChat(bool show); + void setShowOnlyIfSpectatorsCanSeeHands(bool show); + +private: + explicit GameFiltersSettings(const QString &settingPath, QObject *parent = nullptr); + GameFiltersSettings(const GameFiltersSettings & /*other*/); +}; + +#endif // GAMEFILTERSSETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/layouts_settings.cpp b/libcockatrice_settings/libcockatrice/settings/layouts_settings.cpp new file mode 100644 index 000000000..e914dc2d8 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/layouts_settings.cpp @@ -0,0 +1,149 @@ +#include "layouts_settings.h" + +const static QString STATE_PROP = "state"; +const static QString GEOMETRY_PROP = "geometry"; +const static QString SIZE_PROP = "widgetSize"; + +const static QString GROUP_MAIN_WINDOW = "mainWindow"; +const static QString GROUP_DECK_EDITOR = "deckEditor"; +const static QString GROUP_VISUAL_DECK_EDITOR = "visualDeckEditor"; +const static QString GROUP_DECK_EDITOR_DB = "deckEditorDb"; +const static QString GROUP_SETS_DIALOG = "setsDialog"; +const static QString GROUP_TOKEN_DIALOG = "tokenDialog"; +const static QString GROUP_GAME_PLAY_AREA = "gamePlayArea"; +const static QString GROUP_REPLAY_PLAY_AREA = "replayPlayArea"; + +LayoutsSettings::LayoutsSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "layouts.ini", QString(), QString(), parent) +{ +} + +void LayoutsSettings::setMainWindowGeometry(const QByteArray &value) +{ + setValue(value, GEOMETRY_PROP, GROUP_MAIN_WINDOW); +} + +QByteArray LayoutsSettings::getMainWindowGeometry() const +{ + return getValue(GEOMETRY_PROP, GROUP_MAIN_WINDOW).toByteArray(); +} + +QByteArray LayoutsSettings::getDeckEditorLayoutState() const +{ + return getValue(STATE_PROP, GROUP_DECK_EDITOR).toByteArray(); +} + +void LayoutsSettings::setDeckEditorLayoutState(const QByteArray &value) +{ + setValue(value, STATE_PROP, GROUP_DECK_EDITOR); +} + +QByteArray LayoutsSettings::getDeckEditorGeometry() const +{ + return getValue(GEOMETRY_PROP, GROUP_DECK_EDITOR).toByteArray(); +} + +void LayoutsSettings::setDeckEditorGeometry(const QByteArray &value) +{ + setValue(value, GEOMETRY_PROP, GROUP_DECK_EDITOR); +} + +QByteArray LayoutsSettings::getVisualDeckEditorLayoutState() const +{ + return getValue(STATE_PROP, GROUP_VISUAL_DECK_EDITOR).toByteArray(); +} + +void LayoutsSettings::setVisualDeckEditorLayoutState(const QByteArray &value) +{ + setValue(value, STATE_PROP, GROUP_VISUAL_DECK_EDITOR); +} + +QByteArray LayoutsSettings::getVisualDeckEditorGeometry() const +{ + return getValue(GEOMETRY_PROP, GROUP_VISUAL_DECK_EDITOR).toByteArray(); +} + +void LayoutsSettings::setVisualDeckEditorGeometry(const QByteArray &value) +{ + setValue(value, GEOMETRY_PROP, GROUP_VISUAL_DECK_EDITOR); +} + +QByteArray LayoutsSettings::getDeckEditorDbHeaderState() const +{ + return getValue(STATE_PROP, GROUP_DECK_EDITOR_DB, "header").toByteArray(); +} + +void LayoutsSettings::setDeckEditorDbHeaderState(const QByteArray &value) +{ + setValue(value, STATE_PROP, GROUP_DECK_EDITOR_DB, "header"); +} + +QByteArray LayoutsSettings::getSetsDialogHeaderState() const +{ + return getValue(STATE_PROP, GROUP_SETS_DIALOG, "header").toByteArray(); +} + +void LayoutsSettings::setSetsDialogHeaderState(const QByteArray &value) +{ + setValue(value, STATE_PROP, GROUP_SETS_DIALOG, "header"); +} + +void LayoutsSettings::setSetsDialogGeometry(const QByteArray &value) +{ + setValue(value, GEOMETRY_PROP, GROUP_SETS_DIALOG); +} + +QByteArray LayoutsSettings::getSetsDialogGeometry() const +{ + return getValue(GEOMETRY_PROP, GROUP_SETS_DIALOG).toByteArray(); +} + +void LayoutsSettings::setTokenDialogGeometry(const QByteArray &value) +{ + setValue(value, GEOMETRY_PROP, GROUP_TOKEN_DIALOG); +} + +QByteArray LayoutsSettings::getTokenDialogGeometry() const +{ + return getValue(GEOMETRY_PROP, GROUP_TOKEN_DIALOG).toByteArray(); +} + +void LayoutsSettings::setGamePlayAreaGeometry(const QByteArray &value) +{ + setValue(value, GEOMETRY_PROP, GROUP_GAME_PLAY_AREA); +} + +void LayoutsSettings::setGamePlayAreaState(const QByteArray &value) +{ + setValue(value, STATE_PROP, GROUP_GAME_PLAY_AREA); +} + +QByteArray LayoutsSettings::getGamePlayAreaLayoutState() const +{ + return getValue(STATE_PROP, GROUP_GAME_PLAY_AREA).toByteArray(); +} + +QByteArray LayoutsSettings::getGamePlayAreaGeometry() const +{ + return getValue(GEOMETRY_PROP, GROUP_GAME_PLAY_AREA).toByteArray(); +} + +void LayoutsSettings::setReplayPlayAreaGeometry(const QByteArray &value) +{ + setValue(value, GEOMETRY_PROP, GROUP_REPLAY_PLAY_AREA); +} + +void LayoutsSettings::setReplayPlayAreaState(const QByteArray &value) +{ + setValue(value, STATE_PROP, GROUP_REPLAY_PLAY_AREA); +} + +QByteArray LayoutsSettings::getReplayPlayAreaLayoutState() const +{ + return getValue(STATE_PROP, GROUP_REPLAY_PLAY_AREA).toByteArray(); +} + +QByteArray LayoutsSettings::getReplayPlayAreaGeometry() const +{ + return getValue(GEOMETRY_PROP, GROUP_REPLAY_PLAY_AREA).toByteArray(); +} diff --git a/libcockatrice_settings/libcockatrice/settings/layouts_settings.h b/libcockatrice_settings/libcockatrice/settings/layouts_settings.h new file mode 100644 index 000000000..5353ce15a --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/layouts_settings.h @@ -0,0 +1,66 @@ +/** + * @file layouts_settings.h + * @ingroup CoreSettings + * @brief TODO: Document this. + */ + +#ifndef LAYOUTSSETTINGS_H +#define LAYOUTSSETTINGS_H + +#include "settings_manager.h" + +#include + +class LayoutsSettings : public SettingsManager +{ + Q_OBJECT + friend class SettingsCache; + +public: + void setMainWindowGeometry(const QByteArray &value); + + void setDeckEditorLayoutState(const QByteArray &value); + void setDeckEditorGeometry(const QByteArray &value); + + void setVisualDeckEditorLayoutState(const QByteArray &value); + void setVisualDeckEditorGeometry(const QByteArray &value); + + void setDeckEditorDbHeaderState(const QByteArray &value); + void setSetsDialogHeaderState(const QByteArray &value); + void setSetsDialogGeometry(const QByteArray &value); + void setTokenDialogGeometry(const QByteArray &value); + + void setGamePlayAreaGeometry(const QByteArray &value); + void setGamePlayAreaState(const QByteArray &value); + + void setReplayPlayAreaGeometry(const QByteArray &value); + void setReplayPlayAreaState(const QByteArray &value); + + QByteArray getMainWindowGeometry() const; + + QByteArray getDeckEditorLayoutState() const; + QByteArray getDeckEditorGeometry() const; + + QByteArray getVisualDeckEditorLayoutState() const; + QByteArray getVisualDeckEditorGeometry() const; + + QByteArray getDeckEditorDbHeaderState() const; + QByteArray getSetsDialogHeaderState() const; + QByteArray getSetsDialogGeometry() const; + QByteArray getTokenDialogGeometry() const; + + QByteArray getGamePlayAreaLayoutState() const; + QByteArray getGamePlayAreaGeometry() const; + + QByteArray getReplayPlayAreaLayoutState() const; + QByteArray getReplayPlayAreaGeometry() const; +signals: + +public slots: + +private: + explicit LayoutsSettings(const QString &settingPath, QObject *parent = nullptr); + LayoutsSettings(const LayoutsSettings & /*other*/); +}; + +#endif // LAYOUTSSETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/message_settings.cpp b/libcockatrice_settings/libcockatrice/settings/message_settings.cpp new file mode 100644 index 000000000..50da39df6 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/message_settings.cpp @@ -0,0 +1,26 @@ +#include "message_settings.h" + +MessageSettings::MessageSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "messages.ini", "messages", QString(), parent) +{ +} + +QString MessageSettings::getMessageAt(int index) const +{ + return getValue(QString("msg%1").arg(index)).toString(); +} + +int MessageSettings::getCount() const +{ + return getValue("count").toInt(); +} + +void MessageSettings::setCount(int count) +{ + setValue(count, "count"); +} + +void MessageSettings::setMessageAt(int index, QString message) +{ + setValue(message, QString("msg%1").arg(index)); +} diff --git a/libcockatrice_settings/libcockatrice/settings/message_settings.h b/libcockatrice_settings/libcockatrice/settings/message_settings.h new file mode 100644 index 000000000..ec70027af --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/message_settings.h @@ -0,0 +1,33 @@ +/** + * @file message_settings.h + * @ingroup NetworkSettings + * @brief TODO: Document this. + */ + +#ifndef MESSAGESETTINGS_H +#define MESSAGESETTINGS_H + +#include "settings_manager.h" + +class MessageSettings : public SettingsManager +{ + Q_OBJECT + friend class SettingsCache; + +public: + int getCount() const; + QString getMessageAt(int index) const; + + void setCount(int count); + void setMessageAt(int index, QString message); +signals: + void messageMacrosChanged(); + +public slots: + +private: + explicit MessageSettings(const QString &settingPath, QObject *parent = nullptr); + MessageSettings(const MessageSettings & /*other*/); +}; + +#endif // MESSAGESETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/recents_settings.cpp b/libcockatrice_settings/libcockatrice/settings/recents_settings.cpp new file mode 100644 index 000000000..76bc4069e --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/recents_settings.cpp @@ -0,0 +1,42 @@ +#include "recents_settings.h" + +#define MAX_RECENT_DECK_COUNT 10 + +RecentsSettings::RecentsSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "recents.ini", "deckbuilder", QString(), parent) +{ +} + +QStringList RecentsSettings::getRecentlyOpenedDeckPaths() const +{ + return getValue("deckpaths").toStringList(); +} +void RecentsSettings::clearRecentlyOpenedDeckPaths() +{ + deleteValue("deckpaths"); + emit recentlyOpenedDeckPathsChanged(); +} +void RecentsSettings::updateRecentlyOpenedDeckPaths(const QString &deckPath) +{ + auto deckPaths = getValue("deckpaths").toStringList(); + deckPaths.removeAll(deckPath); + + deckPaths.prepend(deckPath); + + while (deckPaths.size() > MAX_RECENT_DECK_COUNT) { + deckPaths.removeLast(); + } + + setValue(deckPaths, "deckpaths"); + emit recentlyOpenedDeckPathsChanged(); +} + +QString RecentsSettings::getLatestDeckDirPath() const +{ + return getValue("latestDeckDir", "dirs").toString(); +} + +void RecentsSettings::setLatestDeckDirPath(const QString &dirPath) +{ + setValue(dirPath, "latestDeckDir", "dirs"); +} \ No newline at end of file diff --git a/libcockatrice_settings/libcockatrice/settings/recents_settings.h b/libcockatrice_settings/libcockatrice/settings/recents_settings.h new file mode 100644 index 000000000..3aebff334 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/recents_settings.h @@ -0,0 +1,32 @@ +/** + * @file recents_settings.h + * @ingroup DeckSettings + * @brief TODO: Document this. + */ + +#ifndef RECENTS_SETTINGS_H +#define RECENTS_SETTINGS_H + +#include "settings_manager.h" + +class RecentsSettings : public SettingsManager +{ + Q_OBJECT + friend class SettingsCache; + + explicit RecentsSettings(const QString &settingPath, QObject *parent = nullptr); + RecentsSettings(const RecentsSettings & /*other*/); + +public: + QStringList getRecentlyOpenedDeckPaths() const; + void clearRecentlyOpenedDeckPaths(); + void updateRecentlyOpenedDeckPaths(const QString &deckPath); + + QString getLatestDeckDirPath() const; + void setLatestDeckDirPath(const QString &dirPath); + +signals: + void recentlyOpenedDeckPathsChanged(); +}; + +#endif // RECENTS_SETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp new file mode 100644 index 000000000..0140182be --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp @@ -0,0 +1,291 @@ +#include "servers_settings.h" + +#include +#include + +ServersSettings::ServersSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "servers.ini", "server", QString(), parent) +{ +} + +void ServersSettings::setPreviousHostLogin(int previous) +{ + setValue(previous, "previoushostlogin"); +} + +int ServersSettings::getPreviousHostLogin() const +{ + QVariant previous = getValue("previoushostlogin"); + return previous == QVariant() ? 1 : previous.toInt(); +} + +void ServersSettings::setPreviousHostList(QStringList list) +{ + setValue(list, "previoushosts"); +} + +QStringList ServersSettings::getPreviousHostList() const +{ + return getValue("previoushosts").toStringList(); +} + +void ServersSettings::setPrevioushostName(const QString &name) +{ + setValue(name, "previoushostName"); +} + +QString ServersSettings::getSaveName(QString defaultname) +{ + int index = getPrevioushostindex(getPrevioushostName()); + QVariant saveName = getValue(QString("saveName%1").arg(index), "server", "server_details"); + return saveName == QVariant() ? std::move(defaultname) : saveName.toString(); +} + +QString ServersSettings::getSite(QString defaultSite) +{ + int index = getPrevioushostindex(getPrevioushostName()); + QVariant site = getValue(QString("site%1").arg(index), "server", "server_details"); + return site == QVariant() ? std::move(defaultSite) : site.toString(); +} + +QString ServersSettings::getPrevioushostName() const +{ + QVariant value = getValue("previoushostName"); + return value == QVariant() ? "Rooster Ranges" : value.toString(); +} + +int ServersSettings::getPrevioushostindex(const QString &saveName) const +{ + int size = getValue("totalServers", "server", "server_details").toInt(); + + for (int i = 0; i <= size; ++i) + if (saveName == getValue(QString("saveName%1").arg(i), "server", "server_details").toString()) + return i; + + return -1; +} + +QString ServersSettings::getHostname(QString defaultHost) const +{ + int index = getPrevioushostindex(getPrevioushostName()); + QVariant hostname = getValue(QString("server%1").arg(index), "server", "server_details"); + return hostname == QVariant() ? std::move(defaultHost) : hostname.toString(); +} + +QString ServersSettings::getPort(QString defaultPort) const +{ + int index = getPrevioushostindex(getPrevioushostName()); + QVariant port = getValue(QString("port%1").arg(index), "server", "server_details"); + qCDebug(ServersSettingsLog) << "getPort() index = " << index << " port.val = " << port.toString(); + return port == QVariant() ? std::move(defaultPort) : port.toString(); +} + +QString ServersSettings::getPlayerName(QString defaultName) const +{ + int index = getPrevioushostindex(getPrevioushostName()); + QVariant name = getValue(QString("username%1").arg(index), "server", "server_details"); + qCDebug(ServersSettingsLog) << "getPlayerName() index = " << index << " name.val = " << name.toString(); + return name == QVariant() ? std::move(defaultName) : name.toString(); +} + +QString ServersSettings::getPassword() +{ + int index = getPrevioushostindex(getPrevioushostName()); + + if (getSavePassword()) + return getValue(QString("password%1").arg(index), "server", "server_details").toString(); + + return QString(); +} + +bool ServersSettings::getSavePassword() const +{ + int index = getPrevioushostindex(getPrevioushostName()); + bool save = getValue(QString("savePassword%1").arg(index), "server", "server_details").toBool(); + return save; +} + +void ServersSettings::setAutoConnect(int autoconnect) +{ + setValue(autoconnect, "auto_connect"); +} + +int ServersSettings::getAutoConnect() const +{ + QVariant autoconnect = getValue("auto_connect"); + return autoconnect == QVariant() ? 0 : autoconnect.toInt(); +} + +void ServersSettings::setFPHostName(QString hostname) +{ + setValue(hostname, "fphostname"); +} + +QString ServersSettings::getFPHostname(QString defaultHost) const +{ + QVariant hostname = getValue("fphostname"); + return hostname == QVariant() ? std::move(defaultHost) : hostname.toString(); +} + +void ServersSettings::setFPPort(QString port) +{ + setValue(port, "fpport"); +} + +QString ServersSettings::getFPPort(QString defaultPort) const +{ + QVariant port = getValue("fpport"); + return port == QVariant() ? std::move(defaultPort) : port.toString(); +} + +void ServersSettings::setFPPlayerName(QString playerName) +{ + setValue(playerName, "fpplayername"); +} + +QString ServersSettings::getFPPlayerName(QString defaultName) const +{ + QVariant name = getValue("fpplayername"); + return name == QVariant() ? std::move(defaultName) : name.toString(); +} + +void ServersSettings::setClearDebugLogStatus(bool abIsChecked) +{ + setValue(abIsChecked, "save_debug_log"); +} + +bool ServersSettings::getClearDebugLogStatus(bool abDefaultValue) const +{ + QVariant cbFlushLog = getValue("save_debug_log"); + return cbFlushLog == QVariant() ? abDefaultValue : cbFlushLog.toBool(); +} + +void ServersSettings::addNewServer(const QString &saveName, + const QString &serv, + const QString &port, + const QString &username, + const QString &password, + bool savePassword, + const QString &site) +{ + if (updateExistingServer(saveName, serv, port, username, password, savePassword, site)) + return; + + int index = getValue("totalServers", "server", "server_details").toInt() + 1; + + setValue(saveName, QString("saveName%1").arg(index), "server", "server_details"); + setValue(serv, QString("server%1").arg(index), "server", "server_details"); + setValue(port, QString("port%1").arg(index), "server", "server_details"); + setValue(username, QString("username%1").arg(index), "server", "server_details"); + setValue(savePassword, QString("savePassword%1").arg(index), "server", "server_details"); + setValue(index, "totalServers", "server", "server_details"); + setValue(password, QString("password%1").arg(index), "server", "server_details"); + setValue(site, QString("site%1").arg(index), "server", "server_details"); +} + +void ServersSettings::removeServer(QString servAddr) +{ + int size = getValue("totalServers", "server", "server_details").toInt(); + + bool found = false; + for (int i = 0; i <= size; ++i) { + if (!found) { + // find entry and overwrite it + if (servAddr == getValue(QString("server%1").arg(i), "server", "server_details").toString()) { + found = true; + } + } else { + // move all other entries after it one back, overwriting the previous one + int previous = i - 1; // we delete only one entry + setValue(getValue(QString("server%1").arg(i), "server", "server_details"), + QString("server%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("port%1").arg(i), "server", "server_details"), QString("port%1").arg(previous), + "server", "server_details"); + setValue(getValue(QString("username%1").arg(i), "server", "server_details"), + QString("username%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("savePassword%1").arg(i), "server", "server_details"), + QString("savePassword%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("password%1").arg(i), "server", "server_details"), + QString("password%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("saveName%1").arg(i), "server", "server_details"), + QString("saveName%1").arg(previous), "server", "server_details"); + setValue(getValue(QString("site%1").arg(i), "server", "server_details"), QString("site%1").arg(previous), + "server", "server_details"); + } + } + + // if we have deleted an entry, adjust the total + if (found) { + setValue(size - 1, "totalServers", "server", "server_details"); + + // delete last value + deleteValue(QString("server%1").arg(size), "server", "server_details"); + deleteValue(QString("port%1").arg(size), "server", "server_details"); + deleteValue(QString("username%1").arg(size), "server", "server_details"); + deleteValue(QString("savePassword%1").arg(size), "server", "server_details"); + deleteValue(QString("password%1").arg(size), "server", "server_details"); + deleteValue(QString("saveName%1").arg(size), "server", "server_details"); + deleteValue(QString("site%1").arg(size), "server", "server_details"); + } +} + +/** + * Will only update fields with new values, ignores empty values + */ +bool ServersSettings::updateExistingServerWithoutLoss(QString saveName, QString serv, QString port, QString site) +{ + int size = getValue("totalServers", "server", "server_details").toInt(); + + for (int i = 0; i <= size; ++i) { + if (serv == getValue(QString("server%1").arg(i), "server", "server_details").toString()) { + if (!port.isEmpty()) { + setValue(port, QString("port%1").arg(i), "server", "server_details"); + } + + if (!site.isEmpty()) { + setValue(site, QString("site%1").arg(i), "server", "server_details"); + } + + setValue(saveName, QString("saveName%1").arg(i), "server", "server_details"); + + return true; + } + } + return false; +} + +bool ServersSettings::updateExistingServer(QString saveName, + QString serv, + QString port, + QString username, + QString password, + bool savePassword, + QString site) +{ + int size = getValue("totalServers", "server", "server_details").toInt(); + + for (int i = 0; i <= size; ++i) { + if (serv == getValue(QString("server%1").arg(i), "server", "server_details").toString()) { + setValue(port, QString("port%1").arg(i), "server", "server_details"); + if (!username.isEmpty()) { + setValue(username, QString("username%1").arg(i), "server", "server_details"); + } + + if (savePassword && !password.isEmpty()) { + setValue(password, QString("password%1").arg(i), "server", "server_details"); + } else { + setValue(QString(), QString("password%1").arg(i), "server", "server_details"); + } + + if (!site.isEmpty()) { + setValue(site, QString("site%1").arg(i), "server", "server_details"); + } + + setValue(savePassword, QString("savePassword%1").arg(i), "server", "server_details"); + setValue(saveName, QString("saveName%1").arg(i), "server", "server_details"); + + return true; + } + } + return false; +} diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.h b/libcockatrice_settings/libcockatrice/settings/servers_settings.h new file mode 100644 index 000000000..22603a356 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.h @@ -0,0 +1,77 @@ +/** + * @file servers_settings.h + * @ingroup NetworkSettings + * @brief TODO: Document this. + */ + +#ifndef SERVERSSETTINGS_H +#define SERVERSSETTINGS_H + +#include "settings_manager.h" + +#include +#include +#define SERVERSETTINGS_DEFAULT_HOST "server.cockatrice.us" +#define SERVERSETTINGS_DEFAULT_PORT "4748" + +inline Q_LOGGING_CATEGORY(ServersSettingsLog, "servers_settings"); + +class ServersSettings : public SettingsManager +{ + Q_OBJECT + friend class SettingsCache; + +public: + int getPreviousHostLogin() const; + int getPrevioushostindex(const QString &) const; + QStringList getPreviousHostList() const; + QString getPrevioushostName() const; + QString getHostname(QString defaultHost = SERVERSETTINGS_DEFAULT_HOST) const; + QString getPort(QString defaultPort = SERVERSETTINGS_DEFAULT_PORT) const; + QString getPlayerName(QString defaultName = "") const; + QString getFPHostname(QString defaultHost = SERVERSETTINGS_DEFAULT_HOST) const; + QString getFPPort(QString defaultPort = SERVERSETTINGS_DEFAULT_PORT) const; + QString getFPPlayerName(QString defaultName = "") const; + QString getPassword(); + QString getSaveName(QString defaultname = ""); + QString getSite(QString defaultName = ""); + bool getSavePassword() const; + int getAutoConnect() const; + + void setPreviousHostLogin(int previous); + void setPrevioushostName(const QString &); + void setPreviousHostList(QStringList list); + void setAutoConnect(int autoconnect); + void setSite(QString site); + void setFPHostName(QString hostname); + void setFPPort(QString port); + void setFPPlayerName(QString playerName); + void addNewServer(const QString &saveName, + const QString &serv, + const QString &port, + const QString &username, + const QString &password, + bool savePassword, + const QString &site = QString()); + void removeServer(QString servAddr); + bool updateExistingServer(QString saveName, + QString serv, + QString port, + QString username, + QString password, + bool savePassword, + QString site = QString()); + + bool updateExistingServerWithoutLoss(QString saveName, + QString serv = QString(), + QString port = QString(), + QString site = QString()); + void setClearDebugLogStatus(bool abIsChecked); + bool getClearDebugLogStatus(bool abDefaultValue) const; + +private: + explicit ServersSettings(const QString &settingPath, QObject *parent = nullptr); + ServersSettings(const ServersSettings & /*other*/); +}; + +#endif // SERVERSSETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp b/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp new file mode 100644 index 000000000..2d4f1c441 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp @@ -0,0 +1,169 @@ +#include "settings_manager.h" + +SettingsManager::SettingsManager(const QString &_settingPath, + const QString &_defaultGroup, + const QString &_defaultSubGroup, + QObject *parent) + : QObject(parent), settingPath(_settingPath), defaultGroup(_defaultGroup), defaultSubGroup(_defaultSubGroup) +{ +} + +QSettings SettingsManager::getSettings() const +{ + return QSettings(settingPath, QSettings::IniFormat); +} + +void SettingsManager::setValue(const QVariant &value, const QString &name) +{ + auto settings = getSettings(); + + if (!defaultGroup.isEmpty()) { + settings.beginGroup(defaultGroup); + } + + if (!defaultSubGroup.isEmpty()) { + settings.beginGroup(defaultSubGroup); + } + + settings.setValue(name, value); + + if (!defaultSubGroup.isEmpty()) { + settings.endGroup(); + } + + if (!defaultGroup.isEmpty()) { + settings.endGroup(); + } +} + +void SettingsManager::setValue(const QVariant &value, + const QString &name, + const QString &group, + const QString &subGroup) +{ + auto settings = getSettings(); + + if (!group.isEmpty()) { + settings.beginGroup(group); + } + + if (!subGroup.isEmpty()) { + settings.beginGroup(subGroup); + } + + settings.setValue(name, value); + + if (!subGroup.isEmpty()) { + settings.endGroup(); + } + + if (!group.isEmpty()) { + settings.endGroup(); + } +} + +void SettingsManager::deleteValue(const QString &name) +{ + auto settings = getSettings(); + + if (!defaultGroup.isEmpty()) { + settings.beginGroup(defaultGroup); + } + + if (!defaultSubGroup.isEmpty()) { + settings.beginGroup(defaultSubGroup); + } + + settings.remove(name); + + if (!defaultSubGroup.isEmpty()) { + settings.endGroup(); + } + + if (!defaultGroup.isEmpty()) { + settings.endGroup(); + } +} + +void SettingsManager::deleteValue(const QString &name, const QString &group, const QString &subGroup) +{ + auto settings = getSettings(); + + if (!group.isEmpty()) { + settings.beginGroup(group); + } + + if (!subGroup.isEmpty()) { + settings.beginGroup(subGroup); + } + + settings.remove(name); + + if (!subGroup.isEmpty()) { + settings.endGroup(); + } + + if (!group.isEmpty()) { + settings.endGroup(); + } +} + +QVariant SettingsManager::getValue(const QString &name) const +{ + auto settings = getSettings(); + + if (!defaultGroup.isEmpty()) { + settings.beginGroup(defaultGroup); + } + + if (!defaultSubGroup.isEmpty()) { + settings.beginGroup(defaultSubGroup); + } + + QVariant value = settings.value(name); + + if (!defaultSubGroup.isEmpty()) { + settings.endGroup(); + } + + if (!defaultGroup.isEmpty()) { + settings.endGroup(); + } + + return value; +} + +QVariant SettingsManager::getValue(const QString &name, const QString &group, const QString &subGroup) const +{ + auto settings = getSettings(); + + if (!group.isEmpty()) { + settings.beginGroup(group); + } + + if (!subGroup.isEmpty()) { + settings.beginGroup(subGroup); + } + + QVariant value = settings.value(name); + + if (!subGroup.isEmpty()) { + settings.endGroup(); + } + + if (!group.isEmpty()) { + settings.endGroup(); + } + + return value; +} + +/** + * Calls sync on the underlying QSettings object + */ +void SettingsManager::sync() +{ + auto settings = getSettings(); + + settings.sync(); +} \ No newline at end of file diff --git a/libcockatrice_settings/libcockatrice/settings/settings_manager.h b/libcockatrice_settings/libcockatrice/settings/settings_manager.h new file mode 100644 index 000000000..ad828f089 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/settings_manager.h @@ -0,0 +1,40 @@ +/** + * @file settings_manager.h + * @ingroup Settings + * @brief TODO: Document this. + */ + +#ifndef SETTINGSMANAGER_H +#define SETTINGSMANAGER_H + +#include +#include +#include + +class SettingsManager : public QObject +{ + Q_OBJECT +public: + explicit SettingsManager(const QString &settingPath, + const QString &defaultGroup = QString(), + const QString &defaultSubGroup = QString(), + QObject *parent = nullptr); + QVariant getValue(const QString &name) const; + QVariant getValue(const QString &name, const QString &group, const QString &subGroup = QString()) const; + void sync(); + +protected: + QString settingPath; + QString defaultGroup; + QString defaultSubGroup; + + QSettings getSettings() const; + + void setValue(const QVariant &value, const QString &name); + void + setValue(const QVariant &value, const QString &name, const QString &group, const QString &subGroup = QString()); + void deleteValue(const QString &name); + void deleteValue(const QString &name, const QString &group, const QString &subGroup = QString()); +}; + +#endif // SETTINGSMANAGER_H diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt new file mode 100644 index 000000000..c0c7d8cc9 --- /dev/null +++ b/libcockatrice_utility/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.16) +project(Utility VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(UTILITY_SOURCES libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp + libcockatrice/utility/passwordhasher.cpp +) + +set(UTILITY_HEADERS + libcockatrice/utility/color.h + libcockatrice/utility/expression.h + libcockatrice/utility/levenshtein.h + libcockatrice/utility/macros.h + libcockatrice/utility/passwordhasher.h + libcockatrice/utility/trice_limits.h + libcockatrice/utility/zone_names.h +) + +add_library(libcockatrice_utility STATIC ${UTILITY_SOURCES} ${UTILITY_HEADERS}) + +target_include_directories(libcockatrice_utility PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(libcockatrice_utility PUBLIC libcockatrice_rng ${QT_CORE_MODULE}) + +set(ORACLE_LIBS) + +include_directories(${${COCKATRICE_QT_VERSION_NAME}Core_INCLUDE_DIRS}) diff --git a/libcockatrice_utility/libcockatrice/utility/card_ref.h b/libcockatrice_utility/libcockatrice/utility/card_ref.h new file mode 100644 index 000000000..d89fe590b --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/card_ref.h @@ -0,0 +1,29 @@ +#ifndef CARD_REF_H +#define CARD_REF_H + +#include + +/** + * The information passed over the server that is required to identify the exact card to display. + * + * @param name The name of the card. Should not be empty, unless to indicate the lack of a card. + * @param providerId Determines which printing of the card to use. Can be empty, in which case Cockatrice should default + * to using the preferred set. + */ +struct CardRef +{ + QString name; + QString providerId = QString(); + + bool operator==(const CardRef &other) const + { + return name == other.name && providerId == other.providerId; + } + + bool isEmpty() const + { + return name.isEmpty() && providerId.isEmpty(); + } +}; + +#endif // CARD_REF_H diff --git a/libcockatrice_utility/libcockatrice/utility/color.h b/libcockatrice_utility/libcockatrice/utility/color.h new file mode 100644 index 000000000..f02df3a0e --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/color.h @@ -0,0 +1,100 @@ +#ifndef COLOR_H +#define COLOR_H + +#ifdef QT_GUI_LIB +#include +#endif + +#include + +#ifdef QT_GUI_LIB +inline QColor convertColorToQColor(const color &c) +{ + return QColor(c.r(), c.g(), c.b()); +} + +inline color convertQColorToColor(const QColor &c) +{ + color result; + result.set_r(c.red()); + result.set_g(c.green()); + result.set_b(c.blue()); + return result; +} + +#include + +namespace GameSpecificColors +{ +namespace MTG +{ +inline QColor colorHelper(const QString &name) +{ + static const QMap colorMap = { + {"W", QColor(245, 245, 220)}, + {"U", QColor(80, 140, 255)}, + {"B", QColor(60, 60, 60)}, + {"R", QColor(220, 60, 50)}, + {"G", QColor(70, 160, 70)}, + {"Creature", QColor(70, 130, 180)}, + {"Instant", QColor(138, 43, 226)}, + {"Sorcery", QColor(199, 21, 133)}, + {"Enchantment", QColor(218, 165, 32)}, + {"Artifact", QColor(169, 169, 169)}, + {"Planeswalker", QColor(210, 105, 30)}, + {"Land", QColor(110, 80, 50)}, + }; + + if (colorMap.contains(name)) + return colorMap[name]; + + if (name.length() == 1 && colorMap.contains(name.toUpper())) + return colorMap[name.toUpper()]; + + uint h = qHash(name); + int r = 100 + (h % 120); + int g = 100 + ((h >> 8) % 120); + int b = 100 + ((h >> 16) % 120); + + return QColor(r, g, b); +} + +inline QList> sortManaMapWUBRGCFirst(const QMap &input) +{ + static const QStringList priorityOrder = {"W", "U", "B", "R", "G", "C"}; + + QList> result; + QSet consumed; + + // 1. Add priority colors in fixed order + for (const QString &key : priorityOrder) { + auto it = input.find(key); + if (it != input.end()) { + result.append({it.key(), it.value()}); + consumed.insert(it.key()); + } + } + + // 2. Add remaining keys (QMap iteration is already sorted) + for (auto it = input.begin(); it != input.end(); ++it) { + if (!consumed.contains(it.key())) { + result.append({it.key(), it.value()}); + } + } + + return result; +} +} // namespace MTG +} // namespace GameSpecificColors +#endif + +inline color makeColor(int r, int g, int b) +{ + color result; + result.set_r(r); + result.set_g(g); + result.set_b(b); + return result; +} + +#endif diff --git a/libcockatrice_utility/libcockatrice/utility/expression.cpp b/libcockatrice_utility/libcockatrice/utility/expression.cpp new file mode 100644 index 000000000..42073670c --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/expression.cpp @@ -0,0 +1,107 @@ +#include "expression.h" + +#include "peglib.h" + +#include +#include +#include +#include + +peg::parser math(R"( + EXPRESSION <- P0 + P0 <- P1 (P1_OPERATOR P1)* + P1 <- P2 (P2_OPERATOR P2)* + P2 <- P3 (P3_OPERATOR P3)* + P3 <- NUMBER / FUNCTION / VARIABLE / '(' P0 ')' + + P1_OPERATOR <- < [-+] > + P2_OPERATOR <- < [/*] > + P3_OPERATOR <- < '^' > + + NUMBER <- < '-'? [0-9]+ > + NAME <- < [a-z][a-z0-9]* > + VARIABLE <- < [xX] > + FUNCTION <- NAME '(' EXPRESSION ( [,\n] EXPRESSION )* ')' + + %whitespace <- [ \t\r]* + )"); + +QMap> *default_functions = nullptr; + +Expression::Expression(double initial) : value(initial) +{ + if (default_functions == nullptr) { + default_functions = new QMap>(); + default_functions->insert("abs", [](double a) { return qFabs(a); }); + default_functions->insert("ceil", [](double a) { return qCeil(a); }); + default_functions->insert("cos", [](double a) { return qCos(a); }); + default_functions->insert("floor", [](double a) { return qFloor(a); }); + default_functions->insert("log", [](double a) { return qLn(a); }); + default_functions->insert("log10", [](double a) { return qLn(a); }); + default_functions->insert("round", [](double a) { return qRound(a); }); + default_functions->insert("sin", [](double a) { return qSin(a); }); + default_functions->insert("sqrt", [](double a) { return qSqrt(a); }); + default_functions->insert("tan", [](double a) { return qTan(a); }); + default_functions->insert("trunc", [](double a) { return std::trunc(a); }); + } + fns = QMap>(*default_functions); +} + +double Expression::eval(const peg::Ast &ast) +{ + const auto &nodes = ast.nodes; + if (ast.name == "NUMBER") { + return stod(std::string(ast.token)); + } else if (ast.name == "FUNCTION") { + QString name = QString::fromStdString(std::string(nodes[0]->token)); + if (!fns.contains(name)) + return 0; + return fns[name](eval(*nodes[1])); + } else if (ast.name == "VARIABLE") { + return value; + } else if (ast.name[0] == 'P') { + double result = eval(*nodes[0]); + for (unsigned int i = 1; i < nodes.size(); i += 2) { + double arg = eval(*nodes[i + 1]); + char operation = nodes[i]->token[0]; + switch (operation) { + case '+': + result += arg; + break; + case '-': + result -= arg; + break; + case '*': + result *= arg; + break; + case '/': + result /= arg; + break; + case '^': + result = qPow(result, arg); + break; + default: + result = 0; + break; + } + } + return result; + } else { + return -1; + } +} + +double Expression::parse(const QString &expr) +{ + QByteArray ba = expr.toUtf8(); + + math.enable_ast(); + + std::shared_ptr ast; + if (math.parse(ba.data(), ast)) { + ast = peg::AstOptimizer(true).optimize(ast); + return eval(*ast); + } + + return 0; +} diff --git a/libcockatrice_utility/libcockatrice/utility/expression.h b/libcockatrice_utility/libcockatrice/utility/expression.h new file mode 100644 index 000000000..8c6a94932 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/expression.h @@ -0,0 +1,28 @@ +#ifndef EXPRESSION_H +#define EXPRESSION_H + +#include +#include +#include + +namespace peg +{ +template struct AstBase; +struct EmptyType; +typedef AstBase Ast; +} // namespace peg + +class Expression +{ +public: + double value; + + explicit Expression(double initial = 0); + double parse(const QString &expr); + +private: + double eval(const peg::Ast &ast); + QMap> fns; +}; + +#endif diff --git a/libcockatrice_utility/libcockatrice/utility/levenshtein.cpp b/libcockatrice_utility/libcockatrice/utility/levenshtein.cpp new file mode 100644 index 000000000..cfb972f91 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/levenshtein.cpp @@ -0,0 +1,25 @@ +#include "levenshtein.h" + +#include +#include + +int levenshteinDistance(const QString &s1, const QString &s2) +{ + int len1 = s1.size(); + int len2 = s2.size(); + std::vector> dp(len1 + 1, std::vector(len2 + 1)); + + for (int i = 0; i <= len1; i++) + dp[i][0] = i; + for (int j = 0; j <= len2; j++) + dp[0][j] = j; + + for (int i = 1; i <= len1; i++) { + for (int j = 1; j <= len2; j++) { + int cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1; + dp[i][j] = std::min({dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost}); + } + } + + return dp[len1][len2]; +} diff --git a/libcockatrice_utility/libcockatrice/utility/levenshtein.h b/libcockatrice_utility/libcockatrice/utility/levenshtein.h new file mode 100644 index 000000000..e83235470 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/levenshtein.h @@ -0,0 +1,14 @@ +/** + * @file levenshtein.h + * @ingroup Core + * @brief TODO: Document this. + */ + +#ifndef LEVENSHTEIN_H +#define LEVENSHTEIN_H + +#include + +int levenshteinDistance(const QString &s1, const QString &s2); + +#endif // LEVENSHTEIN_H diff --git a/libcockatrice_utility/libcockatrice/utility/macros.h b/libcockatrice_utility/libcockatrice/utility/macros.h new file mode 100644 index 000000000..1d7d6d632 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/macros.h @@ -0,0 +1,17 @@ +#ifndef COCKATRICE_MACROS_H +#define COCKATRICE_MACROS_H + +#include + +// Qt6.7 changed how stateChanged functionality +// of QCheckBoxes work. +// See https://doc.qt.io/qt-6/qcheckbox.html#checkStateChanged +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) +#define QT_STATE_CHANGED checkStateChanged +#define QT_STATE_CHANGED_T Qt::CheckState +#else +#define QT_STATE_CHANGED stateChanged +#define QT_STATE_CHANGED_T int +#endif + +#endif // COCKATRICE_MACROS_H diff --git a/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp b/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp new file mode 100644 index 000000000..c40c5f94f --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp @@ -0,0 +1,38 @@ +#include "passwordhasher.h" + +#include +#include + +QString PasswordHasher::computeHash(const QString &password, const QString &salt) +{ + QCryptographicHash::Algorithm algo = QCryptographicHash::Sha512; + const int rounds = 1000; + + QByteArray hash = (salt + password).toUtf8(); + for (int i = 0; i < rounds; ++i) { + hash = QCryptographicHash::hash(hash, algo); + } + QString hashedPass = salt + QString(hash.toBase64()); + return hashedPass; +} + +QString PasswordHasher::generateRandomSalt(const int len) +{ + static const char alphanum[] = "0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; + + QString ret; + int size = sizeof(alphanum) - 1; + + for (int i = 0; i < len; ++i) { + ret.append(alphanum[rng->rand(0, size)]); + } + + return ret; +} + +QString PasswordHasher::generateActivationToken() +{ + return QCryptographicHash::hash(generateRandomSalt().toUtf8(), QCryptographicHash::Md5).toBase64().left(16); +} diff --git a/libcockatrice_utility/libcockatrice/utility/passwordhasher.h b/libcockatrice_utility/libcockatrice/utility/passwordhasher.h new file mode 100644 index 000000000..811ecef15 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/passwordhasher.h @@ -0,0 +1,14 @@ +#ifndef PASSWORDHASHER_H +#define PASSWORDHASHER_H + +#include + +class PasswordHasher +{ +public: + static QString computeHash(const QString &password, const QString &salt); + static QString generateRandomSalt(const int len = 16); + static QString generateActivationToken(); +}; + +#endif diff --git a/libcockatrice_utility/libcockatrice/utility/peglib.h b/libcockatrice_utility/libcockatrice/utility/peglib.h new file mode 100644 index 000000000..3ae6040c4 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/peglib.h @@ -0,0 +1,5758 @@ +// +// peglib.h +// +// Copyright (c) 2022 Yuji Hirose. All rights reserved. +// MIT License +// + +#pragma once + +/* + * Configuration + */ + +#ifndef CPPPEGLIB_HEURISTIC_ERROR_TOKEN_MAX_CHAR_COUNT +#define CPPPEGLIB_HEURISTIC_ERROR_TOKEN_MAX_CHAR_COUNT 32 +#endif + +#include +#include +#include +#include +#include +#if __has_include() +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if !defined(__cplusplus) || __cplusplus < 201703L +#error "Requires complete C++17 support" +#endif + +namespace peg { + +/*----------------------------------------------------------------------------- + * scope_exit + *---------------------------------------------------------------------------*/ + +// This is based on +// "http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4189". + +template struct scope_exit { + explicit scope_exit(EF &&f) + : exit_function(std::move(f)), execute_on_destruction{true} {} + + scope_exit(scope_exit &&rhs) + : exit_function(std::move(rhs.exit_function)), + execute_on_destruction{rhs.execute_on_destruction} { + rhs.release(); + } + + ~scope_exit() { + if (execute_on_destruction) { this->exit_function(); } + } + + void release() { this->execute_on_destruction = false; } + +private: + scope_exit(const scope_exit &) = delete; + void operator=(const scope_exit &) = delete; + scope_exit &operator=(scope_exit &&) = delete; + + EF exit_function; + bool execute_on_destruction; +}; + +/*----------------------------------------------------------------------------- + * UTF8 functions + *---------------------------------------------------------------------------*/ + +inline size_t codepoint_length(const char *s8, size_t l) { + if (l) { + auto b = static_cast(s8[0]); + if ((b & 0x80) == 0) { + return 1; + } else if ((b & 0xE0) == 0xC0 && l >= 2) { + return 2; + } else if ((b & 0xF0) == 0xE0 && l >= 3) { + return 3; + } else if ((b & 0xF8) == 0xF0 && l >= 4) { + return 4; + } + } + return 0; +} + +inline size_t codepoint_count(const char *s8, size_t l) { + size_t count = 0; + for (size_t i = 0; i < l;) { + auto len = codepoint_length(s8 + i, l - i); + if (len == 0) { + // Invalid UTF-8 byte, treat as single byte to avoid infinite loop + len = 1; + } + i += len; + count++; + } + return count; +} + +inline size_t encode_codepoint(char32_t cp, char *buff) { + if (cp < 0x0080) { + buff[0] = static_cast(cp & 0x7F); + return 1; + } else if (cp < 0x0800) { + buff[0] = static_cast(0xC0 | ((cp >> 6) & 0x1F)); + buff[1] = static_cast(0x80 | (cp & 0x3F)); + return 2; + } else if (cp < 0xD800) { + buff[0] = static_cast(0xE0 | ((cp >> 12) & 0xF)); + buff[1] = static_cast(0x80 | ((cp >> 6) & 0x3F)); + buff[2] = static_cast(0x80 | (cp & 0x3F)); + return 3; + } else if (cp < 0xE000) { + // D800 - DFFF is invalid... + return 0; + } else if (cp < 0x10000) { + buff[0] = static_cast(0xE0 | ((cp >> 12) & 0xF)); + buff[1] = static_cast(0x80 | ((cp >> 6) & 0x3F)); + buff[2] = static_cast(0x80 | (cp & 0x3F)); + return 3; + } else if (cp < 0x110000) { + buff[0] = static_cast(0xF0 | ((cp >> 18) & 0x7)); + buff[1] = static_cast(0x80 | ((cp >> 12) & 0x3F)); + buff[2] = static_cast(0x80 | ((cp >> 6) & 0x3F)); + buff[3] = static_cast(0x80 | (cp & 0x3F)); + return 4; + } + return 0; +} + +inline std::string encode_codepoint(char32_t cp) { + char buff[4]; + auto l = encode_codepoint(cp, buff); + return std::string(buff, l); +} + +inline bool decode_codepoint(const char *s8, size_t l, size_t &bytes, + char32_t &cp) { + if (l) { + auto b = static_cast(s8[0]); + if ((b & 0x80) == 0) { + bytes = 1; + cp = b; + return true; + } else if ((b & 0xE0) == 0xC0) { + if (l >= 2) { + bytes = 2; + cp = ((static_cast(s8[0] & 0x1F)) << 6) | + (static_cast(s8[1] & 0x3F)); + return true; + } + } else if ((b & 0xF0) == 0xE0) { + if (l >= 3) { + bytes = 3; + cp = ((static_cast(s8[0] & 0x0F)) << 12) | + ((static_cast(s8[1] & 0x3F)) << 6) | + (static_cast(s8[2] & 0x3F)); + return true; + } + } else if ((b & 0xF8) == 0xF0) { + if (l >= 4) { + bytes = 4; + cp = ((static_cast(s8[0] & 0x07)) << 18) | + ((static_cast(s8[1] & 0x3F)) << 12) | + ((static_cast(s8[2] & 0x3F)) << 6) | + (static_cast(s8[3] & 0x3F)); + return true; + } + } + } + return false; +} + +inline size_t decode_codepoint(const char *s8, size_t l, char32_t &cp) { + size_t bytes; + if (decode_codepoint(s8, l, bytes, cp)) { return bytes; } + return 0; +} + +inline char32_t decode_codepoint(const char *s8, size_t l) { + char32_t cp = 0; + decode_codepoint(s8, l, cp); + return cp; +} + +inline std::u32string decode(const char *s8, size_t l) { + std::u32string out; + size_t i = 0; + while (i < l) { + auto beg = i++; + while (i < l && (s8[i] & 0xc0) == 0x80) { + i++; + } + out += decode_codepoint(&s8[beg], (i - beg)); + } + return out; +} + +template const char *u8(const T *s) { + return reinterpret_cast(s); +} + +/*----------------------------------------------------------------------------- + * escape_characters + *---------------------------------------------------------------------------*/ + +inline std::string escape_characters(const char *s, size_t n) { + std::string str; + for (size_t i = 0; i < n; i++) { + auto c = s[i]; + switch (c) { + case '\f': str += "\\f"; break; + case '\n': str += "\\n"; break; + case '\r': str += "\\r"; break; + case '\t': str += "\\t"; break; + case '\v': str += "\\v"; break; + default: str += c; break; + } + } + return str; +} + +inline std::string escape_characters(std::string_view sv) { + return escape_characters(sv.data(), sv.size()); +} + +/*----------------------------------------------------------------------------- + * resolve_escape_sequence + *---------------------------------------------------------------------------*/ + +inline bool is_hex(char c, int &v) { + if ('0' <= c && c <= '9') { + v = c - '0'; + return true; + } else if ('a' <= c && c <= 'f') { + v = c - 'a' + 10; + return true; + } else if ('A' <= c && c <= 'F') { + v = c - 'A' + 10; + return true; + } + return false; +} + +inline bool is_digit(char c, int &v) { + if ('0' <= c && c <= '9') { + v = c - '0'; + return true; + } + return false; +} + +inline std::pair parse_hex_number(const char *s, size_t n, + size_t i) { + int ret = 0; + int val; + while (i < n && is_hex(s[i], val)) { + ret = static_cast(ret * 16 + val); + i++; + } + return std::pair(ret, i); +} + +inline std::pair parse_octal_number(const char *s, size_t n, + size_t i) { + int ret = 0; + int val; + while (i < n && is_digit(s[i], val)) { + ret = static_cast(ret * 8 + val); + i++; + } + return std::pair(ret, i); +} + +inline std::string resolve_escape_sequence(const char *s, size_t n) { + std::string r; + r.reserve(n); + + size_t i = 0; + while (i < n) { + auto ch = s[i]; + if (ch == '\\') { + i++; + assert(i < n); + + switch (s[i]) { + case 'f': + r += '\f'; + i++; + break; + case 'n': + r += '\n'; + i++; + break; + case 'r': + r += '\r'; + i++; + break; + case 't': + r += '\t'; + i++; + break; + case 'v': + r += '\v'; + i++; + break; + case '\'': + r += '\''; + i++; + break; + case '"': + r += '"'; + i++; + break; + case '[': + r += '['; + i++; + break; + case ']': + r += ']'; + i++; + break; + case '\\': + r += '\\'; + i++; + break; + case 'x': + case 'u': { + char32_t cp; + std::tie(cp, i) = parse_hex_number(s, n, i + 1); + r += encode_codepoint(cp); + break; + } + default: { + char32_t cp; + std::tie(cp, i) = parse_octal_number(s, n, i); + r += encode_codepoint(cp); + break; + } + } + } else { + r += ch; + i++; + } + } + return r; +} + +/*----------------------------------------------------------------------------- + * token_to_number_ - This function should be removed eventually + *---------------------------------------------------------------------------*/ + +template T token_to_number_(std::string_view sv) { + T n = 0; +#if __has_include() + if constexpr (!std::is_floating_point::value) { + std::from_chars(sv.data(), sv.data() + sv.size(), n); +#else + if constexpr (false) { +#endif + } else { + auto s = std::string(sv); + std::istringstream ss(s); + ss >> n; + } + return n; +} + +inline std::string to_lower(std::string s) { + for (auto &c : s) { + c = static_cast(std::tolower(static_cast(c))); + } + return s; +} + +/*----------------------------------------------------------------------------- + * Trie + *---------------------------------------------------------------------------*/ + +class Trie { +public: + Trie(const std::vector &items, bool ignore_case) + : ignore_case_(ignore_case), items_count_(items.size()) { + size_t id = 0; + for (const auto &item : items) { + const auto &s = ignore_case ? to_lower(item) : item; + if (item.size() > max_len_) { max_len_ = item.size(); } + for (size_t len = 1; len <= item.size(); len++) { + auto last = len == item.size(); + std::string_view sv(s.data(), len); + auto it = dic_.find(sv); + if (it == dic_.end()) { + dic_.emplace(sv, Info{last, last, id}); + } else if (last) { + it->second.match = true; + } else { + it->second.done = false; + } + } + id++; + } + } + + size_t match(const char *text, size_t text_len, size_t &id) const { + auto limit = std::min(text_len, max_len_); + std::string lower_text; + if (ignore_case_) { + lower_text = to_lower(std::string(text, limit)); + text = lower_text.data(); + } + + size_t match_len = 0; + auto done = false; + size_t len = 1; + while (!done && len <= limit) { + std::string_view sv(text, len); + auto it = dic_.find(sv); + if (it == dic_.end()) { + done = true; + } else { + if (it->second.match) { + match_len = len; + id = it->second.id; + } + if (it->second.done) { done = true; } + } + len += 1; + } + return match_len; + } + + size_t size() const { return dic_.size(); } + size_t items_count() const { return items_count_; } + + friend struct ComputeFirstSet; + +private: + struct Info { + bool done; + bool match; + size_t id; + }; + + // TODO: Use unordered_map when heterogeneous lookup is supported in C++20 + // std::unordered_map dic_; + std::map> dic_; + + bool ignore_case_; + size_t items_count_; + size_t max_len_ = 0; +}; + +/*----------------------------------------------------------------------------- + * PEG + *---------------------------------------------------------------------------*/ + +/* + * Line information utility function + */ +inline std::pair line_info(const char *start, const char *cur) { + auto p = start; + auto col_ptr = p; + auto no = 1; + + while (p < cur) { + if (*p == '\n') { + no++; + col_ptr = p + 1; + } + p++; + } + + auto col = codepoint_count(col_ptr, p - col_ptr) + 1; + + return std::pair(no, col); +} + +/* + * String tag + */ +inline constexpr unsigned int str2tag_core(const char *s, size_t l, + unsigned int h) { + return (l == 0) ? h + : str2tag_core(s + 1, l - 1, + (h * 33) ^ static_cast(*s)); +} + +inline constexpr unsigned int str2tag(std::string_view sv) { + return str2tag_core(sv.data(), sv.size(), 0); +} + +namespace udl { + +inline constexpr unsigned int operator""_(const char *s, size_t l) { + return str2tag_core(s, l, 0); +} + +} // namespace udl + +/* + * Semantic values + */ +class Context; + +struct SemanticValues : protected std::vector { + SemanticValues() = default; + SemanticValues(Context *c) : c_(c) {} + + // Input text + const char *path = nullptr; + const char *ss = nullptr; + + // Matched string + std::string_view sv() const { return sv_; } + + // Definition name + const std::string &name() const { return name_; } + + std::vector tags; + + // Line number and column at which the matched string is + std::pair line_info() const; + + // Choice count + size_t choice_count() const { return choice_count_; } + + // Choice number (0 based index) + size_t choice() const { return choice_; } + + // Tokens + std::vector tokens; + + std::string_view token(size_t id = 0) const { + if (tokens.empty()) { return sv_; } + assert(id < tokens.size()); + return tokens[id]; + } + + // Token conversion + std::string token_to_string(size_t id = 0) const { + return std::string(token(id)); + } + + template T token_to_number() const { + return token_to_number_(token()); + } + + // Transform the semantic value vector to another vector + template + std::vector transform(size_t beg = 0, + size_t end = static_cast(-1)) const { + std::vector r; + end = (std::min)(end, size()); + for (size_t i = beg; i < end; i++) { + r.emplace_back(std::any_cast((*this)[i])); + } + return r; + } + + using std::vector::iterator; + using std::vector::const_iterator; + using std::vector::size; + using std::vector::empty; + using std::vector::assign; + using std::vector::begin; + using std::vector::end; + using std::vector::rbegin; + using std::vector::rend; + using std::vector::operator[]; + using std::vector::at; + using std::vector::resize; + using std::vector::front; + using std::vector::back; + using std::vector::push_back; + using std::vector::pop_back; + using std::vector::insert; + using std::vector::erase; + using std::vector::clear; + using std::vector::swap; + using std::vector::emplace; + using std::vector::emplace_back; + +private: + friend class Context; + friend class Dictionary; + friend class Sequence; + friend class PrioritizedChoice; + friend class Repetition; + friend class Holder; + friend class PrecedenceClimbing; + + Context *c_ = nullptr; + std::string_view sv_; + size_t choice_count_ = 0; + size_t choice_ = 0; + std::string name_; +}; + +/* + * Semantic action + */ +template std::any call(F fn, Args &&...args) { + using R = decltype(fn(std::forward(args)...)); + if constexpr (std::is_void::value) { + fn(std::forward(args)...); + return std::any(); + } else if constexpr (std::is_same::type, + std::any>::value) { + return fn(std::forward(args)...); + } else { + return std::any(fn(std::forward(args)...)); + } +} + +template +struct argument_count : argument_count {}; +template +struct argument_count + : std::integral_constant {}; +template +struct argument_count + : std::integral_constant {}; +template +struct argument_count + : std::integral_constant {}; + +class Action { +public: + Action() = default; + Action(Action &&rhs) = default; + template Action(F fn) : fn_(make_adaptor(fn)) {} + template void operator=(F fn) { fn_ = make_adaptor(fn); } + Action &operator=(const Action &rhs) = default; + + operator bool() const { return bool(fn_); } + + std::any operator()(SemanticValues &vs, std::any &dt, + const std::any &predicate_data) const { + return fn_(vs, dt, predicate_data); + } + +private: + using Fty = std::function; + + template Fty make_adaptor(F fn) { + if constexpr (argument_count::value == 1) { + return [fn](auto &vs, auto & /*dt*/, const auto & /*predicate_data*/) { + return call(fn, vs); + }; + } else if constexpr (argument_count::value == 2) { + return [fn](auto &vs, auto &dt, const auto & /*predicate_data*/) { + return call(fn, vs, dt); + }; + } else { + return [fn](auto &vs, auto &dt, const auto &predicate_data) { + return call(fn, vs, dt, predicate_data); + }; + } + } + + Fty fn_; +}; + +class Predicate { +public: + Predicate() = default; + Predicate(Predicate &&rhs) = default; + template Predicate(F fn) : fn_(make_adaptor(fn)) {} + template void operator=(F fn) { fn_ = make_adaptor(fn); } + Predicate &operator=(const Predicate &rhs) = default; + + operator bool() const { return bool(fn_); } + + bool operator()(const SemanticValues &vs, const std::any &dt, + std::string &msg, std::any &predicate_data) const { + return fn_(vs, dt, msg, predicate_data); + } + +private: + using Fty = std::function; + + template Fty make_adaptor(F fn) { + if constexpr (argument_count::value == 3) { + return [fn](const auto &vs, const auto &dt, auto &msg, + auto & /*predicate_data*/) { return fn(vs, dt, msg); }; + } else { + return [fn](const auto &vs, const auto &dt, auto &msg, + auto &predicate_data) { + return fn(vs, dt, msg, predicate_data); + }; + } + } + + Fty fn_; +}; + +/* + * Parse result helper + */ +inline bool success(size_t len) { return len != static_cast(-1); } + +inline bool fail(size_t len) { return len == static_cast(-1); } + +/* + * Log + */ +using Log = std::function; + +/* + * ErrorInfo + */ +class Definition; + +struct ErrorInfo { + const char *error_pos = nullptr; + std::vector> expected_tokens; + const char *message_pos = nullptr; + std::string message; + std::string label; + const char *last_output_pos = nullptr; + bool keep_previous_token = false; + + void clear() { + error_pos = nullptr; + expected_tokens.clear(); + message_pos = nullptr; + message.clear(); + } + + void add(const char *error_literal, const Definition *error_rule) { + for (const auto &[t, r] : expected_tokens) { + if (t == error_literal && r == error_rule) { return; } + } + expected_tokens.emplace_back(error_literal, error_rule); + } + + void output_log(const Log &log, const char *s, size_t n); + +private: + int cast_char(char c) const { return static_cast(c); } + + std::string heuristic_error_token(const char *s, size_t n, + const char *pos) const { + auto len = n - std::distance(s, pos); + if (len) { + size_t i = 0; + auto c = cast_char(pos[i++]); + if (!std::ispunct(c) && !std::isspace(c)) { + while (i < len && !std::ispunct(cast_char(pos[i])) && + !std::isspace(cast_char(pos[i]))) { + i++; + } + } + + size_t count = CPPPEGLIB_HEURISTIC_ERROR_TOKEN_MAX_CHAR_COUNT; + size_t j = 0; + while (count > 0 && j < i) { + j += codepoint_length(&pos[j], i - j); + count--; + } + + return escape_characters(pos, j); + } + return std::string(); + } + + std::string replace_all(std::string str, const std::string &from, + const std::string &to) const { + size_t pos = 0; + while ((pos = str.find(from, pos)) != std::string::npos) { + str.replace(pos, from.length(), to); + pos += to.length(); + } + return str; + } +}; + +/* + * Context + */ +class Ope; + +using TracerEnter = std::function; + +using TracerLeave = std::function; + +using TracerStartOrEnd = std::function; + +class Context { +public: + const char *path; + const char *s; + const size_t l; + + ErrorInfo error_info; + bool recovered = false; + + std::vector> value_stack; + size_t value_stack_size = 0; + + std::vector rule_stack; + std::vector>> args_stack; + + size_t in_token_boundary_count = 0; + + std::shared_ptr whitespaceOpe; + bool in_whitespace = false; + + std::shared_ptr wordOpe; + + std::vector> capture_entries; + + std::vector cut_stack; + + const size_t def_count; + const bool enablePackratParsing; + std::vector cache_registered; + std::vector cache_success; + + std::map, std::tuple> + cache_values; + + // Left recursion support + struct LRMemo { + size_t len = static_cast(-1); + std::any val; + }; + std::map, LRMemo> lr_memo; + + // Rules whose lr_memo was hit during the current parse scope. + // Used to track LR cycle membership. + std::set lr_refs_hit; + + // Rules currently in their seeding/growing phase at a given position. + // Protected from having their lr_memo erased by inner growers. + std::set> lr_active_seeds; + + void clear_packrat_cache(const char *pos, size_t def_id) { + if (!enablePackratParsing) { return; } + auto col = static_cast(pos - s); + auto idx = def_count * col + def_id; + if (idx < cache_registered.size()) { + cache_registered[idx] = false; + cache_success[idx] = false; + } + cache_values.erase(std::make_pair(col, def_id)); + } + + void write_packrat_cache(const char *pos, size_t def_id, size_t len, + const std::any &val) { + if (!enablePackratParsing) { return; } + auto col = pos - s; + auto idx = def_count * static_cast(col) + def_id; + if (idx >= cache_registered.size()) { return; } + cache_registered[idx] = true; + cache_success[idx] = true; + auto key = std::pair(col, def_id); + cache_values[key] = std::pair(len, val); + } + + TracerEnter tracer_enter; + TracerLeave tracer_leave; + std::any trace_data; + const bool verbose_trace; + + Log log; + + Context(const char *path, const char *s, size_t l, size_t def_count, + std::shared_ptr whitespaceOpe, std::shared_ptr wordOpe, + bool enablePackratParsing, TracerEnter tracer_enter, + TracerLeave tracer_leave, std::any trace_data, bool verbose_trace, + Log log) + : path(path), s(s), l(l), whitespaceOpe(whitespaceOpe), wordOpe(wordOpe), + def_count(def_count), enablePackratParsing(enablePackratParsing), + cache_registered(enablePackratParsing ? def_count * (l + 1) : 0), + cache_success(enablePackratParsing ? def_count * (l + 1) : 0), + tracer_enter(tracer_enter), tracer_leave(tracer_leave), + trace_data(trace_data), verbose_trace(verbose_trace), log(log) { + + push_args({}); + } + + ~Context() { + assert(!value_stack_size); + assert(cut_stack.empty()); + } + + Context(const Context &) = delete; + Context(Context &&) = delete; + Context operator=(const Context &) = delete; + + // Per-rule packrat stats (populated when packrat_stats is non-null) + struct PackratStats { + size_t hits = 0; + size_t misses = 0; + }; + std::vector *packrat_stats = nullptr; + + // Per-rule packrat filter: if set, only rules with filter[def_id]=true + // use full memoization (cache_values map). Others use bitvector-only + // re-entry guard. + const std::vector *packrat_rule_filter = nullptr; + + template + void packrat(const char *a_s, size_t def_id, size_t &len, std::any &val, + T fn) { + if (!enablePackratParsing) { + fn(val); + return; + } + + auto col = a_s - s; + auto idx = def_count * static_cast(col) + def_id; + + if (cache_registered[idx]) { + if (packrat_stats && def_id < packrat_stats->size()) { + (*packrat_stats)[def_id].hits++; + } + if (cache_success[idx]) { + auto key = std::pair(col, def_id); + std::tie(len, val) = cache_values[key]; + return; + } else { + len = static_cast(-1); + return; + } + } else { + // Pre-register as failure (re-entry guard for all rules) + cache_registered[idx] = true; + cache_success[idx] = false; + + if (packrat_stats && def_id < packrat_stats->size()) { + (*packrat_stats)[def_id].misses++; + } + + fn(val); + + bool full_memo = + !packrat_rule_filter || (def_id < packrat_rule_filter->size() && + (*packrat_rule_filter)[def_id]); + if (full_memo) { + if (success(len)) { write_packrat_cache(a_s, def_id, len, val); } + } else { + // Guard-only: undo registration so future calls re-parse + cache_registered[idx] = false; + } + return; + } + } + + // Semantic values + SemanticValues &push_semantic_values_scope() { + assert(value_stack_size <= value_stack.size()); + if (value_stack_size == value_stack.size()) { + value_stack.emplace_back(std::make_shared(this)); + } else { + auto &vs = *value_stack[value_stack_size]; + if (!vs.empty()) { + vs.clear(); + if (!vs.tags.empty()) { vs.tags.clear(); } + } + vs.sv_ = std::string_view(); + vs.choice_count_ = 0; + vs.choice_ = 0; + if (!vs.tokens.empty()) { vs.tokens.clear(); } + } + + auto &vs = *value_stack[value_stack_size++]; + vs.path = path; + vs.ss = s; + return vs; + } + + void pop_semantic_values_scope() { value_stack_size--; } + + // Arguments + void push_args(std::vector> &&args) { + args_stack.emplace_back(std::move(args)); + } + + void pop_args() { args_stack.pop_back(); } + + const std::vector> &top_args() const { + return args_stack[args_stack.size() - 1]; + } + + // Snapshot/Rollback + struct Snapshot { + size_t sv_size; + size_t sv_tags_size; + size_t sv_tokens_size; + std::string_view sv_sv; + size_t choice_count; + size_t choice; + size_t capture_size; + }; + + Snapshot snapshot(const SemanticValues &vs) const { + return {vs.size(), vs.tags.size(), vs.tokens.size(), vs.sv_, + vs.choice_count_, vs.choice_, capture_entries.size()}; + } + + void rollback(SemanticValues &vs, const Snapshot &snap) { + vs.resize(snap.sv_size); + vs.tags.resize(snap.sv_tags_size); + vs.tokens.resize(snap.sv_tokens_size); + vs.sv_ = snap.sv_sv; + vs.choice_count_ = snap.choice_count; + vs.choice_ = snap.choice; + capture_entries.resize(snap.capture_size); + } + + // Skip trailing whitespace with trace suppression. + // Returns whitespace length, or -1 on failure. + // No-op (returns 0) if inside a token boundary or no whitespaceOpe. + size_t skip_whitespace(const char *a_s, size_t n, SemanticValues &vs, + std::any &dt); + + // Error + void set_error_pos(const char *a_s, const char *literal = nullptr); + + // Trace + void trace_enter(const Ope &ope, const char *a_s, size_t n, + const SemanticValues &vs, std::any &dt); + void trace_leave(const Ope &ope, const char *a_s, size_t n, + const SemanticValues &vs, std::any &dt, size_t len); + bool is_traceable(const Ope &ope) const; + + // Line info + std::pair line_info(const char *cur) const { + std::call_once(source_line_index_init_, [this]() { + for (size_t pos = 0; pos < l; pos++) { + if (s[pos] == '\n') { source_line_index.push_back(pos); } + } + source_line_index.push_back(l); + }); + + auto pos = static_cast(std::distance(s, cur)); + + auto it = std::lower_bound( + source_line_index.begin(), source_line_index.end(), pos, + [](size_t element, size_t value) { return element < value; }); + + auto id = static_cast(std::distance(source_line_index.begin(), it)); + auto off = pos - (id == 0 ? 0 : source_line_index[id - 1] + 1); + return std::pair(id + 1, off + 1); + } + + size_t next_trace_id = 0; + std::vector trace_ids; + bool ignore_trace_state = false; + mutable std::once_flag source_line_index_init_; + mutable std::vector source_line_index; +}; + +/* + * Parser operators + */ +class Ope { +public: + struct Visitor; + + virtual ~Ope() = default; + size_t parse(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const; + virtual size_t parse_core(const char *s, size_t n, SemanticValues &vs, + Context &c, std::any &dt) const = 0; + virtual void accept(Visitor &v) = 0; + + bool is_token_boundary = false; + bool is_choice_like = false; +}; + +// Keyword-guarded identifier data, heap-allocated only for matching Sequences. +// Avoids bloating all Sequence objects with bitsets and keyword sets. +struct KeywordGuardData { + std::bitset<256> identifier_first; // first char of identifier + std::bitset<256> identifier_rest; // subsequent chars of identifier + std::vector exact_keywords; // single-word keywords (lowercase) + std::vector prefix_keywords; // first word of compound keywords + size_t min_keyword_len = 0; + size_t max_keyword_len = 0; + + static bool matches_any(const std::vector &keywords, + std::string_view input) { + return std::any_of(keywords.begin(), keywords.end(), + [&](const auto &kw) { return kw == input; }); + } +}; + +class Sequence : public Ope { +public: + template + Sequence(const Args &...args) + : opes_{static_cast>(args)...} {} + Sequence(const std::vector> &opes) : opes_(opes) {} + Sequence(std::vector> &&opes) : opes_(std::move(opes)) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + // Keyword-guarded identifier fast path: + // Fuses !ReservedKeyword into scan-then-lookup + if (kw_guard_) { + if (auto result = parse_keyword_guarded(s, n, vs, c, dt)) { + return *result; + } + // nullopt means prefix keyword match — fall through to normal path + } + size_t i = 0; + for (const auto &ope : opes_) { + auto len = ope->parse(s + i, n - i, vs, c, dt); + if (fail(len)) { return len; } + i += len; + } + return i; + } + + void accept(Visitor &v) override; + + std::vector> opes_; + +private: + friend struct SetupFirstSets; + std::unique_ptr kw_guard_; + + // Returns parse result, or nullopt to fall through to normal path + std::optional parse_keyword_guarded(const char *s, size_t n, + SemanticValues &vs, Context &c, + std::any &dt) const { + const auto &kw = *kw_guard_; + if (n < 1 || !kw.identifier_first.test(static_cast(*s))) { + c.set_error_pos(s); + return static_cast(-1); + } + // Scan identifier using bitset + size_t id_len = 1; + while (id_len < n && + kw.identifier_rest.test(static_cast(s[id_len]))) { + id_len++; + } + // Skip keyword matching if identifier length is out of range + if (id_len >= kw.min_keyword_len && id_len <= kw.max_keyword_len) { + char lower_buf[64]; + std::unique_ptr lower_heap; + char *lower = lower_buf; + if (id_len > sizeof(lower_buf)) { + lower_heap.reset(new char[id_len]); + lower = lower_heap.get(); + } + std::transform(s, s + id_len, lower, [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + std::string_view lower_sv(lower, id_len); + + if (KeywordGuardData::matches_any(kw.exact_keywords, lower_sv)) { + c.set_error_pos(s); + return static_cast(-1); + } + if (KeywordGuardData::matches_any(kw.prefix_keywords, lower_sv)) { + return std::nullopt; + } + } + // Success: emit token and consume trailing whitespace + vs.tokens.emplace_back(std::string_view(s, id_len)); + auto wl = c.skip_whitespace(s + id_len, n - id_len, vs, dt); + if (fail(wl)) { return wl; } + return id_len + wl; + } +}; + +struct FirstSet { + // First-Set: set of possible first bytes for an expression. + // Used by PrioritizedChoice to skip alternatives that cannot match. + std::bitset<256> chars; // byte values that can appear as the first byte + bool can_be_empty = false; // true if the expression can match empty string + bool any_char = false; // true if any character can appear (cannot filter) + const char *first_literal = nullptr; // first literal for error reporting + const Definition *first_rule = + nullptr; // first token rule for error reporting + + void merge(const FirstSet &other) { + chars |= other.chars; + if (other.can_be_empty) { can_be_empty = true; } + if (other.any_char) { any_char = true; } + // Note: first_literal/first_rule are NOT merged — per-alternative + } +}; + +class PrioritizedChoice : public Ope { +public: + template + PrioritizedChoice(bool for_label, const Args &...args) + : opes_{static_cast>(args)...}, + for_label_(for_label) { + is_choice_like = true; + } + PrioritizedChoice(const std::vector> &opes) + : opes_(opes) { + is_choice_like = true; + } + PrioritizedChoice(std::vector> &&opes) + : opes_(std::move(opes)) { + is_choice_like = true; + } + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + size_t len = static_cast(-1); + + if (!for_label_) { c.cut_stack.push_back(false); } + auto se = scope_exit([&]() { + if (!for_label_) { c.cut_stack.pop_back(); } + }); + + size_t id = 0; + for (const auto &ope : opes_) { + // First-Set filtering: skip if next byte cannot start this alternative + if (n > 0 && id < first_sets_.size()) { + const auto &fs = first_sets_[id]; + if (!fs.any_char && !fs.can_be_empty && + !fs.chars.test(static_cast(*s))) { + if (c.log && (fs.first_literal || fs.first_rule)) { + if (c.error_info.error_pos <= s) { + if (c.error_info.error_pos < s || !(id > 0)) { + c.error_info.error_pos = s; + c.error_info.expected_tokens.clear(); + } + if (fs.first_literal) { + c.error_info.add(fs.first_literal, nullptr); + } else { + c.error_info.add(nullptr, fs.first_rule); + } + } + } + id++; + continue; + } + } + + if (!c.cut_stack.empty()) { c.cut_stack.back() = false; } + + auto snap = c.snapshot(vs); + c.error_info.keep_previous_token = id > 0; + + len = ope->parse(s, n, vs, c, dt); + + if (success(len)) { + vs.choice_count_ = opes_.size(); + vs.choice_ = id; + break; + } + + c.rollback(vs, snap); + + if (!c.cut_stack.empty() && c.cut_stack.back()) { break; } + + id++; + } + + c.error_info.keep_previous_token = false; + return len; + } + + void accept(Visitor &v) override; + + size_t size() const { return opes_.size(); } + + std::vector> opes_; + bool for_label_ = false; + std::vector first_sets_; +}; + +class Repetition : public Ope { +public: + Repetition(const std::shared_ptr &ope, size_t min, size_t max) + : ope_(ope), min_(min), max_(max) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + // ISpan fast path: tight loop for ASCII CharacterClass repetition. + // Safe because each ASCII match is exactly 1 byte, so byte count == match + // count. + if (span_bitset_) { + const auto &bitset = *span_bitset_; + size_t i = 0; + if (max_ == std::numeric_limits::max()) { + // Unbounded repetition (*, +): no per-iteration max check + while (i < n && bitset.test(static_cast(s[i]))) { + i++; + } + } else { + auto limit = std::min(n, max_); + while (i < limit && bitset.test(static_cast(s[i]))) { + i++; + } + } + if (i < min_) { + c.set_error_pos(s + i); + return static_cast(-1); + } + return i; + } + + size_t count = 0; + size_t i = 0; + while (count < min_) { + auto len = ope_->parse(s + i, n - i, vs, c, dt); + if (fail(len)) { return len; } + i += len; + count++; + } + + while (count < max_) { + auto snap = c.snapshot(vs); + auto len = ope_->parse(s + i, n - i, vs, c, dt); + if (fail(len)) { + c.rollback(vs, snap); + break; + } + i += len; + count++; + } + return i; + } + + void accept(Visitor &v) override; + + bool is_zom() const { + return min_ == 0 && max_ == std::numeric_limits::max(); + } + + static std::shared_ptr zom(const std::shared_ptr &ope) { + return std::make_shared(ope, 0, + std::numeric_limits::max()); + } + + static std::shared_ptr oom(const std::shared_ptr &ope) { + return std::make_shared(ope, 1, + std::numeric_limits::max()); + } + + static std::shared_ptr opt(const std::shared_ptr &ope) { + return std::make_shared(ope, 0, 1); + } + + std::shared_ptr ope_; + size_t min_; + size_t max_; + const std::bitset<256> *span_bitset_ = + nullptr; // non-owning, set by SetupFirstSets +}; + +class AndPredicate : public Ope { +public: + AndPredicate(const std::shared_ptr &ope) : ope_(ope) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + auto snap = c.snapshot(vs); + auto len = ope_->parse(s, n, vs, c, dt); + c.rollback(vs, snap); // Always rollback — predicates consume nothing + if (success(len)) { + return 0; + } else { + return len; + } + } + + void accept(Visitor &v) override; + + std::shared_ptr ope_; +}; + +class NotPredicate : public Ope { +public: + NotPredicate(const std::shared_ptr &ope) : ope_(ope) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + auto snap = c.snapshot(vs); + auto len = ope_->parse(s, n, vs, c, dt); + c.rollback(vs, snap); // Always rollback — predicates consume nothing + if (success(len)) { + c.set_error_pos(s); + return static_cast(-1); + } else { + return 0; + } + } + + void accept(Visitor &v) override; + + std::shared_ptr ope_; +}; + +class Dictionary : public Ope, public std::enable_shared_from_this { +public: + Dictionary(const std::vector &v, bool ignore_case) + : trie_(v, ignore_case) { + is_choice_like = true; + } + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; + + void accept(Visitor &v) override; + + Trie trie_; +}; + +class LiteralString : public Ope, + public std::enable_shared_from_this { +public: + LiteralString(std::string &&s, bool ignore_case) + : lit_(std::move(s)), ignore_case_(ignore_case), + lower_lit_(ignore_case ? to_lower(lit_) : std::string()), + is_word_(false) {} + + LiteralString(const std::string &s, bool ignore_case) + : lit_(s), ignore_case_(ignore_case), + lower_lit_(ignore_case ? to_lower(lit_) : std::string()), + is_word_(false) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; + + void accept(Visitor &v) override; + + std::string lit_; + bool ignore_case_; + std::string lower_lit_; // pre-computed for ignore_case + mutable std::once_flag init_is_word_; + mutable bool is_word_; +}; + +class CharacterClass : public Ope, + public std::enable_shared_from_this { +public: + CharacterClass(const std::string &s, bool negated, bool ignore_case) + : negated_(negated), ignore_case_(ignore_case) { + auto chars = decode(s.data(), s.length()); + auto i = 0u; + while (i < chars.size()) { + if (i + 2 < chars.size() && chars[i + 1] == '-') { + auto cp1 = chars[i]; + auto cp2 = chars[i + 2]; + ranges_.emplace_back(std::pair(cp1, cp2)); + i += 3; + } else { + auto cp = chars[i]; + ranges_.emplace_back(std::pair(cp, cp)); + i += 1; + } + } + assert(!ranges_.empty()); + setup_ascii_bitset(); + } + + CharacterClass(const std::vector> &ranges, + bool negated, bool ignore_case) + : ranges_(ranges), negated_(negated), ignore_case_(ignore_case) { + assert(!ranges_.empty()); + setup_ascii_bitset(); + } + + size_t parse_core(const char *s, size_t n, SemanticValues & /*vs*/, + Context &c, std::any & /*dt*/) const override { + if (n < 1) { + c.set_error_pos(s); + return static_cast(-1); + } + + char32_t cp = 0; + auto len = decode_codepoint(s, n, cp); + + for (const auto &range : ranges_) { + if (in_range(range, cp)) { + if (negated_) { + c.set_error_pos(s); + return static_cast(-1); + } else { + return len; + } + } + } + + if (negated_) { + return len; + } else { + c.set_error_pos(s); + return static_cast(-1); + } + } + + void accept(Visitor &v) override; + + friend struct ComputeFirstSet; + + bool is_ascii_only() const { return is_ascii_only_; } + const std::bitset<256> &ascii_bitset() const { return ascii_bitset_; } + +private: + bool in_range(const std::pair &range, char32_t cp) const { + if (ignore_case_) { + auto cpl = std::tolower(cp); + return std::tolower(range.first) <= cpl && + cpl <= std::tolower(range.second); + } else { + return range.first <= cp && cp <= range.second; + } + } + + void setup_ascii_bitset() { + if (negated_) { return; } // negated classes can match non-ASCII + for (const auto &[lo, hi] : ranges_) { + if (lo > 0x7F || hi > 0x7F) { return; } + } + is_ascii_only_ = true; + for (const auto &[lo, hi] : ranges_) { + for (auto cp = lo; cp <= hi; cp++) { + auto ch = static_cast(cp); + ascii_bitset_.set(ch); + if (ignore_case_) { + ascii_bitset_.set(static_cast(std::toupper(ch))); + ascii_bitset_.set(static_cast(std::tolower(ch))); + } + } + } + } + + std::vector> ranges_; + bool negated_; + bool ignore_case_; + std::bitset<256> ascii_bitset_; + bool is_ascii_only_ = false; +}; + +class Character : public Ope, public std::enable_shared_from_this { +public: + Character(char32_t ch) : ch_(ch) {} + + size_t parse_core(const char *s, size_t n, SemanticValues & /*vs*/, + Context &c, std::any & /*dt*/) const override { + if (n < 1) { + c.set_error_pos(s); + return static_cast(-1); + } + + char32_t cp = 0; + auto len = decode_codepoint(s, n, cp); + + if (cp != ch_) { + c.set_error_pos(s); + return static_cast(-1); + } + return len; + } + + void accept(Visitor &v) override; + + char32_t ch_; +}; + +class AnyCharacter : public Ope, + public std::enable_shared_from_this { +public: + size_t parse_core(const char *s, size_t n, SemanticValues & /*vs*/, + Context &c, std::any & /*dt*/) const override { + auto len = codepoint_length(s, n); + if (len < 1) { + c.set_error_pos(s); + return static_cast(-1); + } + return len; + } + + void accept(Visitor &v) override; +}; + +class CaptureScope : public Ope { +public: + CaptureScope(const std::shared_ptr &ope) : ope_(ope) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + auto cap_snap = c.capture_entries.size(); + auto len = ope_->parse(s, n, vs, c, dt); + c.capture_entries.resize(cap_snap); // Always rollback (isolation) + return len; + } + + void accept(Visitor &v) override; + + std::shared_ptr ope_; +}; + +class Capture : public Ope { +public: + using MatchAction = std::function; + + Capture(const std::shared_ptr &ope, MatchAction ma) + : ope_(ope), match_action_(ma) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + auto len = ope_->parse(s, n, vs, c, dt); + if (success(len) && match_action_) { match_action_(s, len, c); } + return len; + } + + void accept(Visitor &v) override; + + std::shared_ptr ope_; + MatchAction match_action_; +}; + +class TokenBoundary : public Ope { +public: + TokenBoundary(const std::shared_ptr &ope) : ope_(ope) { + is_token_boundary = true; + } + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; + + void accept(Visitor &v) override; + + std::shared_ptr ope_; +}; + +class Ignore : public Ope { +public: + Ignore(const std::shared_ptr &ope) : ope_(ope) {} + + size_t parse_core(const char *s, size_t n, SemanticValues & /*vs*/, + Context &c, std::any &dt) const override { + auto &chvs = c.push_semantic_values_scope(); + auto se = scope_exit([&]() { c.pop_semantic_values_scope(); }); + return ope_->parse(s, n, chvs, c, dt); + } + + void accept(Visitor &v) override; + + std::shared_ptr ope_; +}; + +using Parser = std::function; + +class User : public Ope { +public: + User(Parser fn) : fn_(fn) {} + size_t parse_core(const char *s, size_t n, SemanticValues &vs, + Context & /*c*/, std::any &dt) const override { + assert(fn_); + return fn_(s, n, vs, dt); + } + void accept(Visitor &v) override; + std::function + fn_; +}; + +class WeakHolder : public Ope { +public: + WeakHolder(const std::shared_ptr &ope) : weak_(ope) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + auto ope = weak_.lock(); + assert(ope); + return ope->parse(s, n, vs, c, dt); + } + + void accept(Visitor &v) override; + + std::weak_ptr weak_; +}; + +class Holder : public Ope { +public: + Holder(Definition *outer) : outer_(outer) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; + + void accept(Visitor &v) override; + + std::any reduce(SemanticValues &vs, std::any &dt, + const std::any &predicate_data) const; + + const std::string &name() const; + const std::string &trace_name() const; + + std::shared_ptr ope_; + Definition *outer_; + mutable std::once_flag trace_name_init_; + mutable std::string trace_name_; + + friend class Definition; +}; + +using Grammar = std::unordered_map; + +class Reference : public Ope, public std::enable_shared_from_this { +public: + Reference(const Grammar &grammar, const std::string &name, const char *s, + bool is_macro, const std::vector> &args) + : grammar_(grammar), name_(name), s_(s), is_macro_(is_macro), args_(args), + rule_(nullptr), iarg_(0) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; + + void accept(Visitor &v) override; + + std::shared_ptr get_core_operator() const; + + const Grammar &grammar_; + const std::string name_; + const char *s_; + + const bool is_macro_; + const std::vector> args_; + + Definition *rule_; + size_t iarg_; +}; + +class Whitespace : public Ope { +public: + Whitespace(const std::shared_ptr &ope) : ope_(ope) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + if (c.in_whitespace) { return 0; } + c.in_whitespace = true; + auto se = scope_exit([&]() { c.in_whitespace = false; }); + return ope_->parse(s, n, vs, c, dt); + } + + void accept(Visitor &v) override; + + std::shared_ptr ope_; +}; + +class BackReference : public Ope { +public: + BackReference(std::string &&name) : name_(std::move(name)) {} + + BackReference(const std::string &name) : name_(name) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; + + void accept(Visitor &v) override; + + std::string name_; +}; + +class PrecedenceClimbing : public Ope { +public: + using BinOpeInfo = std::map>; + + PrecedenceClimbing(const std::shared_ptr &atom, + const std::shared_ptr &binop, const BinOpeInfo &info, + const Definition &rule) + : atom_(atom), binop_(binop), info_(info), rule_(rule) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + return parse_expression(s, n, vs, c, dt, 0); + } + + void accept(Visitor &v) override; + + std::shared_ptr atom_; + std::shared_ptr binop_; + BinOpeInfo info_; + const Definition &rule_; + +private: + size_t parse_expression(const char *s, size_t n, SemanticValues &vs, + Context &c, std::any &dt, size_t min_prec) const; + + Definition &get_reference_for_binop(Context &c) const; +}; + +class Recovery : public Ope { +public: + Recovery(const std::shared_ptr &ope) : ope_(ope) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; + + void accept(Visitor &v) override; + + std::shared_ptr ope_; +}; + +class Cut : public Ope, public std::enable_shared_from_this { +public: + size_t parse_core(const char * /*s*/, size_t /*n*/, SemanticValues & /*vs*/, + Context &c, std::any & /*dt*/) const override { + if (!c.cut_stack.empty()) { c.cut_stack.back() = true; } + return 0; + } + + void accept(Visitor &v) override; +}; + +/* + * Factories + */ +template std::shared_ptr seq(Args &&...args) { + return std::make_shared(static_cast>(args)...); +} + +template std::shared_ptr cho(Args &&...args) { + return std::make_shared( + false, static_cast>(args)...); +} + +template std::shared_ptr cho4label_(Args &&...args) { + return std::make_shared( + true, static_cast>(args)...); +} + +inline std::shared_ptr zom(const std::shared_ptr &ope) { + return Repetition::zom(ope); +} + +inline std::shared_ptr oom(const std::shared_ptr &ope) { + return Repetition::oom(ope); +} + +inline std::shared_ptr opt(const std::shared_ptr &ope) { + return Repetition::opt(ope); +} + +inline std::shared_ptr rep(const std::shared_ptr &ope, size_t min, + size_t max) { + return std::make_shared(ope, min, max); +} + +inline std::shared_ptr apd(const std::shared_ptr &ope) { + return std::make_shared(ope); +} + +inline std::shared_ptr npd(const std::shared_ptr &ope) { + return std::make_shared(ope); +} + +inline std::shared_ptr dic(const std::vector &v, + bool ignore_case) { + return std::make_shared(v, ignore_case); +} + +inline std::shared_ptr lit(std::string &&s) { + return std::make_shared(s, false); +} + +inline std::shared_ptr liti(std::string &&s) { + return std::make_shared(s, true); +} + +inline std::shared_ptr cls(const std::string &s) { + return std::make_shared(s, false, false); +} + +inline std::shared_ptr +cls(const std::vector> &ranges, + bool ignore_case = false) { + return std::make_shared(ranges, false, ignore_case); +} + +inline std::shared_ptr ncls(const std::string &s) { + return std::make_shared(s, true, false); +} + +inline std::shared_ptr +ncls(const std::vector> &ranges, + bool ignore_case = false) { + return std::make_shared(ranges, true, ignore_case); +} + +inline std::shared_ptr chr(char32_t dt) { + return std::make_shared(dt); +} + +inline std::shared_ptr dot() { return std::make_shared(); } + +inline std::shared_ptr csc(const std::shared_ptr &ope) { + return std::make_shared(ope); +} + +inline std::shared_ptr cap(const std::shared_ptr &ope, + Capture::MatchAction ma) { + return std::make_shared(ope, ma); +} + +inline std::shared_ptr tok(const std::shared_ptr &ope) { + return std::make_shared(ope); +} + +inline std::shared_ptr ign(const std::shared_ptr &ope) { + return std::make_shared(ope); +} + +inline std::shared_ptr +usr(std::function + fn) { + return std::make_shared(fn); +} + +inline std::shared_ptr ref(const Grammar &grammar, const std::string &name, + const char *s, bool is_macro, + const std::vector> &args) { + return std::make_shared(grammar, name, s, is_macro, args); +} + +inline std::shared_ptr wsp(const std::shared_ptr &ope) { + return std::make_shared(std::make_shared(ope)); +} + +inline std::shared_ptr bkr(std::string &&name) { + return std::make_shared(name); +} + +inline std::shared_ptr pre(const std::shared_ptr &atom, + const std::shared_ptr &binop, + const PrecedenceClimbing::BinOpeInfo &info, + const Definition &rule) { + return std::make_shared(atom, binop, info, rule); +} + +inline std::shared_ptr rec(const std::shared_ptr &ope) { + return std::make_shared(ope); +} + +inline std::shared_ptr cut() { return std::make_shared(); } + +/* + * Visitor + */ +struct Ope::Visitor { + virtual ~Visitor() {} + virtual void visit(Sequence &) {} + virtual void visit(PrioritizedChoice &) {} + virtual void visit(Repetition &) {} + virtual void visit(AndPredicate &) {} + virtual void visit(NotPredicate &) {} + virtual void visit(Dictionary &) {} + virtual void visit(LiteralString &) {} + virtual void visit(CharacterClass &) {} + virtual void visit(Character &) {} + virtual void visit(AnyCharacter &) {} + virtual void visit(CaptureScope &) {} + virtual void visit(Capture &) {} + virtual void visit(TokenBoundary &) {} + virtual void visit(Ignore &) {} + virtual void visit(User &) {} + virtual void visit(WeakHolder &) {} + virtual void visit(Holder &) {} + virtual void visit(Reference &) {} + virtual void visit(Whitespace &) {} + virtual void visit(BackReference &) {} + virtual void visit(PrecedenceClimbing &) {} + virtual void visit(Recovery &) {} + virtual void visit(Cut &) {} +}; + +struct TraversalVisitor : public Ope::Visitor { + using Ope::Visitor::visit; + void visit(Sequence &ope) override { + for (auto &op : ope.opes_) { + op->accept(*this); + } + } + void visit(PrioritizedChoice &ope) override { + for (auto &op : ope.opes_) { + op->accept(*this); + } + } + void visit(Repetition &ope) override { ope.ope_->accept(*this); } + void visit(AndPredicate &ope) override { ope.ope_->accept(*this); } + void visit(NotPredicate &ope) override { ope.ope_->accept(*this); } + void visit(CaptureScope &ope) override { ope.ope_->accept(*this); } + void visit(Capture &ope) override { ope.ope_->accept(*this); } + void visit(TokenBoundary &ope) override { ope.ope_->accept(*this); } + void visit(Ignore &ope) override { ope.ope_->accept(*this); } + void visit(WeakHolder &ope) override { ope.weak_.lock()->accept(*this); } + void visit(Holder &ope) override { ope.ope_->accept(*this); } + void visit(Whitespace &ope) override { ope.ope_->accept(*this); } + void visit(Recovery &ope) override { ope.ope_->accept(*this); } + void visit(PrecedenceClimbing &ope) override { ope.atom_->accept(*this); } +}; + +struct TraceOpeName : public Ope::Visitor { + using Ope::Visitor::visit; + + void visit(Sequence &) override { name_ = "Sequence"; } + void visit(PrioritizedChoice &) override { name_ = "PrioritizedChoice"; } + void visit(Repetition &) override { name_ = "Repetition"; } + void visit(AndPredicate &) override { name_ = "AndPredicate"; } + void visit(NotPredicate &) override { name_ = "NotPredicate"; } + void visit(Dictionary &) override { name_ = "Dictionary"; } + void visit(LiteralString &) override { name_ = "LiteralString"; } + void visit(CharacterClass &) override { name_ = "CharacterClass"; } + void visit(Character &) override { name_ = "Character"; } + void visit(AnyCharacter &) override { name_ = "AnyCharacter"; } + void visit(CaptureScope &) override { name_ = "CaptureScope"; } + void visit(Capture &) override { name_ = "Capture"; } + void visit(TokenBoundary &) override { name_ = "TokenBoundary"; } + void visit(Ignore &) override { name_ = "Ignore"; } + void visit(User &) override { name_ = "User"; } + void visit(WeakHolder &) override { name_ = "WeakHolder"; } + void visit(Holder &ope) override { name_ = ope.trace_name().data(); } + void visit(Reference &) override { name_ = "Reference"; } + void visit(Whitespace &) override { name_ = "Whitespace"; } + void visit(BackReference &) override { name_ = "BackReference"; } + void visit(PrecedenceClimbing &) override { name_ = "PrecedenceClimbing"; } + void visit(Recovery &) override { name_ = "Recovery"; } + void visit(Cut &) override { name_ = "Cut"; } + + static std::string get(Ope &ope) { + TraceOpeName vis; + ope.accept(vis); + return vis.name_; + } + +private: + const char *name_ = nullptr; +}; + +struct AssignIDToDefinition : public TraversalVisitor { + using TraversalVisitor::visit; + + void visit(Holder &ope) override; + void visit(Reference &ope) override; + void visit(PrecedenceClimbing &ope) override; + + std::unordered_map ids; +}; + +struct IsLiteralToken : public Ope::Visitor { + using Ope::Visitor::visit; + + void visit(PrioritizedChoice &ope) override { + for (const auto &op : ope.opes_) { + if (!IsLiteralToken::check(*op)) { return; } + } + result_ = true; + } + + void visit(Dictionary &) override { result_ = true; } + void visit(LiteralString &) override { result_ = true; } + + static bool check(Ope &ope) { + IsLiteralToken vis; + ope.accept(vis); + return vis.result_; + } + +private: + bool result_ = false; +}; + +struct TokenChecker : public TraversalVisitor { + using TraversalVisitor::visit; + + void visit(TokenBoundary &) override { has_token_boundary_ = true; } + void visit(AndPredicate &) override {} + void visit(NotPredicate &) override {} + void visit(WeakHolder &) override { has_rule_ = true; } + void visit(Reference &ope) override; + + static bool is_token(Ope &ope) { + if (IsLiteralToken::check(ope)) { return true; } + + TokenChecker vis; + ope.accept(vis); + return vis.has_token_boundary_ || !vis.has_rule_; + } + +private: + bool has_token_boundary_ = false; + bool has_rule_ = false; +}; + +struct FindLiteralToken : public Ope::Visitor { + using Ope::Visitor::visit; + + void visit(LiteralString &ope) override { token_ = ope.lit_.data(); } + void visit(TokenBoundary &ope) override { ope.ope_->accept(*this); } + void visit(Ignore &ope) override { ope.ope_->accept(*this); } + void visit(Reference &ope) override; + void visit(Recovery &ope) override { ope.ope_->accept(*this); } + + static const char *token(Ope &ope) { + FindLiteralToken vis; + ope.accept(vis); + return vis.token_; + } + +private: + const char *token_ = nullptr; +}; + +struct DetectLeftRecursion : public TraversalVisitor { + using TraversalVisitor::visit; + + DetectLeftRecursion(const std::string &name) : name_(name) {} + + void visit(Sequence &ope) override { + for (const auto &op : ope.opes_) { + op->accept(*this); + if (done_) { + break; + } else if (error_s) { + done_ = true; + break; + } + } + } + void visit(PrioritizedChoice &ope) override { + for (const auto &op : ope.opes_) { + op->accept(*this); + if (error_s) { + done_ = true; + break; + } + } + } + void visit(Repetition &ope) override { + ope.ope_->accept(*this); + done_ = ope.min_ > 0; + } + void visit(AndPredicate &ope) override { + ope.ope_->accept(*this); + done_ = false; + } + void visit(NotPredicate &ope) override { + ope.ope_->accept(*this); + done_ = false; + } + void visit(Dictionary &) override { done_ = true; } + void visit(LiteralString &ope) override { done_ = !ope.lit_.empty(); } + void visit(CharacterClass &) override { done_ = true; } + void visit(Character &) override { done_ = true; } + void visit(AnyCharacter &) override { done_ = true; } + void visit(User &) override { done_ = true; } + void visit(Reference &ope) override; + void visit(BackReference &) override { done_ = true; } + void visit(Cut &) override { done_ = true; } + + const char *error_s = nullptr; + + std::shared_ptr resolve_macro_arg(size_t iarg) const; + +private: + std::string name_; + std::unordered_set refs_; + bool done_ = false; + std::vector> *> macro_args_stack_; +}; + +struct ComputeCanBeEmpty : public TraversalVisitor { + using TraversalVisitor::visit; + + bool result = false; + + void visit(Sequence &ope) override { + result = std::all_of(ope.opes_.begin(), ope.opes_.end(), [](auto &op) { + ComputeCanBeEmpty vis; + op->accept(vis); + return vis.result; + }); + } + void visit(PrioritizedChoice &ope) override { + result = std::any_of(ope.opes_.begin(), ope.opes_.end(), [](auto &op) { + ComputeCanBeEmpty vis; + op->accept(vis); + return vis.result; + }); + } + void visit(Repetition &ope) override { result = ope.min_ == 0; } + void visit(AndPredicate &) override { result = true; } + void visit(NotPredicate &) override { result = true; } + void visit(Dictionary &) override { result = false; } + void visit(LiteralString &ope) override { result = ope.lit_.empty(); } + void visit(CharacterClass &) override { result = false; } + void visit(Character &) override { result = false; } + void visit(AnyCharacter &) override { result = false; } + void visit(User &) override { result = false; } + void visit(Reference &ope) override; + void visit(BackReference &) override { result = false; } + void visit(Cut &) override { result = false; } +}; + +struct HasEmptyElement : public TraversalVisitor { + using TraversalVisitor::visit; + + HasEmptyElement(std::vector> &refs, + std::unordered_map &has_error_cache) + : refs_(refs), has_error_cache_(has_error_cache) {} + + void visit(Sequence &ope) override; + void visit(PrioritizedChoice &ope) override { + for (const auto &op : ope.opes_) { + op->accept(*this); + if (is_empty) { return; } + } + } + void visit(Repetition &ope) override { + if (ope.min_ == 0) { + set_error(); + } else { + ope.ope_->accept(*this); + } + } + void visit(AndPredicate &) override { set_error(); } + void visit(NotPredicate &) override { set_error(); } + void visit(LiteralString &ope) override { + if (ope.lit_.empty()) { set_error(); } + } + void visit(Reference &ope) override; + + bool is_empty = false; + const char *error_s = nullptr; + std::string error_name; + +private: + void set_error() { + is_empty = true; + tie(error_s, error_name) = refs_.back(); + } + std::vector> &refs_; + std::unordered_map &has_error_cache_; +}; + +struct DetectInfiniteLoop : public TraversalVisitor { + using TraversalVisitor::visit; + + DetectInfiniteLoop(const char *s, const std::string &name, + std::vector> &refs, + std::unordered_map &has_error_cache) + : refs_(refs), has_error_cache_(has_error_cache) { + refs_.emplace_back(s, name); + } + + DetectInfiniteLoop(std::vector> &refs, + std::unordered_map &has_error_cache) + : refs_(refs), has_error_cache_(has_error_cache) {} + + void visit(Sequence &ope) override { + for (const auto &op : ope.opes_) { + op->accept(*this); + if (has_error) { return; } + } + } + void visit(PrioritizedChoice &ope) override { + for (const auto &op : ope.opes_) { + op->accept(*this); + if (has_error) { return; } + } + } + void visit(Repetition &ope) override { + if (ope.max_ == std::numeric_limits::max()) { + HasEmptyElement vis(refs_, has_error_cache_); + ope.ope_->accept(vis); + if (vis.is_empty) { + has_error = true; + error_s = vis.error_s; + error_name = vis.error_name; + } + } else { + ope.ope_->accept(*this); + } + } + void visit(Reference &ope) override; + + bool has_error = false; + const char *error_s = nullptr; + std::string error_name; + +private: + std::vector> &refs_; + std::unordered_map &has_error_cache_; +}; + +struct ReferenceChecker : public TraversalVisitor { + using TraversalVisitor::visit; + + ReferenceChecker(const Grammar &grammar, + const std::vector ¶ms) + : grammar_(grammar), params_(params) {} + + void visit(Reference &ope) override; + + std::unordered_map error_s; + std::unordered_map error_message; + std::unordered_set referenced; + +private: + const Grammar &grammar_; + const std::vector ¶ms_; +}; + +struct LinkReferences : public TraversalVisitor { + using TraversalVisitor::visit; + + LinkReferences(Grammar &grammar, const std::vector ¶ms) + : grammar_(grammar), params_(params) {} + + void visit(Reference &ope) override; + +private: + Grammar &grammar_; + const std::vector ¶ms_; +}; + +struct FindReference : public Ope::Visitor { + using Ope::Visitor::visit; + + FindReference(const std::vector> &args, + const std::vector ¶ms) + : args_(args), params_(params) {} + + void visit(Sequence &ope) override { + std::vector> opes; + for (const auto &o : ope.opes_) { + o->accept(*this); + opes.emplace_back(std::move(found_ope)); + } + found_ope = std::make_shared(opes); + } + void visit(PrioritizedChoice &ope) override { + std::vector> opes; + for (const auto &o : ope.opes_) { + o->accept(*this); + opes.emplace_back(std::move(found_ope)); + } + found_ope = std::make_shared(opes); + } + void visit(Repetition &ope) override { + ope.ope_->accept(*this); + found_ope = rep(found_ope, ope.min_, ope.max_); + } + void visit(AndPredicate &ope) override { + ope.ope_->accept(*this); + found_ope = apd(found_ope); + } + void visit(NotPredicate &ope) override { + ope.ope_->accept(*this); + found_ope = npd(found_ope); + } + void visit(Dictionary &ope) override { found_ope = ope.shared_from_this(); } + void visit(LiteralString &ope) override { + found_ope = ope.shared_from_this(); + } + void visit(CharacterClass &ope) override { + found_ope = ope.shared_from_this(); + } + void visit(Character &ope) override { found_ope = ope.shared_from_this(); } + void visit(AnyCharacter &ope) override { found_ope = ope.shared_from_this(); } + void visit(CaptureScope &ope) override { + ope.ope_->accept(*this); + found_ope = csc(found_ope); + } + void visit(Capture &ope) override { + ope.ope_->accept(*this); + found_ope = cap(found_ope, ope.match_action_); + } + void visit(TokenBoundary &ope) override { + ope.ope_->accept(*this); + found_ope = tok(found_ope); + } + void visit(Ignore &ope) override { + ope.ope_->accept(*this); + found_ope = ign(found_ope); + } + void visit(WeakHolder &ope) override { ope.weak_.lock()->accept(*this); } + void visit(Holder &ope) override { ope.ope_->accept(*this); } + void visit(Reference &ope) override; + void visit(Whitespace &ope) override { + ope.ope_->accept(*this); + found_ope = wsp(found_ope); + } + void visit(PrecedenceClimbing &ope) override { + ope.atom_->accept(*this); + found_ope = csc(found_ope); + } + void visit(Recovery &ope) override { + ope.ope_->accept(*this); + found_ope = rec(found_ope); + } + void visit(Cut &ope) override { found_ope = ope.shared_from_this(); } + + std::shared_ptr found_ope; + +private: + const std::vector> &args_; + const std::vector ¶ms_; +}; + +/* + * First-Set computation + */ +struct ComputeFirstSet : public TraversalVisitor { + using TraversalVisitor::visit; + + void visit(Sequence &ope) override { + for (const auto &op : ope.opes_) { + auto save = result_; + result_ = FirstSet{}; + op->accept(*this); + auto element_fs = result_; + result_ = save; + result_.chars |= element_fs.chars; + if (element_fs.any_char) { result_.any_char = true; } + if (!result_.first_literal) { + result_.first_literal = element_fs.first_literal; + } + if (!result_.first_rule) { result_.first_rule = element_fs.first_rule; } + if (!element_fs.can_be_empty) { return; } + // This element can be empty, continue to next + } + result_.can_be_empty = true; + } + void visit(PrioritizedChoice &ope) override { + auto save = result_; + for (const auto &op : ope.opes_) { + result_ = FirstSet{}; + op->accept(*this); + save.merge(result_); + } + result_ = save; + } + void visit(Repetition &ope) override { + ope.ope_->accept(*this); + if (ope.min_ == 0) { result_.can_be_empty = true; } + } + void visit(AndPredicate &) override { result_.can_be_empty = true; } + void visit(NotPredicate &) override { result_.can_be_empty = true; } + void visit(Dictionary &ope) override { + for (const auto &[key, info] : ope.trie_.dic_) { + if (!key.empty()) { + auto ch = static_cast(key[0]); + result_.chars.set(ch); + if (ope.trie_.ignore_case_) { + result_.chars.set(static_cast(std::toupper(ch))); + result_.chars.set(static_cast(std::tolower(ch))); + } + } + } + } + void visit(LiteralString &ope) override { + if (ope.lit_.empty()) { + result_.can_be_empty = true; + } else { + auto ch = static_cast(ope.lit_[0]); + result_.chars.set(ch); + if (ope.ignore_case_) { + result_.chars.set(static_cast(std::toupper(ch))); + result_.chars.set(static_cast(std::tolower(ch))); + } + if (!result_.first_literal) { result_.first_literal = ope.lit_.c_str(); } + } + } + void visit(CharacterClass &ope) override { + for (const auto &range : ope.ranges_) { + auto cp1 = range.first; + auto cp2 = range.second; + if (cp1 > 0x7F || cp2 > 0x7F) { + // Non-ASCII range: conservative fallback + result_.any_char = true; + return; + } + for (auto cp = cp1; cp <= cp2; cp++) { + auto ch = static_cast(cp); + result_.chars.set(ch); + if (ope.ignore_case_) { + result_.chars.set(static_cast(std::toupper(ch))); + result_.chars.set(static_cast(std::tolower(ch))); + } + } + } + if (ope.negated_) { + result_.chars.flip(); + result_.any_char = true; // negated class can match non-ASCII + } + } + void visit(Character &ope) override { + if (ope.ch_ > 0x7F) { + result_.any_char = true; + } else { + result_.chars.set(static_cast(ope.ch_)); + } + } + void visit(AnyCharacter &) override { result_.any_char = true; } + void visit(User &) override { result_.any_char = true; } + void visit(Reference &ope) override; + void visit(BackReference &) override { result_.any_char = true; } + void visit(Cut &) override { result_.can_be_empty = true; } + + FirstSet result_; + +private: + std::unordered_set refs_; +}; + +struct SetupFirstSets : public TraversalVisitor { + using TraversalVisitor::visit; + + void visit(Sequence &ope) override; + void setup_keyword_guarded_identifier(Sequence &ope); + + void visit(PrioritizedChoice &ope) override { + ope.first_sets_.clear(); + ope.first_sets_.reserve(ope.opes_.size()); + for (const auto &op : ope.opes_) { + ComputeFirstSet cfs; + op->accept(cfs); + ope.first_sets_.push_back(cfs.result_); + } + for (const auto &op : ope.opes_) { + op->accept(*this); + } + } + void visit(Repetition &ope) override { + ope.ope_->accept(*this); + // ISpan optimization: detect Repetition + ASCII CharacterClass + auto cc = dynamic_cast(ope.ope_.get()); + if (cc && cc->is_ascii_only()) { ope.span_bitset_ = &cc->ascii_bitset(); } + } + void visit(Reference &ope) override; + +private: + std::unordered_set refs_; +}; + +/* + * Keywords + */ +static const char *WHITESPACE_DEFINITION_NAME = "%whitespace"; +static const char *WORD_DEFINITION_NAME = "%word"; +static const char *RECOVER_DEFINITION_NAME = "%recover"; + +/* + * Definition + */ +class Definition { +public: + struct Result { + bool ret; + bool recovered; + size_t len; + ErrorInfo error_info; + }; + + Definition() : holder_(std::make_shared(this)) {} + + Definition(const Definition &rhs) : name(rhs.name), holder_(rhs.holder_) { + holder_->outer_ = this; + } + + Definition(const std::shared_ptr &ope) + : holder_(std::make_shared(this)) { + *this <= ope; + } + + operator std::shared_ptr() { + return std::make_shared(holder_); + } + + Definition &operator<=(const std::shared_ptr &ope) { + holder_->ope_ = ope; + return *this; + } + + Result parse(const char *s, size_t n, const char *path = nullptr, + Log log = nullptr) const { + SemanticValues vs; + std::any dt; + return parse_core(s, n, vs, dt, path, log); + } + + Result parse(const char *s, const char *path = nullptr, + Log log = nullptr) const { + auto n = strlen(s); + return parse(s, n, path, log); + } + + Result parse(const char *s, size_t n, std::any &dt, + const char *path = nullptr, Log log = nullptr) const { + SemanticValues vs; + return parse_core(s, n, vs, dt, path, log); + } + + Result parse(const char *s, std::any &dt, const char *path = nullptr, + Log log = nullptr) const { + auto n = strlen(s); + return parse(s, n, dt, path, log); + } + + template + Result parse_and_get_value(const char *s, size_t n, T &val, + const char *path = nullptr, + Log log = nullptr) const { + SemanticValues vs; + std::any dt; + auto r = parse_core(s, n, vs, dt, path, log); + if (r.ret && !vs.empty() && vs.front().has_value()) { + val = std::any_cast(vs[0]); + } + return r; + } + + template + Result parse_and_get_value(const char *s, T &val, const char *path = nullptr, + Log log = nullptr) const { + auto n = strlen(s); + return parse_and_get_value(s, n, val, path, log); + } + + template + Result parse_and_get_value(const char *s, size_t n, std::any &dt, T &val, + const char *path = nullptr, + Log log = nullptr) const { + SemanticValues vs; + auto r = parse_core(s, n, vs, dt, path, log); + if (r.ret && !vs.empty() && vs.front().has_value()) { + val = std::any_cast(vs[0]); + } + return r; + } + + template + Result parse_and_get_value(const char *s, std::any &dt, T &val, + const char *path = nullptr, + Log log = nullptr) const { + auto n = strlen(s); + return parse_and_get_value(s, n, dt, val, path, log); + } + +#if defined(__cpp_lib_char8_t) + Result parse(const char8_t *s, size_t n, const char *path = nullptr, + Log log = nullptr) const { + return parse(reinterpret_cast(s), n, path, log); + } + + Result parse(const char8_t *s, const char *path = nullptr, + Log log = nullptr) const { + return parse(reinterpret_cast(s), path, log); + } + + Result parse(const char8_t *s, size_t n, std::any &dt, + const char *path = nullptr, Log log = nullptr) const { + return parse(reinterpret_cast(s), n, dt, path, log); + } + + Result parse(const char8_t *s, std::any &dt, const char *path = nullptr, + Log log = nullptr) const { + return parse(reinterpret_cast(s), dt, path, log); + } + + template + Result parse_and_get_value(const char8_t *s, size_t n, T &val, + const char *path = nullptr, + Log log = nullptr) const { + return parse_and_get_value(reinterpret_cast(s), n, val, path, + log); + } + + template + Result parse_and_get_value(const char8_t *s, T &val, + const char *path = nullptr, + Log log = nullptr) const { + return parse_and_get_value(reinterpret_cast(s), val, path, + log); + } + + template + Result parse_and_get_value(const char8_t *s, size_t n, std::any &dt, T &val, + const char *path = nullptr, + Log log = nullptr) const { + return parse_and_get_value(reinterpret_cast(s), n, dt, val, + path, log); + } + + template + Result parse_and_get_value(const char8_t *s, std::any &dt, T &val, + const char *path = nullptr, + Log log = nullptr) const { + return parse_and_get_value(reinterpret_cast(s), dt, val, path, + log); + } +#endif + + void operator=(Action a) { action = a; } + + template Definition &operator,(T fn) { + operator=(fn); + return *this; + } + + Definition &operator~() { + ignoreSemanticValue = true; + return *this; + } + + void accept(Ope::Visitor &v) { holder_->accept(v); } + + std::shared_ptr get_core_operator() const { return holder_->ope_; } + + bool is_token() const { + std::call_once(is_token_init_, [this]() { + is_token_ = TokenChecker::is_token(*get_core_operator()); + }); + return is_token_; + } + + std::string name; + const char *s_ = nullptr; + std::pair line_ = {1, 1}; + + Predicate predicate; + + size_t id = 0; + Action action; + std::function + enter; + std::function + leave; + bool ignoreSemanticValue = false; + std::shared_ptr whitespaceOpe; + std::shared_ptr wordOpe; + bool enablePackratParsing = false; + bool is_macro = false; + std::vector params; + bool disable_action = false; + bool is_left_recursive = false; + bool can_be_empty = false; + + TracerEnter tracer_enter; + TracerLeave tracer_leave; + bool verbose_trace = false; + TracerStartOrEnd tracer_start; + TracerStartOrEnd tracer_end; + + std::string error_message; + bool no_ast_opt = false; + + bool eoi_check = true; + + // Per-rule packrat stats (optional, for profiling) + mutable bool collect_packrat_stats = false; + mutable std::vector packrat_stats_; + +private: + friend class Reference; + friend class ParserGenerator; + + Definition &operator=(const Definition &rhs); + Definition &operator=(Definition &&rhs); + + void initialize_definition_ids() const { + std::call_once(definition_ids_init_, [&]() { + AssignIDToDefinition vis; + holder_->accept(vis); + if (whitespaceOpe) { whitespaceOpe->accept(vis); } + if (wordOpe) { wordOpe->accept(vis); } + definition_ids_.swap(vis.ids); + }); + } + + void initialize_packrat_filter() const; + + Result parse_core(const char *s, size_t n, SemanticValues &vs, std::any &dt, + const char *path, Log log) const { + initialize_definition_ids(); + + std::shared_ptr ope = holder_; + + std::any trace_data; + if (tracer_start) { tracer_start(trace_data); } + auto se = scope_exit([&]() { + if (tracer_end) { tracer_end(trace_data); } + }); + + Context c(path, s, n, definition_ids_.size(), whitespaceOpe, wordOpe, + enablePackratParsing, tracer_enter, tracer_leave, trace_data, + verbose_trace, log); + + if (collect_packrat_stats) { + packrat_stats_.resize(definition_ids_.size()); + c.packrat_stats = &packrat_stats_; + } + + if (enablePackratParsing) { + initialize_packrat_filter(); + if (!packrat_filter_.empty()) { + c.packrat_rule_filter = &packrat_filter_; + } + } + + size_t i = 0; + + if (whitespaceOpe) { + auto save_ignore_trace_state = c.ignore_trace_state; + c.ignore_trace_state = !c.verbose_trace; + auto se = + scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); + + auto len = whitespaceOpe->parse(s, n, vs, c, dt); + if (fail(len)) { return Result{false, c.recovered, i, c.error_info}; } + + i = len; + } + + auto len = ope->parse(s + i, n - i, vs, c, dt); + auto ret = success(len); + if (ret) { + i += len; + if (eoi_check) { + if (i < n) { + if (c.error_info.error_pos - c.s < s + i - c.s) { + c.error_info.message_pos = s + i; + c.error_info.message = "expected end of input"; + } + ret = false; + } + } + } + return Result{ret, c.recovered, i, c.error_info}; + } + + std::shared_ptr holder_; + mutable std::once_flag is_token_init_; + mutable bool is_token_ = false; + mutable std::once_flag assign_id_to_definition_init_; + mutable std::once_flag definition_ids_init_; + mutable std::unordered_map definition_ids_; + mutable std::once_flag packrat_filter_init_; + mutable std::vector packrat_filter_; +}; + +/* + * Implementations + */ + +inline size_t parse_literal(const char *s, size_t n, SemanticValues &vs, + Context &c, std::any &dt, const std::string &lit, + std::once_flag &init_is_word, bool &is_word, + bool ignore_case, const std::string &lower_lit) { + size_t i = 0; + for (; i < lit.size(); i++) { + if (i >= n || + (ignore_case ? (static_cast(std::tolower( + static_cast(s[i]))) != lower_lit[i]) + : (s[i] != lit[i]))) { + c.set_error_pos(s, lit.data()); + return static_cast(-1); + } + } + + // Word check + if (c.wordOpe) { + auto save_ignore_trace_state = c.ignore_trace_state; + c.ignore_trace_state = !c.verbose_trace; + auto se = + scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); + + std::call_once(init_is_word, [&]() { + SemanticValues dummy_vs; + Context dummy_c(nullptr, c.s, c.l, 0, nullptr, nullptr, false, nullptr, + nullptr, nullptr, false, nullptr); + std::any dummy_dt; + + auto len = + c.wordOpe->parse(lit.data(), lit.size(), dummy_vs, dummy_c, dummy_dt); + is_word = success(len); + }); + + if (is_word) { + SemanticValues dummy_vs; + Context dummy_c(nullptr, c.s, c.l, 0, nullptr, nullptr, false, nullptr, + nullptr, nullptr, false, nullptr); + std::any dummy_dt; + + NotPredicate ope(c.wordOpe); + auto len = ope.parse(s + i, n - i, dummy_vs, dummy_c, dummy_dt); + if (fail(len)) { + c.set_error_pos(s, lit.data()); + return len; + } + i += len; + } + } + + // Skip whitespace + auto wl = c.skip_whitespace(s + i, n - i, vs, dt); + if (fail(wl)) { return wl; } + i += wl; + + return i; +} + +inline std::pair SemanticValues::line_info() const { + assert(c_); + return c_->line_info(sv_.data()); +} + +inline void ErrorInfo::output_log(const Log &log, const char *s, size_t n) { + if (message_pos) { + if (message_pos > last_output_pos) { + last_output_pos = message_pos; + auto line = line_info(s, message_pos); + std::string msg; + if (auto unexpected_token = heuristic_error_token(s, n, message_pos); + !unexpected_token.empty()) { + msg = replace_all(message, "%t", unexpected_token); + + auto unexpected_char = unexpected_token.substr( + 0, + codepoint_length(unexpected_token.data(), unexpected_token.size())); + + msg = replace_all(msg, "%c", unexpected_char); + } else { + msg = message; + } + log(line.first, line.second, msg, label); + } + } else if (error_pos) { + if (error_pos > last_output_pos) { + last_output_pos = error_pos; + auto line = line_info(s, error_pos); + + std::string msg; + if (expected_tokens.empty()) { + msg = "syntax error."; + } else { + msg = "syntax error"; + + // unexpected token + if (auto unexpected_token = heuristic_error_token(s, n, error_pos); + !unexpected_token.empty()) { + msg += ", unexpected '"; + msg += unexpected_token; + msg += "'"; + } + + auto first_item = true; + size_t i = 0; + while (i < expected_tokens.size()) { + auto [error_literal, error_rule] = expected_tokens[i]; + + // Skip rules start with '_' + if (!(error_rule && error_rule->name[0] == '_')) { + msg += (first_item ? ", expecting " : ", "); + if (error_literal) { + msg += "'"; + msg += error_literal; + msg += "'"; + } else { + msg += "<" + error_rule->name + ">"; + if (label.empty()) { label = error_rule->name; } + } + first_item = false; + } + + i++; + } + msg += "."; + } + log(line.first, line.second, msg, label); + } + } +} + +inline size_t Context::skip_whitespace(const char *a_s, size_t n, + SemanticValues &vs, std::any &dt) { + if (in_token_boundary_count || !whitespaceOpe) { return 0; } + auto save = ignore_trace_state; + ignore_trace_state = !verbose_trace; + auto se = scope_exit([&]() { ignore_trace_state = save; }); + return whitespaceOpe->parse(a_s, n, vs, *this, dt); +} + +inline void Context::set_error_pos(const char *a_s, const char *literal) { + if (log) { + if (error_info.error_pos <= a_s) { + if (error_info.error_pos < a_s || !error_info.keep_previous_token) { + error_info.error_pos = a_s; + error_info.expected_tokens.clear(); + } + + const char *error_literal = nullptr; + const Definition *error_rule = nullptr; + + if (literal) { + error_literal = literal; + } else if (!rule_stack.empty()) { + auto rule = rule_stack.back(); + auto ope = rule->get_core_operator(); + if (auto token = FindLiteralToken::token(*ope); + token && token[0] != '\0') { + error_literal = token; + } + } + + for (auto r : rule_stack) { + error_rule = r; + if (r->is_token()) { break; } + } + + if (error_literal || error_rule) { + error_info.add(error_literal, error_rule); + } + } + } +} + +inline void Context::trace_enter(const Ope &ope, const char *a_s, size_t n, + const SemanticValues &vs, std::any &dt) { + trace_ids.push_back(next_trace_id++); + tracer_enter(ope, a_s, n, vs, *this, dt, trace_data); +} + +inline void Context::trace_leave(const Ope &ope, const char *a_s, size_t n, + const SemanticValues &vs, std::any &dt, + size_t len) { + tracer_leave(ope, a_s, n, vs, *this, dt, len, trace_data); + trace_ids.pop_back(); +} + +inline bool Context::is_traceable(const Ope &ope) const { + if (tracer_enter && tracer_leave) { + if (ignore_trace_state) { return false; } + return !dynamic_cast(&ope); + } + return false; +} + +inline size_t Ope::parse(const char *s, size_t n, SemanticValues &vs, + Context &c, std::any &dt) const { + if (c.is_traceable(*this)) { + c.trace_enter(*this, s, n, vs, dt); + auto len = parse_core(s, n, vs, c, dt); + c.trace_leave(*this, s, n, vs, dt, len); + return len; + } + return parse_core(s, n, vs, c, dt); +} + +inline size_t Dictionary::parse_core(const char *s, size_t n, + SemanticValues &vs, Context &c, + std::any &dt) const { + size_t id; + auto i = trie_.match(s, n, id); + + if (i == 0) { + c.set_error_pos(s); + return static_cast(-1); + } + + vs.choice_count_ = trie_.items_count(); + vs.choice_ = id; + + // Word check + if (c.wordOpe) { + auto save_ignore_trace_state = c.ignore_trace_state; + c.ignore_trace_state = !c.verbose_trace; + auto se = + scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); + + { + SemanticValues dummy_vs; + Context dummy_c(nullptr, c.s, c.l, 0, nullptr, nullptr, false, nullptr, + nullptr, nullptr, false, nullptr); + std::any dummy_dt; + + NotPredicate ope(c.wordOpe); + auto len = ope.parse(s + i, n - i, dummy_vs, dummy_c, dummy_dt); + if (fail(len)) { + c.set_error_pos(s); + return len; + } + i += len; + } + } + + // Skip whitespace + auto wl = c.skip_whitespace(s + i, n - i, vs, dt); + if (fail(wl)) { return wl; } + i += wl; + + return i; +} + +inline size_t LiteralString::parse_core(const char *s, size_t n, + SemanticValues &vs, Context &c, + std::any &dt) const { + return parse_literal(s, n, vs, c, dt, lit_, init_is_word_, is_word_, + ignore_case_, lower_lit_); +} + +inline size_t TokenBoundary::parse_core(const char *s, size_t n, + SemanticValues &vs, Context &c, + std::any &dt) const { + auto save_ignore_trace_state = c.ignore_trace_state; + c.ignore_trace_state = !c.verbose_trace; + auto se = + scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); + + size_t len; + { + c.in_token_boundary_count++; + auto se = scope_exit([&]() { c.in_token_boundary_count--; }); + len = ope_->parse(s, n, vs, c, dt); + } + + if (success(len)) { + vs.tokens.emplace_back(std::string_view(s, len)); + + auto wl = c.skip_whitespace(s + len, n - len, vs, dt); + if (fail(wl)) { return wl; } + len += wl; + } + return len; +} + +inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, + Context &c, std::any &dt) const { + if (!ope_) { + throw std::logic_error("Uninitialized definition ope was used..."); + } + + // Macro reference + if (outer_->is_macro) { + c.rule_stack.push_back(outer_); + auto len = ope_->parse(s, n, vs, c, dt); + c.rule_stack.pop_back(); + return len; + } + + size_t len; + std::any val; + + // Shared parse body: invokes enter/leave callbacks, parses the rule's + // operator, handles actions/predicates/errors, and calls reduce. + // Returns {parse_len, parse_val}. + auto do_parse = [&]() { + size_t parse_len; + std::any parse_val; + + if (outer_->enter) { outer_->enter(c, s, n, dt); } + auto &chvs = c.push_semantic_values_scope(); + auto se = scope_exit([&]() { + c.pop_semantic_values_scope(); + if (outer_->leave) { outer_->leave(c, s, n, parse_len, parse_val, dt); } + }); + + c.rule_stack.push_back(outer_); + parse_len = ope_->parse(s, n, chvs, c, dt); + c.rule_stack.pop_back(); + + if (success(parse_len)) { + chvs.sv_ = std::string_view(s, parse_len); + chvs.name_ = outer_->name; + + auto ope_ptr = ope_.get(); + if (ope_ptr->is_token_boundary) { + ope_ptr = static_cast(ope_ptr)->ope_.get(); + } + if (!ope_ptr->is_choice_like) { + chvs.choice_count_ = 0; + chvs.choice_ = 0; + } + + std::string msg; + std::any predicate_data; + if (outer_->predicate) { + if (!outer_->predicate(chvs, dt, msg, predicate_data)) { + if (c.log && !msg.empty() && c.error_info.message_pos < s) { + c.error_info.message_pos = s; + c.error_info.message = msg; + c.error_info.label = outer_->name; + } + parse_len = static_cast(-1); + } + } + + if (success(parse_len)) { + if (!c.recovered) { parse_val = reduce(chvs, dt, predicate_data); } + } else { + if (c.log && !msg.empty() && c.error_info.message_pos < s) { + c.error_info.message_pos = s; + c.error_info.message = msg; + c.error_info.label = outer_->name; + } + } + } else { + if (c.log && !outer_->error_message.empty() && + c.error_info.message_pos < s) { + c.error_info.message_pos = s; + c.error_info.message = outer_->error_message; + c.error_info.label = outer_->name; + } + } + + return std::make_pair(parse_len, std::move(parse_val)); + }; + + if (outer_->is_left_recursive) { + auto lr_key = std::make_pair(outer_, s); + + // Check LR memo first + auto it = c.lr_memo.find(lr_key); + if (it != c.lr_memo.end()) { + if (success(it->second.len)) { + len = it->second.len; + val = it->second.val; + } else { + len = static_cast(-1); + } + // Record that this rule's lr_memo was accessed. + // Any LR rule currently seeding will know we're in its cycle. + c.lr_refs_hit.insert(outer_); + } else { + // Seed with FAIL + c.lr_memo[lr_key] = {static_cast(-1), {}}; + + // Mark as active seed (protects our lr_memo from inner growers) + c.lr_active_seeds.insert(lr_key); + auto seed_guard = scope_exit([&]() { c.lr_active_seeds.erase(lr_key); }); + + // Track which LR rules are referenced during our parse + // to identify cycle members + auto saved_refs = std::move(c.lr_refs_hit); + c.lr_refs_hit.clear(); + + // Initial parse (self-references will hit the FAIL seed) + auto [initial_len, initial_val] = do_parse(); + + // Rules whose lr_memo was hit during our parse are in our cycle. + // If we detected cycle members, we ourselves are also part of + // the cycle, so add self — this lets parent seeders see us as + // a transitive cycle member. + auto cycle_rules = c.lr_refs_hit; + if (!cycle_rules.empty()) { cycle_rules.insert(outer_); } + + // Restore parent's refs and propagate cycle info upward + c.lr_refs_hit = std::move(saved_refs); + c.lr_refs_hit.insert(cycle_rules.begin(), cycle_rules.end()); + + if (!success(initial_len)) { + // Keep FAIL in lr_memo so we don't re-seed + len = static_cast(-1); + } else { + // Got initial seed, now grow + len = initial_len; + val = std::move(initial_val); + c.lr_memo[lr_key] = {len, val}; + + while (true) { + // Clear this rule's packrat cache + c.clear_packrat_cache(s, outer_->id); + + // Clear lr_memo for cycle-dependent rules at this position, + // but NOT for rules currently in their own seeding phase + // (lr_active_seeds) — those are outer growers we must not + // interfere with. + for (auto memo_it = c.lr_memo.begin(); memo_it != c.lr_memo.end();) { + if (memo_it->first.second == s && memo_it->first.first != outer_ && + cycle_rules.count(memo_it->first.first) && + !c.lr_active_seeds.count(memo_it->first)) { + memo_it = c.lr_memo.erase(memo_it); + } else { + ++memo_it; + } + } + + auto [new_len, new_val] = do_parse(); + + if (!success(new_len) || new_len <= len) { + break; // No improvement, done growing + } + + len = new_len; + val = std::move(new_val); + c.lr_memo[lr_key] = {len, val}; + } + } + + // Write final result to packrat cache (lr_memo entry is kept as + // the primary lookup for LR rules at this position) + if (success(len)) { c.write_packrat_cache(s, outer_->id, len, val); } + } + } else { + if (c.enablePackratParsing) { + // Packrat cache acts as re-entry guard (pre-registered as + // failure before fn is called). + c.packrat(s, outer_->id, len, val, [&](std::any &a_val) { + auto [parse_len, parse_val] = do_parse(); + len = parse_len; + if (success(len)) { a_val = std::move(parse_val); } + }); + } else { + // Without packrat, use lr_memo as re-entry guard to prevent + // stack overflow from undetected left recursion. + auto guard_key = std::make_pair(outer_, s); + if (c.lr_memo.count(guard_key)) { + len = static_cast(-1); + } else { + c.lr_memo[guard_key] = {static_cast(-1), {}}; + auto [parse_len, parse_val] = do_parse(); + len = parse_len; + val = std::move(parse_val); + c.lr_memo.erase(guard_key); + } + } + } + + if (success(len)) { + if (!outer_->ignoreSemanticValue) { + vs.emplace_back(std::move(val)); + vs.tags.emplace_back(str2tag(outer_->name)); + } + } + + return len; +} + +inline std::any Holder::reduce(SemanticValues &vs, std::any &dt, + const std::any &predicate_data) const { + if (outer_->action && !outer_->disable_action) { + return outer_->action(vs, dt, predicate_data); + } else if (vs.empty()) { + return std::any(); + } else { + return std::move(vs.front()); + } +} + +inline const std::string &Holder::name() const { return outer_->name; } + +inline const std::string &Holder::trace_name() const { + std::call_once(trace_name_init_, + [this]() { trace_name_ = "[" + outer_->name + "]"; }); + return trace_name_; +} + +inline size_t Reference::parse_core(const char *s, size_t n, SemanticValues &vs, + Context &c, std::any &dt) const { + auto save_ignore_trace_state = c.ignore_trace_state; + if (rule_ && rule_->ignoreSemanticValue) { + c.ignore_trace_state = !c.verbose_trace; + } + auto se = + scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); + + if (rule_) { + // Reference rule + if (rule_->is_macro) { + // Macro + FindReference vis(c.top_args(), c.rule_stack.back()->params); + + // Collect arguments + std::vector> args; + for (const auto &arg : args_) { + arg->accept(vis); + args.emplace_back(std::move(vis.found_ope)); + } + + c.push_args(std::move(args)); + auto se = scope_exit([&]() { c.pop_args(); }); + return rule_->holder_->parse(s, n, vs, c, dt); + } else { + // Definition + c.push_args(std::vector>()); + auto se2 = scope_exit([&]() { c.pop_args(); }); + return rule_->holder_->parse(s, n, vs, c, dt); + } + } else { + // Reference parameter in macro + const auto &args = c.top_args(); + return args[iarg_]->parse(s, n, vs, c, dt); + } +} + +inline std::shared_ptr Reference::get_core_operator() const { + return rule_->holder_; +} + +inline size_t BackReference::parse_core(const char *s, size_t n, + SemanticValues &vs, Context &c, + std::any &dt) const { + for (auto it = c.capture_entries.rbegin(); it != c.capture_entries.rend(); + ++it) { + if (it->first == name_) { + const auto &lit = it->second; + std::once_flag init_is_word; + auto is_word = false; + static const std::string empty; + return parse_literal(s, n, vs, c, dt, lit, init_is_word, is_word, false, + empty); + } + } + + c.error_info.message_pos = s; + c.error_info.message = "undefined back reference '$" + name_ + "'..."; + return static_cast(-1); +} + +inline Definition & +PrecedenceClimbing::get_reference_for_binop(Context &c) const { + if (rule_.is_macro) { + // Reference parameter in macro + const auto &args = c.top_args(); + auto iarg = dynamic_cast(*binop_).iarg_; + auto arg = args[iarg]; + return *dynamic_cast(*arg).rule_; + } + + return *dynamic_cast(*binop_).rule_; +} + +inline size_t PrecedenceClimbing::parse_expression(const char *s, size_t n, + SemanticValues &vs, + Context &c, std::any &dt, + size_t min_prec) const { + auto len = atom_->parse(s, n, vs, c, dt); + if (fail(len)) { return len; } + + std::string tok; + auto &rule = get_reference_for_binop(c); + auto action = std::move(rule.action); + + rule.action = [&](SemanticValues &vs2, std::any &dt2, + const std::any &predicate_data2) { + tok = vs2.token(); + if (action) { + return action(vs2, dt2, predicate_data2); + } else if (!vs2.empty()) { + return vs2[0]; + } + return std::any(); + }; + auto action_se = scope_exit([&]() { rule.action = std::move(action); }); + + auto i = len; + while (i < n) { + std::vector save_values(vs.begin(), vs.end()); + auto save_tokens = vs.tokens; + + auto chvs = c.push_semantic_values_scope(); + auto chlen = binop_->parse(s + i, n - i, chvs, c, dt); + c.pop_semantic_values_scope(); + + if (fail(chlen)) { break; } + + auto it = info_.find(tok); + if (it == info_.end()) { break; } + + auto level = std::get<0>(it->second); + auto assoc = std::get<1>(it->second); + + if (level < min_prec) { break; } + + vs.emplace_back(std::move(chvs[0])); + i += chlen; + + auto next_min_prec = level; + if (assoc == 'L') { next_min_prec = level + 1; } + + chvs = c.push_semantic_values_scope(); + chlen = parse_expression(s + i, n - i, chvs, c, dt, next_min_prec); + c.pop_semantic_values_scope(); + + if (fail(chlen)) { + vs.assign(save_values.begin(), save_values.end()); + vs.tokens = save_tokens; + i = chlen; + break; + } + + vs.emplace_back(std::move(chvs[0])); + i += chlen; + + std::any val; + if (rule_.action) { + vs.sv_ = std::string_view(s, i); + static const std::any empty_predicate_data; + val = rule_.action(vs, dt, empty_predicate_data); + } else if (!vs.empty()) { + val = vs[0]; + } + vs.clear(); + vs.emplace_back(std::move(val)); + } + + return i; +} + +inline size_t Recovery::parse_core(const char *s, size_t n, + SemanticValues & /*vs*/, Context &c, + std::any & /*dt*/) const { + const auto &rule = dynamic_cast(*ope_); + + // Custom error message + if (c.log) { + auto label = dynamic_cast(rule.args_[0].get()); + if (label && !label->rule_->error_message.empty()) { + c.error_info.message_pos = s; + c.error_info.message = label->rule_->error_message; + c.error_info.label = label->rule_->name; + } + } + + // Recovery + auto len = static_cast(-1); + { + auto save_log = c.log; + c.log = nullptr; + auto se = scope_exit([&]() { c.log = save_log; }); + + SemanticValues dummy_vs; + std::any dummy_dt; + + len = rule.parse(s, n, dummy_vs, c, dummy_dt); + } + + if (success(len)) { + c.recovered = true; + + if (c.log) { + c.error_info.output_log(c.log, c.s, c.l); + c.error_info.clear(); + } + } + + // Cut + if (!c.cut_stack.empty()) { + c.cut_stack.back() = true; + + if (c.cut_stack.size() == 1) { + // TODO: Remove unneeded entries in packrat memoise table + } + } + + return len; +} + +inline void Sequence::accept(Visitor &v) { v.visit(*this); } +inline void PrioritizedChoice::accept(Visitor &v) { v.visit(*this); } +inline void Repetition::accept(Visitor &v) { v.visit(*this); } +inline void AndPredicate::accept(Visitor &v) { v.visit(*this); } +inline void NotPredicate::accept(Visitor &v) { v.visit(*this); } +inline void Dictionary::accept(Visitor &v) { v.visit(*this); } +inline void LiteralString::accept(Visitor &v) { v.visit(*this); } +inline void CharacterClass::accept(Visitor &v) { v.visit(*this); } +inline void Character::accept(Visitor &v) { v.visit(*this); } +inline void AnyCharacter::accept(Visitor &v) { v.visit(*this); } +inline void CaptureScope::accept(Visitor &v) { v.visit(*this); } +inline void Capture::accept(Visitor &v) { v.visit(*this); } +inline void TokenBoundary::accept(Visitor &v) { v.visit(*this); } +inline void Ignore::accept(Visitor &v) { v.visit(*this); } +inline void User::accept(Visitor &v) { v.visit(*this); } +inline void WeakHolder::accept(Visitor &v) { v.visit(*this); } +inline void Holder::accept(Visitor &v) { v.visit(*this); } +inline void Reference::accept(Visitor &v) { v.visit(*this); } +inline void Whitespace::accept(Visitor &v) { v.visit(*this); } +inline void BackReference::accept(Visitor &v) { v.visit(*this); } +inline void PrecedenceClimbing::accept(Visitor &v) { v.visit(*this); } +inline void Recovery::accept(Visitor &v) { v.visit(*this); } +inline void Cut::accept(Visitor &v) { v.visit(*this); } + +inline void AssignIDToDefinition::visit(Holder &ope) { + auto p = static_cast(ope.outer_); + if (ids.count(p)) { return; } + auto id = ids.size(); + ids[p] = id; + ope.outer_->id = id; + ope.ope_->accept(*this); +} + +inline void AssignIDToDefinition::visit(Reference &ope) { + if (ope.rule_) { + for (const auto &arg : ope.args_) { + arg->accept(*this); + } + ope.rule_->accept(*this); + } +} + +inline void AssignIDToDefinition::visit(PrecedenceClimbing &ope) { + ope.atom_->accept(*this); + ope.binop_->accept(*this); +} + +inline void TokenChecker::visit(Reference &ope) { + if (ope.is_macro_) { + for (const auto &arg : ope.args_) { + arg->accept(*this); + } + } else { + has_rule_ = true; + } +} + +inline void FindLiteralToken::visit(Reference &ope) { + if (ope.is_macro_) { + ope.rule_->accept(*this); + for (const auto &arg : ope.args_) { + arg->accept(*this); + } + } +} + +inline void ComputeCanBeEmpty::visit(Reference &ope) { + result = ope.rule_ && ope.rule_->can_be_empty; +} + +inline void DetectLeftRecursion::visit(Reference &ope) { + if (ope.name_ == name_) { + error_s = ope.s_; + } else if (!ope.rule_ && !macro_args_stack_.empty()) { + // Macro parameter reference: resolve through nested macro arg + // stacks (e.g. B(X) <- C(X) where X is itself a param ref). + auto resolved = resolve_macro_arg(ope.iarg_); + if (resolved) { + resolved->accept(*this); + if (done_ == false) { return; } + } + } else if (!refs_.count(ope.name_)) { + refs_.insert(ope.name_); + if (ope.rule_) { + if (ope.is_macro_) { macro_args_stack_.push_back(&ope.args_); } + ope.rule_->accept(*this); + if (ope.is_macro_) { macro_args_stack_.pop_back(); } + if (done_ == false) { return; } + } + } + // If the referenced rule can match empty, don't mark as done — + // the sequence may continue past this element to find LR. + if (!ope.rule_ && !macro_args_stack_.empty()) { + auto resolved = resolve_macro_arg(ope.iarg_); + if (resolved) { + ComputeCanBeEmpty cbe; + resolved->accept(cbe); + done_ = !cbe.result; + } else { + done_ = true; + } + } else { + done_ = !(ope.rule_ && ope.rule_->can_be_empty); + } +} + +inline std::shared_ptr +DetectLeftRecursion::resolve_macro_arg(size_t iarg) const { + for (int i = static_cast(macro_args_stack_.size()) - 1; i >= 0; i--) { + auto &args = *macro_args_stack_[i]; + if (iarg >= args.size()) { return nullptr; } + auto ref = dynamic_cast(args[iarg].get()); + if (ref && !ref->rule_) { + // Another param ref — resolve using parent level's args + iarg = ref->iarg_; + continue; + } + return args[iarg]; + } + return nullptr; +} + +inline void HasEmptyElement::visit(Sequence &ope) { + auto save_is_empty = false; + const char *save_error_s = nullptr; + std::string save_error_name; + + auto it = ope.opes_.begin(); + while (it != ope.opes_.end()) { + (*it)->accept(*this); + if (!is_empty) { + ++it; + while (it != ope.opes_.end()) { + DetectInfiniteLoop vis(refs_, has_error_cache_); + (*it)->accept(vis); + if (vis.has_error) { + is_empty = true; + error_s = vis.error_s; + error_name = vis.error_name; + } + ++it; + } + return; + } + + save_is_empty = is_empty; + save_error_s = error_s; + save_error_name = error_name; + + is_empty = false; + error_name.clear(); + ++it; + } + + is_empty = save_is_empty; + error_s = save_error_s; + error_name = save_error_name; +} + +inline void HasEmptyElement::visit(Reference &ope) { + auto it = std::find_if(refs_.begin(), refs_.end(), + [&](const std::pair &ref) { + return ope.name_ == ref.second; + }); + if (it != refs_.end()) { return; } + + if (ope.rule_) { + refs_.emplace_back(ope.s_, ope.name_); + ope.rule_->accept(*this); + refs_.pop_back(); + } +} + +inline void DetectInfiniteLoop::visit(Reference &ope) { + auto it = std::find_if(refs_.begin(), refs_.end(), + [&](const std::pair &ref) { + return ope.name_ == ref.second; + }); + if (it != refs_.end()) { return; } + + if (ope.rule_) { + auto it = has_error_cache_.find(ope.name_); + if (it != has_error_cache_.end()) { + has_error = it->second; + } else { + refs_.emplace_back(ope.s_, ope.name_); + ope.rule_->accept(*this); + refs_.pop_back(); + has_error_cache_[ope.name_] = has_error; + } + } + + if (ope.is_macro_) { + for (const auto &arg : ope.args_) { + arg->accept(*this); + } + } +} + +inline void ReferenceChecker::visit(Reference &ope) { + auto it = std::find(params_.begin(), params_.end(), ope.name_); + if (it != params_.end()) { return; } + + if (!grammar_.count(ope.name_)) { + error_s[ope.name_] = ope.s_; + error_message[ope.name_] = "'" + ope.name_ + "' is not defined."; + } else { + if (!referenced.count(ope.name_)) { referenced.insert(ope.name_); } + const auto &rule = grammar_.at(ope.name_); + if (rule.is_macro) { + if (!ope.is_macro_ || ope.args_.size() != rule.params.size()) { + error_s[ope.name_] = ope.s_; + error_message[ope.name_] = "incorrect number of arguments."; + } + } else if (ope.is_macro_) { + error_s[ope.name_] = ope.s_; + error_message[ope.name_] = "'" + ope.name_ + "' is not macro."; + } + for (const auto &arg : ope.args_) { + arg->accept(*this); + } + } +} + +inline void ComputeFirstSet::visit(Reference &ope) { + if (!ope.rule_) { + // Macro parameter reference — can't predict what it will match + result_.any_char = true; + return; + } + if (refs_.count(ope.name_)) { return; } + refs_.insert(ope.name_); + ope.rule_->accept(*this); + if (!result_.first_rule && ope.rule_->is_token()) { + result_.first_rule = ope.rule_; + } + refs_.erase(ope.name_); +} + +inline void SetupFirstSets::visit(Reference &ope) { + if (!ope.rule_ || refs_.count(ope.name_)) { return; } + refs_.insert(ope.name_); + ope.rule_->accept(*this); + refs_.erase(ope.name_); +} + +inline void SetupFirstSets::visit(Sequence &ope) { + ope.kw_guard_.reset(); + setup_keyword_guarded_identifier(ope); + for (const auto &op : ope.opes_) { + op->accept(*this); + } +} + +inline void SetupFirstSets::setup_keyword_guarded_identifier(Sequence &seq) { + // Detect pattern: NotPredicate(Reference→PrioritizedChoice) + // TokenBoundary(Sequence[CharacterClass, + // Repetition(CharacterClass)]) + // This is the pattern used by: PlainIdentifier <- !ReservedKeyword + // <[a-z_]i[a-z0-9_]i*> + if (seq.opes_.size() != 2) { return; } + + // Child 0 must be NotPredicate + auto *not_pred = dynamic_cast(seq.opes_[0].get()); + if (!not_pred) { return; } + + // NotPredicate's child must be Reference to a rule + auto *ref = dynamic_cast(not_pred->ope_.get()); + if (!ref || !ref->rule_) { return; } + + // The referenced rule's inner operator (Holder) must contain + // PrioritizedChoice + auto *holder = dynamic_cast(ref->get_core_operator().get()); + if (!holder) { return; } + auto *choice = dynamic_cast(holder->ope_.get()); + if (!choice) { return; } + + // Extract keywords from PrioritizedChoice alternatives + std::vector exact_keywords; + std::vector prefix_keywords; + + for (const auto &alt : choice->opes_) { + auto *lit = dynamic_cast(alt.get()); + if (lit) { + if (!lit->ignore_case_) { return; } + exact_keywords.push_back(to_lower(lit->lit_)); + continue; + } + // Check for compound keyword (Sequence of LiteralStrings) + auto *sub_seq = dynamic_cast(alt.get()); + if (sub_seq && !sub_seq->opes_.empty()) { + auto *first_lit = dynamic_cast(sub_seq->opes_[0].get()); + if (first_lit) { + auto all_ignore_case_lits = + std::all_of(sub_seq->opes_.begin(), sub_seq->opes_.end(), + [](const auto &child) { + auto *l = dynamic_cast(child.get()); + return l && l->ignore_case_; + }); + if (all_ignore_case_lits) { + prefix_keywords.push_back(to_lower(first_lit->lit_)); + continue; + } + } + } + // Unrecognized alternative — bail out + return; + } + + if (exact_keywords.empty()) { return; } + + // Child 1 must be TokenBoundary + auto *tb = dynamic_cast(seq.opes_[1].get()); + if (!tb) { return; } + + // TokenBoundary content: Sequence[CharacterClass, Repetition(CharacterClass)] + // or just CharacterClass (single char identifier) + CharacterClass *first_cc = nullptr; + CharacterClass *rest_cc = nullptr; + + auto *inner_seq = dynamic_cast(tb->ope_.get()); + if (inner_seq && inner_seq->opes_.size() == 2) { + first_cc = dynamic_cast(inner_seq->opes_[0].get()); + auto *rep = dynamic_cast(inner_seq->opes_[1].get()); + if (rep) { rest_cc = dynamic_cast(rep->ope_.get()); } + } + + if (!first_cc || !rest_cc) { return; } + if (!first_cc->is_ascii_only() || !rest_cc->is_ascii_only()) { return; } + + // All conditions met — set up the fast path + auto kw = std::make_unique(); + kw->identifier_first = first_cc->ascii_bitset(); + kw->identifier_rest = rest_cc->ascii_bitset(); + + // Compute keyword length range for early-out in hot path + size_t min_len = SIZE_MAX, max_len = 0; + for (const auto &k : exact_keywords) { + min_len = std::min(min_len, k.size()); + max_len = std::max(max_len, k.size()); + } + for (const auto &k : prefix_keywords) { + min_len = std::min(min_len, k.size()); + max_len = std::max(max_len, k.size()); + } + kw->min_keyword_len = min_len; + kw->max_keyword_len = max_len; + + kw->exact_keywords = std::move(exact_keywords); + kw->prefix_keywords = std::move(prefix_keywords); + seq.kw_guard_ = std::move(kw); +} + +// Compute which rules benefit from packrat memoization. +// A rule benefits if it's reachable from 2+ alternatives of the same +// PrioritizedChoice (backtracking will re-visit it at the same position). +inline void Definition::initialize_packrat_filter() const { + std::call_once(packrat_filter_init_, [&]() { + auto def_count = definition_ids_.size(); + if (def_count == 0) { return; } + + // Collect rule IDs reachable from an Ope subtree (bitvector indexed by + // def_id) + struct CollectReachableRules : public TraversalVisitor { + using TraversalVisitor::visit; + std::vector reachable; // indexed by def_id + + CollectReachableRules(size_t n) : reachable(n, false) {} + + void visit(Holder &ope) override { + auto id = ope.outer_->id; + if (id < reachable.size()) { reachable[id] = true; } + ope.ope_->accept(*this); + } + void visit(Reference &ope) override { + if (ope.rule_ && ope.rule_->id < reachable.size() && + !reachable[ope.rule_->id]) { + reachable[ope.rule_->id] = true; + ope.rule_->accept(*this); + } + } + }; + + // Find rules that benefit: reachable from 2+ alternatives of same choice + std::vector benefits(def_count, false); + + struct FindBacktrackRules : public TraversalVisitor { + using TraversalVisitor::visit; + std::vector &benefits; + size_t def_count; + std::vector visited_rules; // indexed by def_id + + FindBacktrackRules(std::vector &b, size_t n) + : benefits(b), def_count(n), visited_rules(n, false) {} + + void visit(PrioritizedChoice &ope) override { + // For each alternative, collect reachable rules as bitvectors + std::vector> alt_reachable; + for (auto &op : ope.opes_) { + CollectReachableRules crr(def_count); + op->accept(crr); + alt_reachable.push_back(std::move(crr.reachable)); + } + + // Mark rules reachable from 2+ alternatives + for (size_t id = 0; id < def_count; id++) { + size_t count = 0; + for (auto &alt : alt_reachable) { + if (alt[id]) { count++; } + } + if (count >= 2) { benefits[id] = true; } + } + + // Recurse into alternatives + for (auto &op : ope.opes_) { + op->accept(*this); + } + } + void visit(Holder &ope) override { + auto id = ope.outer_->id; + if (id < visited_rules.size() && !visited_rules[id]) { + visited_rules[id] = true; + ope.ope_->accept(*this); + } + } + void visit(Reference &ope) override { + if (ope.rule_) { ope.rule_->accept(*this); } + } + }; + + FindBacktrackRules finder(benefits, def_count); + holder_->accept(finder); + if (whitespaceOpe) { whitespaceOpe->accept(finder); } + if (wordOpe) { wordOpe->accept(finder); } + + packrat_filter_ = std::move(benefits); + }); +} + +inline void LinkReferences::visit(Reference &ope) { + // Check if the reference is a macro parameter + auto found_param = false; + for (size_t i = 0; i < params_.size(); i++) { + const auto ¶m = params_[i]; + if (param == ope.name_) { + ope.iarg_ = i; + found_param = true; + break; + } + } + + // Check if the reference is a definition rule + if (!found_param && grammar_.count(ope.name_)) { + auto &rule = grammar_.at(ope.name_); + ope.rule_ = &rule; + } + + for (const auto &arg : ope.args_) { + arg->accept(*this); + } +} + +inline void FindReference::visit(Reference &ope) { + for (size_t i = 0; i < args_.size(); i++) { + const auto &name = params_[i]; + if (name == ope.name_) { + found_ope = args_[i]; + return; + } + } + found_ope = ope.shared_from_this(); +} + +/*----------------------------------------------------------------------------- + * PEG parser generator + *---------------------------------------------------------------------------*/ + +using Rules = std::unordered_map>; + +class ParserGenerator { +public: + struct ParserContext { + std::shared_ptr grammar; + std::string start; + bool enablePackratParsing = false; + }; + + static ParserContext parse(const char *s, size_t n, const Rules &rules, + Log log, std::string_view start, + bool enable_left_recursion = true) { + return get_instance().perform_core(s, n, rules, log, std::string(start), + enable_left_recursion); + } + + // For debugging purpose + static bool parse_test(const char *d, const char *s) { + Data data; + std::any dt = &data; + + auto n = strlen(s); + auto r = get_instance().g[d].parse(s, n, dt); + return r.ret && r.len == n; + } + +#if defined(__cpp_lib_char8_t) + static bool parse_test(const char *d, const char8_t *s) { + return parse_test(d, reinterpret_cast(s)); + } +#endif + +private: + static ParserGenerator &get_instance() { + static ParserGenerator instance; + return instance; + } + + ParserGenerator() { + make_grammar(); + setup_actions(); + } + + struct Instruction { + std::string type; + std::any data; + std::string_view sv; + }; + + struct Data { + std::shared_ptr grammar; + std::string start; + const char *start_pos = nullptr; + + std::vector> duplicates_of_definition; + + std::vector> duplicates_of_instruction; + std::map> instructions; + + std::vector> undefined_back_references; + std::vector> captures_stack{{}}; + + std::set captures_in_current_definition; + bool enablePackratParsing = true; + + Data() : grammar(std::make_shared()) {} + }; + + class SyntaxErrorException : public std::runtime_error { + public: + SyntaxErrorException(const char *what_arg, std::pair r) + : std::runtime_error(what_arg), r_(r) {} + + std::pair line_info() const { return r_; } + + private: + std::pair r_; + }; + + void make_grammar() { + // Setup PEG syntax parser + g["Grammar"] <= seq(g["Spacing"], oom(g["Definition"]), g["EndOfFile"]); + g["Definition"] <= + cho(seq(g["Ignore"], g["IdentCont"], g["Parameters"], g["LEFTARROW"], + g["Expression"], opt(g["Instruction"])), + seq(g["Ignore"], g["Identifier"], g["LEFTARROW"], g["Expression"], + opt(g["Instruction"]))); + g["Expression"] <= seq(g["Sequence"], zom(seq(g["SLASH"], g["Sequence"]))); + g["Sequence"] <= zom(cho(g["CUT"], g["Prefix"])); + g["Prefix"] <= seq(opt(cho(g["AND"], g["NOT"])), g["SuffixWithLabel"]); + g["SuffixWithLabel"] <= + seq(g["Suffix"], opt(seq(g["LABEL"], g["Identifier"]))); + g["Suffix"] <= seq(g["Primary"], opt(g["Loop"])); + g["Loop"] <= cho(g["QUESTION"], g["STAR"], g["PLUS"], g["Repetition"]); + g["Primary"] <= cho(seq(g["Ignore"], g["IdentCont"], g["Arguments"], + npd(g["LEFTARROW"])), + seq(g["Ignore"], g["Identifier"], + npd(seq(opt(g["Parameters"]), g["LEFTARROW"]))), + seq(g["OPEN"], g["Expression"], g["CLOSE"]), + seq(g["BeginTok"], g["Expression"], g["EndTok"]), + g["CapScope"], + seq(g["BeginCap"], g["Expression"], g["EndCap"]), + g["BackRef"], g["DictionaryI"], g["LiteralI"], + g["Dictionary"], g["Literal"], g["NegatedClassI"], + g["NegatedClass"], g["ClassI"], g["Class"], g["DOT"]); + + g["Identifier"] <= seq(g["IdentCont"], g["Spacing"]); + g["IdentCont"] <= tok(seq(g["IdentStart"], zom(g["IdentRest"]))); + + const static std::vector> range = { + {0x0080, 0xFFFF}}; + g["IdentStart"] <= seq(npd(lit(u8(u8"↑"))), npd(lit(u8(u8"⇑"))), + cho(cls("a-zA-Z_%"), cls(range))); + + g["IdentRest"] <= cho(g["IdentStart"], cls("0-9")); + + g["Dictionary"] <= seq(g["LiteralD"], oom(seq(g["PIPE"], g["LiteralD"]))); + + g["DictionaryI"] <= + seq(g["LiteralID"], oom(seq(g["PIPE"], g["LiteralID"]))); + + auto lit_ope = cho(seq(cls("'"), tok(zom(seq(npd(cls("'")), g["Char"]))), + cls("'"), g["Spacing"]), + seq(cls("\""), tok(zom(seq(npd(cls("\"")), g["Char"]))), + cls("\""), g["Spacing"])); + g["Literal"] <= lit_ope; + g["LiteralD"] <= lit_ope; + + auto lit_case_ignore_ope = + cho(seq(cls("'"), tok(zom(seq(npd(cls("'")), g["Char"]))), lit("'i"), + g["Spacing"]), + seq(cls("\""), tok(zom(seq(npd(cls("\"")), g["Char"]))), lit("\"i"), + g["Spacing"])); + g["LiteralI"] <= lit_case_ignore_ope; + g["LiteralID"] <= lit_case_ignore_ope; + + // NOTE: The original Brian Ford's paper uses 'zom' instead of 'oom'. + g["Class"] <= seq(chr('['), npd(chr('^')), + tok(oom(seq(npd(chr(']')), g["Range"]))), chr(']'), + g["Spacing"]); + g["ClassI"] <= seq(chr('['), npd(chr('^')), + tok(oom(seq(npd(chr(']')), g["Range"]))), lit("]i"), + g["Spacing"]); + + g["NegatedClass"] <= seq(lit("[^"), + tok(oom(seq(npd(chr(']')), g["Range"]))), chr(']'), + g["Spacing"]); + g["NegatedClassI"] <= seq(lit("[^"), + tok(oom(seq(npd(chr(']')), g["Range"]))), + lit("]i"), g["Spacing"]); + + // NOTE: This is different from The original Brian Ford's paper, and this + // modification allows us to specify `[+-]` as a valid char class. + g["Range"] <= + cho(seq(g["Char"], chr('-'), npd(chr(']')), g["Char"]), g["Char"]); + + g["Char"] <= + cho(seq(chr('\\'), cls("fnrtv'\"[]\\^-")), + seq(chr('\\'), cls("0-3"), cls("0-7"), cls("0-7")), + seq(chr('\\'), cls("0-7"), opt(cls("0-7"))), + seq(lit("\\x"), cls("0-9a-fA-F"), opt(cls("0-9a-fA-F"))), + seq(lit("\\u"), + cho(seq(cho(seq(chr('0'), cls("0-9a-fA-F")), lit("10")), + rep(cls("0-9a-fA-F"), 4, 4)), + rep(cls("0-9a-fA-F"), 4, 5))), + seq(npd(chr('\\')), dot())); + + g["Repetition"] <= + seq(g["BeginBracket"], g["RepetitionRange"], g["EndBracket"]); + g["RepetitionRange"] <= cho(seq(g["Number"], g["COMMA"], g["Number"]), + seq(g["Number"], g["COMMA"]), g["Number"], + seq(g["COMMA"], g["Number"])); + g["Number"] <= seq(oom(cls("0-9")), g["Spacing"]); + + g["CapScope"] <= seq(g["BeginCapScope"], g["Expression"], g["EndCapScope"]); + + g["LEFTARROW"] <= seq(cho(lit("<-"), lit(u8(u8"←"))), g["Spacing"]); + ~g["SLASH"] <= seq(chr('/'), g["Spacing"]); + ~g["PIPE"] <= seq(chr('|'), g["Spacing"]); + g["AND"] <= seq(chr('&'), g["Spacing"]); + g["NOT"] <= seq(chr('!'), g["Spacing"]); + g["QUESTION"] <= seq(chr('?'), g["Spacing"]); + g["STAR"] <= seq(chr('*'), g["Spacing"]); + g["PLUS"] <= seq(chr('+'), g["Spacing"]); + ~g["OPEN"] <= seq(chr('('), g["Spacing"]); + ~g["CLOSE"] <= seq(chr(')'), g["Spacing"]); + g["DOT"] <= seq(chr('.'), g["Spacing"]); + + g["CUT"] <= seq(lit(u8(u8"↑")), g["Spacing"]); + ~g["LABEL"] <= seq(cho(chr('^'), lit(u8(u8"⇑"))), g["Spacing"]); + + ~g["Spacing"] <= zom(cho(g["Space"], g["Comment"])); + g["Comment"] <= seq(chr('#'), zom(seq(npd(g["EndOfLine"]), dot())), + opt(g["EndOfLine"])); + g["Space"] <= cho(chr(' '), chr('\t'), g["EndOfLine"]); + g["EndOfLine"] <= cho(lit("\r\n"), chr('\n'), chr('\r')); + g["EndOfFile"] <= npd(dot()); + + ~g["BeginTok"] <= seq(chr('<'), g["Spacing"]); + ~g["EndTok"] <= seq(chr('>'), g["Spacing"]); + + ~g["BeginCapScope"] <= seq(chr('$'), chr('('), g["Spacing"]); + ~g["EndCapScope"] <= seq(chr(')'), g["Spacing"]); + + g["BeginCap"] <= seq(chr('$'), tok(g["IdentCont"]), chr('<'), g["Spacing"]); + ~g["EndCap"] <= seq(chr('>'), g["Spacing"]); + + g["BackRef"] <= seq(chr('$'), tok(g["IdentCont"]), g["Spacing"]); + + g["IGNORE"] <= chr('~'); + + g["Ignore"] <= opt(g["IGNORE"]); + g["Parameters"] <= seq(g["OPEN"], g["Identifier"], + zom(seq(g["COMMA"], g["Identifier"])), g["CLOSE"]); + g["Arguments"] <= seq(g["OPEN"], g["Expression"], + zom(seq(g["COMMA"], g["Expression"])), g["CLOSE"]); + ~g["COMMA"] <= seq(chr(','), g["Spacing"]); + + // Instruction grammars + g["Instruction"] <= + seq(g["BeginBracket"], + opt(seq(g["InstructionItem"], zom(seq(g["InstructionItemSeparator"], + g["InstructionItem"])))), + g["EndBracket"]); + g["InstructionItem"] <= + cho(g["PrecedenceClimbing"], g["ErrorMessage"], g["NoAstOpt"]); + ~g["InstructionItemSeparator"] <= seq(chr(';'), g["Spacing"]); + + ~g["SpacesZom"] <= zom(g["Space"]); + ~g["SpacesOom"] <= oom(g["Space"]); + ~g["BeginBracket"] <= seq(chr('{'), g["Spacing"]); + ~g["EndBracket"] <= seq(chr('}'), g["Spacing"]); + + // PrecedenceClimbing instruction + g["PrecedenceClimbing"] <= + seq(lit("precedence"), g["SpacesOom"], g["PrecedenceInfo"], + zom(seq(g["SpacesOom"], g["PrecedenceInfo"])), g["SpacesZom"]); + g["PrecedenceInfo"] <= + seq(g["PrecedenceAssoc"], + oom(seq(ign(g["SpacesOom"]), g["PrecedenceOpe"]))); + g["PrecedenceOpe"] <= + cho(seq(cls("'"), + tok(zom(seq(npd(cho(g["Space"], cls("'"))), g["Char"]))), + cls("'")), + seq(cls("\""), + tok(zom(seq(npd(cho(g["Space"], cls("\""))), g["Char"]))), + cls("\"")), + tok(oom(seq(npd(cho(g["PrecedenceAssoc"], g["Space"], chr('}'))), + dot())))); + g["PrecedenceAssoc"] <= cls("LR"); + + // Error message instruction + g["ErrorMessage"] <= seq(lit("error_message"), g["SpacesOom"], + g["LiteralD"], g["SpacesZom"]); + + // No Ast node optimization instruction + g["NoAstOpt"] <= seq(lit("no_ast_opt"), g["SpacesZom"]); + + // Set definition names + for (auto &x : g) { + x.second.name = x.first; + } + } + + void setup_actions() { + g["Definition"] = [&](const SemanticValues &vs, std::any &dt) { + auto &data = *std::any_cast(dt); + + auto is_macro = vs.choice() == 0; + auto ignore = std::any_cast(vs[0]); + auto name = std::any_cast(vs[1]); + + std::vector params; + std::shared_ptr ope; + auto has_instructions = false; + + if (is_macro) { + params = std::any_cast>(vs[2]); + ope = std::any_cast>(vs[4]); + if (vs.size() == 6) { has_instructions = true; } + } else { + ope = std::any_cast>(vs[3]); + if (vs.size() == 5) { has_instructions = true; } + } + + if (has_instructions) { + auto index = is_macro ? 5 : 4; + std::unordered_set types; + for (const auto &instruction : + std::any_cast>(vs[index])) { + const auto &type = instruction.type; + if (types.find(type) == types.end()) { + data.instructions[name].push_back(instruction); + types.insert(instruction.type); + if (type == "declare_symbol" || type == "check_symbol") { + if (!TokenChecker::is_token(*ope)) { ope = tok(ope); } + } + } else { + data.duplicates_of_instruction.emplace_back(type, + instruction.sv.data()); + } + } + } + + auto &grammar = *data.grammar; + if (!grammar.count(name)) { + auto &rule = grammar[name]; + rule <= ope; + rule.name = name; + rule.s_ = vs.sv().data(); + rule.line_ = line_info(vs.ss, rule.s_); + rule.ignoreSemanticValue = ignore; + rule.is_macro = is_macro; + rule.params = params; + + if (data.start.empty()) { + data.start = rule.name; + data.start_pos = rule.s_; + } + } else { + data.duplicates_of_definition.emplace_back(name, vs.sv().data()); + } + }; + + g["Definition"].enter = [](const Context & /*c*/, const char * /*s*/, + size_t /*n*/, std::any &dt) { + auto &data = *std::any_cast(dt); + data.captures_in_current_definition.clear(); + }; + + g["Expression"] = [&](const SemanticValues &vs) { + if (vs.size() == 1) { + return std::any_cast>(vs[0]); + } else { + std::vector> opes; + for (auto i = 0u; i < vs.size(); i++) { + opes.emplace_back(std::any_cast>(vs[i])); + } + const std::shared_ptr ope = + std::make_shared(opes); + return ope; + } + }; + + g["Sequence"] = [&](const SemanticValues &vs) { + if (vs.empty()) { + return npd(lit("")); + } else if (vs.size() == 1) { + return std::any_cast>(vs[0]); + } else { + std::vector> opes; + for (const auto &x : vs) { + opes.emplace_back(std::any_cast>(x)); + } + const std::shared_ptr ope = std::make_shared(opes); + return ope; + } + }; + + g["Prefix"] = [&](const SemanticValues &vs) { + std::shared_ptr ope; + if (vs.size() == 1) { + ope = std::any_cast>(vs[0]); + } else { + assert(vs.size() == 2); + auto tok = std::any_cast(vs[0]); + ope = std::any_cast>(vs[1]); + if (tok == '&') { + ope = apd(ope); + } else { // '!' + ope = npd(ope); + } + } + return ope; + }; + + g["SuffixWithLabel"] = [&](const SemanticValues &vs, std::any &dt) { + auto ope = std::any_cast>(vs[0]); + if (vs.size() == 1) { + return ope; + } else { + assert(vs.size() == 2); + auto &data = *std::any_cast(dt); + const auto &ident = std::any_cast(vs[1]); + auto label = ref(*data.grammar, ident, vs.sv().data(), false, {}); + auto recovery = rec(ref(*data.grammar, RECOVER_DEFINITION_NAME, + vs.sv().data(), true, {label})); + return cho4label_(ope, recovery); + } + }; + + struct Loop { + enum class Type { opt = 0, zom, oom, rep }; + Type type; + std::pair range; + }; + + g["Suffix"] = [&](const SemanticValues &vs) { + auto ope = std::any_cast>(vs[0]); + if (vs.size() == 1) { + return ope; + } else { + assert(vs.size() == 2); + auto loop = std::any_cast(vs[1]); + switch (loop.type) { + case Loop::Type::opt: return opt(ope); + case Loop::Type::zom: return zom(ope); + case Loop::Type::oom: return oom(ope); + default: // Regex-like repetition + return rep(ope, loop.range.first, loop.range.second); + } + } + }; + + g["Loop"] = [&](const SemanticValues &vs) { + switch (vs.choice()) { + case 0: // Option + return Loop{Loop::Type::opt, std::pair()}; + case 1: // Zero or More + return Loop{Loop::Type::zom, std::pair()}; + case 2: // One or More + return Loop{Loop::Type::oom, std::pair()}; + default: // Regex-like repetition + return Loop{Loop::Type::rep, + std::any_cast>(vs[0])}; + } + }; + + g["Primary"] = [&](const SemanticValues &vs, std::any &dt) { + auto &data = *std::any_cast(dt); + + switch (vs.choice()) { + case 0: // Macro Reference + case 1: { // Reference + auto is_macro = vs.choice() == 0; + auto ignore = std::any_cast(vs[0]); + const auto &ident = std::any_cast(vs[1]); + + std::vector> args; + if (is_macro) { + args = std::any_cast>>(vs[2]); + } + + auto ope = ref(*data.grammar, ident, vs.sv().data(), is_macro, args); + if (ident == RECOVER_DEFINITION_NAME) { ope = rec(ope); } + + if (ignore) { + return ign(ope); + } else { + return ope; + } + } + case 2: { // (Expression) + return std::any_cast>(vs[0]); + } + case 3: { // TokenBoundary + return tok(std::any_cast>(vs[0])); + } + case 4: { // CaptureScope + return csc(std::any_cast>(vs[0])); + } + case 5: { // Capture + const auto &name = std::any_cast(vs[0]); + auto ope = std::any_cast>(vs[1]); + + data.captures_stack.back().insert(name); + data.captures_in_current_definition.insert(name); + + return cap(ope, [name](const char *a_s, size_t a_n, Context &c) { + c.capture_entries.emplace_back(name, std::string(a_s, a_n)); + }); + } + default: { + return std::any_cast>(vs[0]); + } + } + }; + + g["IdentCont"] = [](const SemanticValues &vs) { + return std::string(vs.sv().data(), vs.sv().length()); + }; + + g["Dictionary"] = [](const SemanticValues &vs) { + auto items = vs.transform(); + return dic(items, false); + }; + g["DictionaryI"] = [](const SemanticValues &vs) { + auto items = vs.transform(); + return dic(items, true); + }; + + g["Literal"] = [](const SemanticValues &vs) { + const auto &tok = vs.tokens.front(); + return lit(resolve_escape_sequence(tok.data(), tok.size())); + }; + g["LiteralI"] = [](const SemanticValues &vs) { + const auto &tok = vs.tokens.front(); + return liti(resolve_escape_sequence(tok.data(), tok.size())); + }; + g["LiteralD"] = [](const SemanticValues &vs) { + auto &tok = vs.tokens.front(); + return resolve_escape_sequence(tok.data(), tok.size()); + }; + g["LiteralID"] = [](const SemanticValues &vs) { + auto &tok = vs.tokens.front(); + return resolve_escape_sequence(tok.data(), tok.size()); + }; + + g["Class"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return cls(ranges); + }; + g["ClassI"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return cls(ranges, true); + }; + g["NegatedClass"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return ncls(ranges); + }; + g["NegatedClassI"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return ncls(ranges, true); + }; + g["Range"] = [](const SemanticValues &vs) { + switch (vs.choice()) { + case 0: { + auto s1 = std::any_cast(vs[0]); + auto s2 = std::any_cast(vs[1]); + auto cp1 = decode_codepoint(s1.data(), s1.length()); + auto cp2 = decode_codepoint(s2.data(), s2.length()); + if (cp1 > cp2) { + throw SyntaxErrorException("characer range is out of order...", + vs.line_info()); + } + return std::pair(cp1, cp2); + } + case 1: { + auto s = std::any_cast(vs[0]); + auto cp = decode_codepoint(s.data(), s.length()); + return std::pair(cp, cp); + } + } + return std::pair(0, 0); + }; + g["Char"] = [](const SemanticValues &vs) { + return resolve_escape_sequence(vs.sv().data(), vs.sv().length()); + }; + + g["RepetitionRange"] = [&](const SemanticValues &vs) { + switch (vs.choice()) { + case 0: { // Number COMMA Number + auto min = std::any_cast(vs[0]); + auto max = std::any_cast(vs[1]); + return std::pair(min, max); + } + case 1: // Number COMMA + return std::pair(std::any_cast(vs[0]), + std::numeric_limits::max()); + case 2: { // Number + auto n = std::any_cast(vs[0]); + return std::pair(n, n); + } + default: // COMMA Number + return std::pair(std::numeric_limits::min(), + std::any_cast(vs[0])); + } + }; + g["Number"] = [&](const SemanticValues &vs) { + return vs.token_to_number(); + }; + + g["CapScope"].enter = [](const Context & /*c*/, const char * /*s*/, + size_t /*n*/, std::any &dt) { + auto &data = *std::any_cast(dt); + data.captures_stack.emplace_back(); + }; + g["CapScope"].leave = [](const Context & /*c*/, const char * /*s*/, + size_t /*n*/, size_t /*matchlen*/, + std::any & /*value*/, std::any &dt) { + auto &data = *std::any_cast(dt); + data.captures_stack.pop_back(); + }; + + g["AND"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; + g["NOT"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; + g["QUESTION"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; + g["STAR"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; + g["PLUS"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; + + g["DOT"] = [](const SemanticValues & /*vs*/) { return dot(); }; + + g["CUT"] = [](const SemanticValues & /*vs*/) { return cut(); }; + + g["BeginCap"] = [](const SemanticValues &vs) { return vs.token(); }; + + g["BackRef"] = [&](const SemanticValues &vs, std::any &dt) { + auto &data = *std::any_cast(dt); + + // Undefined back reference check + { + auto found = false; + auto it = data.captures_stack.rbegin(); + while (it != data.captures_stack.rend()) { + if (it->find(vs.token()) != it->end()) { + found = true; + break; + } + ++it; + } + if (!found) { + auto ptr = vs.token().data() - 1; // include '$' symbol + data.undefined_back_references.emplace_back(vs.token(), ptr); + } + } + + // NOTE: Disable packrat parsing if a back reference is not defined in + // captures in the current definition rule. + if (data.captures_in_current_definition.find(vs.token()) == + data.captures_in_current_definition.end()) { + data.enablePackratParsing = false; + } + + return bkr(vs.token_to_string()); + }; + + g["Ignore"] = [](const SemanticValues &vs) { return vs.size() > 0; }; + + g["Parameters"] = [](const SemanticValues &vs) { + return vs.transform(); + }; + + g["Arguments"] = [](const SemanticValues &vs) { + return vs.transform>(); + }; + + g["PrecedenceClimbing"] = [](const SemanticValues &vs) { + PrecedenceClimbing::BinOpeInfo binOpeInfo; + size_t level = 1; + for (const auto &v : vs) { + auto tokens = std::any_cast>(v); + auto assoc = tokens[0][0]; + for (size_t i = 1; i < tokens.size(); i++) { + binOpeInfo[tokens[i]] = std::pair(level, assoc); + } + level++; + } + Instruction instruction; + instruction.type = "precedence"; + instruction.data = binOpeInfo; + instruction.sv = vs.sv(); + return instruction; + }; + g["PrecedenceInfo"] = [](const SemanticValues &vs) { + return vs.transform(); + }; + g["PrecedenceOpe"] = [](const SemanticValues &vs) { return vs.token(); }; + g["PrecedenceAssoc"] = [](const SemanticValues &vs) { return vs.token(); }; + + g["ErrorMessage"] = [](const SemanticValues &vs) { + Instruction instruction; + instruction.type = "error_message"; + instruction.data = std::any_cast(vs[0]); + instruction.sv = vs.sv(); + return instruction; + }; + + g["NoAstOpt"] = [](const SemanticValues &vs) { + Instruction instruction; + instruction.type = "no_ast_opt"; + instruction.sv = vs.sv(); + return instruction; + }; + + g["Instruction"] = [](const SemanticValues &vs) { + return vs.transform(); + }; + } + + bool apply_precedence_instruction(Definition &rule, + const PrecedenceClimbing::BinOpeInfo &info, + const char *s, Log log) { + try { + auto &seq = dynamic_cast(*rule.get_core_operator()); + auto atom = seq.opes_[0]; + auto &rep = dynamic_cast(*seq.opes_[1]); + auto &seq1 = dynamic_cast(*rep.ope_); + auto binop = seq1.opes_[0]; + auto atom1 = seq1.opes_[1]; + + auto atom_name = dynamic_cast(*atom).name_; + auto binop_name = dynamic_cast(*binop).name_; + auto atom1_name = dynamic_cast(*atom1).name_; + + if (!rep.is_zom() || atom_name != atom1_name || atom_name == binop_name) { + if (log) { + auto line = line_info(s, rule.s_); + log(line.first, line.second, + "'precedence' instruction cannot be applied to '" + rule.name + + "'.", + ""); + } + return false; + } + + rule.holder_->ope_ = pre(atom, binop, info, rule); + rule.disable_action = true; + } catch (...) { + if (log) { + auto line = line_info(s, rule.s_); + log(line.first, line.second, + "'precedence' instruction cannot be applied to '" + rule.name + + "'.", + ""); + } + return false; + } + return true; + } + + ParserContext perform_core(const char *s, size_t n, const Rules &rules, + Log log, std::string requested_start, + bool enable_left_recursion = true) { + Data data; + auto &grammar = *data.grammar; + + // Built-in macros + { + // `%recover` + { + auto &rule = grammar[RECOVER_DEFINITION_NAME]; + rule <= ref(grammar, "x", "", false, {}); + rule.name = RECOVER_DEFINITION_NAME; + rule.s_ = "[native]"; + rule.ignoreSemanticValue = true; + rule.is_macro = true; + rule.params = {"x"}; + } + } + + try { + std::any dt = &data; + auto r = g["Grammar"].parse(s, n, dt, nullptr, log); + + if (!r.ret) { + if (log) { + if (r.error_info.message_pos) { + auto line = line_info(s, r.error_info.message_pos); + log(line.first, line.second, r.error_info.message, + r.error_info.label); + } else { + auto line = line_info(s, r.error_info.error_pos); + log(line.first, line.second, "syntax error", r.error_info.label); + } + } + return {}; + } + } catch (const SyntaxErrorException &e) { + if (log) { + auto line = e.line_info(); + log(line.first, line.second, e.what(), ""); + } + return {}; + } + + // User provided rules + for (auto [user_name, user_rule] : rules) { + auto name = user_name; + auto ignore = false; + if (!name.empty() && name[0] == '~') { + ignore = true; + name.erase(0, 1); + } + if (!name.empty()) { + auto &rule = grammar[name]; + rule <= user_rule; + rule.name = name; + rule.ignoreSemanticValue = ignore; + } + } + + // Check duplicated definitions + auto ret = true; + + if (!data.duplicates_of_definition.empty()) { + for (const auto &[name, ptr] : data.duplicates_of_definition) { + if (log) { + auto line = line_info(s, ptr); + log(line.first, line.second, + "the definition '" + name + "' is already defined.", ""); + } + } + ret = false; + } + + // Check duplicated instructions + if (!data.duplicates_of_instruction.empty()) { + for (const auto &[type, ptr] : data.duplicates_of_instruction) { + if (log) { + auto line = line_info(s, ptr); + log(line.first, line.second, + "the instruction '" + type + "' is already defined.", ""); + } + } + ret = false; + } + + // Check undefined back references + if (!data.undefined_back_references.empty()) { + for (const auto &[name, ptr] : data.undefined_back_references) { + if (log) { + auto line = line_info(s, ptr); + log(line.first, line.second, + "the back reference '" + name + "' is undefined.", ""); + } + } + ret = false; + } + + // Set root definition + auto start = data.start; + + if (!requested_start.empty()) { + if (grammar.count(requested_start)) { + start = requested_start; + } else { + if (log) { + auto line = line_info(s, s); + log(line.first, line.second, + "the specified start rule '" + requested_start + + "' is undefined.", + ""); + } + ret = false; + } + } + + if (!ret) { return {}; } + + auto &start_rule = grammar[start]; + + // Check if the start rule has ignore operator + { + if (start_rule.ignoreSemanticValue) { + if (log) { + auto line = line_info(s, start_rule.s_); + log(line.first, line.second, + "ignore operator cannot be applied to '" + start_rule.name + "'.", + ""); + } + ret = false; + } + } + + if (!ret) { return {}; } + + // Check missing definitions + auto referenced = std::unordered_set{ + WHITESPACE_DEFINITION_NAME, + WORD_DEFINITION_NAME, + RECOVER_DEFINITION_NAME, + start_rule.name, + }; + + for (auto &[_, rule] : grammar) { + ReferenceChecker vis(grammar, rule.params); + rule.accept(vis); + referenced.insert(vis.referenced.begin(), vis.referenced.end()); + for (const auto &[name, ptr] : vis.error_s) { + if (log) { + auto line = line_info(s, ptr); + log(line.first, line.second, vis.error_message[name], ""); + } + ret = false; + } + } + + for (auto &[name, rule] : grammar) { + if (!referenced.count(name)) { + if (log) { + auto line = line_info(s, rule.s_); + auto msg = "'" + name + "' is not referenced."; + log(line.first, line.second, msg, ""); + } + } + } + + if (!ret) { return {}; } + + // Link references + for (auto &x : grammar) { + auto &rule = x.second; + LinkReferences vis(grammar, rule.params); + rule.accept(vis); + } + + // Compute can_be_empty for each rule (fixed-point iteration) + { + bool changed = true; + while (changed) { + changed = false; + for (auto &[name, rule] : grammar) { + ComputeCanBeEmpty vis; + rule.accept(vis); + if (vis.result != rule.can_be_empty) { + rule.can_be_empty = vis.result; + changed = true; + } + } + } + } + + // Check left recursion + if (enable_left_recursion) { + for (auto &[name, rule] : grammar) { + DetectLeftRecursion vis(name); + rule.accept(vis); + if (vis.error_s) { rule.is_left_recursive = true; } + } + } else { + ret = true; + + for (auto &[name, rule] : grammar) { + DetectLeftRecursion vis(name); + rule.accept(vis); + if (vis.error_s) { + if (log) { + auto line = line_info(s, vis.error_s); + log(line.first, line.second, "'" + name + "' is left recursive.", + ""); + } + ret = false; + } + } + + if (!ret) { return {}; } + } + + // Check infinite loop + if (detect_infiniteLoop(data, start_rule, log, s)) { return {}; } + + // Automatic whitespace skipping + if (grammar.count(WHITESPACE_DEFINITION_NAME)) { + for (auto &x : grammar) { + auto &rule = x.second; + auto ope = rule.get_core_operator(); + if (IsLiteralToken::check(*ope)) { rule <= tok(ope); } + } + + auto &rule = grammar[WHITESPACE_DEFINITION_NAME]; + start_rule.whitespaceOpe = wsp(rule.get_core_operator()); + + if (detect_infiniteLoop(data, rule, log, s)) { return {}; } + } + + // Word expression + if (grammar.count(WORD_DEFINITION_NAME)) { + auto &rule = grammar[WORD_DEFINITION_NAME]; + start_rule.wordOpe = rule.get_core_operator(); + + if (detect_infiniteLoop(data, rule, log, s)) { return {}; } + } + + // Apply instructions + for (const auto &[name, instructions] : data.instructions) { + auto &rule = grammar[name]; + + for (const auto &instruction : instructions) { + if (instruction.type == "precedence") { + const auto &info = + std::any_cast(instruction.data); + + if (!apply_precedence_instruction(rule, info, s, log)) { return {}; } + } else if (instruction.type == "error_message") { + rule.error_message = std::any_cast(instruction.data); + } else if (instruction.type == "no_ast_opt") { + rule.no_ast_opt = true; + } + } + } + + // Setup First-Set and ISpan optimizations + for (auto &x : grammar) { + SetupFirstSets vis; + x.second.accept(vis); + } + + return {data.grammar, start, data.enablePackratParsing}; + } + + bool detect_infiniteLoop(const Data &data, Definition &rule, const Log &log, + const char *s) const { + std::vector> refs; + std::unordered_map has_error_cache; + DetectInfiniteLoop vis(data.start_pos, rule.name, refs, has_error_cache); + rule.accept(vis); + if (vis.has_error) { + if (log) { + auto line = line_info(s, vis.error_s); + log(line.first, line.second, + "infinite loop is detected in '" + vis.error_name + "'.", ""); + } + return true; + } + return false; + } + + Grammar g; +}; + +/*----------------------------------------------------------------------------- + * AST + *---------------------------------------------------------------------------*/ + +template struct AstBase : public Annotation { + AstBase(const char *path, size_t line, size_t column, const char *name, + const std::vector> &nodes, + size_t position = 0, size_t length = 0, size_t choice_count = 0, + size_t choice = 0) + : path(path ? path : ""), line(line), column(column), name(name), + position(position), length(length), choice_count(choice_count), + choice(choice), original_name(name), + original_choice_count(choice_count), original_choice(choice), + tag(str2tag(name)), original_tag(tag), is_token(false), nodes(nodes) {} + + AstBase(const char *path, size_t line, size_t column, const char *name, + const std::string_view &token, size_t position = 0, size_t length = 0, + size_t choice_count = 0, size_t choice = 0) + : path(path ? path : ""), line(line), column(column), name(name), + position(position), length(length), choice_count(choice_count), + choice(choice), original_name(name), + original_choice_count(choice_count), original_choice(choice), + tag(str2tag(name)), original_tag(tag), is_token(true), token(token) {} + + AstBase(const AstBase &ast, const char *original_name, size_t position = 0, + size_t length = 0, size_t original_choice_count = 0, + size_t original_choice = 0) + : path(ast.path), line(ast.line), column(ast.column), name(ast.name), + position(position), length(length), choice_count(ast.choice_count), + choice(ast.choice), original_name(original_name), + original_choice_count(original_choice_count), + original_choice(original_choice), tag(ast.tag), + original_tag(str2tag(original_name)), is_token(ast.is_token), + token(ast.token), nodes(ast.nodes), parent(ast.parent) {} + + const std::string path; + const size_t line = 1; + const size_t column = 1; + + const std::string name; + size_t position; + size_t length; + const size_t choice_count; + const size_t choice; + const std::string original_name; + const size_t original_choice_count; + const size_t original_choice; + const unsigned int tag; + const unsigned int original_tag; + + const bool is_token; + const std::string_view token; + + std::vector>> nodes; + std::weak_ptr> parent; + + std::string token_to_string() const { + assert(is_token); + return std::string(token); + } + + template T token_to_number() const { + return token_to_number_(token); + } +}; + +template +void ast_to_s_core(const std::shared_ptr &ptr, std::string &s, int level, + std::function fn) { + const auto &ast = *ptr; + for (auto i = 0; i < level; i++) { + s += " "; + } + auto name = ast.original_name; + if (ast.original_choice_count > 0) { + name += "/" + std::to_string(ast.original_choice); + } + if (ast.name != ast.original_name) { name += "[" + ast.name + "]"; } + if (ast.is_token) { + s += "- " + name + " ("; + s += ast.token; + s += ")\n"; + } else { + s += "+ " + name + "\n"; + } + if (fn) { s += fn(ast, level + 1); } + for (const auto &node : ast.nodes) { + ast_to_s_core(node, s, level + 1, fn); + } +} + +template +std::string +ast_to_s(const std::shared_ptr &ptr, + std::function fn = nullptr) { + std::string s; + ast_to_s_core(ptr, s, 0, fn); + return s; +} + +struct AstOptimizer { + AstOptimizer(bool mode, const std::vector &rules = {}) + : mode_(mode), rules_(rules) {} + + template + std::shared_ptr optimize(std::shared_ptr original, + std::shared_ptr parent = nullptr) { + auto found = + std::find(rules_.begin(), rules_.end(), original->name) != rules_.end(); + auto opt = mode_ ? !found : found; + + if (opt && original->nodes.size() == 1) { + auto child = optimize(original->nodes[0], parent); + auto ast = std::make_shared(*child, original->name.data(), + original->position, original->length, + original->choice_count, original->choice); + for (auto &node : ast->nodes) { + node->parent = ast; + } + return ast; + } + + auto ast = std::make_shared(*original); + ast->parent = parent; + ast->nodes.clear(); + for (const auto &node : original->nodes) { + auto child = optimize(node, ast); + ast->nodes.push_back(child); + } + return ast; + } + +private: + const bool mode_; + const std::vector rules_; +}; + +struct EmptyType {}; +using Ast = AstBase; + +template void add_ast_action(Definition &rule) { + rule.action = [&](const SemanticValues &vs) { + auto line = vs.line_info(); + + if (rule.is_token()) { + return std::make_shared( + vs.path, line.first, line.second, rule.name.data(), vs.token(), + std::distance(vs.ss, vs.sv().data()), vs.sv().length(), + vs.choice_count(), vs.choice()); + } + + auto ast = + std::make_shared(vs.path, line.first, line.second, rule.name.data(), + vs.transform>(), + std::distance(vs.ss, vs.sv().data()), + vs.sv().length(), vs.choice_count(), vs.choice()); + + for (auto &node : ast->nodes) { + node->parent = ast; + } + return ast; + }; +} + +#define PEG_EXPAND(...) __VA_ARGS__ +#define PEG_CONCAT(a, b) a##b +#define PEG_CONCAT2(a, b) PEG_CONCAT(a, b) + +#define PEG_PICK( \ + a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, \ + a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, \ + a32, a33, a34, a35, a36, a37, a38, a39, a40, a41, a42, a43, a44, a45, a46, \ + a47, a48, a49, a50, a51, a52, a53, a54, a55, a56, a57, a58, a59, a60, a61, \ + a62, a63, a64, a65, a66, a67, a68, a69, a70, a71, a72, a73, a74, a75, a76, \ + a77, a78, a79, a80, a81, a82, a83, a84, a85, a86, a87, a88, a89, a90, a91, \ + a92, a93, a94, a95, a96, a97, a98, a99, a100, ...) \ + a100 + +#define PEG_COUNT(...) \ + PEG_EXPAND(PEG_PICK( \ + __VA_ARGS__, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, \ + 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, \ + 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, \ + 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, \ + 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, \ + 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)) + +#define PEG_DEF_1(r) \ + peg::Definition r; \ + r.name = #r; \ + peg::add_ast_action(r); + +#define PEG_DEF_2(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_1(__VA_ARGS__)) +#define PEG_DEF_3(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_2(__VA_ARGS__)) +#define PEG_DEF_4(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_3(__VA_ARGS__)) +#define PEG_DEF_5(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_4(__VA_ARGS__)) +#define PEG_DEF_6(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_5(__VA_ARGS__)) +#define PEG_DEF_7(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_6(__VA_ARGS__)) +#define PEG_DEF_8(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_7(__VA_ARGS__)) +#define PEG_DEF_9(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_8(__VA_ARGS__)) +#define PEG_DEF_10(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_9(__VA_ARGS__)) +#define PEG_DEF_11(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_10(__VA_ARGS__)) +#define PEG_DEF_12(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_11(__VA_ARGS__)) +#define PEG_DEF_13(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_12(__VA_ARGS__)) +#define PEG_DEF_14(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_13(__VA_ARGS__)) +#define PEG_DEF_15(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_14(__VA_ARGS__)) +#define PEG_DEF_16(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_15(__VA_ARGS__)) +#define PEG_DEF_17(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_16(__VA_ARGS__)) +#define PEG_DEF_18(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_17(__VA_ARGS__)) +#define PEG_DEF_19(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_18(__VA_ARGS__)) +#define PEG_DEF_20(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_19(__VA_ARGS__)) +#define PEG_DEF_21(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_20(__VA_ARGS__)) +#define PEG_DEF_22(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_21(__VA_ARGS__)) +#define PEG_DEF_23(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_22(__VA_ARGS__)) +#define PEG_DEF_24(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_23(__VA_ARGS__)) +#define PEG_DEF_25(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_24(__VA_ARGS__)) +#define PEG_DEF_26(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_25(__VA_ARGS__)) +#define PEG_DEF_27(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_26(__VA_ARGS__)) +#define PEG_DEF_28(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_27(__VA_ARGS__)) +#define PEG_DEF_29(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_28(__VA_ARGS__)) +#define PEG_DEF_30(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_29(__VA_ARGS__)) +#define PEG_DEF_31(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_30(__VA_ARGS__)) +#define PEG_DEF_32(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_31(__VA_ARGS__)) +#define PEG_DEF_33(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_32(__VA_ARGS__)) +#define PEG_DEF_34(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_33(__VA_ARGS__)) +#define PEG_DEF_35(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_34(__VA_ARGS__)) +#define PEG_DEF_36(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_35(__VA_ARGS__)) +#define PEG_DEF_37(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_36(__VA_ARGS__)) +#define PEG_DEF_38(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_37(__VA_ARGS__)) +#define PEG_DEF_39(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_38(__VA_ARGS__)) +#define PEG_DEF_40(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_39(__VA_ARGS__)) +#define PEG_DEF_41(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_40(__VA_ARGS__)) +#define PEG_DEF_42(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_41(__VA_ARGS__)) +#define PEG_DEF_43(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_42(__VA_ARGS__)) +#define PEG_DEF_44(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_43(__VA_ARGS__)) +#define PEG_DEF_45(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_44(__VA_ARGS__)) +#define PEG_DEF_46(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_45(__VA_ARGS__)) +#define PEG_DEF_47(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_46(__VA_ARGS__)) +#define PEG_DEF_48(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_47(__VA_ARGS__)) +#define PEG_DEF_49(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_48(__VA_ARGS__)) +#define PEG_DEF_50(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_49(__VA_ARGS__)) +#define PEG_DEF_51(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_50(__VA_ARGS__)) +#define PEG_DEF_52(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_51(__VA_ARGS__)) +#define PEG_DEF_53(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_52(__VA_ARGS__)) +#define PEG_DEF_54(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_53(__VA_ARGS__)) +#define PEG_DEF_55(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_54(__VA_ARGS__)) +#define PEG_DEF_56(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_55(__VA_ARGS__)) +#define PEG_DEF_57(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_56(__VA_ARGS__)) +#define PEG_DEF_58(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_57(__VA_ARGS__)) +#define PEG_DEF_59(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_58(__VA_ARGS__)) +#define PEG_DEF_60(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_59(__VA_ARGS__)) +#define PEG_DEF_61(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_60(__VA_ARGS__)) +#define PEG_DEF_62(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_61(__VA_ARGS__)) +#define PEG_DEF_63(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_62(__VA_ARGS__)) +#define PEG_DEF_64(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_63(__VA_ARGS__)) +#define PEG_DEF_65(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_64(__VA_ARGS__)) +#define PEG_DEF_66(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_65(__VA_ARGS__)) +#define PEG_DEF_67(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_66(__VA_ARGS__)) +#define PEG_DEF_68(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_67(__VA_ARGS__)) +#define PEG_DEF_69(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_68(__VA_ARGS__)) +#define PEG_DEF_70(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_69(__VA_ARGS__)) +#define PEG_DEF_71(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_70(__VA_ARGS__)) +#define PEG_DEF_72(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_71(__VA_ARGS__)) +#define PEG_DEF_73(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_72(__VA_ARGS__)) +#define PEG_DEF_74(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_73(__VA_ARGS__)) +#define PEG_DEF_75(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_74(__VA_ARGS__)) +#define PEG_DEF_76(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_75(__VA_ARGS__)) +#define PEG_DEF_77(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_76(__VA_ARGS__)) +#define PEG_DEF_78(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_77(__VA_ARGS__)) +#define PEG_DEF_79(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_78(__VA_ARGS__)) +#define PEG_DEF_80(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_79(__VA_ARGS__)) +#define PEG_DEF_81(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_80(__VA_ARGS__)) +#define PEG_DEF_82(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_81(__VA_ARGS__)) +#define PEG_DEF_83(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_82(__VA_ARGS__)) +#define PEG_DEF_84(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_83(__VA_ARGS__)) +#define PEG_DEF_85(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_84(__VA_ARGS__)) +#define PEG_DEF_86(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_85(__VA_ARGS__)) +#define PEG_DEF_87(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_86(__VA_ARGS__)) +#define PEG_DEF_88(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_87(__VA_ARGS__)) +#define PEG_DEF_89(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_88(__VA_ARGS__)) +#define PEG_DEF_90(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_89(__VA_ARGS__)) +#define PEG_DEF_91(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_90(__VA_ARGS__)) +#define PEG_DEF_92(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_91(__VA_ARGS__)) +#define PEG_DEF_93(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_92(__VA_ARGS__)) +#define PEG_DEF_94(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_93(__VA_ARGS__)) +#define PEG_DEF_95(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_94(__VA_ARGS__)) +#define PEG_DEF_96(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_95(__VA_ARGS__)) +#define PEG_DEF_97(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_96(__VA_ARGS__)) +#define PEG_DEF_98(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_97(__VA_ARGS__)) +#define PEG_DEF_99(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_98(__VA_ARGS__)) +#define PEG_DEF_100(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_99(__VA_ARGS__)) + +#define AST_DEFINITIONS(...) \ + PEG_EXPAND(PEG_CONCAT2(PEG_DEF_, PEG_COUNT(__VA_ARGS__))(__VA_ARGS__)) + +/*----------------------------------------------------------------------------- + * parser + *---------------------------------------------------------------------------*/ + +class parser { +public: + parser() = default; + + parser(const char *s, size_t n, const Rules &rules, + std::string_view start = {}) { + load_grammar(s, n, rules, start); + } + + parser(const char *s, size_t n, std::string_view start = {}) + : parser(s, n, Rules(), start) {} + + parser(std::string_view sv, const Rules &rules, std::string_view start = {}) + : parser(sv.data(), sv.size(), rules, start) {} + + parser(std::string_view sv, std::string_view start = {}) + : parser(sv.data(), sv.size(), Rules(), start) {} + +#if defined(__cpp_lib_char8_t) + parser(std::u8string_view sv, const Rules &rules, std::string_view start = {}) + : parser(reinterpret_cast(sv.data()), sv.size(), rules, + start) {} + + parser(std::u8string_view sv, std::string_view start = {}) + : parser(reinterpret_cast(sv.data()), sv.size(), Rules(), + start) {} +#endif + + operator bool() const { return grammar_ != nullptr; } + + bool load_grammar(const char *s, size_t n, const Rules &rules, + std::string_view start = {}) { + auto cxt = + ParserGenerator::parse(s, n, rules, log_, start, enableLeftRecursion_); + grammar_ = cxt.grammar; + start_ = cxt.start; + enablePackratParsing_ = cxt.enablePackratParsing; + return grammar_ != nullptr; + } + + bool load_grammar(const char *s, size_t n, std::string_view start = {}) { + return load_grammar(s, n, Rules(), start); + } + + bool load_grammar(std::string_view sv, const Rules &rules, + std::string_view start = {}) { + return load_grammar(sv.data(), sv.size(), rules, start); + } + + bool load_grammar(std::string_view sv, std::string_view start = {}) { + return load_grammar(sv.data(), sv.size(), Rules(), start); + } + + bool parse_n(const char *s, size_t n, const char *path = nullptr) const { + if (grammar_ != nullptr) { + const auto &rule = (*grammar_)[start_]; + auto result = rule.parse(s, n, path, log_); + return post_process(s, n, result); + } + return false; + } + + bool parse_n(const char *s, size_t n, std::any &dt, + const char *path = nullptr) const { + if (grammar_ != nullptr) { + const auto &rule = (*grammar_)[start_]; + auto result = rule.parse(s, n, dt, path, log_); + return post_process(s, n, result); + } + return false; + } + + template + bool parse_n(const char *s, size_t n, T &val, + const char *path = nullptr) const { + if (grammar_ != nullptr) { + const auto &rule = (*grammar_)[start_]; + auto result = rule.parse_and_get_value(s, n, val, path, log_); + return post_process(s, n, result); + } + return false; + } + + template + bool parse_n(const char *s, size_t n, std::any &dt, T &val, + const char *path = nullptr) const { + if (grammar_ != nullptr) { + const auto &rule = (*grammar_)[start_]; + auto result = rule.parse_and_get_value(s, n, dt, val, path, log_); + return post_process(s, n, result); + } + return false; + } + + bool parse(std::string_view sv, const char *path = nullptr) const { + return parse_n(sv.data(), sv.size(), path); + } + + bool parse(std::string_view sv, std::any &dt, + const char *path = nullptr) const { + return parse_n(sv.data(), sv.size(), dt, path); + } + + template + bool parse(std::string_view sv, T &val, const char *path = nullptr) const { + return parse_n(sv.data(), sv.size(), val, path); + } + + template + bool parse(std::string_view sv, std::any &dt, T &val, + const char *path = nullptr) const { + return parse_n(sv.data(), sv.size(), dt, val, path); + } + +#if defined(__cpp_lib_char8_t) + bool parse(std::u8string_view sv, const char *path = nullptr) const { + return parse_n(reinterpret_cast(sv.data()), sv.size(), path); + } + + bool parse(std::u8string_view sv, std::any &dt, + const char *path = nullptr) const { + return parse_n(reinterpret_cast(sv.data()), sv.size(), dt, + path); + } + + template + bool parse(std::u8string_view sv, T &val, const char *path = nullptr) const { + return parse_n(reinterpret_cast(sv.data()), sv.size(), val, + path); + } + + template + bool parse(std::u8string_view sv, std::any &dt, T &val, + const char *path = nullptr) const { + return parse_n(reinterpret_cast(sv.data()), sv.size(), dt, + val, path); + } +#endif + + Definition &operator[](const char *s) { return (*grammar_)[s]; } + + const Definition &operator[](const char *s) const { return (*grammar_)[s]; } + + const Grammar &get_grammar() const { return *grammar_; } + + void disable_eoi_check() { + if (grammar_ != nullptr) { + auto &rule = (*grammar_)[start_]; + rule.eoi_check = false; + } + } + + void enable_left_recursion(bool enable = true) { + enableLeftRecursion_ = enable; + } + + void enable_packrat_parsing() { + if (grammar_ != nullptr) { + auto &rule = (*grammar_)[start_]; + rule.enablePackratParsing = enablePackratParsing_; + } + } + + void enable_trace(TracerEnter tracer_enter, TracerLeave tracer_leave) { + if (grammar_ != nullptr) { + auto &rule = (*grammar_)[start_]; + rule.tracer_enter = tracer_enter; + rule.tracer_leave = tracer_leave; + } + } + + void enable_trace(TracerEnter tracer_enter, TracerLeave tracer_leave, + TracerStartOrEnd tracer_start, + TracerStartOrEnd tracer_end) { + if (grammar_ != nullptr) { + auto &rule = (*grammar_)[start_]; + rule.tracer_enter = tracer_enter; + rule.tracer_leave = tracer_leave; + rule.tracer_start = tracer_start; + rule.tracer_end = tracer_end; + } + } + + void set_verbose_trace(bool verbose_trace) { + if (grammar_ != nullptr) { + auto &rule = (*grammar_)[start_]; + rule.verbose_trace = verbose_trace; + } + } + + template parser &enable_ast() { + for (auto &[_, rule] : *grammar_) { + if (!rule.action) { add_ast_action(rule); } + } + return *this; + } + + template + std::shared_ptr optimize_ast(std::shared_ptr ast, + bool opt_mode = true) const { + return AstOptimizer(opt_mode, get_no_ast_opt_rules()).optimize(ast); + } + + void set_logger(Log log) { log_ = log; } + + void set_logger( + std::function + log) { + log_ = [log](size_t line, size_t col, const std::string &msg, + const std::string & /*rule*/) { log(line, col, msg); }; + } + +private: + bool post_process(const char *s, size_t n, Definition::Result &r) const { + if (log_ && !r.ret) { r.error_info.output_log(log_, s, n); } + return r.ret && !r.recovered; + } + + std::vector get_no_ast_opt_rules() const { + std::vector rules; + for (auto &[name, rule] : *grammar_) { + if (rule.no_ast_opt) { rules.push_back(name); } + } + return rules; + } + + std::shared_ptr grammar_; + std::string start_; + bool enableLeftRecursion_ = true; + bool enablePackratParsing_ = false; + Log log_; +}; + +/*----------------------------------------------------------------------------- + * enable_tracing + *---------------------------------------------------------------------------*/ + +inline void enable_tracing(parser &parser, std::ostream &os) { + parser.enable_trace( + [&](auto &ope, auto s, auto, auto &, auto &c, auto &, auto &trace_data) { + auto prev_pos = std::any_cast(trace_data); + auto pos = static_cast(s - c.s); + auto backtrack = (pos < prev_pos ? "*" : ""); + std::string indent; + auto level = c.trace_ids.size() - 1; + while (level--) { + indent += "│"; + } + std::string name; + { + name = peg::TraceOpeName::get(const_cast(ope)); + + auto lit = dynamic_cast(&ope); + if (lit) { name += " '" + peg::escape_characters(lit->lit_) + "'"; } + } + os << "E " << pos + 1 << backtrack << "\t" << indent << "┌" << name + << " #" << c.trace_ids.back() << std::endl; + trace_data = static_cast(pos); + }, + [&](auto &ope, auto s, auto, auto &sv, auto &c, auto &, auto len, + auto &) { + auto pos = static_cast(s - c.s); + if (len != static_cast(-1)) { pos += len; } + std::string indent; + auto level = c.trace_ids.size() - 1; + while (level--) { + indent += "│"; + } + auto ret = len != static_cast(-1) ? "└o " : "└x "; + auto name = peg::TraceOpeName::get(const_cast(ope)); + std::stringstream choice; + if (sv.choice_count() > 0) { + choice << " " << sv.choice() << "/" << sv.choice_count(); + } + std::string token; + if (!sv.tokens.empty()) { + token += ", token '"; + token += sv.tokens[0]; + token += "'"; + } + std::string matched; + if (peg::success(len) && + peg::TokenChecker::is_token(const_cast(ope))) { + matched = ", match '" + peg::escape_characters(s, len) + "'"; + } + os << "L " << pos + 1 << "\t" << indent << ret << name << " #" + << c.trace_ids.back() << choice.str() << token << matched + << std::endl; + }, + [&](auto &trace_data) { trace_data = static_cast(0); }, + [&](auto &) {}); +} + +/*----------------------------------------------------------------------------- + * enable_profiling + *---------------------------------------------------------------------------*/ + +inline void enable_profiling(parser &parser, std::ostream &os) { + struct Stats { + struct Item { + std::string name; + size_t success; + size_t fail; + }; + std::vector items; + std::map index; + size_t total = 0; + std::chrono::steady_clock::time_point start; + }; + + parser.enable_trace( + [&](auto &ope, auto, auto, auto &, auto &, auto &, std::any &trace_data) { + if (auto holder = dynamic_cast(&ope)) { + auto &stats = *std::any_cast(trace_data); + + auto &name = holder->name(); + if (stats.index.find(name) == stats.index.end()) { + stats.index[name] = stats.index.size(); + stats.items.push_back({name, 0, 0}); + } + stats.total++; + } + }, + [&](auto &ope, auto, auto, auto &, auto &, auto &, auto len, + std::any &trace_data) { + if (auto holder = dynamic_cast(&ope)) { + auto &stats = *std::any_cast(trace_data); + + auto &name = holder->name(); + auto index = stats.index[name]; + auto &stat = stats.items[index]; + if (len != static_cast(-1)) { + stat.success++; + } else { + stat.fail++; + } + + if (index == 0) { + auto end = std::chrono::steady_clock::now(); + auto nano = std::chrono::duration_cast( + end - stats.start) + .count(); + auto sec = nano / 1000000.0; + os << "duration: " << sec << "s (" << nano << "µs)" << std::endl + << std::endl; + + char buff[BUFSIZ]; + size_t total_success = 0; + size_t total_fail = 0; + for (auto &[name, success, fail] : stats.items) { + total_success += success; + total_fail += fail; + } + + os << " id total % success fail " + "definition" + << std::endl; + + auto grand_total = total_success + total_fail; + snprintf(buff, BUFSIZ, "%4s %10zu %5s %10zu %10zu %s", "", + grand_total, "", total_success, total_fail, + "Total counters"); + os << buff << std::endl; + + snprintf(buff, BUFSIZ, "%4s %10s %5s %10.2f %10.2f %s", "", "", + "", total_success * 100.0 / grand_total, + total_fail * 100.0 / grand_total, "% success/fail"); + os << buff << std::endl << std::endl; + ; + + size_t id = 0; + for (auto &[name, success, fail] : stats.items) { + auto total = success + fail; + auto ratio = total * 100.0 / stats.total; + snprintf(buff, BUFSIZ, "%4zu %10zu %5.2f %10zu %10zu %s", id, + total, ratio, success, fail, name.c_str()); + os << buff << std::endl; + id++; + } + } + } + }, + [&](auto &trace_data) { + auto stats = new Stats{}; + stats->start = std::chrono::steady_clock::now(); + trace_data = stats; + }, + [&](auto &trace_data) { + auto stats = std::any_cast(trace_data); + delete stats; + }); +} +} // namespace peg diff --git a/libcockatrice_utility/libcockatrice/utility/qt_utils.h b/libcockatrice_utility/libcockatrice/utility/qt_utils.h new file mode 100644 index 000000000..606947143 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/qt_utils.h @@ -0,0 +1,34 @@ +#ifndef COCKATRICE_QT_UTILS_H +#define COCKATRICE_QT_UTILS_H +#include + +namespace QtUtils +{ +template T *findParentOfType(const QObject *obj) +{ + const QObject *p = obj ? obj->parent() : nullptr; + while (p) { + if (auto casted = qobject_cast(const_cast(p))) { + return casted; + } + p = p->parent(); + } + return nullptr; +} + +static inline void clearLayoutRec(QLayout *l) +{ + if (!l) + return; + QLayoutItem *it; + while ((it = l->takeAt(0)) != nullptr) { + if (QWidget *w = it->widget()) + w->deleteLater(); + if (QLayout *sub = it->layout()) + clearLayoutRec(sub); + delete it; + } +} +} // namespace QtUtils + +#endif // COCKATRICE_QT_UTILS_H diff --git a/libcockatrice_utility/libcockatrice/utility/trice_limits.h b/libcockatrice_utility/libcockatrice/utility/trice_limits.h new file mode 100644 index 000000000..fa7ce7489 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/trice_limits.h @@ -0,0 +1,32 @@ +#ifndef TRICE_LIMITS_H +#define TRICE_LIMITS_H + +#include + +// max size for short strings, like names and things that are generally a single phrase +constexpr int MAX_NAME_LENGTH = 0xff; +// max size for chat messages and text contents +constexpr int MAX_TEXT_LENGTH = 0xfff; +// max size for deck files and pictures +constexpr int MAX_FILE_LENGTH = 0x1fffff; // about 2 megabytes + +constexpr uint MINIMUM_DIE_SIDES = 2; +constexpr uint MAXIMUM_DIE_SIDES = 1000000; +constexpr uint MINIMUM_DICE_TO_ROLL = 1; +constexpr uint MAXIMUM_DICE_TO_ROLL = 100; + +// optimized functions to get qstrings that are at most that long +static inline QString nameFromStdString(const std::string &_string) +{ + return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_NAME_LENGTH)); +} +static inline QString textFromStdString(const std::string &_string) +{ + return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_TEXT_LENGTH)); +} +static inline QString fileFromStdString(const std::string &_string) +{ + return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_FILE_LENGTH)); +} + +#endif // TRICE_LIMITS_H diff --git a/libcockatrice_utility/libcockatrice/utility/zone_names.h b/libcockatrice_utility/libcockatrice/utility/zone_names.h new file mode 100644 index 000000000..d1463de6a --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/zone_names.h @@ -0,0 +1,19 @@ +#ifndef ZONE_NAMES_H +#define ZONE_NAMES_H + +namespace ZoneNames +{ +// Protocol-level zone identifiers shared between client and server. +// These must match exactly across all components. + +constexpr const char *TABLE = "table"; +constexpr const char *GRAVE = "grave"; +constexpr const char *EXILE = "rfg"; // "removed from game" +constexpr const char *HAND = "hand"; +constexpr const char *DECK = "deck"; +constexpr const char *SIDEBOARD = "sb"; +constexpr const char *STACK = "stack"; + +} // namespace ZoneNames + +#endif // ZONE_NAMES_H diff --git a/nsis/cockatrice.nsi b/nsis/cockatrice.nsi deleted file mode 100644 index 27f55ce30..000000000 --- a/nsis/cockatrice.nsi +++ /dev/null @@ -1,132 +0,0 @@ -!include "MUI2.nsh" -!include "FileFunc.nsh" - -!define /date TIMESTAMP "%Y%m%d" -!searchparse /file ../build/cockatrice/version_string.cpp '= "' VERSION '";' - -Name "Cockatrice" -OutFile "cockatrice_win32_${TIMESTAMP}_git-${VERSION}.exe" -SetCompressor /SOLID lzma -InstallDir "$PROGRAMFILES\Cockatrice" - -; set the Qt install dir here (and make sure you use the latest 4.8 version for packaging) -!define QTDIR "C:\Qt\4.8.6" - -!define MUI_ABORTWARNING -!define MUI_WELCOMEFINISHPAGE_BITMAP "leftimage.bmp" -!define MUI_UNWELCOMEFINISHPAGE_BITMAP "leftimage.bmp" -!define MUI_HEADERIMAGE -!define MUI_HEADERIMAGE_BITMAP "headerimage.bmp" -!define MUI_HEADERIMAGE_UNBITMAP "headerimage.bmp" -!define MUI_WELCOMEPAGE_TEXT "This wizard will guide you through the installation of Cockatrice.$\r$\n$\r$\nClick Next to continue." -!define MUI_FINISHPAGE_RUN "$INSTDIR/oracle.exe" -!define MUI_FINISHPAGE_RUN_TEXT "Run card database downloader now" -!define MUI_FINISHPAGE_RUN_PARAMETERS "-dlsets" - -!insertmacro MUI_PAGE_WELCOME -!insertmacro MUI_PAGE_LICENSE "..\COPYING" -!insertmacro MUI_PAGE_COMPONENTS -!insertmacro MUI_PAGE_DIRECTORY -!insertmacro MUI_PAGE_INSTFILES -!insertmacro MUI_PAGE_FINISH - -!insertmacro MUI_UNPAGE_CONFIRM -!insertmacro MUI_UNPAGE_INSTFILES -!insertmacro MUI_UNPAGE_FINISH - -!insertmacro MUI_LANGUAGE "English" - -Section "Application" SecApplication - SetShellVarContext all - SetOutPath "$INSTDIR" - File ..\build\cockatrice\Release\cockatrice.exe - File ..\build\oracle\Release\oracle.exe - File ..\doc\usermanual\Usermanual.pdf - File ..\build\protobuf-2.5.0\protobuf-2.5.0\vsprojects\Release\libprotobuf.lib - File "${QTDIR}\bin\QtCore4.dll" - File "${QTDIR}\bin\QtGui4.dll" - File "${QTDIR}\bin\QtNetwork4.dll" - File "${QTDIR}\bin\QtSvg4.dll" - File "${QTDIR}\bin\QtXml4.dll" - File "${QTDIR}\bin\QtMultimedia4.dll" - - SetOutPath "$INSTDIR\zonebg" - File /r ..\zonebg\*.* - - SetOutPath "$INSTDIR\plugins" - SetOutPath "$INSTDIR\plugins\codecs" - File "${QTDIR}\plugins\codecs\qcncodecs4.dll" - File "${QTDIR}\plugins\codecs\qjpcodecs4.dll" - File "${QTDIR}\plugins\codecs\qkrcodecs4.dll" - File "${QTDIR}\plugins\codecs\qtwcodecs4.dll" - SetOutPath "$INSTDIR\plugins\iconengines" - File "${QTDIR}\plugins\iconengines\qsvgicon4.dll" - SetOutPath "$INSTDIR\plugins\imageformats" - File "${QTDIR}\plugins\imageformats\qjpeg4.dll" - File "${QTDIR}\plugins\imageformats\qsvg4.dll" - - SetOutPath "$INSTDIR\sounds" - File /r ..\sounds\*.* - - SetOutPath "$INSTDIR\translations" - File /r ..\build\cockatrice\*.qm - - WriteUninstaller "$INSTDIR\uninstall.exe" - ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 - IntFmt $0 "0x%08X" $0 - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayName" "Cockatrice" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "InstallLocation" "$INSTDIR" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayIcon" "$INSTDIR\cockatrice.exe" - WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "EstimatedSize" "$0" -SectionEnd - -Section "Update configuration" SecUpdateConfig - WriteRegStr HKCU "Software\Cockatrice\Cockatrice\paths" "carddatabase" "$APPDATA\Cockatrice\cards.xml" - WriteRegStr HKCU "Software\Cockatrice\Cockatrice\paths" "decks" "$APPDATA\Cockatrice\decks" - WriteRegStr HKCU "Software\Cockatrice\Cockatrice\paths" "pics" "$APPDATA\Cockatrice\pics" - WriteRegStr HKCU "Software\Cockatrice\Cockatrice\sound" "path" "$APPDATA\Cockatrice\sounds" -SectionEnd - -Section "Start menu item" SecStartMenu - createDirectory "$SMPROGRAMS\Cockatrice" - createShortCut "$SMPROGRAMS\Cockatrice\Cockatrice.lnk" "$INSTDIR\cockatrice.exe" '--debug-output' - createShortCut "$SMPROGRAMS\Cockatrice\Oracle.lnk" "$INSTDIR\oracle.exe" - createShortCut "$SMPROGRAMS\Cockatrice\Usermanual.lnk" "$INSTDIR\Usermanual.pdf" -SectionEnd - -Section Uninstall -SetShellVarContext all - RMDir /r "$INSTDIR\zonebg" - RMDir /r "$INSTDIR\plugins" - RMDir /r "$INSTDIR\sounds" - RMDir /r "$INSTDIR\translations" - Delete "$INSTDIR\uninstall.exe" - Delete "$INSTDIR\cockatrice.exe" - Delete "$INSTDIR\oracle.exe" - Delete "$INSTDIR\Usermanual.pdf" - Delete "$INSTDIR\libprotobuf.lib" - Delete "$INSTDIR\QtCore4.dll" - Delete "$INSTDIR\QtGui4.dll" - Delete "$INSTDIR\QtNetwork4.dll" - Delete "$INSTDIR\QtSvg4.dll" - Delete "$INSTDIR\QtXml4.dll" - Delete "$INSTDIR\QtMultimedia4.dll" - RMDir "$INSTDIR" - - RMDir "$SMPROGRAMS\Cockatrice" - - DeleteRegKey HKCU "Software\Cockatrice" - DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" -SectionEnd - -LangString DESC_SecApplication ${LANG_ENGLISH} "Cockatrice program files" -LangString DESC_SecUpdateConfig ${LANG_ENGLISH} "Update the paths in the application settings according to the installation paths." -LangString DESC_SecStartMenu ${LANG_ENGLISH} "Create start menu items for Cockatrice and Oracle." -!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN - !insertmacro MUI_DESCRIPTION_TEXT ${SecApplication} $(DESC_SecApplication) - !insertmacro MUI_DESCRIPTION_TEXT ${SecUpdateConfig} $(DESC_SecUpdateConfig) - !insertmacro MUI_DESCRIPTION_TEXT ${SecStartMenu} $(DESC_SecStartMenu) -!insertmacro MUI_FUNCTION_DESCRIPTION_END - diff --git a/nsis/headerimage.bmp b/nsis/headerimage.bmp deleted file mode 100644 index 73c88d6d4..000000000 Binary files a/nsis/headerimage.bmp and /dev/null differ diff --git a/nsis/leftimage.bmp b/nsis/leftimage.bmp deleted file mode 100644 index e89fdd95e..000000000 Binary files a/nsis/leftimage.bmp and /dev/null differ diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index 05bcd494b..3bb4de5df 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -1,80 +1,316 @@ -# CMakeLists for oracle directory -# -# provides the oracle binary +cmake_minimum_required(VERSION 3.16) +project(Oracle VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") -PROJECT(oracle) +# ------------------------ +# Paths and directories +# ------------------------ +set(DESKTOPDIR + share/applications + CACHE STRING "path to .desktop files" +) -# paths -set(DESKTOPDIR share/applications CACHE STRING "path to .desktop files") +set(ORACLE_MAC_QM_INSTALL_DIR "oracle.app/Contents/Resources/translations") +set(ORACLE_UNIX_QM_INSTALL_DIR "share/oracle/translations") +set(ORACLE_WIN32_QM_INSTALL_DIR "translations") -SET(oracle_SOURCES +# ------------------------ +# Sources +# ------------------------ +set(oracle_SOURCES src/main.cpp src/oraclewizard.cpp src/oracleimporter.cpp - ../cockatrice/src/carddatabase.cpp - ../cockatrice/src/settingscache.cpp - ../cockatrice/src/qt-json/json.cpp - ) + src/pages.cpp + src/pagetemplates.cpp + src/parsehelpers.cpp + src/qt-json/json.cpp + ../cockatrice/src/client/settings/cache_settings.cpp + ../cockatrice/src/client/settings/card_counter_settings.cpp + ../cockatrice/src/client/settings/shortcuts_settings.cpp + ../cockatrice/src/client/network/update/client/release_channel.cpp + ../cockatrice/src/interface/theme_manager.cpp + ../cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp + ../cockatrice/src/interface/widgets/quick_settings/settings_popup_widget.cpp + ${VERSION_STRING_CPP} +) -SET(QT_USE_QTNETWORK TRUE) -SET(QT_USE_QTXML TRUE) -SET(QT_USE_QTSVG TRUE) +# ------------------------ +# Translations +# ------------------------ -# Include directories -INCLUDE(${QT_USE_FILE}) -INCLUDE_DIRECTORIES(../cockatrice/src) +if(UPDATE_TRANSLATIONS) + file(GLOB_RECURSE translate_oracle_SRCS src/*.cpp src/*.h ../cockatrice/src/settingscache.cpp) + set(translate_SRCS ${translate_oracle_SRCS}) + set(oracle_TS "${CMAKE_CURRENT_SOURCE_DIR}/oracle_en@source.ts") +else() + file(GLOB oracle_TS "${CMAKE_CURRENT_SOURCE_DIR}/translations/*.ts") +endif(UPDATE_TRANSLATIONS) -# Build oracle binary and link it -ADD_EXECUTABLE(oracle WIN32 MACOSX_BUNDLE ${oracle_SOURCES} ${oracle_MOC_SRCS}) -TARGET_LINK_LIBRARIES(oracle ${QT_QTMAIN_LIBRARY} ${QT_LIBRARIES}) - -if(MSVC) - set_target_properties(oracle PROPERTIES LINK_FLAGS "/SUBSYSTEM:WINDOWS") -endif(MSVC) - -if(UNIX) - if(APPLE) - INSTALL(TARGETS oracle BUNDLE DESTINATION ./) - else() - # Assume linux - INSTALL(TARGETS oracle RUNTIME DESTINATION bin/) - endif() -elseif(WIN32) - INSTALL(TARGETS oracle RUNTIME DESTINATION ./) -endif() - -IF (NOT WIN32 AND NOT APPLE) - INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/oracle.desktop DESTINATION ${DESKTOPDIR}) -ENDIF (NOT WIN32 AND NOT APPLE) +if(WIN32) + set(oracle_SOURCES ${oracle_SOURCES} oracle.rc) +endif(WIN32) if(APPLE) - # these needs to be relative to CMAKE_INSTALL_PREFIX - set(plugin_dest_dir oracle.app/Contents/Plugins) - set(qtconf_dest_dir oracle.app/Contents/Resources) + set(MACOSX_BUNDLE_ICON_FILE appicon.icns) + set_source_files_properties( + ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources + ) + set(oracle_SOURCES ${oracle_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns) +endif(APPLE) - # note: no codecs in qt5 - # note: phonon_backend => mediaservice - # note: needs platform on osx +set(oracle_RESOURCES oracle.qrc) - if (CMAKE_BUILD_TYPE STREQUAL "Debug") - install(DIRECTORY "${QT_PLUGINS_DIR}/" DESTINATION ${plugin_dest_dir} COMPONENT Runtime - FILES_MATCHING REGEX "(codecs|iconengines|imageformats|mediaservice|phonon_backend|platforms)/.*_debug\\.dylib") +# ------------------------ +# Qt resources +# ------------------------ +if(Qt6_FOUND) + qt6_add_resources(oracle_RESOURCES_RCC ${oracle_RESOURCES}) +elseif(Qt5_FOUND) + qt5_add_resources(oracle_RESOURCES_RCC ${oracle_RESOURCES}) +endif() + +# ------------------------ +# Include directories +# ------------------------ +include_directories(../cockatrice/src) + +# ------------------------ +# Optional libraries +# ------------------------ +# ZLIB +find_package(ZLIB) +if(ZLIB_FOUND) + include_directories(${ZLIB_INCLUDE_DIRS}) + add_definitions("-DHAS_ZLIB") + list(APPEND oracle_SOURCES src/zip/unzip.cpp src/zip/zipglobal.cpp) +else() + message(STATUS "Oracle: zlib not found; ZIP support disabled") +endif() + +# LZMA +find_package(LibLZMA) +if(LIBLZMA_FOUND) + include_directories(${LIBLZMA_INCLUDE_DIRS}) + add_definitions("-DHAS_LZMA") + list(APPEND oracle_SOURCES src/lzma/decompress.cpp) +else() + message(STATUS "Oracle: LibLZMA not found; xz support disabled") +endif() + +# ------------------------ +# Build executable +# ------------------------ + +set(ORACLE_MAC_QM_INSTALL_DIR "oracle.app/Contents/Resources/translations") +set(ORACLE_UNIX_QM_INSTALL_DIR "share/oracle/translations") +set(ORACLE_WIN32_QM_INSTALL_DIR "translations") + +if(Qt6_FOUND) + # Qt6 Translations are linked after the executable is created in manual mode + qt6_add_executable( + oracle + WIN32 + MACOSX_BUNDLE + ${oracle_SOURCES} + ${oracle_RESOURCES_RCC} + ${oracle_MOC_SRCS} + MANUAL_FINALIZATION + ) +elseif(Qt5_FOUND) + # Qt5 Translations need to be linked at executable creation time + if(Qt5LinguistTools_FOUND) + if(UPDATE_TRANSLATIONS) + qt5_create_translation(oracle_QM ${translate_SRCS} ${oracle_TS}) else() - install(DIRECTORY "${QT_PLUGINS_DIR}/" DESTINATION ${plugin_dest_dir} COMPONENT Runtime - FILES_MATCHING REGEX "(codecs|iconengines|imageformats|mediaservice|phonon_backend|platforms)/[^_]*\\.dylib") + qt5_add_translation(oracle_QM ${oracle_TS}) endif() + endif() + add_executable(oracle WIN32 MACOSX_BUNDLE ${oracle_MOC_SRCS} ${oracle_QM} ${oracle_RESOURCES_RCC} ${oracle_SOURCES}) + if(UNIX) + if(APPLE) + install(FILES ${oracle_QM} DESTINATION ${ORACLE_MAC_QM_INSTALL_DIR}) + else() + install(FILES ${oracle_QM} DESTINATION ${ORACLE_UNIX_QM_INSTALL_DIR}) + endif() + elseif(WIN32) + install(FILES ${oracle_QM} DESTINATION ${ORACLE_WIN32_QM_INSTALL_DIR}) + endif() +endif() - install(CODE " +# ------------------------ +# Link libraries +# ------------------------ +target_link_libraries( + oracle + PUBLIC libcockatrice_card + PUBLIC libcockatrice_settings + PUBLIC libcockatrice_network + PUBLIC ${ORACLE_QT_MODULES} +) + +if(ZLIB_FOUND) + target_link_libraries(oracle PUBLIC ${ZLIB_LIBRARIES}) +endif() + +if(LIBLZMA_FOUND) + target_link_libraries(oracle PUBLIC ${LIBLZMA_LIBRARIES}) +endif() + +# ------------------------ +# Install rules +# ------------------------ +if(UNIX) + if(APPLE) + set(MACOSX_BUNDLE_INFO_STRING "${PROJECT_NAME}") + set(MACOSX_BUNDLE_GUI_IDENTIFIER "com.cockatrice.${PROJECT_NAME}") + set(MACOSX_BUNDLE_LONG_VERSION_STRING "${PROJECT_NAME}-${PROJECT_VERSION}") + set(MACOSX_BUNDLE_BUNDLE_NAME ${PROJECT_NAME}) + set(MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION}) + set(MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}) + set_target_properties(oracle PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/cmake/Info.plist) + + install(TARGETS oracle BUNDLE DESTINATION ./) + else() + # Assume linux + install(TARGETS oracle RUNTIME DESTINATION bin/) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/oracle.png DESTINATION ${ICONDIR}/hicolor/48x48/apps) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/oracle.svg DESTINATION ${ICONDIR}/hicolor/scalable/apps) + endif() +elseif(WIN32) + install(TARGETS oracle RUNTIME DESTINATION ./) +endif() + +if(NOT WIN32 AND NOT APPLE) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/oracle.desktop DESTINATION ${DESKTOPDIR}) +endif(NOT WIN32 AND NOT APPLE) + +# ------------------------ +# Qt plugin handling +# ------------------------ +if(APPLE) + # these needs to be relative to CMAKE_INSTALL_PREFIX + set(plugin_dest_dir oracle.app/Contents/Plugins) + set(qtconf_dest_dir oracle.app/Contents/Resources) + + # Qt plugins: iconengines, platforms, styles, tls (Qt6) + install( + DIRECTORY "${QT_PLUGINS_DIR}/" + DESTINATION ${plugin_dest_dir} + COMPONENT Runtime + FILES_MATCHING + PATTERN "*.dSYM" EXCLUDE + PATTERN "*_debug.dylib" EXCLUDE + PATTERN "iconengines/*.dylib" + PATTERN "platforms/*.dylib" + PATTERN "styles/*.dylib" + PATTERN "tls/*.dylib" + ) + + install( + CODE " file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${qtconf_dest_dir}/qt.conf\" \"[Paths] Plugins = Plugins Translations = Resources/translations\") - " COMPONENT Runtime) + " + COMPONENT Runtime + ) - install(CODE " + install( + CODE " file(GLOB_RECURSE QTPLUGINS \"\${CMAKE_INSTALL_PREFIX}/${plugin_dest_dir}/*.dylib\") set(BU_CHMOD_BUNDLE_ITEMS ON) include(BundleUtilities) fixup_bundle(\"\${CMAKE_INSTALL_PREFIX}/oracle.app\" \"\${QTPLUGINS}\" \"${QT_LIBRARY_DIR}\") - " COMPONENT Runtime) + " + COMPONENT Runtime + ) +endif() + +if(WIN32) + # these needs to be relative to CMAKE_INSTALL_PREFIX + set(plugin_dest_dir Plugins) + set(qtconf_dest_dir .) + list(APPEND libSearchDirs ${QT_LIBRARY_DIR}) + + install( + DIRECTORY "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/${CMAKE_BUILD_TYPE}/" + DESTINATION ./ + FILES_MATCHING + PATTERN "*.dll" + ) + + # Qt plugins: iconengines, platforms, styles, tls (Qt6) + install( + DIRECTORY "${QT_PLUGINS_DIR}/" + DESTINATION ${plugin_dest_dir} + COMPONENT Runtime + FILES_MATCHING + PATTERN "iconengines/qsvgicon.dll" + PATTERN "platforms/qdirect2d.dll" + PATTERN "platforms/qminimal.dll" + PATTERN "platforms/qoffscreen.dll" + PATTERN "platforms/qwindows.dll" + PATTERN "styles/qcertonlybackend.dll" + PATTERN "styles/qopensslbackend.dll" + PATTERN "styles/qschannelbackend.dll" + PATTERN "styles/qwindowsvistastyle.dll" + PATTERN "tls/qcertonlybackend.dll" + PATTERN "tls/qopensslbackend.dll" + PATTERN "tls/qschannelbackend.dll" + ) + + install( + CODE " + file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${qtconf_dest_dir}/qt.conf\" \"[Paths] +Plugins = Plugins +Translations = Resources/translations\") + " + COMPONENT Runtime + ) + + install( + CODE " + file(GLOB_RECURSE QTPLUGINS + \"\${CMAKE_INSTALL_PREFIX}/${plugin_dest_dir}/*.dll\") + set(BU_CHMOD_BUNDLE_ITEMS ON) + include(BundleUtilities) + fixup_bundle(\"\${CMAKE_INSTALL_PREFIX}/Oracle.exe\" \"\${QTPLUGINS}\" \"${libSearchDirs}\") + " + COMPONENT Runtime + ) +endif() + +# ------------------------ +# Qt translations +# ------------------------ +if(Qt6_FOUND AND Qt6LinguistTools_FOUND) + #Qt6 Translations happen after the executable is built up + if(UPDATE_TRANSLATIONS) + qt6_add_translations( + oracle + TS_FILES + ${oracle_TS} + SOURCES + ${translate_SRCS} + QM_FILES_OUTPUT_VARIABLE + oracle_QM + ) + else() + qt6_add_translations(oracle TS_FILES ${oracle_TS} QM_FILES_OUTPUT_VARIABLE oracle_QM) + endif() + + if(UNIX) + if(APPLE) + install(FILES ${oracle_QM} DESTINATION ${ORACLE_MAC_QM_INSTALL_DIR}) + else() + install(FILES ${oracle_QM} DESTINATION ${ORACLE_UNIX_QM_INSTALL_DIR}) + endif() + elseif(WIN32) + install(FILES ${oracle_QM} DESTINATION ${ORACLE_WIN32_QM_INSTALL_DIR}) + endif() +endif() + +if(Qt6_FOUND) + qt6_finalize_target(oracle) endif() diff --git a/oracle/oracle.desktop b/oracle/oracle.desktop index db8d4b617..c4943b859 100644 --- a/oracle/oracle.desktop +++ b/oracle/oracle.desktop @@ -4,5 +4,5 @@ Version=1.0 Type=Application Name=Cockatrice Oracle downloader Exec=oracle -Icon=cockatrice +Icon=oracle Categories=Game;CardGame; diff --git a/oracle/oracle.qrc b/oracle/oracle.qrc new file mode 100644 index 000000000..b58518c41 --- /dev/null +++ b/oracle/oracle.qrc @@ -0,0 +1,5 @@ + + + resources/oracle.svg + + diff --git a/oracle/oracle.rc b/oracle/oracle.rc new file mode 100644 index 000000000..cf949f313 --- /dev/null +++ b/oracle/oracle.rc @@ -0,0 +1 @@ +ID1_ICON1 ICON DISCARDABLE "resources/appicon.ico" diff --git a/oracle/oracle_en@source.ts b/oracle/oracle_en@source.ts new file mode 100644 index 000000000..943b44a97 --- /dev/null +++ b/oracle/oracle_en@source.ts @@ -0,0 +1,610 @@ + + + + + IntroPage + + + Introduction + + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + + + + + Interface language: + + + + + Version: + + + + + LoadSetsPage + + + Source selection + + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + + + + + Download URL: + + + + + Local file: + + + + + Restore default URL + + + + + Choose file... + + + + + Load sets file + + + + + Sets file (%1) + Sets JSON file (%1) + + + + + + + + + + + Error + + + + + The provided URL is not valid. + + + + + Downloading (0MB) + + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + + + + + Network error: %1. + + + + + Parsing file + + + + + Xz extraction failed. + + + + + Sorry, this version of Oracle does not support xz compressed files. + + + + + Failed to open Zip archive: %1. + + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + + + + + Zip extraction failed: %1. + + + + + Sorry, this version of Oracle does not support zipped files. + + + + + Failed to interpret downloaded data. + + + + + Do you want to download the uncompressed file instead? + + + + + The file was retrieved successfully, but it does not contain any sets data. + + + + + LoadSpoilersPage + + + Save spoiler database + + + + + XML; spoiler database (*.xml) + + + + + spoiler + + + + + Spoilers import + + + + + Please specify a compatible source for spoiler data. + + + + + Download URL: + + + + + Local file: + + + + + Restore default URL + + + + + Choose file... + + + + + The spoiler database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + LoadTokensPage + + + Save token database + + + + + XML; token database (*.xml) + + + + + tokens + + + + + Tokens import + + + + + Please specify a compatible source for token data. + + + + + Download URL: + + + + + Local file: + + + + + Restore default URL + + + + + Choose file... + + + + + The token database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + OracleImporter + + + Dummy set containing tokens + + + + + OracleWizard + + + Oracle Importer + + + + + OutroPage + + + Finished + + + + + The wizard has finished. + + + + + You can now start using Cockatrice with the newly updated cards. + + + + + If the card databases don't reload automatically, restart the Cockatrice client. + + + + + SaveSetsPage + + + + Error + + + + + No set has been imported. + + + + + Sets imported + + + + + A cockatrice database file of %1 MB has been downloaded. + + + + + The following sets have been found: + + + + + Press "Save" to store the imported cards in the Cockatrice database. + + + + + The card database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + &Save + + + + + Import finished: %1 cards. + + + + + %1: %2 cards imported + + + + + Save card database + + + + + XML; card database (*.xml) + + + + + The file could not be saved to %1 + + + + + SimpleDownloadFilePage + + + Load %1 file + + + + + %1 file (%1) + + + + + + + + + Error + + + + + The provided URL is not valid: + + + + + Downloading (0MB) + + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + + + + + Network error: %1. + + + + + The file could not be saved to %1 + + + + + UnZip + + + ZIP operation completed successfully. + + + + + Failed to initialize or load zlib library. + + + + + zlib library error. + + + + + Unable to create or open file. + + + + + Partially corrupted archive. Some files might be extracted. + + + + + Corrupted archive. + + + + + Wrong password. + + + + + No archive has been created yet. + + + + + File or directory does not exist. + + + + + File read error. + + + + + File write error. + + + + + File seek error. + + + + + Unable to create a directory. + + + + + Invalid device. + + + + + Invalid or incompatible zip archive. + + + + + Inconsistent headers. Archive might be corrupted. + + + + + Unknown error. + + + + + Zip + + + ZIP operation completed successfully. + + + + + Failed to initialize or load zlib library. + + + + + zlib library error. + + + + + Unable to create or open file. + + + + + No archive has been created yet. + + + + + File or directory does not exist. + + + + + File read error. + + + + + File write error. + + + + + File seek error. + + + + + Unknown error. + + + + + i18n + + + English + + + + + main + + + Only run in spoiler mode + + + + + Run in no-confirm background mode + + + + diff --git a/oracle/resources/appicon.icns b/oracle/resources/appicon.icns new file mode 100644 index 000000000..fbbee56c8 Binary files /dev/null and b/oracle/resources/appicon.icns differ diff --git a/oracle/resources/appicon.ico b/oracle/resources/appicon.ico new file mode 100644 index 000000000..e484c2a4d Binary files /dev/null and b/oracle/resources/appicon.ico differ diff --git a/oracle/resources/oracle.png b/oracle/resources/oracle.png new file mode 100644 index 000000000..80806c65d Binary files /dev/null and b/oracle/resources/oracle.png differ diff --git a/oracle/resources/oracle.svg b/oracle/resources/oracle.svg new file mode 100644 index 000000000..59805a331 --- /dev/null +++ b/oracle/resources/oracle.svg @@ -0,0 +1,337 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/oracle/src/lzma/decompress.cpp b/oracle/src/lzma/decompress.cpp new file mode 100644 index 000000000..718cde207 --- /dev/null +++ b/oracle/src/lzma/decompress.cpp @@ -0,0 +1,250 @@ +/* + * Simple routing to extract a single file from a xz archive + * Heavily based from doc/examples/02_decompress.c obtained from + * the official xz git repository: git.tukaani.org/xz.git + * The license from the original file header follows + * + * Author: Lasse Collin + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + + +#include +#include + +#include "decompress.h" + +XzDecompressor::XzDecompressor(QObject *parent) + : QObject(parent) +{ + +} + +bool XzDecompressor::decompress(QBuffer *in, QBuffer *out) +{ + lzma_stream strm = LZMA_STREAM_INIT; + bool success; + + if (!init_decoder(&strm)) { + return false; + } + + success = internal_decompress(&strm, in, out); + + // Free the memory allocated for the decoder. This only needs to be + // done after the last file. + lzma_end(&strm); + + return success; +} + +bool XzDecompressor::init_decoder(lzma_stream *strm) +{ + // Initialize a .xz decoder. The decoder supports a memory usage limit + // and a set of flags. + // + // The memory usage of the decompressor depends on the settings used + // to compress a .xz file. It can vary from less than a megabyte to + // a few gigabytes, but in practice (at least for now) it rarely + // exceeds 65 MiB because that's how much memory is required to + // decompress files created with "xz -9". Settings requiring more + // memory take extra effort to use and don't (at least for now) + // provide significantly better compression in most cases. + // + // Memory usage limit is useful if it is important that the + // decompressor won't consume gigabytes of memory. The need + // for limiting depends on the application. In this example, + // no memory usage limiting is used. This is done by setting + // the limit to UINT64_MAX. + // + // The .xz format allows concatenating compressed files as is: + // + // echo foo | xz > foobar.xz + // echo bar | xz >> foobar.xz + // + // When decompressing normal standalone .xz files, LZMA_CONCATENATED + // should always be used to support decompression of concatenated + // .xz files. If LZMA_CONCATENATED isn't used, the decoder will stop + // after the first .xz stream. This can be useful when .xz data has + // been embedded inside another file format. + // + // Flags other than LZMA_CONCATENATED are supported too, and can + // be combined with bitwise-or. See lzma/container.h + // (src/liblzma/api/lzma/container.h in the source package or e.g. + // /usr/include/lzma/container.h depending on the install prefix) + // for details. + lzma_ret ret = lzma_stream_decoder( + strm, UINT64_MAX, LZMA_CONCATENATED); + + // Return successfully if the initialization went fine. + if (ret == LZMA_OK) + return true; + + // Something went wrong. The possible errors are documented in + // lzma/container.h (src/liblzma/api/lzma/container.h in the source + // package or e.g. /usr/include/lzma/container.h depending on the + // install prefix). + // + // Note that LZMA_MEMLIMIT_ERROR is never possible here. If you + // specify a very tiny limit, the error will be delayed until + // the first headers have been parsed by a call to lzma_code(). + const char *msg; + switch (ret) { + case LZMA_MEM_ERROR: + msg = "Memory allocation failed"; + break; + + case LZMA_OPTIONS_ERROR: + msg = "Unsupported decompressor flags"; + break; + + default: + // This is most likely LZMA_PROG_ERROR indicating a bug in + // this program or in liblzma. It is inconvenient to have a + // separate error message for errors that should be impossible + // to occur, but knowing the error code is important for + // debugging. That's why it is good to print the error code + // at least when there is no good error message to show. + msg = "Unknown error, possibly a bug"; + break; + } + + qDebug() << "Error initializing the decoder:" << msg << "(error code " << ret << ")"; + return false; +} + + +bool XzDecompressor::internal_decompress(lzma_stream *strm, QBuffer *in, QBuffer *out) +{ + // When LZMA_CONCATENATED flag was used when initializing the decoder, + // we need to tell lzma_code() when there will be no more input. + // This is done by setting action to LZMA_FINISH instead of LZMA_RUN + // in the same way as it is done when encoding. + // + // When LZMA_CONCATENATED isn't used, there is no need to use + // LZMA_FINISH to tell when all the input has been read, but it + // is still OK to use it if you want. When LZMA_CONCATENATED isn't + // used, the decoder will stop after the first .xz stream. In that + // case some unused data may be left in strm->next_in. + lzma_action action = LZMA_RUN; + + uint8_t inbuf[BUFSIZ]; + uint8_t outbuf[BUFSIZ]; + qint64 bytesAvailable; + + strm->next_in = NULL; + strm->avail_in = 0; + strm->next_out = outbuf; + strm->avail_out = sizeof(outbuf); + while (true) { + if (strm->avail_in == 0) { + strm->next_in = inbuf; + bytesAvailable = in->bytesAvailable(); + if(bytesAvailable == 0) { + // Once the end of the input file has been reached, + // we need to tell lzma_code() that no more input + // will be coming. As said before, this isn't required + // if the LZMA_CONCATENATED flag isn't used when + // initializing the decoder. + action = LZMA_FINISH; + } else if(bytesAvailable >= BUFSIZ) { + in->read((char*) inbuf, BUFSIZ); + strm->avail_in = BUFSIZ; + } else { + in->read((char*) inbuf, bytesAvailable); + strm->avail_in = bytesAvailable; + } + } + + lzma_ret ret = lzma_code(strm, action); + + if (strm->avail_out == 0 || ret == LZMA_STREAM_END) { + qint64 write_size = sizeof(outbuf) - strm->avail_out; + + if (out->write((char *) outbuf, write_size) != write_size) { + qDebug() << "Write error"; + return false; + } + + strm->next_out = outbuf; + strm->avail_out = sizeof(outbuf); + } + + if (ret != LZMA_OK) { + // Once everything has been decoded successfully, the + // return value of lzma_code() will be LZMA_STREAM_END. + // + // It is important to check for LZMA_STREAM_END. Do not + // assume that getting ret != LZMA_OK would mean that + // everything has gone well or that when you aren't + // getting more output it must have successfully + // decoded everything. + if (ret == LZMA_STREAM_END) + return true; + + // It's not LZMA_OK nor LZMA_STREAM_END, + // so it must be an error code. See lzma/base.h + // (src/liblzma/api/lzma/base.h in the source package + // or e.g. /usr/include/lzma/base.h depending on the + // install prefix) for the list and documentation of + // possible values. Many values listen in lzma_ret + // enumeration aren't possible in this example, but + // can be made possible by enabling memory usage limit + // or adding flags to the decoder initialization. + const char *msg; + switch (ret) { + case LZMA_MEM_ERROR: + msg = "Memory allocation failed"; + break; + + case LZMA_FORMAT_ERROR: + // .xz magic bytes weren't found. + msg = "The input is not in the .xz format"; + break; + + case LZMA_OPTIONS_ERROR: + // For example, the headers specify a filter + // that isn't supported by this liblzma + // version (or it hasn't been enabled when + // building liblzma, but no-one sane does + // that unless building liblzma for an + // embedded system). Upgrading to a newer + // liblzma might help. + // + // Note that it is unlikely that the file has + // accidentally became corrupt if you get this + // error. The integrity of the .xz headers is + // always verified with a CRC32, so + // unintentionally corrupt files can be + // distinguished from unsupported files. + msg = "Unsupported compression options"; + break; + + case LZMA_DATA_ERROR: + msg = "Compressed file is corrupt"; + break; + + case LZMA_BUF_ERROR: + // Typically this error means that a valid + // file has got truncated, but it might also + // be a damaged part in the file that makes + // the decoder think the file is truncated. + // If you prefer, you can use the same error + // message for this as for LZMA_DATA_ERROR. + msg = "Compressed file is truncated or " + "otherwise corrupt"; + break; + + default: + // This is most likely LZMA_PROG_ERROR. + msg = "Unknown error, possibly a bug"; + break; + } + + qDebug() << "Decoder error:" << msg << "(error code " << ret << ")"; + return false; + } + } +} + diff --git a/oracle/src/lzma/decompress.h b/oracle/src/lzma/decompress.h new file mode 100644 index 000000000..f0e315f8b --- /dev/null +++ b/oracle/src/lzma/decompress.h @@ -0,0 +1,19 @@ +#ifndef XZ_DECOMPRESS_H +#define XZ_DECOMPRESS_H + +#include +#include + +class XzDecompressor : public QObject +{ + Q_OBJECT +public: + XzDecompressor(QObject *parent = 0); + ~XzDecompressor() { }; + bool decompress(QBuffer *in, QBuffer *out); +private: + bool init_decoder(lzma_stream *strm); + bool internal_decompress(lzma_stream *strm, QBuffer *in, QBuffer *out); +}; + +#endif diff --git a/oracle/src/main.cpp b/oracle/src/main.cpp index 44b900f38..5def0c887 100644 --- a/oracle/src/main.cpp +++ b/oracle/src/main.cpp @@ -1,25 +1,98 @@ -#include -#include -#include "oraclewizard.h" -#include "settingscache.h" +#include "main.h" -SettingsCache *settingsCache; +#include "interface/theme_manager.h" +#include "oraclewizard.h" + +#include <../../cockatrice/src/client/settings/cache_settings.h> +#include +#include +#include +#include +#include +#include + +QTranslator *translator, *qtTranslator; +ThemeManager *themeManager; + +const QString translationPrefix = "oracle"; +QString translationPath; +bool isSpoilersOnly; +bool isBackgrounded; + +void installNewTranslator() +{ + QString lang = SettingsCache::instance().getLang(); + + QString qtNameHint = "qt_" + lang; +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + QString qtTranslationPath = QLibraryInfo::path(QLibraryInfo::TranslationsPath); +#else + QString qtTranslationPath = QLibraryInfo::location(QLibraryInfo::TranslationsPath); +#endif + + bool qtTranslationLoaded = qtTranslator->load(qtNameHint, qtTranslationPath); + if (!qtTranslationLoaded) { + qDebug() << "Unable to load qt translation" << qtNameHint << "at" << qtTranslationPath; + } else { + qDebug() << "Loaded qt translation" << qtNameHint << "at" << qtTranslationPath; + } + qApp->installTranslator(qtTranslator); + + QString appNameHint = translationPrefix + "_" + lang; + bool appTranslationLoaded = qtTranslator->load(appNameHint, translationPath); + if (!appTranslationLoaded) { + qDebug() << "Unable to load" << translationPrefix << "translation" << appNameHint << "at" << translationPath; + } else { + qDebug() << "Loaded" << translationPrefix << "translation" << appNameHint << "at" << translationPath; + } + qApp->installTranslator(translator); +} int main(int argc, char *argv[]) { - QApplication app(argc, argv); - - QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8")); - - QCoreApplication::setOrganizationName("Cockatrice"); - QCoreApplication::setOrganizationDomain("cockatrice"); - // this can't be changed, as it influences the default savepath for cards.xml - QCoreApplication::setApplicationName("Cockatrice"); - - settingsCache = new SettingsCache; + QApplication app(argc, argv); - OracleWizard wizard; - wizard.show(); + QCoreApplication::setOrganizationName("Cockatrice"); + QCoreApplication::setOrganizationDomain("cockatrice"); + // this can't be changed, as it influences the default save path for cards.xml + QCoreApplication::setApplicationName("Cockatrice"); - return app.exec(); + // If the program is opened with the -s flag, it will only do spoilers. Otherwise it will do MTGJSON/Tokens + QCommandLineParser parser; + QCommandLineOption spoilersOnlyOption("s", QCoreApplication::translate("main", "Only run in spoiler mode")); + QCommandLineOption backgroundOption("b", QCoreApplication::translate("main", "Run in no-confirm background mode")); + parser.addOption(spoilersOnlyOption); + parser.addOption(backgroundOption); + parser.process(app); + isSpoilersOnly = parser.isSet(spoilersOnlyOption); + isBackgrounded = parser.isSet(backgroundOption); + +#ifdef Q_OS_MAC + translationPath = qApp->applicationDirPath() + "/../Resources/translations"; +#elif defined(Q_OS_WIN) + translationPath = qApp->applicationDirPath() + "/translations"; +#else // linux + translationPath = qApp->applicationDirPath() + "/../share/oracle/translations"; +#endif + + themeManager = new ThemeManager; + + qtTranslator = new QTranslator; + translator = new QTranslator; + installNewTranslator(); + + OracleWizard wizard; + + QIcon icon("theme:appicon.svg"); + wizard.setWindowIcon(icon); + // set name of the app desktop file; used by wayland to load the window icon + QGuiApplication::setDesktopFileName("oracle"); + + wizard.show(); + + if (isBackgrounded) { + QTimer::singleShot(0, &wizard, [&wizard]() { wizard.runInBackground(); }); + } + + return app.exec(); } diff --git a/oracle/src/main.h b/oracle/src/main.h new file mode 100644 index 000000000..2541b68b8 --- /dev/null +++ b/oracle/src/main.h @@ -0,0 +1,14 @@ +#ifndef MAIN_H +#define MAIN_H + +class QTranslator; +class QString; + +extern QTranslator *translator; +extern const QString translationPrefix; +extern QString translationPath; +extern bool isSpoilersOnly; + +void installNewTranslator(); + +#endif diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index 70972d92d..578afd98d 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -1,242 +1,575 @@ #include "oracleimporter.h" -#include -#include +#include "libcockatrice/interfaces/noop_card_preference_provider.h" +#include "libcockatrice/interfaces/noop_card_set_priority_controller.h" +#include "parsehelpers.h" #include "qt-json/json.h" -OracleImporter::OracleImporter(const QString &_dataDir, QObject *parent) - : CardDatabase(parent), dataDir(_dataDir) +#include +#include +#include +#include +#include +#include + +static const QList kConstructedCounts = {{4, "legal"}, {0, "banned"}}; + +static const QList kSingletonCounts = {{1, "legal"}, {0, "banned"}}; + +SplitCardPart::SplitCardPart(const QString &_name, + const QString &_text, + const QVariantHash &_properties, + const PrintingInfo &_printingInfo) + : name(_name), text(_text), properties(_properties), printingInfo(_printingInfo) { } +const QRegularExpression OracleImporter::formatRegex = QRegularExpression("^format-"); + +OracleImporter::OracleImporter(QObject *parent) : QObject(parent) +{ +} + +static CardSet::Priority getSetPriority(const QString &setType, const QString &shortName) +{ + if (!setTypePriorities.contains(setType.toLower())) { + qDebug() << "warning: Set type" << setType << "unrecognized for prioritization"; + } + CardSet::Priority priority = setTypePriorities.value(setType.toLower(), CardSet::PriorityOther); + if (nonEnglishSets.contains(shortName)) { + priority = CardSet::PriorityLowest; + } + return priority; +} + bool OracleImporter::readSetsFromByteArray(const QByteArray &data) { - QList newSetList; - bool ok; - setsMap = QtJson::Json::parse(QString(data), ok).toMap(); + auto setsMap = QtJson::Json::parse(QString(data), ok).toMap().value("data").toMap(); if (!ok) { qDebug() << "error: QtJson::Json::parse()"; - return 0; + return false; } - - QListIterator it(setsMap.values()); - QVariantMap map; - QString edition; - QString editionLong; - QVariant editionCards; - bool import; + QList newSetList; + + QListIterator it(setsMap.values()); while (it.hasNext()) { - map = it.next().toMap(); - edition = map.value("code").toString(); - editionLong = map.value("name").toString(); - editionCards = map.value("cards"); - - // core and expansion sets are marked to be imported by default - import = (0 == QString::compare(map.value("type").toString(), QString("core"), Qt::CaseInsensitive) || - 0 == QString::compare(map.value("type").toString(), QString("expansion"), Qt::CaseInsensitive)); - - newSetList.append(SetToDownload(edition, editionLong, editionCards, import)); + QVariantMap map = it.next().toMap(); + QString shortName = map.value("code").toString().toUpper(); + QString longName = map.value("name").toString(); + QList setCards = map.value("cards").toList(); + QString setType = map.value("type").toString(); + QDate releaseDate = map.value("releaseDate").toDate(); + CardSet::Priority priority = getSetPriority(setType, shortName); + // capitalize set type + if (setType.length() > 0) { + // basic grammar for words that aren't capitalized, like in "From the Vault" + const QStringList noCapitalize = {"the", "a", "an", "on", "to", "for", "of", "in", "and", "with", "or"}; + QStringList words = setType.split("_"); + setType.clear(); + bool first = false; + for (auto &item : words) { + if (first && noCapitalize.contains(item)) { + setType += item + QString(" "); + } else { + setType += item[0].toUpper() + item.mid(1, -1) + QString(" "); + first = true; + } + } + setType = setType.trimmed(); + } + newSetList.append(SetToDownload(shortName, longName, setCards, priority, setType, releaseDate)); } - qSort(newSetList); + std::sort(newSetList.begin(), newSetList.end()); - if (newSetList.isEmpty()) + if (newSetList.isEmpty()) { return false; + } allSets = newSetList; return true; } -CardInfo *OracleImporter::addCard(const QString &setName, - QString cardName, - bool isToken, - int cardId, - QString &cardCost, - const QString &cardType, - const QString &cardPT, - int cardLoyalty, - const QStringList &cardText) +static QString getMainCardType(const QStringList &typeList) { - QString fullCardText = cardText.join("\n"); - bool splitCard = false; - if (cardName.contains('(')) { - cardName.remove(QRegExp(" \\(.*\\)")); - splitCard = true; + if (typeList.isEmpty()) { + return {}; } - // Workaround for card name weirdness - if (cardName.contains("XX")) - cardName.remove("XX"); - cardName = cardName.replace("Æ", "AE"); - cardName = cardName.replace("’", "'"); - // Remove {} around mana costs - cardCost.remove(QChar('{')); - cardCost.remove(QChar('}')); + static const QStringList typePriority = {"Planeswalker", "Creature", "Land", "Sorcery", + "Instant", "Artifact", "Enchantment"}; - CardInfo *card; - if (cardHash.contains(cardName)) { - card = cardHash.value(cardName); - if (splitCard && !card->getText().contains(fullCardText)) - card->setText(card->getText() + "\n---\n" + fullCardText); - } else { - bool mArtifact = false; - if (cardType.endsWith("Artifact")) - for (int i = 0; i < cardText.size(); ++i) - if (cardText[i].contains("{T}") && cardText[i].contains("to your mana pool")) - mArtifact = true; - - QStringList colors; - QStringList allColors = QStringList() << "W" << "U" << "B" << "R" << "G"; - for (int i = 0; i < allColors.size(); i++) - if (cardCost.contains(allColors[i])) - colors << allColors[i]; - - if (cardText.contains(cardName + " is white.")) - colors << "W"; - if (cardText.contains(cardName + " is blue.")) - colors << "U"; - if (cardText.contains(cardName + " is black.")) - colors << "B"; - if (cardText.contains(cardName + " is red.")) - colors << "R"; - if (cardText.contains(cardName + " is green.")) - colors << "G"; - - bool cipt = (cardText.contains(cardName + " enters the battlefield tapped.")); - - card = new CardInfo(this, cardName, isToken, cardCost, cardType, cardPT, fullCardText, colors, cardLoyalty, cipt); - int tableRow = 1; - QString mainCardType = card->getMainCardType(); - if ((mainCardType == "Land") || mArtifact) - tableRow = 0; - else if ((mainCardType == "Sorcery") || (mainCardType == "Instant")) - tableRow = 3; - else if (mainCardType == "Creature") - tableRow = 2; - card->setTableRow(tableRow); - - cardHash.insert(cardName, card); + for (const auto &type : typePriority) { + if (typeList.contains(type)) { + return type; + } } - card->setMuId(setName, cardId); - return card; + return typeList.first(); } -int OracleImporter::importTextSpoiler(CardSet *set, const QVariant &data) +/** + * Sorts and deduplicates the color chars in the string by WUBRG order. + * + * @param colors The string containing the color chars. Will be modified in-place + */ +static void sortAndReduceColors(QString &colors) { - int cards = 0; - - QListIterator it(data.toList()); - QVariantMap map; - QString cardName; - QString cardCost; - QString cardType; - QString cardPT; - QString cardText; - int cardId; - int cardLoyalty; - QMap splitCards; + // sort + static const QHash colorOrder{{'W', 0}, {'U', 1}, {'B', 2}, {'R', 3}, {'G', 4}}; + std::sort(colors.begin(), colors.end(), + [](const QChar a, const QChar b) { return colorOrder.value(a, INT_MAX) < colorOrder.value(b, INT_MAX); }); + // reduce + QChar lastChar = '\0'; + for (int i = 0; i < colors.size(); ++i) { + if (colors.at(i) == lastChar) + colors.remove(i, 1); + else + lastChar = colors.at(i); + } +} - while (it.hasNext()) { - map = it.next().toMap(); - if(0 == QString::compare(map.value("layout").toString(), QString("split"), Qt::CaseInsensitive)) - { - // Split card handling - cardId = map.contains("multiverseid") ? map.value("multiverseid").toInt() : 0; - if(splitCards.contains(cardId)) - { - // merge two split cards - QVariantMap tmpMap = splitCards.take(cardId); - QVariantMap * card1 = 0, * card2 = 0; - // same cardid - cardId = map.contains("multiverseid") ? map.value("multiverseid").toInt() : 0; - // this is currently an integer; can't accept 2 values - cardLoyalty = 0; +CardInfoPtr OracleImporter::addCard(QString name, + const QString &text, + bool isToken, + QVariantHash properties, + const QList &relatedCards, + const PrintingInfo &printingInfo) +{ + // Workaround for card name weirdness + name = name.replace("Æ", "AE"); + name = name.replace("’", "'"); + if (cards.contains(name)) { + CardInfoPtr card = cards.value(name); + card->addToSet(printingInfo.getSet(), printingInfo); + if (card->getProperties().filter(formatRegex).empty()) { + card->combineLegalities(properties); + } + return card; + } - // determine which subcard is the first one in the split - QStringList names=map.contains("names") ? map.value("names").toStringList() : QStringList(""); - if(names.count()>0 && - map.contains("name") && - 0 == QString::compare(map.value("name").toString(), names.at(0))) - { - // map is the left part of the split card, tmpMap is right part - card1 = ↦ - card2 = &tmpMap; - } else { - //tmpMap is the left part of the split card, map is right part - card1 = &tmpMap; - card2 = ↦ - } - - // add first card's data - cardName = card1->contains("name") ? card1->value("name").toString() : QString(""); - cardCost = card1->contains("manaCost") ? card1->value("manaCost").toString() : QString(""); - cardType = card1->contains("type") ? card1->value("type").toString() : QString(""); - cardPT = card1->contains("power") || card1->contains("toughness") ? card1->value("power").toString() + QString('/') + card1->value("toughness").toString() : QString(""); - cardText = card1->contains("text") ? card1->value("text").toString() : QString(""); - - // add second card's data - cardName += card2->contains("name") ? QString(" // ") + card2->value("name").toString() : QString(""); - cardCost += card2->contains("manaCost") ? QString(" // ") + card2->value("manaCost").toString() : QString(""); - cardType += card2->contains("type") ? QString(" // ") + card2->value("type").toString() : QString(""); - cardPT += card2->contains("power") || card2->contains("toughness") ? QString(" // ") + card2->value("power").toString() + QString('/') + card2->value("toughness").toString() : QString(""); - cardText += card2->contains("text") ? QString("\n\n---\n\n") + card2->value("text").toString() : QString(""); + // Remove {} around mana costs, except if it's split cost + QString manacost = properties.value("manacost").toString(); + if (!manacost.isEmpty()) { + QStringList symbols = manacost.split("}"); + QString formattedCardCost; + for (QString symbol : symbols) { + static const auto manaCostPattern = QRegularExpression("[0-9WUBGRP]/[0-9WUBGRP]"); + if (symbol.contains(manaCostPattern)) { + symbol.append("}"); } else { - // first card od a pair; enqueue for later merging - splitCards.insert(cardId, map); - continue; + symbol.remove(QChar('{')); + } + formattedCardCost.append(symbol); + } + properties.insert("manacost", formattedCardCost); + } + + // fix colors + QString allColors = properties.value("colors").toString(); + if (allColors.size() > 1) { + sortAndReduceColors(allColors); + properties.insert("colors", allColors); + } + QString allColorIdent = properties.value("coloridentity").toString(); + if (allColorIdent.size() > 1) { + sortAndReduceColors(allColorIdent); + properties.insert("coloridentity", allColorIdent); + } + + // DETECT CARD POSITIONING INFO + + bool landscapeOrientation = properties.value("maintype").toString() == "Battle" || + properties.value("layout").toString() == "split" || + properties.value("layout").toString() == "planar"; + + // cards that enter the field tapped + bool cipt = parseCipt(name, text) || landscapeOrientation; + + // table row + int tableRow = 1; + QString mainCardType = properties.value("maintype").toString(); + if (mainCardType == "Land") + tableRow = 0; + else if (mainCardType == "Sorcery" || mainCardType == "Instant") + tableRow = 3; + else if (mainCardType == "Creature") + tableRow = 2; + + // card side + QString side = properties.value("side").toString() == "b" ? "back" : "front"; + properties.insert("side", side); + + // upsideDown (flip cards) + QString layout = properties.value("layout").toString(); + bool upsideDown = layout == "flip" && side == "back"; + + // insert the card and its properties + SetToPrintingsMap setsInfo; + setsInfo[printingInfo.getSet()->getShortName()].append(printingInfo); + CardInfo::UiAttributes attributes = {cipt, landscapeOrientation, tableRow, upsideDown}; + CardInfoPtr newCard = + CardInfo::newInstance(name, text, isToken, properties, relatedCards, {}, setsInfo, attributes); + + if (name.isEmpty()) { + qDebug() << "warning: an empty card was added to set" << printingInfo.getSet()->getShortName(); + } + cards.insert(name, newCard); + + return newCard; +} + +static QString getStringPropertyFromMap(const QVariantMap &card, const QString &propertyName) +{ + return card.contains(propertyName) ? card.value(propertyName).toString() : QString(""); +} + +int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList &cardsList) +{ + // mtgjson name => xml name + static const QMap cardProperties{ + {"manaCost", "manacost"}, {"manaValue", "cmc"}, {"type", "type"}, + {"loyalty", "loyalty"}, {"layout", "layout"}, {"side", "side"}, + {"convertedManaCost", "cmc"}, // old name for manaValue, for backwards compatibility + }; + + // mtgjson name => xml name + static const QMap setInfoProperties{ + {"number", "num"}, {"rarity", "rarity"}, {"isOnlineOnly", "isOnlineOnly"}, {"isRebalanced", "isRebalanced"}}; + + // mtgjson name => xml name + static const QMap identifierProperties{{"multiverseId", "muid"}, {"scryfallId", "uuid"}}; + + static const QString ptSeparator = "/"; + static constexpr bool isToken = false; + static const QList setsWithCardsWithSameNameButDifferentText = {"UST"}; + + int numCards = 0; + + // Keeps track of any split card faces encountered so far + QMap, QString>> splitCards; + + // Keeps track of all names encountered so far + QList allNameProps; + + for (const QVariant &cardVar : cardsList) { + QVariantMap card = cardVar.toMap(); + + /* Currently used layouts are: + * augment, double_faced_token, flip, host, leveler, meld, normal, planar, + * saga, scheme, split, token, transform, vanguard + */ + QString layout = getStringPropertyFromMap(card, "layout"); + + // don't import tokens from the json file + if (layout == "token") { + continue; + } + + // normal cards handling + QString name = getStringPropertyFromMap(card, "name"); + QString text = getStringPropertyFromMap(card, "text"); + QString faceName = getStringPropertyFromMap(card, "faceName"); + if (faceName.isEmpty()) { + faceName = name; + } + + // card properties + QVariantHash properties; + for (auto i = cardProperties.cbegin(), end = cardProperties.cend(); i != end; ++i) { + QString mtgjsonProperty = i.key(); + QString xmlPropertyName = i.value(); + QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty); + if (!propertyValue.isEmpty()) + properties.insert(xmlPropertyName, propertyValue); + } + + // per-set properties + PrintingInfo printingInfo = PrintingInfo(currentSet); + for (auto i = setInfoProperties.cbegin(), end = setInfoProperties.cend(); i != end; ++i) { + QString mtgjsonProperty = i.key(); + QString xmlPropertyName = i.value(); + QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty); + if (!propertyValue.isEmpty()) + printingInfo.setProperty(xmlPropertyName, propertyValue); + } + + // handle flavorNames specially due to double-faced cards + QString faceFlavorName = getStringPropertyFromMap(card, "faceFlavorName"); + QString flavorName = !faceFlavorName.isEmpty() ? faceFlavorName : getStringPropertyFromMap(card, "flavorName"); + if (!flavorName.isEmpty()) { + printingInfo.setProperty("flavorName", flavorName); + } + + // Identifiers + for (auto i = identifierProperties.cbegin(), end = identifierProperties.cend(); i != end; ++i) { + QString mtgjsonProperty = i.key(); + QString xmlPropertyName = i.value(); + QString propertyValue = getStringPropertyFromMap(card.value("identifiers").toMap(), mtgjsonProperty); + if (!propertyValue.isEmpty()) { + printingInfo.setProperty(xmlPropertyName, propertyValue); + } + } + + QString numComponent; + const QString numProperty = printingInfo.getProperty("num"); + const QChar lastChar = numProperty.isEmpty() ? QChar() : numProperty.back(); + + // Un-Sets do some wonky stuff. Split up these cards as individual entries. + // these cards will have a num with a letter (abc) behind it, put that letter into the name + if (setsWithCardsWithSameNameButDifferentText.contains(currentSet->getShortName()) && + allNameProps.contains(faceName) && layout == "normal" && lastChar.isLetter()) { + numComponent = " (" + QString(lastChar).toLower() + ")"; + } + allNameProps.append(faceName); + + // special handling properties + QString colors = card.value("colors").toStringList().join(""); + if (!colors.isEmpty()) { + properties.insert("colors", colors); + } + + // special handling properties + QString colorIdentity = card.value("colorIdentity").toStringList().join(""); + if (!colorIdentity.isEmpty()) { + properties.insert("coloridentity", colorIdentity); + } + + const auto &mainCardType = getMainCardType(card.value("types").toStringList()); + if (mainCardType.isEmpty()) { + qDebug() << "warning: no mainCardType for card:" << name; + } else { + properties.insert("maintype", mainCardType); + } + + // Depending on whether power and/or toughness are present, the format + // is either P/T (most common), P (no toughness), or /T (no power). + QString power = getStringPropertyFromMap(card, "power"); + QString toughness = getStringPropertyFromMap(card, "toughness"); + if (toughness.isEmpty() && !power.isEmpty()) { + properties.insert("pt", power); + } else if (!toughness.isEmpty()) { + properties.insert("pt", power + ptSeparator + toughness); + } + + auto legalities = card.value("legalities").toMap(); + for (auto i = legalities.cbegin(), end = legalities.cend(); i != end; ++i) { + properties.insert(QString("format-%1").arg(i.key()), i.value().toString().toLower()); + } + + // split cards are considered a single card, enqueue for later merging + if (layout == "split" || layout == "aftermath" || layout == "adventure" || layout == "prepare") { + auto _faceName = getStringPropertyFromMap(card, "faceName"); + SplitCardPart split(_faceName, text, properties, printingInfo); + auto found_iter = splitCards.find(name + numProperty); + if (found_iter == splitCards.end()) { + splitCards.insert(name + numProperty, {{split}, name}); + } else { + found_iter->first.append(split); } } else { - // normal cards handling - cardName = map.contains("name") ? map.value("name").toString() : QString(""); - cardCost = map.contains("manaCost") ? map.value("manaCost").toString() : QString(""); - cardType = map.contains("type") ? map.value("type").toString() : QString(""); - cardPT = map.contains("power") || map.contains("toughness") ? map.value("power").toString() + QString('/') + map.value("toughness").toString() : QString(""); - cardText = map.contains("text") ? map.value("text").toString() : QString(""); - cardId = map.contains("multiverseid") ? map.value("multiverseid").toInt() : 0; - cardLoyalty = map.contains("loyalty") ? map.value("loyalty").toInt() : 0; - } + // relations + QList relatedCards; - CardInfo *card = addCard(set->getShortName(), cardName, false, cardId, cardCost, cardType, cardPT, cardLoyalty, cardText.split("\n")); + // add other face for split cards as card relation + if (!getStringPropertyFromMap(card, "side").isEmpty()) { + auto faceManaValue = getStringPropertyFromMap(card, "faceManaValue"); + if (faceManaValue.isEmpty()) { + // check the old name for the property, for backwards compatibility purposes + faceManaValue = getStringPropertyFromMap(card, "faceConvertedManaCost"); + } + properties["cmc"] = faceManaValue; - if (!set->contains(card)) { - card->addToSet(set); - cards++; + if (layout == "meld") { // meld cards don't work + static const QRegularExpression meldNameRegex{"then meld them into ([^\\.]*)"}; + QString additionalName = meldNameRegex.match(text).captured(1); + if (!additionalName.isNull()) { + relatedCards.append(new CardRelation(additionalName, CardRelationType::TransformInto)); + } + } else { + for (const QString &additionalName : name.split(" // ")) { + if (additionalName != faceName) { + relatedCards.append(new CardRelation(additionalName, CardRelationType::TransformInto)); + } + } + } + name = faceName; + } + + // mtgjon related cards + if (card.contains("relatedCards")) { + QVariantMap givenRelated = card.value("relatedCards").toMap(); + // conjured cards from a spellbook + if (givenRelated.contains("spellbook")) { + auto spbk = givenRelated.value("spellbook").toStringList(); + for (const QString &spbkName : spbk) { + relatedCards.append( + new CardRelation(spbkName, CardRelationType::DoesNotAttach, false, false, 1, true)); + } + } + } + + CardInfoPtr newCard = addCard(name + numComponent, text, isToken, properties, relatedCards, printingInfo); + numCards++; } } - - return cards; + + // split cards handling + static const QString splitCardPropSeparator = QString(" // "); + static const QString splitCardTextSeparator = QString("\n\n---\n\n"); + static const QList noRelatedCards = {}; + + QList, QString>> partsAndNames = splitCards.values(); + for (auto [splitCardParts, name] : partsAndNames) { + QString text; + QVariantHash properties; + PrintingInfo printingInfo; + + for (const SplitCardPart &tmp : splitCardParts) { + if (!text.isEmpty()) { + text.append(splitCardTextSeparator); + } + text.append(tmp.getText()); + + if (properties.isEmpty()) { + properties = tmp.getProperties(); + printingInfo = tmp.getPrintingInfo(); + } else { + const QVariantHash &tmpProps = tmp.getProperties(); + for (auto i = tmpProps.cbegin(), end = tmpProps.cend(); i != end; ++i) { + QString prop = i.key(); + QString originalPropertyValue = properties.value(prop).toString(); + QString thisCardPropertyValue = i.value().toString(); + if (!thisCardPropertyValue.isEmpty() && originalPropertyValue != thisCardPropertyValue) { + if (originalPropertyValue.isEmpty()) { // don't create //es if one field is empty + properties.insert(prop, thisCardPropertyValue); + } else if (prop == "colors") { // the card is both colors + properties.insert(prop, originalPropertyValue + thisCardPropertyValue); + } else if (prop == "maintype") { // don't create maintypes with //es in them + continue; + } else { + properties.insert(prop, + originalPropertyValue + splitCardPropSeparator + thisCardPropertyValue); + } + } + } + } + } + CardInfoPtr newCard = addCard(name, text, isToken, properties, noRelatedCards, printingInfo); + numCards++; + } + + return numCards; +} + +FormatRulesNameMap OracleImporter::createDefaultMagicFormats() +{ + // Predefined common exceptions + CardCondition superTypeIsBasic; + superTypeIsBasic.field = "type"; + superTypeIsBasic.matchType = "contains"; + superTypeIsBasic.value = "Basic Land"; + + ExceptionRule basicLands; + basicLands.conditions.append(superTypeIsBasic); + + CardCondition anyNumberAllowed; + anyNumberAllowed.field = "text"; + anyNumberAllowed.matchType = "contains"; + anyNumberAllowed.value = "A deck can have any number of"; + + ExceptionRule mayContainAnyNumber; + mayContainAnyNumber.conditions.append(anyNumberAllowed); + + // Map to store default rules + FormatRulesNameMap defaultFormatRulesNameMap; + + // ----------------- Helper lambda to create format ----------------- + auto makeFormat = [&](const QString &name, int minDeck = 60, int maxDeck = -1, int maxSideboardSize = 15, + const QList &allowedCounts = kConstructedCounts) -> FormatRulesPtr { + FormatRulesPtr f(new FormatRules); + f->formatName = name; + f->allowedCounts = allowedCounts; + f->minDeckSize = minDeck; + f->maxDeckSize = maxDeck; + f->maxSideboardSize = maxSideboardSize; + f->exceptions.append(basicLands); + f->exceptions.append(mayContainAnyNumber); + defaultFormatRulesNameMap.insert(name.toLower(), f); + return f; + }; + + // ----------------- Standard formats ----------------- + makeFormat("Standard"); + makeFormat("Modern"); + makeFormat("Legacy"); + makeFormat("Pioneer"); + makeFormat("Historic"); + makeFormat("Timeless"); + makeFormat("Future"); + makeFormat("OldSchool"); + makeFormat("Premodern"); + makeFormat("Pauper"); + makeFormat("Penny"); + + // ----------------- Singleton formats ----------------- + makeFormat("Commander", 100, 100, 15, kSingletonCounts); + makeFormat("Duel", 100, 100, 15, kSingletonCounts); + makeFormat("Brawl", 60, 60, 15, kSingletonCounts); + makeFormat("StandardBrawl", 60, 60, 15, kSingletonCounts); + makeFormat("Oathbreaker", 60, 60, 15, kSingletonCounts); + makeFormat("PauperCommander", 100, 100, 15, kSingletonCounts); + makeFormat("Predh", 100, 100, 15, kSingletonCounts); + + // ----------------- Restricted formats ----------------- + makeFormat("Vintage", 60, -1, 15, {{4, "legal"}, {1, "restricted"}, {0, "banned"}}); + + return defaultFormatRulesNameMap; } int OracleImporter::startImport() { - clear(); + static ICardSetPriorityController *noOpController = new NoopCardSetPriorityController(); - int setCards = 0, setIndex= 0; - QListIterator it(allSets); - const SetToDownload * curSet; + // add an empty set for tokens + CardSetPtr tokenSet = + CardSet::newInstance(noOpController, CardSet::TOKENS_SETNAME, tr("Dummy set containing tokens"), "Tokens"); + sets.insert(CardSet::TOKENS_SETNAME, tokenSet); - while (it.hasNext()) - { - curSet = & it.next(); - if(!curSet->getImport()) - continue; - - CardSet *set = new CardSet(curSet->getShortName(), curSet->getLongName()); - if (!setHash.contains(set->getShortName())) - setHash.insert(set->getShortName(), set); + int setIndex = 0; - int setCards = importTextSpoiler(set, curSet->getCards()); + for (const SetToDownload &curSetToParse : allSets) { + CardSetPtr newSet = CardSet::newInstance(noOpController, curSetToParse.getShortName(), + curSetToParse.getLongName(), curSetToParse.getSetType(), + curSetToParse.getReleaseDate(), curSetToParse.getPriority()); + if (!sets.contains(newSet->getShortName())) + sets.insert(newSet->getShortName(), newSet); + + int numCardsInSet = importCardsFromSet(newSet, curSetToParse.getCards()); ++setIndex; - - emit setIndexChanged(setCards, setIndex, curSet->getLongName()); + + emit setIndexChanged(numCardsInSet, setIndex, curSetToParse.getLongName()); } - - emit setIndexChanged(setCards, setIndex, QString()); + + emit setIndexChanged(0, setIndex, QString()); // total number of sets return setIndex; } + +bool OracleImporter::saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion) +{ + CockatriceXml4Parser parser(new NoopCardPreferenceProvider(), new NoopCardSetPriorityController()); + + return parser.saveToFile(createDefaultMagicFormats(), sets, cards, fileName, sourceUrl, sourceVersion); +} + +void OracleImporter::clear() +{ + sets.clear(); + cards.clear(); + allSets.clear(); +} diff --git a/oracle/src/oracleimporter.h b/oracle/src/oracleimporter.h index 1f705eeb2..5bf352594 100644 --- a/oracle/src/oracleimporter.h +++ b/oracle/src/oracleimporter.h @@ -2,43 +2,169 @@ #define ORACLEIMPORTER_H #include +#include +#include +#include +#include -#include +// many users prefer not to see these sets with non english arts +// they will given priority PriorityLowest +const QStringList nonEnglishSets = {"4BB", "FBB", "PS11", "PSAL", "REN", "RIN"}; +const QMap setTypePriorities{ + {"core", CardSet::PriorityPrimary}, + {"expansion", CardSet::PriorityPrimary}, -class SetToDownload { -private: - QString shortName, longName; - bool import; - QVariant cards; -public: - const QString &getShortName() const { return shortName; } - const QString &getLongName() const { return longName; } - const QVariant &getCards() const { return cards; } - bool getImport() const { return import; } - void setImport(bool _import) { import = _import; } - SetToDownload(const QString &_shortName, const QString &_longName, const QVariant &_cards, bool _import) - : shortName(_shortName), longName(_longName), cards(_cards), import(_import) { } - bool operator<(const SetToDownload &set) const { return longName.compare(set.longName, Qt::CaseInsensitive) < 0; } + {"commander", CardSet::PrioritySecondary}, + {"starter", CardSet::PrioritySecondary}, + {"draft_innovation", CardSet::PrioritySecondary}, + {"duel_deck", CardSet::PrioritySecondary}, + + {"archenemy", CardSet::PriorityReprint}, + {"arsenal", CardSet::PriorityReprint}, + {"box", CardSet::PriorityReprint}, + {"eternal", CardSet::PriorityReprint}, + {"from_the_vault", CardSet::PriorityReprint}, + {"masterpiece", CardSet::PriorityReprint}, + {"masters", CardSet::PriorityReprint}, + {"memorabilia", CardSet::PriorityReprint}, + {"planechase", CardSet::PriorityReprint}, + {"premium_deck", CardSet::PriorityReprint}, + {"promo", CardSet::PriorityReprint}, + {"spellbook", CardSet::PriorityReprint}, + {"token", CardSet::PriorityReprint}, + {"treasure_chest", CardSet::PriorityReprint}, + + {"alchemy", CardSet::PriorityOther}, + {"funny", CardSet::PriorityOther}, + {"minigame", CardSet::PriorityOther}, + {"vanguard", CardSet::PriorityOther}, }; -class OracleImporter : public CardDatabase { - Q_OBJECT +class SetToDownload +{ private: - QList allSets; - QVariantMap setsMap; - QString dataDir; - - CardInfo *addCard(const QString &setName, QString cardName, bool isToken, int cardId, QString &cardCost, const QString &cardType, const QString &cardPT, int cardLoyalty, const QStringList &cardText); -signals: - void setIndexChanged(int cardsImported, int setIndex, const QString &setName); - void dataReadProgress(int bytesRead, int totalBytes); + QString shortName, longName; + QList cards; + QDate releaseDate; + QString setType; + CardSet::Priority priority; + public: - OracleImporter(const QString &_dataDir, QObject *parent = 0); - bool readSetsFromByteArray(const QByteArray &data); - int startImport(); - int importTextSpoiler(CardSet *set, const QVariant &data); - QList &getSets() { return allSets; } - const QString &getDataDir() const { return dataDir; } + const QString &getShortName() const + { + return shortName; + } + const QString &getLongName() const + { + return longName; + } + const QList &getCards() const + { + return cards; + } + const QString &getSetType() const + { + return setType; + } + const QDate &getReleaseDate() const + { + return releaseDate; + } + CardSet::Priority getPriority() const + { + return priority; + } + SetToDownload(QString _shortName, + QString _longName, + QList _cards, + CardSet::Priority _priority, + QString _setType = QString(), + const QDate &_releaseDate = QDate()) + : shortName(std::move(_shortName)), longName(std::move(_longName)), cards(std::move(_cards)), + releaseDate(_releaseDate), setType(std::move(_setType)), priority(_priority) + { + } + bool operator<(const SetToDownload &set) const + { + return longName.compare(set.longName, Qt::CaseInsensitive) < 0; + } +}; + +class SplitCardPart +{ +public: + SplitCardPart(const QString &_name, + const QString &_text, + const QVariantHash &_properties, + const PrintingInfo &_printingInfo); + inline const QString &getName() const + { + return name; + } + inline const QString &getText() const + { + return text; + } + inline const QVariantHash &getProperties() const + { + return properties; + } + inline const PrintingInfo &getPrintingInfo() const + { + return printingInfo; + } + +private: + QString name; + QString text; + QVariantHash properties; + PrintingInfo printingInfo; +}; + +class OracleImporter : public QObject +{ + Q_OBJECT +private: + static const QRegularExpression formatRegex; + + /** + * The cards, indexed by name. + */ + CardNameMap cards; + + /** + * The sets, indexed by short name. + */ + SetNameMap sets; + + QList allSets; + + CardInfoPtr addCard(QString name, + const QString &text, + bool isToken, + QVariantHash properties, + const QList &relatedCards, + const PrintingInfo &printingInfo); +signals: + void setIndexChanged(int cardsImported, int setIndex, const QString &setName); + void dataReadProgress(int bytesRead, int totalBytes); + +public: + explicit OracleImporter(QObject *parent = nullptr); + bool readSetsFromByteArray(const QByteArray &data); + int startImport(); + bool saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion); + int importCardsFromSet(const CardSetPtr ¤tSet, const QList &cardsList); + FormatRulesNameMap createDefaultMagicFormats(); + const CardNameMap &getCardList() const + { + return cards; + } + QList &getSets() + { + return allSets; + } + void clear(); }; #endif diff --git a/oracle/src/oraclewizard.cpp b/oracle/src/oraclewizard.cpp index 75381b1d5..2edb9e561 100644 --- a/oracle/src/oraclewizard.cpp +++ b/oracle/src/oraclewizard.cpp @@ -1,27 +1,107 @@ -#include -#include -#include -#include -#include - #include "oraclewizard.h" + +#include "client/settings/cache_settings.h" +#include "main.h" #include "oracleimporter.h" +#include "pages.h" +#include "pagetemplates.h" -#define ALLSETS_URL "http://mtgjson.com/json/AllSets.json" +#include +#include +#include +#include +#include +#include +#include +#include +#include -OracleWizard::OracleWizard(QWidget *parent) - : QWizard(parent) +OracleWizard::OracleWizard(QWidget *parent) : QWizard(parent) { - settings = new QSettings(this); - importer = new OracleImporter(QDesktopServices::storageLocation(QDesktopServices::DataLocation), this); + // define a dummy context that will be used where needed + QString dummy = QT_TRANSLATE_NOOP("i18n", "English"); - addPage(new IntroPage); - addPage(new LoadSetsPage); - addPage(new ChooseSetsPage); - addPage(new SaveSetsPage); +#ifdef Q_OS_WIN + setWizardStyle(QWizard::ModernStyle); +#endif + QString oracleSettingsFile = SettingsCache::instance().getSettingsPath() + "oracle.ini"; + settings = new QSettings(oracleSettingsFile, QSettings::IniFormat, this); + + // We moved the oracle-specific settings from global.ini to a separate oracle.ini after 2.10 + if (!QFile::exists(oracleSettingsFile)) { + migrateOracleSettings(); + } + + connect(&SettingsCache::instance(), &SettingsCache::langChanged, this, &OracleWizard::updateLanguage); + + importer = new OracleImporter(this); + + nam = new QNetworkAccessManager(this); + + QList pages; + + if (!isSpoilersOnly) { + pages << new IntroPage << new LoadSetsPage << new SaveSetsPage << new LoadTokensPage << new OutroPage; + } else { + pages << new LoadSpoilersPage << new OutroPage; + } + + for (OracleWizardPage *page : pages) { + addPage(page); + + // Connect background auto-advance + connect(page, &OracleWizardPage::readyToContinue, this, [this]() { + if (backgroundMode) { + next(); + } + }); + } + + retranslateUi(); +} + +/** + * Migrates the oracle-specific settings from global.ini to oracle.ini + */ +void OracleWizard::migrateOracleSettings() +{ + QString filePath = SettingsCache::instance().getSettingsPath() + "global.ini"; + auto globalSettings = QSettings(filePath, QSettings::IniFormat, this); + + auto tryMigrateValue = [this, &globalSettings](const QString &name) { + QVariant variant = globalSettings.value(name); + if (variant.isValid()) { + settings->setValue(name, variant.toString()); + } + }; + + tryMigrateValue("allsetsurl"); + tryMigrateValue("tokensurl"); + tryMigrateValue("spoilersurl"); +} + +void OracleWizard::updateLanguage() +{ + qApp->removeTranslator(translator); + installNewTranslator(); +} + +void OracleWizard::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::LanguageChange) { + retranslateUi(); + } + + QDialog::changeEvent(event); +} + +void OracleWizard::retranslateUi() +{ setWindowTitle(tr("Oracle Importer")); - QWizard::setButtonText(QWizard::FinishButton, tr("Save")); + for (int i = 0; i < pageIds().count(); i++) { + dynamic_cast(page(i))->retranslateUi(); + } } void OracleWizard::accept() @@ -41,358 +121,19 @@ void OracleWizard::disableButtons() button(QWizard::BackButton)->setDisabled(true); } -IntroPage::IntroPage(QWidget *parent) - : OracleWizardPage(parent) +bool OracleWizard::saveTokensToFile(const QString &fileName) { - setTitle(tr("Introduction")); - - label = new QLabel(tr("This wizard will import the list of sets and cards " - "that will be used by Cockatrice. You will need to " - "specify an url or a filename that will be used as a " - "source, and then choose the wanted sets from the list " - "of the available ones."), - this); - label->setWordWrap(true); - - QVBoxLayout *layout = new QVBoxLayout(this); - layout->addWidget(label); - setLayout(layout); -} - -LoadSetsPage::LoadSetsPage(QWidget *parent) - : OracleWizardPage(parent), nam(0) -{ - setTitle(tr("Source selection")); - setSubTitle(tr("Please specify a source for the list of sets and cards. " - "You can specify an url address that will be download or " - "use an existing file from your computer.")); - - urlRadioButton = new QRadioButton(tr("Download url:"), this); - fileRadioButton = new QRadioButton(tr("Local file:"), this); - - urlLineEdit = new QLineEdit(this); - fileLineEdit = new QLineEdit(this); - - progressLabel = new QLabel(this); - progressBar = new QProgressBar(this); - - urlRadioButton->setChecked(true); - - fileButton = new QPushButton(tr("Choose file..."), this); - connect(fileButton, SIGNAL(clicked()), this, SLOT(actLoadSetsFile())); - - QGridLayout *layout = new QGridLayout(this); - layout->addWidget(urlRadioButton, 0, 0); - layout->addWidget(urlLineEdit, 0, 1); - layout->addWidget(fileRadioButton, 1, 0); - layout->addWidget(fileLineEdit, 1, 1); - layout->addWidget(fileButton, 2, 1, Qt::AlignRight); - layout->addWidget(progressLabel, 3, 0); - layout->addWidget(progressBar, 3, 1); - - connect(&watcher, SIGNAL(finished()), this, SLOT(importFinished())); - - setLayout(layout); -} - -void LoadSetsPage::initializePage() -{ - urlLineEdit->setText(wizard()->settings->value("allsetsurl", ALLSETS_URL).toString()); - - progressLabel->hide(); - progressBar->hide(); -} - -void LoadSetsPage::actLoadSetsFile() -{ - QFileDialog dialog(this, tr("Load sets file")); - dialog.setFileMode(QFileDialog::ExistingFile); - dialog.setNameFilter("Sets JSON file (*.json)"); - - if(!fileLineEdit->text().isEmpty() && QFile::exists(fileLineEdit->text())) - dialog.selectFile(fileLineEdit->text()); - - if (!dialog.exec()) - return; - - fileLineEdit->setText(dialog.selectedFiles().at(0)); -} - -bool LoadSetsPage::validatePage() -{ - // once the import is finished, we call next(); skip validation - if(wizard()->importer->getSets().count() > 0) - return true; - - // else, try to import sets - if(urlRadioButton->isChecked()) - { - QUrl url = QUrl::fromUserInput(urlLineEdit->text()); - if(!url.isValid()) - { - QMessageBox::critical(this, tr("Error"), tr("The provided url is not valid.")); - return false; - } - - progressLabel->setText(tr("Downloading (0MB)")); - // show an infinite progressbar - progressBar->setMaximum(0); - progressBar->setMinimum(0); - progressBar->setValue(0); - progressLabel->show(); - progressBar->show(); - - wizard()->disableButtons(); - setEnabled(false); - - if(!nam) - nam = new QNetworkAccessManager(this); - QNetworkReply *reply = nam->get(QNetworkRequest(url)); - - connect(reply, SIGNAL(finished()), this, SLOT(actDownloadFinishedSetsFile())); - connect(reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(actDownloadProgressSetsFile(qint64, qint64))); - - } else if(fileRadioButton->isChecked()) { - QFile setsFile(fileLineEdit->text()); - if(!setsFile.exists()) - { - QMessageBox::critical(this, tr("Error"), tr("Please choose a file.")); - return false; - } - - if (!setsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { - QMessageBox::critical(0, tr("Error"), tr("Cannot open file '%1'.").arg(fileLineEdit->text())); - return false; - } - - wizard()->disableButtons(); - setEnabled(false); - - readSetsFromByteArray(setsFile.readAll()); - - } - return false; -} - -void LoadSetsPage::actDownloadProgressSetsFile(qint64 received, qint64 total) -{ - if(total > 0 && progressBar->maximum()==0) - { - progressBar->setMaximum(total); - progressBar->setValue(received); - } - progressLabel->setText(tr("Downloading (%1MB)").arg((int) received / 1048576)); -} - -void LoadSetsPage::actDownloadFinishedSetsFile() -{ - progressLabel->hide(); - progressBar->hide(); - - // check for a reply - QNetworkReply *reply = static_cast(sender()); - QNetworkReply::NetworkError errorCode = reply->error(); - if (errorCode != QNetworkReply::NoError) { - QMessageBox::critical(this, tr("Error"), tr("Network error: %1.").arg(reply->errorString())); - - wizard()->enableButtons(); - setEnabled(true); - - reply->deleteLater(); - return; + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + qDebug() << "File open (w) failed for" << fileName; + return false; } - // save allsets.json url, but only if the user customized it and download was successfull - if(urlLineEdit->text() != QString(ALLSETS_URL)) - wizard()->settings->setValue("allsetsurl", urlLineEdit->text()); - else - wizard()->settings->remove("allsetsurl"); - - readSetsFromByteArray(reply->readAll()); - reply->deleteLater(); -} - -void LoadSetsPage::readSetsFromByteArray(QByteArray data) -{ - // show an infinite progressbar - progressBar->setMaximum(0); - progressBar->setMinimum(0); - progressBar->setValue(0); - progressLabel->setText(tr("Parsing file")); - progressLabel->show(); - progressBar->show(); - - // Start the computation. - future = QtConcurrent::run(wizard()->importer, &OracleImporter::readSetsFromByteArray, data); - watcher.setFuture(future); -} - -void LoadSetsPage::importFinished() -{ - wizard()->enableButtons(); - setEnabled(true); - progressLabel->hide(); - progressBar->hide(); - - if(watcher.future().result()) - { - wizard()->next(); - } else { - QMessageBox::critical(this, tr("Error"), tr("The file was retrieved successfully, but it does not contain any sets data.")); - } -} - -ChooseSetsPage::ChooseSetsPage(QWidget *parent) - : OracleWizardPage(parent) -{ - setTitle(tr("Sets selection")); - setSubTitle(tr("The following sets has been found in the source file. " - "Please mark the sets that will be imported.")); - - checkBoxLayout = new QVBoxLayout; - - QWidget *checkboxFrame = new QWidget(this); - checkboxFrame->setLayout(checkBoxLayout); - - QScrollArea *checkboxArea = new QScrollArea(this); - checkboxArea->setWidget(checkboxFrame); - checkboxArea->setWidgetResizable(true); - - checkAllButton = new QPushButton(tr("&Check all")); - connect(checkAllButton, SIGNAL(clicked()), this, SLOT(actCheckAll())); - uncheckAllButton = new QPushButton(tr("&Uncheck all")); - connect(uncheckAllButton, SIGNAL(clicked()), this, SLOT(actUncheckAll())); - - QGridLayout *layout = new QGridLayout(this); - layout->addWidget(checkboxArea, 0, 0, 1, 2); - layout->addWidget(checkAllButton, 1, 0); - layout->addWidget(uncheckAllButton, 1, 1); - - setLayout(layout); -} - -void ChooseSetsPage::initializePage() -{ - // populate checkbox list - for (int i = 0; i < checkBoxList.size(); ++i) - delete checkBoxList[i]; - checkBoxList.clear(); - - QList &sets = wizard()->importer->getSets(); - for (int i = 0; i < sets.size(); ++i) { - QCheckBox *checkBox = new QCheckBox(sets[i].getLongName()); - checkBox->setChecked(sets[i].getImport()); - connect(checkBox, SIGNAL(stateChanged(int)), this, SLOT(checkBoxChanged(int))); - checkBoxLayout->addWidget(checkBox); - checkBoxList << checkBox; - } -} - -void ChooseSetsPage::checkBoxChanged(int state) -{ - QCheckBox *checkBox = qobject_cast(sender()); - QList &sets = wizard()->importer->getSets(); - for (int i = 0; i < sets.size(); ++i) - if (sets[i].getLongName() == checkBox->text()) { - sets[i].setImport(state); - break; - } -} - -void ChooseSetsPage::actCheckAll() -{ - for (int i = 0; i < checkBoxList.size(); ++i) - checkBoxList[i]->setChecked(true); -} - -void ChooseSetsPage::actUncheckAll() -{ - for (int i = 0; i < checkBoxList.size(); ++i) - checkBoxList[i]->setChecked(false); -} - -bool ChooseSetsPage::validatePage() -{ - for (int i = 0; i < checkBoxList.size(); ++i) - { - if(checkBoxList[i]->isChecked()) - return true; + if (file.write(tokensData) == -1) { + qDebug() << "File write (w) failed for" << fileName; + return false; } - QMessageBox::critical(this, tr("Error"), tr("Please mark at least one set.")); - return false; -} - -SaveSetsPage::SaveSetsPage(QWidget *parent) - : OracleWizardPage(parent) -{ - setTitle(tr("Sets imported")); - setSubTitle(tr("The following sets has been imported. " - "Press \"Save\" to save the imported cards to the Cockatrice database.")); - - defaultPathCheckBox = new QCheckBox(this); - defaultPathCheckBox->setText(tr("Save to the default path (recommended)")); - defaultPathCheckBox->setChecked(true); - - messageLog = new QTextEdit(this); - messageLog->setReadOnly(true); - - QGridLayout *layout = new QGridLayout(this); - layout->addWidget(defaultPathCheckBox, 0, 0); - layout->addWidget(messageLog, 1, 0); - - setLayout(layout); -} - -void SaveSetsPage::cleanupPage() -{ - disconnect(wizard()->importer, SIGNAL(setIndexChanged(int, int, const QString &)), 0, 0); -} - -void SaveSetsPage::initializePage() -{ - messageLog->clear(); - - connect(wizard()->importer, SIGNAL(setIndexChanged(int, int, const QString &)), this, SLOT(updateTotalProgress(int, int, const QString &))); - - if (!wizard()->importer->startImport()) - QMessageBox::critical(this, tr("Error"), tr("No set has been imported.")); -} - -void SaveSetsPage::updateTotalProgress(int cardsImported, int setIndex, const QString &setName) -{ - if (setName.isEmpty()) { - messageLog->append("" + tr("Import finished: %1 cards.").arg(wizard()->importer->getCardList().size()) + ""); - } else { - messageLog->append(tr("%1: %2 cards imported").arg(setName).arg(cardsImported)); - } - messageLog->verticalScrollBar()->setValue(messageLog->verticalScrollBar()->maximum()); -} - -bool SaveSetsPage::validatePage() -{ - bool ok = false; - const QString dataDir = QDesktopServices::storageLocation(QDesktopServices::DataLocation); - QDir dir(dataDir); - if (!dir.exists()) - dir.mkpath(dataDir); - QString savePath = dataDir + "/cards.xml"; - do { - QString fileName; - if (savePath.isEmpty() || !defaultPathCheckBox->isChecked()) - fileName = QFileDialog::getSaveFileName(this, tr("Save card database"), dataDir + "/cards.xml", tr("XML card database (*.xml)")); - else { - fileName = savePath; - savePath.clear(); - } - if (fileName.isEmpty()) { - return false; - } - if (wizard()->importer->saveToFile(fileName)) - ok = true; - else - QMessageBox::critical(this, tr("Error"), tr("The file could not be saved to the desired location.")); - } while (!ok); - + file.close(); return true; } diff --git a/oracle/src/oraclewizard.h b/oracle/src/oraclewizard.h index 6e6a6ea80..78427175c 100644 --- a/oracle/src/oraclewizard.h +++ b/oracle/src/oraclewizard.h @@ -2,11 +2,11 @@ #define ORACLEWIZARD_H #include -#include -#include +#include class QCheckBox; class QGroupBox; +class QComboBox; class QLabel; class QLineEdit; class QRadioButton; @@ -19,96 +19,66 @@ class QSettings; class OracleWizard : public QWizard { - Q_OBJECT + Q_OBJECT public: - OracleWizard(QWidget *parent = 0); - void accept(); - void enableButtons(); - void disableButtons(); -public: - OracleImporter *importer; - QSettings * settings; -}; + explicit OracleWizard(QWidget *parent = nullptr); + void accept() override; + void enableButtons(); + void disableButtons(); + void retranslateUi(); + void setTokensData(QByteArray _tokensData) + { + tokensData = std::move(_tokensData); + } + bool hasTokensData() + { + return !tokensData.isEmpty(); + } + void setCardSourceUrl(const QString &sourceUrl) + { + cardSourceUrl = sourceUrl; + } + void setCardSourceVersion(const QString &sourceVersion) + { + cardSourceVersion = sourceVersion; + } + const QString &getCardSourceUrl() const + { + return cardSourceUrl; + } + const QString &getCardSourceVersion() const + { + return cardSourceVersion; + } + bool saveTokensToFile(const QString &fileName); + void runInBackground() + { + backgroundMode = true; + hide(); + currentPage()->initializePage(); + } -class OracleWizardPage : public QWizardPage -{ - Q_OBJECT public: - OracleWizardPage(QWidget *parent = 0): QWizardPage(parent) {}; -protected: - inline OracleWizard *wizard() { return (OracleWizard*) QWizardPage::wizard(); }; -}; + OracleImporter *importer; + QSettings *settings; + QNetworkAccessManager *nam; + bool downloadedPlainXml = false; + QByteArray xmlData; + bool backgroundMode = false; -class IntroPage : public OracleWizardPage -{ - Q_OBJECT -public: - IntroPage(QWidget *parent = 0); -private: - QLabel *label; -}; - -class LoadSetsPage : public OracleWizardPage -{ - Q_OBJECT -public: - LoadSetsPage(QWidget *parent = 0); -protected: - void initializePage(); - bool validatePage(); - void readSetsFromByteArray(QByteArray data); -private: - QRadioButton *urlRadioButton; - QRadioButton *fileRadioButton; - QLineEdit *urlLineEdit; - QLineEdit *fileLineEdit; - QPushButton *fileButton; - QLabel *progressLabel; - QProgressBar * progressBar; - - QNetworkAccessManager *nam; - QFutureWatcher watcher; - QFuture future; private slots: - void actLoadSetsFile(); - void actDownloadProgressSetsFile(qint64 received, qint64 total); - void actDownloadFinishedSetsFile(); - void importFinished(); -}; + void updateLanguage(); -class ChooseSetsPage : public OracleWizardPage -{ - Q_OBJECT -public: - ChooseSetsPage(QWidget *parent = 0); -protected: - void initializePage(); - bool validatePage(); private: - QPushButton *checkAllButton, *uncheckAllButton; - QVBoxLayout *checkBoxLayout; - QList checkBoxList; -private slots: - void actCheckAll(); - void actUncheckAll(); - void checkBoxChanged(int state); -}; + QByteArray tokensData; + QString cardSourceUrl; + QString cardSourceVersion; + + void migrateOracleSettings(); -class SaveSetsPage : public OracleWizardPage -{ - Q_OBJECT -public: - SaveSetsPage(QWidget *parent = 0); -private: - QTextEdit *messageLog; - QCheckBox * defaultPathCheckBox; protected: - void initializePage(); - void cleanupPage(); - bool validatePage(); -private slots: - void updateTotalProgress(int cardsImported, int setIndex, const QString &setName); + void changeEvent(QEvent *event) override; }; -#endif \ No newline at end of file +#endif diff --git a/oracle/src/pages.cpp b/oracle/src/pages.cpp new file mode 100644 index 000000000..7629a291b --- /dev/null +++ b/oracle/src/pages.cpp @@ -0,0 +1,742 @@ +#include "pages.h" + +#include "client/settings/cache_settings.h" +#include "main.h" +#include "oracleimporter.h" +#include "oraclewizard.h" +#include "pages.h" +#include "pagetemplates.h" +#include "version_string.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAS_LZMA +#include "lzma/decompress.h" +#endif + +#ifdef HAS_ZLIB +#include "zip/unzip.h" +#endif + +#define ZIP_SIGNATURE "PK" +// Xz stream header: 0xFD + "7zXZ" +#define XZ_SIGNATURE "\xFD\x37\x7A\x58\x5A" +#define MTGJSON_V4_URL_COMPONENT "mtgjson.com/files/" +#define ALLSETS_URL_FALLBACK "https://www.mtgjson.com/api/v5/AllPrintings.json" +#define MTGJSON_VERSION_URL "https://www.mtgjson.com/api/v5/Meta.json" + +#ifdef HAS_LZMA +#define ALLSETS_URL "https://www.mtgjson.com/api/v5/AllPrintings.json.xz" +#elif defined(HAS_ZLIB) +#define ALLSETS_URL "https://www.mtgjson.com/api/v5/AllPrintings.json.zip" +#else +#define ALLSETS_URL "https://www.mtgjson.com/api/v5/AllPrintings.json" +#endif + +#define TOKENS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Token/master/tokens.xml" +#define SPOILERS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml" + +IntroPage::IntroPage(QWidget *parent) : OracleWizardPage(parent) +{ + label = new QLabel(this); + label->setWordWrap(true); + + languageLabel = new QLabel(this); + versionLabel = new QLabel(this); + languageBox = new QComboBox(this); + + QStringList languageCodes = findQmFiles(); + for (const QString &code : languageCodes) { + QString langName = languageName(code); + languageBox->addItem(langName, code); + } + + QString setLanguage = QCoreApplication::translate("i18n", DEFAULT_LANG_NAME); + int index = languageBox->findText(setLanguage, Qt::MatchExactly); + if (index == -1) { + qWarning() << "could not find language" << setLanguage; + } else { + languageBox->setCurrentIndex(index); + } + + connect(languageBox, qOverload(&QComboBox::currentIndexChanged), this, &IntroPage::languageBoxChanged); + + auto *layout = new QGridLayout(this); + layout->addWidget(label, 0, 0, 1, 2); + layout->addWidget(languageLabel, 1, 0); + layout->addWidget(languageBox, 1, 1); + layout->addWidget(versionLabel, 2, 0, 1, 2); + + setLayout(layout); +} + +void IntroPage::initializePage() +{ + if (wizard()->backgroundMode) { + emit readyToContinue(); + } +} + +QStringList IntroPage::findQmFiles() +{ + QDir dir(translationPath); + QStringList fileNames = dir.entryList(QStringList(translationPrefix + "_*.qm"), QDir::Files, QDir::Name); + fileNames.replaceInStrings(QRegularExpression(translationPrefix + "_(.*)\\.qm"), "\\1"); + return fileNames; +} + +QString IntroPage::languageName(const QString &lang) +{ + QTranslator qTranslator; + + QString appNameHint = translationPrefix + "_" + lang; + bool appTranslationLoaded = qTranslator.load(appNameHint, translationPath); + if (!appTranslationLoaded) { + qDebug() << "Unable to load" << translationPrefix << "translation" << appNameHint << "at" << translationPath; + } + + return qTranslator.translate("i18n", DEFAULT_LANG_NAME); +} + +void IntroPage::languageBoxChanged(int index) +{ + SettingsCache::instance().setLang(languageBox->itemData(index).toString()); +} + +void IntroPage::retranslateUi() +{ + setTitle(tr("Introduction")); + label->setText(tr("This wizard will import the list of sets, cards, and tokens " + "that will be used by Cockatrice.")); + languageLabel->setText(tr("Interface language:")); + versionLabel->setText(tr("Version:") + QString(" %1").arg(VERSION_STRING)); +} + +void OutroPage::retranslateUi() +{ + setTitle(tr("Finished")); + setSubTitle(tr("The wizard has finished.") + "
" + + tr("You can now start using Cockatrice with the newly updated cards.") + "

" + + tr("If the card databases don't reload automatically, restart the Cockatrice client.")); +} + +void OutroPage::initializePage() +{ + if (wizard()->backgroundMode) { + wizard()->accept(); + exit(0); + } +} + +LoadSetsPage::LoadSetsPage(QWidget *parent) : OracleWizardPage(parent) +{ + urlRadioButton = new QRadioButton(this); + fileRadioButton = new QRadioButton(this); + + urlLineEdit = new QLineEdit(this); + fileLineEdit = new QLineEdit(this); + + progressLabel = new QLabel(this); + progressBar = new QProgressBar(this); + + urlRadioButton->setChecked(true); + + urlButton = new QPushButton(this); + connect(urlButton, &QPushButton::clicked, this, &LoadSetsPage::actRestoreDefaultUrl); + + fileButton = new QPushButton(this); + connect(fileButton, &QPushButton::clicked, this, &LoadSetsPage::actLoadSetsFile); + + auto *layout = new QGridLayout(this); + layout->addWidget(urlRadioButton, 0, 0); + layout->addWidget(urlLineEdit, 0, 1); + layout->addWidget(urlButton, 1, 1, Qt::AlignRight); + layout->addWidget(fileRadioButton, 2, 0); + layout->addWidget(fileLineEdit, 2, 1); + layout->addWidget(fileButton, 3, 1, Qt::AlignRight); + layout->addWidget(progressLabel, 4, 0); + layout->addWidget(progressBar, 4, 1); + + connect(&watcher, &QFutureWatcher::finished, this, &LoadSetsPage::importFinished); + + setLayout(layout); +} + +void LoadSetsPage::initializePage() +{ + urlLineEdit->setText(wizard()->settings->value("allsetsurl", ALLSETS_URL).toString()); + + progressLabel->hide(); + progressBar->hide(); + + if (wizard()->backgroundMode) { + if (isEnabled()) { + validatePage(); + } + } +} + +void LoadSetsPage::retranslateUi() +{ + setTitle(tr("Source selection")); + setSubTitle(tr("Please specify a compatible source for the list of sets and cards. " + "You can specify a URL address that will be downloaded or " + "use an existing file from your computer.")); + + urlRadioButton->setText(tr("Download URL:")); + fileRadioButton->setText(tr("Local file:")); + urlButton->setText(tr("Restore default URL")); + fileButton->setText(tr("Choose file...")); +} + +void LoadSetsPage::actRestoreDefaultUrl() +{ + urlLineEdit->setText(ALLSETS_URL); +} + +void LoadSetsPage::actLoadSetsFile() +{ + QFileDialog dialog(this, tr("Load sets file")); + dialog.setFileMode(QFileDialog::ExistingFile); + + QString extensions = "*.json *.xml"; +#ifdef HAS_ZLIB + extensions += " *.zip"; +#endif +#ifdef HAS_LZMA + extensions += " *.xz"; +#endif + dialog.setNameFilter(tr("Sets file (%1)").arg(extensions)); + + if (!fileLineEdit->text().isEmpty() && QFile::exists(fileLineEdit->text())) { + dialog.selectFile(fileLineEdit->text()); + } + + if (!dialog.exec()) { + return; + } + + fileLineEdit->setText(dialog.selectedFiles().at(0)); +} + +bool LoadSetsPage::validatePage() +{ + // once the import is finished, we call next(); skip validation + if (wizard()->downloadedPlainXml || wizard()->importer->getSets().count() > 0) { + return true; + } + + // else, try to import sets + if (urlRadioButton->isChecked()) { + // If a user attempts to download from V4, redirect them to V5 + if (urlLineEdit->text().contains(MTGJSON_V4_URL_COMPONENT)) { + actRestoreDefaultUrl(); + } + + const auto url = QUrl::fromUserInput(urlLineEdit->text()); + + if (!url.isValid()) { + QMessageBox::critical(this, tr("Error"), tr("The provided URL is not valid.")); + return false; + } + + progressLabel->setText(tr("Downloading (0MB)")); + // show an infinite progressbar + progressBar->setMaximum(0); + progressBar->setMinimum(0); + progressBar->setValue(0); + progressLabel->show(); + progressBar->show(); + + wizard()->disableButtons(); + setEnabled(false); + + downloadSetsFile(url); + } else if (fileRadioButton->isChecked()) { + QFile setsFile(fileLineEdit->text()); + if (!setsFile.exists()) { + QMessageBox::critical(this, tr("Error"), tr("Please choose a file.")); + return false; + } + + if (!setsFile.open(QIODevice::ReadOnly)) { + QMessageBox::critical(nullptr, tr("Error"), tr("Cannot open file '%1'.").arg(fileLineEdit->text())); + return false; + } + + wizard()->disableButtons(); + setEnabled(false); + + wizard()->setCardSourceUrl(setsFile.fileName()); + wizard()->setCardSourceVersion("unknown"); + + readSetsFromByteArray(setsFile.readAll()); + } + + return false; +} + +void LoadSetsPage::downloadSetsFile(const QUrl &url) +{ + wizard()->setCardSourceVersion("unknown"); + + const auto urlString = url.toString(); + if (urlString == ALLSETS_URL || urlString == ALLSETS_URL_FALLBACK) { + const auto versionUrl = QUrl::fromUserInput(MTGJSON_VERSION_URL); + QNetworkRequest request = QNetworkRequest(versionUrl); + request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); + auto *versionReply = wizard()->nam->get(request); + connect(versionReply, &QNetworkReply::finished, [this, versionReply]() { + if (versionReply->error() == QNetworkReply::NoError) { + auto data = versionReply->readAll(); + QJsonParseError jsonError{}; + auto jsonResponse = QJsonDocument::fromJson(data, &jsonError); + + if (jsonError.error == QJsonParseError::NoError) { + const auto jsonMap = jsonResponse.toVariant().toMap(); + + auto versionString = jsonMap.value("meta").toMap().value("version").toString(); + if (versionString.isEmpty()) { + versionString = "unknown"; + } + wizard()->setCardSourceVersion(versionString); + } + } + + versionReply->deleteLater(); + }); + } + + wizard()->setCardSourceUrl(url.toString()); + + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); + auto *reply = wizard()->nam->get(request); + + connect(reply, &QNetworkReply::finished, this, &LoadSetsPage::actDownloadFinishedSetsFile); + connect(reply, &QNetworkReply::downloadProgress, this, &LoadSetsPage::actDownloadProgressSetsFile); +} + +void LoadSetsPage::actDownloadProgressSetsFile(qint64 received, qint64 total) +{ + if (total > 0) { + progressBar->setMaximum(static_cast(total)); + progressBar->setValue(static_cast(received)); + } + progressLabel->setText(tr("Downloading (%1MB)").arg((int)received / (1024 * 1024))); +} + +void LoadSetsPage::actDownloadFinishedSetsFile() +{ + // check for a reply + auto *reply = dynamic_cast(sender()); + auto errorCode = reply->error(); + if (errorCode != QNetworkReply::NoError) { + QMessageBox::critical(this, tr("Error"), tr("Network error: %1.").arg(reply->errorString())); + + wizard()->enableButtons(); + setEnabled(true); + + reply->deleteLater(); + return; + } + + auto statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (statusCode == 301 || statusCode == 302) { + const auto redirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); + qDebug() << "following redirect url:" << redirectUrl.toString(); + downloadSetsFile(redirectUrl); + reply->deleteLater(); + return; + } + + progressLabel->hide(); + progressBar->hide(); + + // save AllPrintings.json url, but only if the user customized it and download was successful + if (urlLineEdit->text() != QString(ALLSETS_URL)) { + wizard()->settings->setValue("allsetsurl", urlLineEdit->text()); + } else { + wizard()->settings->remove("allsetsurl"); + } + + readSetsFromByteArray(reply->readAll()); + reply->deleteLater(); +} + +void LoadSetsPage::readSetsFromByteArray(QByteArray _data) +{ + // show an infinite progressbar + progressBar->setMaximum(0); + progressBar->setMinimum(0); + progressBar->setValue(0); + progressLabel->setText(tr("Parsing file")); + progressLabel->show(); + progressBar->show(); + + wizard()->downloadedPlainXml = false; + wizard()->xmlData.clear(); + readSetsFromByteArrayRef(_data); +} + +void LoadSetsPage::readSetsFromByteArrayRef(QByteArray &_data) +{ + // unzip the file if needed + if (_data.startsWith(XZ_SIGNATURE)) { +#ifdef HAS_LZMA + // zipped file + auto *inBuffer = new QBuffer(&_data); + auto newData = QByteArray(); + auto *outBuffer = new QBuffer(&newData); + inBuffer->open(QBuffer::ReadOnly); + outBuffer->open(QBuffer::WriteOnly); + XzDecompressor xz; + if (!xz.decompress(inBuffer, outBuffer)) { + zipDownloadFailed(tr("Xz extraction failed.")); + return; + } + _data.clear(); + readSetsFromByteArrayRef(newData); + return; +#else + zipDownloadFailed(tr("Sorry, this version of Oracle does not support xz compressed files.")); + + wizard()->enableButtons(); + setEnabled(true); + progressLabel->hide(); + progressBar->hide(); + return; +#endif + } else if (_data.startsWith(ZIP_SIGNATURE)) { +#ifdef HAS_ZLIB + // zipped file + auto *inBuffer = new QBuffer(&_data); + auto newData = QByteArray(); + auto *outBuffer = new QBuffer(&newData); + QString fileName; + UnZip::ErrorCode ec; + UnZip uz; + + ec = uz.openArchive(inBuffer); + if (ec != UnZip::Ok) { + zipDownloadFailed(tr("Failed to open Zip archive: %1.").arg(uz.formatError(ec))); + return; + } + + if (uz.fileList().size() != 1) { + zipDownloadFailed(tr("Zip extraction failed: the Zip archive doesn't contain exactly one file.")); + return; + } + fileName = uz.fileList().at(0); + + outBuffer->open(QBuffer::ReadWrite); + ec = uz.extractFile(fileName, outBuffer); + if (ec != UnZip::Ok) { + zipDownloadFailed(tr("Zip extraction failed: %1.").arg(uz.formatError(ec))); + uz.closeArchive(); + return; + } + _data.clear(); + readSetsFromByteArrayRef(newData); + return; +#else + zipDownloadFailed(tr("Sorry, this version of Oracle does not support zipped files.")); + + wizard()->enableButtons(); + setEnabled(true); + progressLabel->hide(); + progressBar->hide(); + return; +#endif + } else if (_data.startsWith("{")) { + // Start the computation. + jsonData = std::move(_data); + future = QtConcurrent::run([this] { return wizard()->importer->readSetsFromByteArray(std::move(jsonData)); }); + watcher.setFuture(future); + } else if (_data.startsWith("<")) { + // save xml file and don't do any processing + wizard()->downloadedPlainXml = true; + wizard()->xmlData = std::move(_data); + importFinished(); + } else { + wizard()->enableButtons(); + setEnabled(true); + progressLabel->hide(); + progressBar->hide(); + QMessageBox::critical(this, tr("Error"), tr("Failed to interpret downloaded data.")); + } +} + +void LoadSetsPage::zipDownloadFailed(const QString &message) +{ + wizard()->enableButtons(); + setEnabled(true); + progressLabel->hide(); + progressBar->hide(); + + QMessageBox::StandardButton reply; + reply = static_cast(QMessageBox::question( + this, tr("Error"), message + "
" + tr("Do you want to download the uncompressed file instead?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes)); + + if (reply == QMessageBox::Yes) { + urlRadioButton->setChecked(true); + urlLineEdit->setText(ALLSETS_URL_FALLBACK); + + wizard()->next(); + } +} + +void LoadSetsPage::importFinished() +{ + wizard()->enableButtons(); + setEnabled(true); + progressLabel->hide(); + progressBar->hide(); + + if (wizard()->downloadedPlainXml || watcher.future().result()) { + wizard()->next(); + } else { + QMessageBox::critical(this, tr("Error"), + tr("The file was retrieved successfully, but it does not contain any sets data.")); + } +} + +SaveSetsPage::SaveSetsPage(QWidget *parent) : OracleWizardPage(parent) +{ + pathLabel = new QLabel(this); + saveLabel = new QLabel(this); + + defaultPathCheckBox = new QCheckBox(this); + + messageLog = new QTextEdit(this); + messageLog->setReadOnly(true); + + auto *layout = new QGridLayout(this); + layout->addWidget(messageLog, 0, 0); + layout->addWidget(saveLabel, 1, 0); + layout->addWidget(pathLabel, 2, 0); + layout->addWidget(defaultPathCheckBox, 3, 0); + + setLayout(layout); +} + +void SaveSetsPage::cleanupPage() +{ + wizard()->importer->clear(); + disconnect(wizard()->importer, &OracleImporter::setIndexChanged, nullptr, nullptr); +} + +void SaveSetsPage::initializePage() +{ + messageLog->clear(); + + retranslateUi(); + if (wizard()->downloadedPlainXml) { + messageLog->hide(); + } else { + messageLog->show(); + connect(wizard()->importer, &OracleImporter::setIndexChanged, this, &SaveSetsPage::updateTotalProgress); + + int setsImported = wizard()->importer->startImport(); + + if (setsImported == 0) { + QMessageBox::critical(this, tr("Error"), tr("No set has been imported.")); + } + } + + if (wizard()->backgroundMode) { + emit readyToContinue(); + } +} + +void SaveSetsPage::retranslateUi() +{ + setTitle(tr("Sets imported")); + if (wizard()->downloadedPlainXml) { + setSubTitle(tr("A cockatrice database file of %1 MB has been downloaded.") + .arg(qRound(wizard()->xmlData.size() / 1000000.0))); + } else { + setSubTitle(tr("The following sets have been found:")); + } + + saveLabel->setText(tr("Press \"Save\" to store the imported cards in the Cockatrice database.")); + pathLabel->setText(tr("The card database will be saved at the following location:") + "
" + + SettingsCache::instance().getCardDatabasePath()); + defaultPathCheckBox->setText(tr("Save to a custom path (not recommended)")); + + setButtonText(QWizard::NextButton, tr("&Save")); +} + +void SaveSetsPage::updateTotalProgress(int cardsImported, int /* setIndex */, const QString &setName) +{ + if (setName.isEmpty()) { + messageLog->append("" + tr("Import finished: %1 cards.").arg(wizard()->importer->getCardList().size()) + + ""); + } else { + messageLog->append(tr("%1: %2 cards imported").arg(setName).arg(cardsImported)); + } + + messageLog->verticalScrollBar()->setValue(messageLog->verticalScrollBar()->maximum()); +} + +bool SaveSetsPage::validatePage() +{ + QString defaultPath = SettingsCache::instance().getCardDatabasePath(); + QString windowName = tr("Save card database"); + QString fileType = tr("XML; card database (*.xml)"); + + QString fileName; + if (defaultPathCheckBox->isChecked()) { + fileName = QFileDialog::getSaveFileName(this, windowName, defaultPath, fileType); + } else { + fileName = defaultPath; + } + + if (fileName.isEmpty()) { + return false; + } + + QFileInfo fi(fileName); + QDir fileDir(fi.path()); + if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) { + return false; + } + + if (wizard()->downloadedPlainXml) { + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + qDebug() << "File write (w) failed for" << fileName; + return false; + } + if (file.write(wizard()->xmlData) < 1) { + qDebug() << "File write (w) failed for" << fileName; + return false; + } + wizard()->xmlData.clear(); + } else if (!wizard()->importer->saveToFile(fileName, wizard()->getCardSourceUrl(), + wizard()->getCardSourceVersion())) { + QMessageBox::critical(this, tr("Error"), tr("The file could not be saved to %1").arg(fileName)); + return false; + } + + return true; +} + +void LoadTokensPage::initializePage() +{ + SimpleDownloadFilePage::initializePage(); + + if (wizard()->backgroundMode) { + emit readyToContinue(); + } +} + +QString LoadTokensPage::getDefaultUrl() +{ + return TOKENS_URL; +} + +QString LoadTokensPage::getCustomUrlSettingsKey() +{ + return "tokensurl"; +} + +QString LoadTokensPage::getDefaultSavePath() +{ + return SettingsCache::instance().getTokenDatabasePath(); +} + +QString LoadTokensPage::getWindowTitle() +{ + return tr("Save token database"); +} + +QString LoadTokensPage::getFileType() +{ + return tr("XML; token database (*.xml)"); +} + +QString LoadTokensPage::getFilePromptName() +{ + return tr("tokens"); +} + +void LoadTokensPage::retranslateUi() +{ + setTitle(tr("Tokens import")); + setSubTitle(tr("Please specify a compatible source for token data.")); + + urlRadioButton->setText(tr("Download URL:")); + fileRadioButton->setText(tr("Local file:")); + urlButton->setText(tr("Restore default URL")); + fileButton->setText(tr("Choose file...")); + + pathLabel->setText(tr("The token database will be saved at the following location:") + "
" + + SettingsCache::instance().getTokenDatabasePath()); + defaultPathCheckBox->setText(tr("Save to a custom path (not recommended)")); +} + +QString LoadSpoilersPage::getDefaultUrl() +{ + return SPOILERS_URL; +} + +QString LoadSpoilersPage::getCustomUrlSettingsKey() +{ + return "spoilersurl"; +} + +QString LoadSpoilersPage::getDefaultSavePath() +{ + return SettingsCache::instance().getTokenDatabasePath(); +} + +QString LoadSpoilersPage::getWindowTitle() +{ + return tr("Save spoiler database"); +} + +QString LoadSpoilersPage::getFileType() +{ + return tr("XML; spoiler database (*.xml)"); +} + +QString LoadSpoilersPage::getFilePromptName() +{ + return tr("spoiler"); +} + +void LoadSpoilersPage::retranslateUi() +{ + setTitle(tr("Spoilers import")); + setSubTitle(tr("Please specify a compatible source for spoiler data.")); + + urlRadioButton->setText(tr("Download URL:")); + fileRadioButton->setText(tr("Local file:")); + urlButton->setText(tr("Restore default URL")); + fileButton->setText(tr("Choose file...")); + + pathLabel->setText(tr("The spoiler database will be saved at the following location:") + "
" + + SettingsCache::instance().getSpoilerCardDatabasePath()); + defaultPathCheckBox->setText(tr("Save to a custom path (not recommended)")); +} \ No newline at end of file diff --git a/oracle/src/pages.h b/oracle/src/pages.h new file mode 100644 index 000000000..066cc2e1b --- /dev/null +++ b/oracle/src/pages.h @@ -0,0 +1,156 @@ +#ifndef COCKATRICE_PAGES_H +#define COCKATRICE_PAGES_H + +#include "pagetemplates.h" + +#include +#include +#include +#include +#include + +class QCheckBox; +class QGroupBox; +class QComboBox; +class QLabel; +class QLineEdit; +class QRadioButton; +class QProgressBar; +class QNetworkAccessManager; +class QTextEdit; +class QVBoxLayout; +class OracleImporter; +class QSettings; + +class IntroPage : public OracleWizardPage +{ + Q_OBJECT +public: + explicit IntroPage(QWidget *parent = nullptr); + void retranslateUi() override; + +private: + QStringList findQmFiles(); + QString languageName(const QString &lang); + +private: + QLabel *label, *languageLabel, *versionLabel; + QComboBox *languageBox; + +private slots: + void languageBoxChanged(int index); + +protected slots: + void initializePage() override; +}; + +class OutroPage : public OracleWizardPage +{ + Q_OBJECT +public: + explicit OutroPage(QWidget * = nullptr) + { + } + void retranslateUi() override; + +protected: + void initializePage() override; +}; + +class LoadSetsPage : public OracleWizardPage +{ + Q_OBJECT +public: + explicit LoadSetsPage(QWidget *parent = nullptr); + void retranslateUi() override; + +protected: + void initializePage() override; + bool validatePage() override; + void readSetsFromByteArray(QByteArray _data); + void readSetsFromByteArrayRef(QByteArray &_data); + void downloadSetsFile(const QUrl &url); + +private: + QRadioButton *urlRadioButton; + QRadioButton *fileRadioButton; + QLineEdit *urlLineEdit; + QLineEdit *fileLineEdit; + QPushButton *urlButton; + QPushButton *fileButton; + QLabel *progressLabel; + QProgressBar *progressBar; + + QFutureWatcher watcher; + QFuture future; + QByteArray jsonData; + +private slots: + void actLoadSetsFile(); + void actRestoreDefaultUrl(); + void actDownloadProgressSetsFile(qint64 received, qint64 total); + void actDownloadFinishedSetsFile(); + void importFinished(); + void zipDownloadFailed(const QString &message); +}; + +class SaveSetsPage : public OracleWizardPage +{ + Q_OBJECT +public: + explicit SaveSetsPage(QWidget *parent = nullptr); + void retranslateUi() override; + +private: + QTextEdit *messageLog; + QCheckBox *defaultPathCheckBox; + QLabel *pathLabel; + QLabel *saveLabel; + +protected: + void initializePage() override; + void cleanupPage() override; + bool validatePage() override; + +private slots: + void updateTotalProgress(int cardsImported, int setIndex, const QString &setName); +}; + +class LoadSpoilersPage : public SimpleDownloadFilePage +{ + Q_OBJECT +public: + explicit LoadSpoilersPage(QWidget * = nullptr) + { + } + void retranslateUi() override; + +protected: + QString getDefaultUrl() override; + QString getCustomUrlSettingsKey() override; + QString getDefaultSavePath() override; + QString getWindowTitle() override; + QString getFileType() override; + QString getFilePromptName() override; +}; + +class LoadTokensPage : public SimpleDownloadFilePage +{ + Q_OBJECT +public: + explicit LoadTokensPage(QWidget * = nullptr) + { + } + void retranslateUi() override; + +protected: + QString getDefaultUrl() override; + QString getCustomUrlSettingsKey() override; + QString getDefaultSavePath() override; + QString getWindowTitle() override; + QString getFileType() override; + QString getFilePromptName() override; + void initializePage() override; +}; + +#endif // COCKATRICE_PAGES_H diff --git a/oracle/src/pagetemplates.cpp b/oracle/src/pagetemplates.cpp new file mode 100644 index 000000000..4a27fef30 --- /dev/null +++ b/oracle/src/pagetemplates.cpp @@ -0,0 +1,248 @@ +#include "pagetemplates.h" + +#include "oraclewizard.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +SimpleDownloadFilePage::SimpleDownloadFilePage(QWidget *parent) : OracleWizardPage(parent) +{ + urlRadioButton = new QRadioButton(this); + fileRadioButton = new QRadioButton(this); + + urlLineEdit = new QLineEdit(this); + fileLineEdit = new QLineEdit(this); + + progressLabel = new QLabel(this); + progressBar = new QProgressBar(this); + + urlRadioButton->setChecked(true); + + urlButton = new QPushButton(this); + connect(urlButton, &QPushButton::clicked, this, &SimpleDownloadFilePage::actRestoreDefaultUrl); + + fileButton = new QPushButton(this); + connect(fileButton, &QPushButton::clicked, this, &SimpleDownloadFilePage::actLoadCardFile); + + defaultPathCheckBox = new QCheckBox(this); + pathLabel = new QLabel(this); + pathLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + + auto *layout = new QGridLayout(this); + layout->addWidget(urlRadioButton, 0, 0); + layout->addWidget(urlLineEdit, 0, 1); + layout->addWidget(urlButton, 1, 1, Qt::AlignRight); + layout->addWidget(fileRadioButton, 2, 0); + layout->addWidget(fileLineEdit, 2, 1); + layout->addWidget(fileButton, 3, 1, Qt::AlignRight); + layout->addWidget(pathLabel, 4, 0, 1, 2); + layout->addWidget(defaultPathCheckBox, 5, 0, 1, 2); + layout->addWidget(progressLabel, 6, 0); + layout->addWidget(progressBar, 6, 1); + + setLayout(layout); +} + +void SimpleDownloadFilePage::initializePage() +{ + // get custom url from settings if any; otherwise use default url + urlLineEdit->setText(wizard()->settings->value(getCustomUrlSettingsKey(), getDefaultUrl()).toString()); + + progressLabel->hide(); + progressBar->hide(); +} + +void SimpleDownloadFilePage::actRestoreDefaultUrl() +{ + urlLineEdit->setText(getDefaultUrl()); +} + +void SimpleDownloadFilePage::actLoadCardFile() +{ + QFileDialog dialog(this, tr("Load %1 file").arg(getFilePromptName())); + dialog.setFileMode(QFileDialog::ExistingFile); + + QString extensions = "*.json *.xml"; +#ifdef HAS_ZLIB + extensions += " *.zip"; +#endif +#ifdef HAS_LZMA + extensions += " *.xz"; +#endif + dialog.setNameFilter(tr("%1 file (%1)").arg(getFilePromptName(), extensions)); + + if (!fileLineEdit->text().isEmpty() && QFile::exists(fileLineEdit->text())) { + dialog.selectFile(fileLineEdit->text()); + } + + if (!dialog.exec()) { + return; + } + + fileLineEdit->setText(dialog.selectedFiles().at(0)); +} + +bool SimpleDownloadFilePage::validatePage() +{ + // if data has already been downloaded, pass directly to the "save" step + if (!downloadData.isEmpty()) { + if (saveToFile()) { + return true; + } else { + wizard()->enableButtons(); + return false; + } + } + + // else, try to import sets + if (urlRadioButton->isChecked()) { + QUrl url = QUrl::fromUserInput(urlLineEdit->text()); + if (!url.isValid()) { + QMessageBox::critical(this, tr("Error"), tr("The provided URL is not valid: ") + url.toString()); + return false; + } + + progressLabel->setText(tr("Downloading (0MB)")); + // show an infinite progressbar + progressBar->setMaximum(0); + progressBar->setMinimum(0); + progressBar->setValue(0); + progressLabel->show(); + progressBar->show(); + + wizard()->disableButtons(); + downloadFile(url); + + } else if (fileRadioButton->isChecked()) { + QFile cardFile(fileLineEdit->text()); + if (!cardFile.exists()) { + QMessageBox::critical(this, tr("Error"), tr("Please choose a file.")); + return false; + } + + if (!cardFile.open(QIODevice::ReadOnly)) { + QMessageBox::critical(nullptr, tr("Error"), tr("Cannot open file '%1'.").arg(fileLineEdit->text())); + return false; + } + + downloadData = cardFile.readAll(); + wizard()->next(); + } + + return false; +} + +void SimpleDownloadFilePage::downloadFile(QUrl url) +{ + QNetworkReply *reply = wizard()->nam->get(QNetworkRequest(url)); + + connect(reply, &QNetworkReply::finished, this, &SimpleDownloadFilePage::actDownloadFinished); + connect(reply, &QNetworkReply::downloadProgress, this, &SimpleDownloadFilePage::actDownloadProgress); +} + +void SimpleDownloadFilePage::actDownloadProgress(qint64 received, qint64 total) +{ + if (total > 0) { + progressBar->setMaximum(static_cast(total)); + progressBar->setValue(static_cast(received)); + } + progressLabel->setText(tr("Downloading (%1MB)").arg((int)received / (1024 * 1024))); +} + +void SimpleDownloadFilePage::actDownloadFinished() +{ + // check for a reply + auto *reply = dynamic_cast(sender()); + QNetworkReply::NetworkError errorCode = reply->error(); + if (errorCode != QNetworkReply::NoError) { + QMessageBox::critical(this, tr("Error"), tr("Network error: %1.").arg(reply->errorString())); + wizard()->enableButtons(); + reply->deleteLater(); + return; + } + + int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (statusCode == 301 || statusCode == 302) { + QUrl redirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); + qDebug() << "following redirect url:" << redirectUrl.toString(); + downloadFile(redirectUrl); + reply->deleteLater(); + return; + } + + // save downloaded file url, but only if the user customized it and download was successful + if (urlLineEdit->text() != getDefaultUrl()) { + wizard()->settings->setValue(getCustomUrlSettingsKey(), urlLineEdit->text()); + } else { + wizard()->settings->remove(getCustomUrlSettingsKey()); + } + + downloadData = reply->readAll(); + reply->deleteLater(); + + wizard()->enableButtons(); + progressLabel->hide(); + progressBar->hide(); + + wizard()->next(); +} + +bool SimpleDownloadFilePage::saveToFile() +{ + QString defaultPath = getDefaultSavePath(); + QString windowName = getWindowTitle(); + QString fileType = getFileType(); + + QString fileName; + if (defaultPathCheckBox->isChecked()) { + fileName = QFileDialog::getSaveFileName(this, windowName, defaultPath, fileType); + } else { + fileName = defaultPath; + } + + if (fileName.isEmpty()) { + return false; + } + + QFileInfo fi(fileName); + QDir fileDir(fi.path()); + if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) { + return false; + } + + if (!internalSaveToFile(fileName)) { + QMessageBox::critical(this, tr("Error"), tr("The file could not be saved to %1").arg(fileName)); + return false; + } + + // clean saved downloadData + downloadData = QByteArray(); + return true; +} + +bool SimpleDownloadFilePage::internalSaveToFile(const QString &fileName) +{ + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + qDebug() << "File open (w) failed for" << fileName; + return false; + } + + if (file.write(downloadData) == -1) { + qDebug() << "File write (w) failed for" << fileName; + return false; + } + + file.close(); + return true; +} diff --git a/oracle/src/pagetemplates.h b/oracle/src/pagetemplates.h new file mode 100644 index 000000000..79dcdd632 --- /dev/null +++ b/oracle/src/pagetemplates.h @@ -0,0 +1,74 @@ +#ifndef PAGETEMPLATES_H +#define PAGETEMPLATES_H + +#include + +class QFile; +class QRadioButton; +class OracleWizard; +class QCheckBox; +class QLabel; +class QLineEdit; +class QProgressBar; + +class OracleWizardPage : public QWizardPage +{ + Q_OBJECT +public: + explicit OracleWizardPage(QWidget *parent = nullptr) : QWizardPage(parent) + { + } + virtual void retranslateUi() = 0; + +signals: + void readyToContinue(); + +protected: + inline OracleWizard *wizard() + { + return (OracleWizard *)QWizardPage::wizard(); + }; +}; + +class SimpleDownloadFilePage : public OracleWizardPage +{ + Q_OBJECT +public: + explicit SimpleDownloadFilePage(QWidget *parent = nullptr); + +protected: + void initializePage() override; + bool validatePage() override; + void downloadFile(QUrl url); + virtual QString getDefaultUrl() = 0; + virtual QString getCustomUrlSettingsKey() = 0; + virtual QString getDefaultSavePath() = 0; + virtual QString getWindowTitle() = 0; + virtual QString getFileType() = 0; + virtual QString getFilePromptName() = 0; + bool saveToFile(); + bool internalSaveToFile(const QString &fileName); + +protected: + QByteArray downloadData; + QRadioButton *urlRadioButton; + QRadioButton *fileRadioButton; + QLineEdit *urlLineEdit; + QLineEdit *fileLineEdit; + QPushButton *urlButton; + QPushButton *fileButton; + QLabel *pathLabel; + QLabel *progressLabel; + QProgressBar *progressBar; + QCheckBox *defaultPathCheckBox; + +signals: + void parsedDataReady(); +private slots: + void actRestoreDefaultUrl(); + void actLoadCardFile(); + void actDownloadProgress(qint64 received, qint64 total); + void actDownloadFinished(); +}; + +#endif // PAGETEMPLATES_H diff --git a/oracle/src/parsehelpers.cpp b/oracle/src/parsehelpers.cpp new file mode 100644 index 000000000..a97bf67c9 --- /dev/null +++ b/oracle/src/parsehelpers.cpp @@ -0,0 +1,69 @@ +#include "parsehelpers.h" + +#include +#include + +/** + * Parses the card text to determine if the card should have the cipt tag + * + * The parsing logic is able to handle the following cases: + * - " enters tapped" + * - " enters tapped", if the card name starts with the shortname + * - "This enters tapped" + * - "..., it enters tapped" + * - Any naming scheme that appends a non-alphanumeric character plus extra text to the end of the name. + * (e.g. name is "Card Name_SET" or "Card Name (Set)" and text contains "Card Name enters tapped") + * + * However, it will still miss on certain cases: + * - shortnames that aren't the at the beginning of the card name + * + * Note that "...enters tapped unless..." returns false. + * + * @param name The name of the card + * @param text The oracle text of the card + */ +bool parseCipt(const QString &name, const QString &text) +{ + // Use precompiled regex to check if text is a possible candidate, and early return if not + static auto prelimCheck = QRegularExpression(" enters( the battlefield)? tapped(?! unless)"); + if (!prelimCheck.match(text).hasMatch()) { + return false; + } + + // Try to split shortname on most non-alphanumeric characters (including _) + auto isShortnameDivider = [](const QChar &c) { + return c == '_' || (!c.isLetterOrNumber() && c != '\'' && c != '\"'); + }; + + // Try all possible shortnames. + // This also handles the case of extra text appended at end. + QStringList possibleNames; + bool inAlphanumericPart = true; + for (int i = 0; i < name.length(); ++i) { + if (isShortnameDivider(name.at(i))) { + if (inAlphanumericPart) { + // only add to names on a "falling edge", in order to reduce the amount of redundant splits + possibleNames.append(QRegularExpression::escape(name.left(i))); + inAlphanumericPart = false; + } + } else { + inAlphanumericPart = true; + } + } + + // and the full name + possibleNames.append(QRegularExpression::escape(name)); + + QString subject = "(it|" // "..., it enters tapped" + "(T|t)his [^ ]+|" // "This enters tapped" + + possibleNames.join("|") + ")"; + + auto ciptPattern = QRegularExpression( + // cipt phrase is either first sentence of line, or is after a punctuation mark + "(^|(, |\\. ))" + subject + + // support old wording, and exclude the "unless" case + " enters( the battlefield)? tapped(?! unless)", + QRegularExpression::MultilineOption); + + return ciptPattern.match(text).hasMatch(); +} diff --git a/oracle/src/parsehelpers.h b/oracle/src/parsehelpers.h new file mode 100644 index 000000000..40a36c753 --- /dev/null +++ b/oracle/src/parsehelpers.h @@ -0,0 +1,8 @@ +#ifndef PARSEHELPERS_H +#define PARSEHELPERS_H + +#include + +bool parseCipt(const QString &name, const QString &text); + +#endif // PARSEHELPERS_H diff --git a/cockatrice/src/qt-json/AUTHORS b/oracle/src/qt-json/AUTHORS similarity index 100% rename from cockatrice/src/qt-json/AUTHORS rename to oracle/src/qt-json/AUTHORS diff --git a/cockatrice/src/qt-json/LICENSE b/oracle/src/qt-json/LICENSE similarity index 100% rename from cockatrice/src/qt-json/LICENSE rename to oracle/src/qt-json/LICENSE diff --git a/cockatrice/src/qt-json/README b/oracle/src/qt-json/README similarity index 100% rename from cockatrice/src/qt-json/README rename to oracle/src/qt-json/README diff --git a/oracle/src/qt-json/json.cpp b/oracle/src/qt-json/json.cpp new file mode 100644 index 000000000..2fffd0f70 --- /dev/null +++ b/oracle/src/qt-json/json.cpp @@ -0,0 +1,573 @@ +/* Copyright 2011 Eeli Reilin. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL EELI REILIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation + * are those of the authors and should not be interpreted as representing + * official policies, either expressed or implied, of Eeli Reilin. + */ + +/** + * \file json.cpp + */ + +#include "json.h" + +#include +#include + +namespace QtJson +{ + +static QString sanitizeString(QString str) +{ + str.replace(QLatin1String("\\"), QLatin1String("\\\\")); + str.replace(QLatin1String("\""), QLatin1String("\\\"")); + str.replace(QLatin1String("\b"), QLatin1String("\\b")); + str.replace(QLatin1String("\f"), QLatin1String("\\f")); + str.replace(QLatin1String("\n"), QLatin1String("\\n")); + str.replace(QLatin1String("\r"), QLatin1String("\\r")); + str.replace(QLatin1String("\t"), QLatin1String("\\t")); + return QString(QLatin1String("\"%1\"")).arg(str); +} + +static QByteArray join(const QList &list, const QByteArray &sep) +{ + QByteArray res; + for (const QByteArray &i : list) { + if (!res.isEmpty()) { + res += sep; + } + res += i; + } + return res; +} + +/** + * parse + */ +QVariant Json::parse(const QString &json) +{ + bool success = true; + return Json::parse(json, success); +} + +/** + * parse + */ +QVariant Json::parse(const QString &json, bool &success) +{ + success = true; + + // Return an empty QVariant if the JSON data is either null or empty + if (!json.isNull() || !json.isEmpty()) { + // We'll start from index 0 + int index = 0; + + // Parse the first value + QVariant value = Json::parseValue(json, index, success); + + // Return the parsed value + return value; + } else { + // Return the empty QVariant + return QVariant(); + } +} + +QByteArray Json::serialize(const QVariant &data) +{ + bool success = true; + return Json::serialize(data, success); +} + +QByteArray Json::serialize(const QVariant &data, bool &success) +{ + QByteArray str; + success = true; + + if (!data.isValid()) // invalid or null? + { + str = "null"; + } +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + else if ((data.typeId() == QMetaType::Type::QVariantList) || + (data.typeId() == QMetaType::Type::QStringList)) // variant is a list? +#else + else if ((data.type() == QVariant::List) || (data.type() == QVariant::StringList)) // variant is a list? +#endif + { + QList values; + const QVariantList list = data.toList(); + for (const QVariant &v : list) { + QByteArray serializedValue = serialize(v); + if (serializedValue.isNull()) { + success = false; + break; + } + values << serializedValue; + } + + str = "[ " + join(values, ", ") + " ]"; + } +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + else if ((data.typeId() == QMetaType::Type::QVariantHash)) // variant is a list? +#else + else if (data.type() == QVariant::Hash) // variant is a hash? +#endif + { + const QVariantHash vhash = data.toHash(); + QHashIterator it(vhash); + str = "{ "; + QList pairs; + + while (it.hasNext()) { + it.next(); + QByteArray serializedValue = serialize(it.value()); + + if (serializedValue.isNull()) { + success = false; + break; + } + + pairs << sanitizeString(it.key()).toUtf8() + " : " + serializedValue; + } + + str += join(pairs, ", "); + str += " }"; + } +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + else if ((data.typeId() == QMetaType::Type::QVariantMap)) // variant is a list? +#else + else if (data.type() == QVariant::Map) // variant is a map? +#endif + { + const QVariantMap vmap = data.toMap(); + QMapIterator it(vmap); + str = "{ "; + QList pairs; + while (it.hasNext()) { + it.next(); + QByteArray serializedValue = serialize(it.value()); + if (serializedValue.isNull()) { + success = false; + break; + } + pairs << sanitizeString(it.key()).toUtf8() + " : " + serializedValue; + } + str += join(pairs, ", "); + str += " }"; + } +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + else if ((data.typeId() == QMetaType::Type::QString) || + (data.typeId() == QMetaType::Type::QByteArray)) // variant is a list? +#else + else if ((data.type() == QVariant::String) || (data.type() == QVariant::ByteArray)) // a string or a byte array? +#endif + { + str = sanitizeString(data.toString()).toUtf8(); + } +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + else if (data.typeId() == QMetaType::Type::Double) +#else + else if (data.type() == QVariant::Double) // double? +#endif + { + str = QByteArray::number(data.toDouble(), 'g', 20); + if (!str.contains(".") && !str.contains("e")) { + str += ".0"; + } + } +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + else if (data.typeId() == QMetaType::Type::Bool) +#else + else if (data.type() == QVariant::Bool) // boolean value? +#endif + { + str = data.toBool() ? "true" : "false"; + } +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + else if (data.typeId() == QMetaType::Type::ULongLong) +#else + else if (data.type() == QVariant::ULongLong) // large unsigned number? +#endif + { + str = QByteArray::number(data.value()); + } else if (data.canConvert()) // any signed number? + { + str = QByteArray::number(data.value()); + } else if (data.canConvert()) { + str = QString::number(data.value()).toUtf8(); + } else if (data.canConvert()) // can value be converted to string? + { + // this will catch QDate, QDateTime, QUrl, ... + str = sanitizeString(data.toString()).toUtf8(); + } else { + success = false; + } + if (success) { + return str; + } else { + return QByteArray(); + } +} + +/** + * parseValue + */ +QVariant Json::parseValue(const QString &json, int &index, bool &success) +{ + // Determine what kind of data we should parse by + // checking out the upcoming token + switch (Json::lookAhead(json, index)) { + case JsonTokenString: + return Json::parseString(json, index, success); + case JsonTokenNumber: + return Json::parseNumber(json, index); + case JsonTokenCurlyOpen: + return Json::parseObject(json, index, success); + case JsonTokenSquaredOpen: + return Json::parseArray(json, index, success); + case JsonTokenTrue: + Json::nextToken(json, index); + return QVariant(true); + case JsonTokenFalse: + Json::nextToken(json, index); + return QVariant(false); + case JsonTokenNull: + Json::nextToken(json, index); + return QVariant(); + case JsonTokenNone: + break; + } + + // If there were no tokens, flag the failure and return an empty QVariant + success = false; + return QVariant(); +} + +/** + * parseObject + */ +QVariant Json::parseObject(const QString &json, int &index, bool &success) +{ + QVariantMap map; + int token; + + // Get rid of the whitespace and increment index + Json::nextToken(json, index); + + // Loop through all of the key/value pairs of the object + bool done = false; + while (!done) { + // Get the upcoming token + token = Json::lookAhead(json, index); + + if (token == JsonTokenNone) { + success = false; + return QVariantMap(); + } else if (token == JsonTokenComma) { + Json::nextToken(json, index); + } else if (token == JsonTokenCurlyClose) { + Json::nextToken(json, index); + return map; + } else { + // Parse the key/value pair's name + QString name = Json::parseString(json, index, success).toString(); + + if (!success) { + return QVariantMap(); + } + + // Get the next token + token = Json::nextToken(json, index); + + // If the next token is not a colon, flag the failure + // return an empty QVariant + if (token != JsonTokenColon) { + success = false; + return QVariant(QVariantMap()); + } + + // Parse the key/value pair's value + QVariant value = Json::parseValue(json, index, success); + + if (!success) { + return QVariantMap(); + } + + // Assign the value to the key in the map + map[name] = value; + } + } + + // Return the map successfully + return QVariant(map); +} + +/** + * parseArray + */ +QVariant Json::parseArray(const QString &json, int &index, bool &success) +{ + QVariantList list; + + Json::nextToken(json, index); + + bool done = false; + while (!done) { + int token = Json::lookAhead(json, index); + + if (token == JsonTokenNone) { + success = false; + return QVariantList(); + } else if (token == JsonTokenComma) { + Json::nextToken(json, index); + } else if (token == JsonTokenSquaredClose) { + Json::nextToken(json, index); + break; + } else { + QVariant value = Json::parseValue(json, index, success); + + if (!success) { + return QVariantList(); + } + + list.push_back(value); + } + } + + return QVariant(list); +} + +/** + * parseString + */ +QVariant Json::parseString(const QString &json, int &index, bool &success) +{ + QString s; + QChar c; + + Json::eatWhitespace(json, index); + + c = json[index++]; + + bool complete = false; + while (!complete) { + if (index == json.size()) { + break; + } + + c = json[index++]; + + if (c == '\"') { + complete = true; + break; + } else if (c == '\\') { + if (index == json.size()) { + break; + } + + c = json[index++]; + + if (c == '\"') { + s.append('\"'); + } else if (c == '\\') { + s.append('\\'); + } else if (c == '/') { + s.append('/'); + } else if (c == 'b') { + s.append('\b'); + } else if (c == 'f') { + s.append('\f'); + } else if (c == 'n') { + s.append('\n'); + } else if (c == 'r') { + s.append('\r'); + } else if (c == 't') { + s.append('\t'); + } else if (c == 'u') { + int remainingLength = json.size() - index; + + if (remainingLength >= 4) { + QString unicodeStr = json.mid(index, 4); + + int symbol = unicodeStr.toInt(0, 16); + + s.append(QChar(symbol)); + + index += 4; + } else { + break; + } + } + } else { + s.append(c); + } + } + + if (!complete) { + success = false; + return QVariant(); + } + + return QVariant(s); +} + +/** + * parseNumber + */ +QVariant Json::parseNumber(const QString &json, int &index) +{ + Json::eatWhitespace(json, index); + + int lastIndex = Json::lastIndexOfNumber(json, index); + int charLength = (lastIndex - index) + 1; + QString numberStr; + + numberStr = json.mid(index, charLength); + + index = lastIndex + 1; + + if (numberStr.contains('.')) { + return QVariant(numberStr.toDouble(NULL)); + } else if (numberStr.startsWith('-')) { + return QVariant(numberStr.toLongLong(NULL)); + } else { + return QVariant(numberStr.toULongLong(NULL)); + } +} + +/** + * lastIndexOfNumber + */ +int Json::lastIndexOfNumber(const QString &json, int index) +{ + static const QString numericCharacters("0123456789+-.eE"); + int lastIndex; + + for (lastIndex = index; lastIndex < json.size(); lastIndex++) { + if (numericCharacters.indexOf(json[lastIndex]) == -1) { + break; + } + } + + return lastIndex - 1; +} + +/** + * eatWhitespace + */ +void Json::eatWhitespace(const QString &json, int &index) +{ + static const QString whitespaceChars(" \t\n\r"); + for (; index < json.size(); index++) { + if (whitespaceChars.indexOf(json[index]) == -1) { + break; + } + } +} + +/** + * lookAhead + */ +int Json::lookAhead(const QString &json, int index) +{ + int saveIndex = index; + return Json::nextToken(json, saveIndex); +} + +/** + * nextToken + */ +int Json::nextToken(const QString &json, int &index) +{ + Json::eatWhitespace(json, index); + + if (index == json.size()) { + return JsonTokenNone; + } + + QChar c = json[index]; + index++; + switch (c.toLatin1()) { + case '{': + return JsonTokenCurlyOpen; + case '}': + return JsonTokenCurlyClose; + case '[': + return JsonTokenSquaredOpen; + case ']': + return JsonTokenSquaredClose; + case ',': + return JsonTokenComma; + case '"': + return JsonTokenString; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + return JsonTokenNumber; + case ':': + return JsonTokenColon; + } + + index--; + + int remainingLength = json.size() - index; + + // True + if (remainingLength >= 4) { + if (json[index] == 't' && json[index + 1] == 'r' && json[index + 2] == 'u' && json[index + 3] == 'e') { + index += 4; + return JsonTokenTrue; + } + } + + // False + if (remainingLength >= 5) { + if (json[index] == 'f' && json[index + 1] == 'a' && json[index + 2] == 'l' && json[index + 3] == 's' && + json[index + 4] == 'e') { + index += 5; + return JsonTokenFalse; + } + } + + // Null + if (remainingLength >= 4) { + if (json[index] == 'n' && json[index + 1] == 'u' && json[index + 2] == 'l' && json[index + 3] == 'l') { + index += 4; + return JsonTokenNull; + } + } + + return JsonTokenNone; +} + +} // namespace QtJson diff --git a/cockatrice/src/qt-json/json.h b/oracle/src/qt-json/json.h similarity index 100% rename from cockatrice/src/qt-json/json.h rename to oracle/src/qt-json/json.h diff --git a/oracle/src/zip/unzip.cpp b/oracle/src/zip/unzip.cpp new file mode 100755 index 000000000..8e52ece18 --- /dev/null +++ b/oracle/src/zip/unzip.cpp @@ -0,0 +1,1425 @@ +/**************************************************************************** +** Filename: unzip.cpp +** Last updated [dd/mm/yyyy]: 08/07/2010 +** +** pkzip 2.0 decompression. +** +** Some of the code has been inspired by other open source projects, +** (mainly Info-Zip and Gilles Vollant's minizip). +** Compression and decompression actually uses the zlib library. +** +** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. +** +** This file is part of the OSDaB project (http://osdab.42cows.org/). +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See the file LICENSE.GPL that came with this software distribution or +** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. +** +**********************************************************************/ + +#include "unzip.h" +#include "unzip_p.h" +#include "zipentry_p.h" + +#include +#include +#include +#include +#include + +// You can remove this #include if you replace the qDebug() statements. +#include + +/*! + \class UnZip unzip.h + + \brief PKZip 2.0 file decompression. + Compatibility with later versions is not ensured as they may use + unsupported compression algorithms. + Versions after 2.7 may have an incompatible header format and thus be + completely incompatible. +*/ + +/*! \enum UnZip::ErrorCode The result of a decompression operation. + \value UnZip::Ok No error occurred. + \value UnZip::ZlibInit Failed to init or load the zlib library. + \value UnZip::ZlibError The zlib library returned some error. + \value UnZip::OpenFailed Unable to create or open a device. + \value UnZip::PartiallyCorrupted Corrupted zip archive - some files could be extracted. + \value UnZip::Corrupted Corrupted or invalid zip archive. + \value UnZip::WrongPassword Unable to decrypt a password protected file. + \value UnZip::NoOpenArchive No archive has been opened yet. + \value UnZip::FileNotFound Unable to find the requested file in the archive. + \value UnZip::ReadFailed Reading of a file failed. + \value UnZip::WriteFailed Writing of a file failed. + \value UnZip::SeekFailed Seek failed. + \value UnZip::CreateDirFailed Could not create a directory. + \value UnZip::InvalidDevice A null device has been passed as parameter. + \value UnZip::InvalidArchive This is not a valid (or supported) ZIP archive. + \value UnZip::HeaderConsistencyError Local header record info does not match with the central directory record info. The archive may be corrupted. + + \value UnZip::Skip Internal use only. + \value UnZip::SkipAll Internal use only. +*/ + +/*! \enum UnZip::ExtractionOptions Some options for the file extraction methods. + \value UnZip::ExtractPaths Default. Does not ignore the path of the zipped files. + \value UnZip::SkipPaths Default. Ignores the path of the zipped files and extracts them all to the same root directory. + \value UnZip::VerifyOnly Doesn't actually extract files. + \value UnZip::NoSilentDirectoryCreation Doesn't attempt to silently create missing output directories. +*/ + +//! Local header size (excluding signature, excluding variable length fields) +#define UNZIP_LOCAL_HEADER_SIZE 26 +//! Central Directory file entry size (excluding signature, excluding variable length fields) +#define UNZIP_CD_ENTRY_SIZE_NS 42 +//! Data descriptor size (excluding signature) +#define UNZIP_DD_SIZE 12 +//! End Of Central Directory size (including signature, excluding variable length fields) +#define UNZIP_EOCD_SIZE 22 +//! Local header entry encryption header size +#define UNZIP_LOCAL_ENC_HEADER_SIZE 12 + +// Some offsets inside a CD record (excluding signature) +#define UNZIP_CD_OFF_VERSION_MADE 0 +#define UNZIP_CD_OFF_VERSION 2 +#define UNZIP_CD_OFF_GPFLAG 4 +#define UNZIP_CD_OFF_CMETHOD 6 +#define UNZIP_CD_OFF_MODT 8 +#define UNZIP_CD_OFF_MODD 10 +#define UNZIP_CD_OFF_CRC32 12 +#define UNZIP_CD_OFF_CSIZE 16 +#define UNZIP_CD_OFF_USIZE 20 +#define UNZIP_CD_OFF_NAMELEN 24 +#define UNZIP_CD_OFF_XLEN 26 +#define UNZIP_CD_OFF_COMMLEN 28 +#define UNZIP_CD_OFF_LHOFFSET 38 + +// Some offsets inside a local header record (excluding signature) +#define UNZIP_LH_OFF_VERSION 0 +#define UNZIP_LH_OFF_GPFLAG 2 +#define UNZIP_LH_OFF_CMETHOD 4 +#define UNZIP_LH_OFF_MODT 6 +#define UNZIP_LH_OFF_MODD 8 +#define UNZIP_LH_OFF_CRC32 10 +#define UNZIP_LH_OFF_CSIZE 14 +#define UNZIP_LH_OFF_USIZE 18 +#define UNZIP_LH_OFF_NAMELEN 22 +#define UNZIP_LH_OFF_XLEN 24 + +// Some offsets inside a data descriptor record (excluding signature) +#define UNZIP_DD_OFF_CRC32 0 +#define UNZIP_DD_OFF_CSIZE 4 +#define UNZIP_DD_OFF_USIZE 8 + +// Some offsets inside a EOCD record +#define UNZIP_EOCD_OFF_ENTRIES 6 +#define UNZIP_EOCD_OFF_CDOFF 12 +#define UNZIP_EOCD_OFF_COMMLEN 16 + +/*! + Max version handled by this API. + 0x14 = 2.0 --> full compatibility only up to this version; + later versions use unsupported features +*/ +#define UNZIP_VERSION 0x14 + +//! CRC32 routine +#define CRC32(c, b) crcTable[((int)c^b) & 0xff] ^ (c >> 8) + +OSDAB_BEGIN_NAMESPACE(Zip) + + +/************************************************************************ + ZipEntry +*************************************************************************/ + +/*! + ZipEntry constructor - initialize data. Type is set to File. +*/ +UnZip::ZipEntry::ZipEntry() +{ + compressedSize = uncompressedSize = crc32 = 0; + compression = NoCompression; + type = File; + encrypted = false; +} + + +/************************************************************************ + Private interface +*************************************************************************/ + +//! \internal +UnzipPrivate::UnzipPrivate() : + password(), + skipAllEncrypted(false), + headers(0), + device(0), + file(0), + uBuffer(0), + crcTable(0), + cdOffset(0), + eocdOffset(0), + cdEntryCount(0), + unsupportedEntryCount(0), + comment() +{ + uBuffer = (unsigned char*) buffer1; + crcTable = (quint32*) get_crc_table(); +} + +//! \internal +void UnzipPrivate::deviceDestroyed(QObject*) +{ + qDebug("Unexpected device destruction detected."); + do_closeArchive(); +} + +//! \internal Parses a Zip archive. +UnZip::ErrorCode UnzipPrivate::openArchive(QIODevice* dev) +{ + Q_ASSERT(!device); + Q_ASSERT(dev); + + if (!(dev->isOpen() || dev->open(QIODevice::ReadOnly))) { + qDebug() << "Unable to open device for reading"; + return UnZip::OpenFailed; + } + + device = dev; + if (device != file) + connect(device, SIGNAL(destroyed(QObject*)), this, SLOT(deviceDestroyed(QObject*))); + + UnZip::ErrorCode ec; + + ec = seekToCentralDirectory(); + if (ec != UnZip::Ok) { + closeArchive(); + return ec; + } + + //! \todo Ignore CD entry count? CD may be corrupted. + if (cdEntryCount == 0) { + return UnZip::Ok; + } + + bool continueParsing = true; + + while (continueParsing) { + if (device->read(buffer1, 4) != 4) { + if (headers) { + qDebug() << "Corrupted zip archive. Some files might be extracted."; + ec = headers->size() != 0 ? UnZip::PartiallyCorrupted : UnZip::Corrupted; + break; + } else { + closeArchive(); + qDebug() << "Corrupted or invalid zip archive. Closing."; + ec = UnZip::Corrupted; + break; + } + } + + if (! (buffer1[0] == 'P' && buffer1[1] == 'K' && buffer1[2] == 0x01 && buffer1[3] == 0x02) ) + break; + + if ((ec = parseCentralDirectoryRecord()) != UnZip::Ok) + break; + } + + if (ec != UnZip::Ok) + closeArchive(); + + return ec; +} + +/* + \internal Parses a local header record and makes some consistency check + with the information stored in the Central Directory record for this entry + that has been previously parsed. + + local file header signature 4 bytes (0x04034b50) + version needed to extract 2 bytes + general purpose bit flag 2 bytes + compression method 2 bytes + last mod file time 2 bytes + last mod file date 2 bytes + crc-32 4 bytes + compressed size 4 bytes + uncompressed size 4 bytes + file name length 2 bytes + extra field length 2 bytes + + file name (variable size) + extra field (variable size) +*/ +//! \todo Optional consistency check (as a ExtractionOptions flag) +UnZip::ErrorCode UnzipPrivate::parseLocalHeaderRecord(const QString& path, const ZipEntryP& entry) +{ + Q_ASSERT(device); + + if (!device->seek(entry.lhOffset)) + return UnZip::SeekFailed; + + // Test signature + if (device->read(buffer1, 4) != 4) + return UnZip::ReadFailed; + + if ((buffer1[0] != 'P') || (buffer1[1] != 'K') || (buffer1[2] != 0x03) || (buffer1[3] != 0x04)) + return UnZip::InvalidArchive; + + if (device->read(buffer1, UNZIP_LOCAL_HEADER_SIZE) != UNZIP_LOCAL_HEADER_SIZE) + return UnZip::ReadFailed; + + /* + Check 3rd general purpose bit flag. + + "bit 3: If this bit is set, the fields crc-32, compressed size + and uncompressed size are set to zero in the local + header. The correct values are put in the data descriptor + immediately following the compressed data." + */ + bool hasDataDescriptor = entry.hasDataDescriptor(); + bool checkFailed = entry.compMethod != getUShort(uBuffer, UNZIP_LH_OFF_CMETHOD); + + if (!checkFailed) + checkFailed = entry.gpFlag[0] != uBuffer[UNZIP_LH_OFF_GPFLAG]; + if (!checkFailed) + checkFailed = entry.gpFlag[1] != uBuffer[UNZIP_LH_OFF_GPFLAG + 1]; + if (!checkFailed) + checkFailed = entry.modTime[0] != uBuffer[UNZIP_LH_OFF_MODT]; + if (!checkFailed) + checkFailed = entry.modTime[1] != uBuffer[UNZIP_LH_OFF_MODT + 1]; + if (!checkFailed) + checkFailed = entry.modDate[0] != uBuffer[UNZIP_LH_OFF_MODD]; + if (!checkFailed) + checkFailed = entry.modDate[1] != uBuffer[UNZIP_LH_OFF_MODD + 1]; + if (!hasDataDescriptor) + { + if (!checkFailed) + checkFailed = entry.crc != getULong(uBuffer, UNZIP_LH_OFF_CRC32); + if (!checkFailed) + checkFailed = entry.szComp != getULong(uBuffer, UNZIP_LH_OFF_CSIZE); + if (!checkFailed) + checkFailed = entry.szUncomp != getULong(uBuffer, UNZIP_LH_OFF_USIZE); + } + + if (checkFailed) + return UnZip::HeaderConsistencyError; + + // Check filename + quint16 szName = getUShort(uBuffer, UNZIP_LH_OFF_NAMELEN); + if (szName == 0) + return UnZip::HeaderConsistencyError; + + if (device->read(buffer2, szName) != szName) + return UnZip::ReadFailed; + + QString filename = QString::fromLatin1(buffer2, szName); + if (filename != path) { + qDebug() << "Filename in local header mismatches."; + return UnZip::HeaderConsistencyError; + } + + // Skip extra field + quint16 szExtra = getUShort(uBuffer, UNZIP_LH_OFF_XLEN); + if (szExtra != 0) { + if (!device->seek(device->pos() + szExtra)) + return UnZip::SeekFailed; + } + + entry.dataOffset = device->pos(); + + if (hasDataDescriptor) { + /* + The data descriptor has this OPTIONAL signature: PK\7\8 + We try to skip the compressed data relying on the size set in the + Central Directory record. + */ + if (!device->seek(device->pos() + entry.szComp)) + return UnZip::SeekFailed; + + // Read 4 bytes and check if there is a data descriptor signature + if (device->read(buffer2, 4) != 4) + return UnZip::ReadFailed; + + bool hasSignature = buffer2[0] == 'P' && buffer2[1] == 'K' && buffer2[2] == 0x07 && buffer2[3] == 0x08; + if (hasSignature) { + if (device->read(buffer2, UNZIP_DD_SIZE) != UNZIP_DD_SIZE) + return UnZip::ReadFailed; + } else { + if (device->read(buffer2 + 4, UNZIP_DD_SIZE - 4) != UNZIP_DD_SIZE - 4) + return UnZip::ReadFailed; + } + + // DD: crc, compressed size, uncompressed size + if ( + entry.crc != getULong((unsigned char*)buffer2, UNZIP_DD_OFF_CRC32) || + entry.szComp != getULong((unsigned char*)buffer2, UNZIP_DD_OFF_CSIZE) || + entry.szUncomp != getULong((unsigned char*)buffer2, UNZIP_DD_OFF_USIZE) + ) + return UnZip::HeaderConsistencyError; + } + + return UnZip::Ok; +} + +/*! \internal Attempts to find the start of the central directory record. + + We seek the file back until we reach the "End Of Central Directory" + signature PK\5\6. + + end of central dir signature 4 bytes (0x06054b50) + number of this disk 2 bytes + number of the disk with the + start of the central directory 2 bytes + total number of entries in the + central directory on this disk 2 bytes + total number of entries in + the central directory 2 bytes + size of the central directory 4 bytes + offset of start of central + directory with respect to + the starting disk number 4 bytes + .ZIP file comment length 2 bytes + --- SIZE UNTIL HERE: UNZIP_EOCD_SIZE --- + .ZIP file comment (variable size) +*/ +UnZip::ErrorCode UnzipPrivate::seekToCentralDirectory() +{ + Q_ASSERT(device); + + qint64 length = device->size(); + qint64 offset = length - UNZIP_EOCD_SIZE; + + if (length < UNZIP_EOCD_SIZE) + return UnZip::InvalidArchive; + + if (!device->seek( offset )) + return UnZip::SeekFailed; + + if (device->read(buffer1, UNZIP_EOCD_SIZE) != UNZIP_EOCD_SIZE) + return UnZip::ReadFailed; + + bool eocdFound = (buffer1[0] == 'P' && buffer1[1] == 'K' && buffer1[2] == 0x05 && buffer1[3] == 0x06); + + if (eocdFound) { + // Zip file has no comment (the only variable length field in the EOCD record) + eocdOffset = offset; + } else { + qint64 read; + char* p = 0; + + offset -= UNZIP_EOCD_SIZE; + + if (offset <= 0) + return UnZip::InvalidArchive; + + if (!device->seek( offset )) + return UnZip::SeekFailed; + + while ((read = device->read(buffer1, UNZIP_EOCD_SIZE)) >= 0) { + if ( (p = strstr(buffer1, "PK\5\6")) != 0) { + // Seek to the start of the EOCD record so we can read it fully + // Yes... we could simply read the missing bytes and append them to the buffer + // but this is far easier so heck it! + device->seek( offset + (p - buffer1) ); + eocdFound = true; + eocdOffset = offset + (p - buffer1); + + // Read EOCD record + if (device->read(buffer1, UNZIP_EOCD_SIZE) != UNZIP_EOCD_SIZE) + return UnZip::ReadFailed; + + break; + } + + // TODO: This is very slow and only a temporary bug fix. Need some pattern matching algorithm here. + offset -= 1 /*UNZIP_EOCD_SIZE*/; + if (offset <= 0) + return UnZip::InvalidArchive; + + if (!device->seek( offset )) + return UnZip::SeekFailed; + } + } + + if (!eocdFound) + return UnZip::InvalidArchive; + + // Parse EOCD to locate CD offset + offset = getULong((const unsigned char*)buffer1, UNZIP_EOCD_OFF_CDOFF + 4); + + cdOffset = offset; + + cdEntryCount = getUShort((const unsigned char*)buffer1, UNZIP_EOCD_OFF_ENTRIES + 4); + + quint16 commentLength = getUShort((const unsigned char*)buffer1, UNZIP_EOCD_OFF_COMMLEN + 4); + if (commentLength != 0) { + QByteArray c = device->read(commentLength); + if (c.size() != commentLength) + return UnZip::ReadFailed; + + comment = c; + } + + // Seek to the start of the CD record + if (!device->seek( cdOffset )) + return UnZip::SeekFailed; + + return UnZip::Ok; +} + +/*! + \internal Parses a central directory record. + + Central Directory record structure: + + [file header 1] + . + . + . + [file header n] + [digital signature] // PKZip 6.2 or later only + + File header: + + central file header signature 4 bytes (0x02014b50) + version made by 2 bytes + version needed to extract 2 bytes + general purpose bit flag 2 bytes + compression method 2 bytes + last mod file time 2 bytes + last mod file date 2 bytes + crc-32 4 bytes + compressed size 4 bytes + uncompressed size 4 bytes + file name length 2 bytes + extra field length 2 bytes + file comment length 2 bytes + disk number start 2 bytes + internal file attributes 2 bytes + external file attributes 4 bytes + relative offset of local header 4 bytes + + file name (variable size) + extra field (variable size) + file comment (variable size) +*/ +UnZip::ErrorCode UnzipPrivate::parseCentralDirectoryRecord() +{ + Q_ASSERT(device); + + // Read CD record + if (device->read(buffer1, UNZIP_CD_ENTRY_SIZE_NS) != UNZIP_CD_ENTRY_SIZE_NS) + return UnZip::ReadFailed; + + bool skipEntry = false; + + // Get compression type so we can skip non compatible algorithms + quint16 compMethod = getUShort(uBuffer, UNZIP_CD_OFF_CMETHOD); + + // Get variable size fields length so we can skip the whole record + // if necessary + quint16 szName = getUShort(uBuffer, UNZIP_CD_OFF_NAMELEN); + quint16 szExtra = getUShort(uBuffer, UNZIP_CD_OFF_XLEN); + quint16 szComment = getUShort(uBuffer, UNZIP_CD_OFF_COMMLEN); + + quint32 skipLength = szName + szExtra + szComment; + + UnZip::ErrorCode ec = UnZip::Ok; + + if ((compMethod != 0) && (compMethod != 8)) { + qDebug() << "Unsupported compression method. Skipping file."; + skipEntry = true; + } + + if (!skipEntry && szName == 0) { + qDebug() << "Skipping file with no name."; + skipEntry = true; + } + + QString filename; + if (device->read(buffer2, szName) != szName) { + ec = UnZip::ReadFailed; + skipEntry = true; + } else { + filename = QString::fromLatin1(buffer2, szName); + } + + // Unsupported features if version is bigger than UNZIP_VERSION + if (!skipEntry && buffer1[UNZIP_CD_OFF_VERSION] > UNZIP_VERSION) { + QString v = QString::number(buffer1[UNZIP_CD_OFF_VERSION]); + if (v.length() == 2) + v.insert(1, QLatin1Char('.')); + v = QString::fromLatin1("Unsupported PKZip version (%1). Skipping file: %2") + .arg(v, filename.isEmpty() ? QString::fromLatin1("") : filename); + qDebug() << v.toLatin1().constData(); + skipEntry = true; + } + + if (skipEntry) { + if (ec == UnZip::Ok) { + if (!device->seek( device->pos() + skipLength )) + ec = UnZip::SeekFailed; + unsupportedEntryCount++; + } + + return ec; + } + + ZipEntryP* h = new ZipEntryP; + h->compMethod = compMethod; + + h->gpFlag[0] = buffer1[UNZIP_CD_OFF_GPFLAG]; + h->gpFlag[1] = buffer1[UNZIP_CD_OFF_GPFLAG + 1]; + + h->modTime[0] = buffer1[UNZIP_CD_OFF_MODT]; + h->modTime[1] = buffer1[UNZIP_CD_OFF_MODT + 1]; + + h->modDate[0] = buffer1[UNZIP_CD_OFF_MODD]; + h->modDate[1] = buffer1[UNZIP_CD_OFF_MODD + 1]; + + h->crc = getULong(uBuffer, UNZIP_CD_OFF_CRC32); + h->szComp = getULong(uBuffer, UNZIP_CD_OFF_CSIZE); + h->szUncomp = getULong(uBuffer, UNZIP_CD_OFF_USIZE); + + // Skip extra field (if any) + if (szExtra != 0) { + if (!device->seek( device->pos() + szExtra )) { + delete h; + return UnZip::SeekFailed; + } + } + + // Read comment field (if any) + if (szComment != 0) { + if (device->read(buffer2, szComment) != szComment) { + delete h; + return UnZip::ReadFailed; + } + + h->comment = QString::fromLatin1(buffer2, szComment); + } + + h->lhOffset = getULong(uBuffer, UNZIP_CD_OFF_LHOFFSET); + + if (!headers) + headers = new QMap(); + headers->insert(filename, h); + + return UnZip::Ok; +} + +//! \internal Closes the archive and resets the internal status. +void UnzipPrivate::closeArchive() +{ + if (!device) { + Q_ASSERT(!file); + return; + } + + if (device != file) + disconnect(device, 0, this, 0); + + do_closeArchive(); +} + +//! \internal +void UnzipPrivate::do_closeArchive() +{ + skipAllEncrypted = false; + + if (headers) { + if (headers) + qDeleteAll(*headers); + delete headers; + headers = 0; + } + + device = 0; + + if (file) + delete file; + file = 0; + + cdOffset = eocdOffset = 0; + cdEntryCount = 0; + unsupportedEntryCount = 0; + + comment.clear(); +} + +//! \internal +UnZip::ErrorCode UnzipPrivate::extractFile(const QString& path, const ZipEntryP& entry, + const QDir& dir, UnZip::ExtractionOptions options) +{ + QString name(path); + QString dirname; + QString directory; + + const bool verify = (options & UnZip::VerifyOnly); + const int pos = name.lastIndexOf('/'); + + // This entry is for a directory + if (pos == name.length() - 1) { + if (verify) + return UnZip::Ok; + + if (options & UnZip::SkipPaths) + return UnZip::Ok; + + directory = QString("%1/%2").arg(dir.absolutePath()).arg(QDir::cleanPath(name)); + if (!createDirectory(directory)) { + qDebug() << QString("Unable to create directory: %1").arg(directory); + return UnZip::CreateDirFailed; + } + + return UnZip::Ok; + } + + // Extract path from entry + if (verify) { + return extractFile(path, entry, 0, options); + } + + if (pos > 0) { + // get directory part + dirname = name.left(pos); + if (options & UnZip::SkipPaths) { + directory = dir.absolutePath(); + } else { + directory = QString("%1/%2").arg(dir.absolutePath()).arg(QDir::cleanPath(dirname)); + if (!createDirectory(directory)) { + qDebug() << QString("Unable to create directory: %1").arg(directory); + return UnZip::CreateDirFailed; + } + } + name = name.right(name.length() - pos - 1); + } else { + directory = dir.absolutePath(); + } + + const bool silentDirectoryCreation = !(options & UnZip::NoSilentDirectoryCreation); + if (silentDirectoryCreation) { + if (!createDirectory(directory)) { + qDebug() << QString("Unable to create output directory %1").arg(directory); + return UnZip::CreateDirFailed; + } + } + + name = QString("%1/%2").arg(directory).arg(name); + + QFile outFile(name); + if (!outFile.open(QIODevice::WriteOnly)) { + qDebug() << QString("Unable to open %1 for writing").arg(name); + return UnZip::OpenFailed; + } + + UnZip::ErrorCode ec = extractFile(path, entry, &outFile, options); + outFile.close(); + + const QDateTime lastModified = convertDateTime(entry.modDate, entry.modTime); + const bool setTimeOk = OSDAB_ZIP_MANGLE(setFileTimestamp)(name, lastModified); + if (!setTimeOk) { + qDebug() << QString("Unable to set last modified time on file: %1").arg(name); + } + + if (ec != UnZip::Ok) { + if (!outFile.remove()) + qDebug() << QString("Unable to remove corrupted file: %1").arg(name); + } + + return ec; +} + +//! \internal +UnZip::ErrorCode UnzipPrivate::extractStoredFile( + const quint32 szComp, quint32** keys, quint32& myCRC, QIODevice* outDev, + UnZip::ExtractionOptions options) +{ + const bool verify = (options & UnZip::VerifyOnly); + const bool isEncrypted = keys != 0; + + uInt rep = szComp / UNZIP_READ_BUFFER; + uInt rem = szComp % UNZIP_READ_BUFFER; + uInt cur = 0; + + // extract data + qint64 read; + quint64 tot = 0; + + while ( (read = device->read(buffer1, cur < rep ? UNZIP_READ_BUFFER : rem)) > 0 ) { + if (isEncrypted) + decryptBytes(*keys, buffer1, read); + + myCRC = crc32(myCRC, uBuffer, read); + if (!verify) { + if (outDev->write(buffer1, read) != read) + return UnZip::WriteFailed; + } + + cur++; + tot += read; + if (tot == szComp) + break; + } + + return (read < 0) + ? UnZip::ReadFailed + : UnZip::Ok; +} + +//! \internal +UnZip::ErrorCode UnzipPrivate::inflateFile( + const quint32 szComp, quint32** keys, quint32& myCRC, QIODevice* outDev, + UnZip::ExtractionOptions options) +{ + const bool verify = (options & UnZip::VerifyOnly); + const bool isEncrypted = keys != 0; + Q_ASSERT(verify ? true : outDev != 0); + + uInt rep = szComp / UNZIP_READ_BUFFER; + uInt rem = szComp % UNZIP_READ_BUFFER; + uInt cur = 0; + + // extract data + qint64 read; + + /* Allocate inflate state */ + z_stream zstr; + zstr.zalloc = Z_NULL; + zstr.zfree = Z_NULL; + zstr.opaque = Z_NULL; + zstr.next_in = Z_NULL; + zstr.avail_in = 0; + + int zret; + + // Use inflateInit2 with negative windowBits to get raw decompression + if ( (zret = inflateInit2_(&zstr, -MAX_WBITS, ZLIB_VERSION, sizeof(z_stream))) != Z_OK ) + return UnZip::ZlibError; + + int szDecomp; + + // Decompress until deflate stream ends or end of file + do { + read = device->read(buffer1, cur < rep ? UNZIP_READ_BUFFER : rem); + if (!read) + break; + + if (read < 0) { + (void)inflateEnd(&zstr); + return UnZip::ReadFailed; + } + + if (isEncrypted) + decryptBytes(*keys, buffer1, read); + + cur++; + + zstr.avail_in = (uInt) read; + zstr.next_in = (Bytef*) buffer1; + + // Run inflate() on input until output buffer not full + do { + zstr.avail_out = UNZIP_READ_BUFFER; + zstr.next_out = (Bytef*) buffer2;; + + zret = inflate(&zstr, Z_NO_FLUSH); + + switch (zret) { + case Z_NEED_DICT: + case Z_DATA_ERROR: + case Z_MEM_ERROR: + inflateEnd(&zstr); + return UnZip::WriteFailed; + default: + ; + } + + szDecomp = UNZIP_READ_BUFFER - zstr.avail_out; + if (!verify) { + if (outDev->write(buffer2, szDecomp) != szDecomp) { + inflateEnd(&zstr); + return UnZip::ZlibError; + } + } + + myCRC = crc32(myCRC, (const Bytef*) buffer2, szDecomp); + + } while (zstr.avail_out == 0); + + } while (zret != Z_STREAM_END); + + inflateEnd(&zstr); + return UnZip::Ok; +} + +//! \internal \p outDev is null if the VerifyOnly option is set +UnZip::ErrorCode UnzipPrivate::extractFile(const QString& path, const ZipEntryP& entry, + QIODevice* outDev, UnZip::ExtractionOptions options) +{ + const bool verify = (options & UnZip::VerifyOnly); + + Q_UNUSED(options); + Q_ASSERT(device); + Q_ASSERT(verify ? true : outDev != 0); + + if (!entry.lhEntryChecked) { + UnZip::ErrorCode ec = parseLocalHeaderRecord(path, entry); + entry.lhEntryChecked = true; + if (ec != UnZip::Ok) + return ec; + } + + if (!device->seek(entry.dataOffset)) + return UnZip::SeekFailed; + + // Encryption keys + quint32 keys[3]; + quint32 szComp = entry.szComp; + if (entry.isEncrypted()) { + UnZip::ErrorCode e = testPassword(keys, path, entry); + if (e != UnZip::Ok) + { + qDebug() << QString("Unable to decrypt %1").arg(path); + return e; + }//! Encryption header size + szComp -= UNZIP_LOCAL_ENC_HEADER_SIZE; // remove encryption header size + } + + if (szComp == 0) { + if (entry.crc != 0) + return UnZip::Corrupted; + return UnZip::Ok; + } + + quint32 myCRC = crc32(0L, Z_NULL, 0); + quint32* k = keys; + + UnZip::ErrorCode ec = UnZip::Ok; + if (entry.compMethod == 0) { + ec = extractStoredFile(szComp, entry.isEncrypted() ? &k : 0, myCRC, outDev, options); + } else if (entry.compMethod == 8) { + ec = inflateFile(szComp, entry.isEncrypted() ? &k : 0, myCRC, outDev, options); + } + + if (ec == UnZip::Ok && myCRC != entry.crc) + return UnZip::Corrupted; + + return UnZip::Ok; +} + +//! \internal Creates a new directory and all the needed parent directories. +bool UnzipPrivate::createDirectory(const QString& path) +{ + QDir d(path); + if (!d.exists() && !d.mkpath(path)) { + qDebug() << QString("Unable to create directory: %1").arg(path); + return false; + } + + return true; +} + +/*! + \internal Reads an quint32 (4 bytes) from a byte array starting at given offset. +*/ +quint32 UnzipPrivate::getULong(const unsigned char* data, quint32 offset) const +{ + quint32 res = (quint32) data[offset]; + res |= (((quint32)data[offset+1]) << 8); + res |= (((quint32)data[offset+2]) << 16); + res |= (((quint32)data[offset+3]) << 24); + + return res; +} + +/*! + \internal Reads an quint64 (8 bytes) from a byte array starting at given offset. +*/ +quint64 UnzipPrivate::getULLong(const unsigned char* data, quint32 offset) const +{ + quint64 res = (quint64) data[offset]; + res |= (((quint64)data[offset+1]) << 8); + res |= (((quint64)data[offset+2]) << 16); + res |= (((quint64)data[offset+3]) << 24); + res |= (((quint64)data[offset+1]) << 32); + res |= (((quint64)data[offset+2]) << 40); + res |= (((quint64)data[offset+3]) << 48); + res |= (((quint64)data[offset+3]) << 56); + + return res; +} + +/*! + \internal Reads an quint16 (2 bytes) from a byte array starting at given offset. +*/ +quint16 UnzipPrivate::getUShort(const unsigned char* data, quint32 offset) const +{ + return (quint16) data[offset] | (((quint16)data[offset+1]) << 8); +} + +/*! + \internal Return the next byte in the pseudo-random sequence + */ +int UnzipPrivate::decryptByte(quint32 key2) const +{ + quint16 temp = ((quint16)(key2) & 0xffff) | 2; + return (int)(((temp * (temp ^ 1)) >> 8) & 0xff); +} + +/*! + \internal Update the encryption keys with the next byte of plain text + */ +void UnzipPrivate::updateKeys(quint32* keys, int c) const +{ + keys[0] = CRC32(keys[0], c); + keys[1] += keys[0] & 0xff; + keys[1] = keys[1] * 134775813L + 1; + keys[2] = CRC32(keys[2], ((int)keys[1]) >> 24); +} + +/*! + \internal Initialize the encryption keys and the random header according to + the given password. + */ +void UnzipPrivate::initKeys(const QString& pwd, quint32* keys) const +{ + keys[0] = 305419896L; + keys[1] = 591751049L; + keys[2] = 878082192L; + + QByteArray pwdBytes = pwd.toLatin1(); + int sz = pwdBytes.size(); + const char* ascii = pwdBytes.data(); + + for (int i = 0; i < sz; ++i) + updateKeys(keys, (int)ascii[i]); +} + +/*! + \internal Attempts to test a password without actually extracting a file. + The \p file parameter can be used in the user interface or for debugging purposes + as it is the name of the encrypted file for wich the password is being tested. +*/ +UnZip::ErrorCode UnzipPrivate::testPassword(quint32* keys, const QString&_file, const ZipEntryP& header) +{ + Q_UNUSED(_file); + Q_ASSERT(device); + + // read encryption keys + if (device->read(buffer1, 12) != 12) + return UnZip::Corrupted; + + // Replace this code if you want to i.e. call some dialog and ask the user for a password + initKeys(password, keys); + if (testKeys(header, keys)) + return UnZip::Ok; + + return UnZip::Skip; +} + +/*! + \internal Tests a set of keys on the encryption header. +*/ +bool UnzipPrivate::testKeys(const ZipEntryP& header, quint32* keys) +{ + char lastByte; + + // decrypt encryption header + for (int i = 0; i < 11; ++i) + updateKeys(keys, lastByte = buffer1[i] ^ decryptByte(keys[2])); + updateKeys(keys, lastByte = buffer1[11] ^ decryptByte(keys[2])); + + // if there is an extended header (bit in the gp flag) buffer[11] is a byte from the file time + // with no extended header we have to check the crc high-order byte + char c = ((header.gpFlag[0] & 0x08) == 8) ? header.modTime[1] : header.crc >> 24; + + return (lastByte == c); +} + +/*! + \internal Decrypts an array of bytes long \p read. +*/ +void UnzipPrivate::decryptBytes(quint32* keys, char* buffer, qint64 read) +{ + for (int i = 0; i < (int)read; ++i) + updateKeys(keys, buffer[i] ^= decryptByte(keys[2])); +} + +/*! + \internal Converts date and time values from ZIP format to a QDateTime object. +*/ +QDateTime UnzipPrivate::convertDateTime(const unsigned char date[2], const unsigned char time[2]) const +{ + QDateTime dt; + + // Usual PKZip low-byte to high-byte order + + // Date: 7 bits = years from 1980, 4 bits = month, 5 bits = day + quint16 year = (date[1] >> 1) & 127; + quint16 month = ((date[1] << 3) & 14) | ((date[0] >> 5) & 7); + quint16 day = date[0] & 31; + + // Time: 5 bits hour, 6 bits minutes, 5 bits seconds with a 2sec precision + quint16 hour = (time[1] >> 3) & 31; + quint16 minutes = ((time[1] << 3) & 56) | ((time[0] >> 5) & 7); + quint16 seconds = (time[0] & 31) * 2; + + dt.setDate(QDate(1980 + year, month, day)); + dt.setTime(QTime(hour, minutes, seconds)); + return dt; +} + + +/************************************************************************ + Public interface +*************************************************************************/ + +/*! + Creates a new Zip file decompressor. +*/ +UnZip::UnZip() : d(new UnzipPrivate) +{ +} + +/*! + Closes any open archive and releases used resources. +*/ +UnZip::~UnZip() +{ + closeArchive(); + delete d; +} + +/*! + Returns true if there is an open archive. +*/ +bool UnZip::isOpen() const +{ + return d->device; +} + +/*! + Opens a zip archive and reads the files list. Closes any previously opened archive. +*/ +UnZip::ErrorCode UnZip::openArchive(const QString& filename) +{ + closeArchive(); + + // closeArchive will destroy the file + d->file = new QFile(filename); + + if (!d->file->exists()) { + delete d->file; + d->file = 0; + return UnZip::FileNotFound; + } + + if (!d->file->open(QIODevice::ReadOnly)) { + delete d->file; + d->file = 0; + return UnZip::OpenFailed; + } + + return d->openArchive(d->file); +} + +/*! + Opens a zip archive and reads the entries list. + Closes any previously opened archive. + \warning The class takes DOES NOT take ownership of the device. +*/ +UnZip::ErrorCode UnZip::openArchive(QIODevice* device) +{ + closeArchive(); + + if (!device) { + qDebug() << "Invalid device."; + return UnZip::InvalidDevice; + } + + return d->openArchive(device); +} + +/*! + Closes the archive and releases all the used resources (like cached passwords). +*/ +void UnZip::closeArchive() +{ + d->closeArchive(); +} + +QString UnZip::archiveComment() const +{ + return d->comment; +} + +/*! + Returns a locale translated error string for a given error code. +*/ +QString UnZip::formatError(UnZip::ErrorCode c) const +{ + switch (c) + { + case Ok: return QCoreApplication::translate("UnZip", "ZIP operation completed successfully."); break; + case ZlibInit: return QCoreApplication::translate("UnZip", "Failed to initialize or load zlib library."); break; + case ZlibError: return QCoreApplication::translate("UnZip", "zlib library error."); break; + case OpenFailed: return QCoreApplication::translate("UnZip", "Unable to create or open file."); break; + case PartiallyCorrupted: return QCoreApplication::translate("UnZip", "Partially corrupted archive. Some files might be extracted."); break; + case Corrupted: return QCoreApplication::translate("UnZip", "Corrupted archive."); break; + case WrongPassword: return QCoreApplication::translate("UnZip", "Wrong password."); break; + case NoOpenArchive: return QCoreApplication::translate("UnZip", "No archive has been created yet."); break; + case FileNotFound: return QCoreApplication::translate("UnZip", "File or directory does not exist."); break; + case ReadFailed: return QCoreApplication::translate("UnZip", "File read error."); break; + case WriteFailed: return QCoreApplication::translate("UnZip", "File write error."); break; + case SeekFailed: return QCoreApplication::translate("UnZip", "File seek error."); break; + case CreateDirFailed: return QCoreApplication::translate("UnZip", "Unable to create a directory."); break; + case InvalidDevice: return QCoreApplication::translate("UnZip", "Invalid device."); break; + case InvalidArchive: return QCoreApplication::translate("UnZip", "Invalid or incompatible zip archive."); break; + case HeaderConsistencyError: return QCoreApplication::translate("UnZip", "Inconsistent headers. Archive might be corrupted."); break; + default: ; + } + + return QCoreApplication::translate("UnZip", "Unknown error."); +} + +/*! + Returns true if the archive contains a file with the given path and name. +*/ +bool UnZip::contains(const QString& file) const +{ + return d->headers ? d->headers->contains(file) : false; +} + +/*! + Returns complete paths of files and directories in this archive. +*/ +QStringList UnZip::fileList() const +{ + return d->headers ? d->headers->keys() : QStringList(); +} + +/*! + Returns information for each (correctly parsed) entry of this archive. +*/ +QList UnZip::entryList() const +{ + QList list; + if (!d->headers) + return list; + + for (QMap::ConstIterator it = d->headers->constBegin(); + it != d->headers->constEnd(); ++it) { + const ZipEntryP* entry = it.value(); + Q_ASSERT(entry != 0); + + ZipEntry z; + + z.filename = it.key(); + if (!entry->comment.isEmpty()) + z.comment = entry->comment; + z.compressedSize = entry->szComp; + z.uncompressedSize = entry->szUncomp; + z.crc32 = entry->crc; + z.lastModified = d->convertDateTime(entry->modDate, entry->modTime); + + z.compression = entry->compMethod == 0 ? NoCompression : entry->compMethod == 8 ? Deflated : UnknownCompression; + z.type = z.filename.endsWith("/") ? Directory : File; + + z.encrypted = entry->isEncrypted(); + + list.append(z); + } + + return list; +} + +/*! + Extracts the whole archive to a directory. +*/ +UnZip::ErrorCode UnZip::verifyArchive() +{ + return extractAll(QDir(), VerifyOnly); +} + +/*! + Extracts the whole archive to a directory. +*/ +UnZip::ErrorCode UnZip::extractAll(const QString& dirname, ExtractionOptions options) +{ + return extractAll(QDir(dirname), options); +} + +/*! + Extracts the whole archive to a directory. + Stops extraction at the first error. +*/ +UnZip::ErrorCode UnZip::extractAll(const QDir& dir, ExtractionOptions options) +{ + // this should only happen if we didn't call openArchive() yet + if (!d->device) + return NoOpenArchive; + + if (!d->headers) + return Ok; + + ErrorCode ec = Ok; + + QMap::ConstIterator it = d->headers->constBegin(); + const QMap::ConstIterator end = d->headers->constEnd(); + while (it != end) { + ZipEntryP* entry = it.value(); + Q_ASSERT(entry != 0); + if ((entry->isEncrypted()) && d->skipAllEncrypted) { + ++it; + continue; + } + + bool skip = false; + ec = d->extractFile(it.key(), *entry, dir, options); + switch (ec) { + case Corrupted: + qDebug() << "Corrupted entry" << it.key(); + break; + case CreateDirFailed: + break; + case Skip: + skip = true; + break; + case SkipAll: + skip = true; + d->skipAllEncrypted = true; + break; + default: + ; + } + + if (ec != Ok && !skip) { + break; + } + + ++it; + } + + return ec; +} + +/*! + Extracts a single file to a directory. +*/ +UnZip::ErrorCode UnZip::extractFile(const QString& filename, const QString& dirname, ExtractionOptions options) +{ + return extractFile(filename, QDir(dirname), options); +} + +/*! + Extracts a single file to a directory. +*/ +UnZip::ErrorCode UnZip::extractFile(const QString& filename, const QDir& dir, ExtractionOptions options) +{ + if (!d->device) + return NoOpenArchive; + if (!d->headers) + return FileNotFound; + + QMap::Iterator itr = d->headers->find(filename); + if (itr != d->headers->end()) { + ZipEntryP* entry = itr.value(); + Q_ASSERT(entry != 0); + return d->extractFile(itr.key(), *entry, dir, options); + } + + return FileNotFound; +} + +/*! + Extracts a single file to a directory. +*/ +UnZip::ErrorCode UnZip::extractFile(const QString& filename, QIODevice* outDev, ExtractionOptions options) +{ + if (!d->device) + return NoOpenArchive; + if (!d->headers) + return FileNotFound; + if (!outDev) + return InvalidDevice; + + QMap::Iterator itr = d->headers->find(filename); + if (itr != d->headers->end()) { + ZipEntryP* entry = itr.value(); + Q_ASSERT(entry != 0); + return d->extractFile(itr.key(), *entry, outDev, options); + } + + return FileNotFound; +} + +/*! + Extracts a list of files. + Stops extraction at the first error (but continues if a file does not exist in the archive). + */ +UnZip::ErrorCode UnZip::extractFiles(const QStringList& filenames, const QString& dirname, ExtractionOptions options) +{ + if (!d->device) + return NoOpenArchive; + if (!d->headers) + return Ok; + + QDir dir(dirname); + ErrorCode ec; + + for (QStringList::ConstIterator itr = filenames.constBegin(); itr != filenames.constEnd(); ++itr) { + ec = extractFile(*itr, dir, options); + if (ec == FileNotFound) + continue; + if (ec != Ok) + return ec; + } + + return Ok; +} + +/*! + Extracts a list of files. + Stops extraction at the first error (but continues if a file does not exist in the archive). + */ +UnZip::ErrorCode UnZip::extractFiles(const QStringList& filenames, const QDir& dir, ExtractionOptions options) +{ + if (!d->device) + return NoOpenArchive; + if (!d->headers) + return Ok; + + ErrorCode ec; + + for (QStringList::ConstIterator itr = filenames.constBegin(); itr != filenames.constEnd(); ++itr) { + ec = extractFile(*itr, dir, options); + if (ec == FileNotFound) + continue; + if (ec != Ok) + return ec; + } + + return Ok; +} + +/*! + Remove/replace this method to add your own password retrieval routine. +*/ +void UnZip::setPassword(const QString& pwd) +{ + d->password = pwd; +} + +OSDAB_END_NAMESPACE diff --git a/oracle/src/zip/unzip.h b/oracle/src/zip/unzip.h new file mode 100644 index 000000000..ab57fdc36 --- /dev/null +++ b/oracle/src/zip/unzip.h @@ -0,0 +1,155 @@ +/**************************************************************************** +** Filename: unzip.h +** Last updated [dd/mm/yyyy]: 27/03/2011 +** +** pkzip 2.0 decompression. +** +** Some of the code has been inspired by other open source projects, +** (mainly Info-Zip and Gilles Vollant's minizip). +** Compression and decompression actually uses the zlib library. +** +** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. +** +** This file is part of the OSDaB project (http://osdab.42cows.org/). +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See the file LICENSE.GPL that came with this software distribution or +** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. +** +**********************************************************************/ + +#ifndef OSDAB_UNZIP__H +#define OSDAB_UNZIP__H + +#include "zipglobal.h" + +#include +#include +#include +#include + +class QDir; +class QFile; +class QIODevice; +class QString; + +OSDAB_BEGIN_NAMESPACE(Zip) + +class UnzipPrivate; + +class OSDAB_ZIP_EXPORT UnZip +{ +public: + enum ErrorCode + { + Ok, + ZlibInit, + ZlibError, + OpenFailed, + PartiallyCorrupted, + Corrupted, + WrongPassword, + NoOpenArchive, + FileNotFound, + ReadFailed, + WriteFailed, + SeekFailed, + CreateDirFailed, + InvalidDevice, + InvalidArchive, + HeaderConsistencyError, + + Skip, + SkipAll // internal use only + }; + + enum ExtractionOption + { + ExtractPaths = 0x0001, + SkipPaths = 0x0002, + VerifyOnly = 0x0004, + NoSilentDirectoryCreation = 0x0008 + }; + Q_DECLARE_FLAGS(ExtractionOptions, ExtractionOption) + + enum CompressionMethod + { + NoCompression, + Deflated, + UnknownCompression + }; + + enum FileType + { + File, + Directory + }; + + struct ZipEntry + { + ZipEntry(); + + QString filename; + QString comment; + + quint32 compressedSize; + quint32 uncompressedSize; + quint32 crc32; + + QDateTime lastModified; + + CompressionMethod compression; + FileType type; + + bool encrypted; + }; + + UnZip(); + virtual ~UnZip(); + + bool isOpen() const; + + ErrorCode openArchive(const QString &filename); + ErrorCode openArchive(QIODevice *device); + void closeArchive(); + + QString archiveComment() const; + + QString formatError(UnZip::ErrorCode c) const; + + bool contains(const QString &file) const; + + QStringList fileList() const; + QList entryList() const; + + ErrorCode verifyArchive(); + + ErrorCode extractAll(const QString &dirname, ExtractionOptions options = ExtractPaths); + ErrorCode extractAll(const QDir &dir, ExtractionOptions options = ExtractPaths); + + ErrorCode extractFile(const QString &filename, const QString &dirname, ExtractionOptions options = ExtractPaths); + ErrorCode extractFile(const QString &filename, const QDir &dir, ExtractionOptions options = ExtractPaths); + ErrorCode extractFile(const QString &filename, QIODevice *device, ExtractionOptions options = ExtractPaths); + + ErrorCode + extractFiles(const QStringList &filenames, const QString &dirname, ExtractionOptions options = ExtractPaths); + ErrorCode extractFiles(const QStringList &filenames, const QDir &dir, ExtractionOptions options = ExtractPaths); + + void setPassword(const QString &pwd); + +private: + UnzipPrivate *d; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(UnZip::ExtractionOptions) + +OSDAB_END_NAMESPACE + +#endif // OSDAB_UNZIP__H diff --git a/oracle/src/zip/unzip_p.h b/oracle/src/zip/unzip_p.h new file mode 100755 index 000000000..fca2b071d --- /dev/null +++ b/oracle/src/zip/unzip_p.h @@ -0,0 +1,130 @@ +/**************************************************************************** +** Filename: unzip_p.h +** Last updated [dd/mm/yyyy]: 27/03/2011 +** +** pkzip 2.0 decompression. +** +** Some of the code has been inspired by other open source projects, +** (mainly Info-Zip and Gilles Vollant's minizip). +** Compression and decompression actually uses the zlib library. +** +** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. +** +** This file is part of the OSDaB project (http://osdab.42cows.org/). +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See the file LICENSE.GPL that came with this software distribution or +** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. +** +**********************************************************************/ + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Zip/UnZip API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#ifndef OSDAB_UNZIP_P__H +#define OSDAB_UNZIP_P__H + +#include "unzip.h" +#include "zipentry_p.h" + +#include +#include + +// zLib authors suggest using larger buffers (128K or 256K) for (de)compression (especially for inflate()) +// we use a 256K buffer here - if you want to use this code on a pre-iceage mainframe please change it ;) +#define UNZIP_READ_BUFFER (256*1024) + +OSDAB_BEGIN_NAMESPACE(Zip) + +class UnzipPrivate : public QObject +{ + Q_OBJECT + +public: + UnzipPrivate(); + + // Replace this with whatever else you use to store/retrieve the password. + QString password; + + bool skipAllEncrypted; + + QMap* headers; + + QIODevice* device; + QFile* file; + + char buffer1[UNZIP_READ_BUFFER]; + char buffer2[UNZIP_READ_BUFFER]; + + unsigned char* uBuffer; + const quint32* crcTable; + + // Central Directory (CD) offset + quint32 cdOffset; + // End of Central Directory (EOCD) offset + quint32 eocdOffset; + + // Number of entries in the Central Directory (as to the EOCD record) + quint16 cdEntryCount; + + // The number of detected entries that have been skipped because of a non compatible format + quint16 unsupportedEntryCount; + + QString comment; + + UnZip::ErrorCode openArchive(QIODevice* device); + + UnZip::ErrorCode seekToCentralDirectory(); + UnZip::ErrorCode parseCentralDirectoryRecord(); + UnZip::ErrorCode parseLocalHeaderRecord(const QString& path, const ZipEntryP& entry); + + void closeArchive(); + + UnZip::ErrorCode extractFile(const QString& path, const ZipEntryP& entry, const QDir& dir, UnZip::ExtractionOptions options); + UnZip::ErrorCode extractFile(const QString& path, const ZipEntryP& entry, QIODevice* device, UnZip::ExtractionOptions options); + + UnZip::ErrorCode testPassword(quint32* keys, const QString&_file, const ZipEntryP& header); + bool testKeys(const ZipEntryP& header, quint32* keys); + + bool createDirectory(const QString& path); + + inline void decryptBytes(quint32* keys, char* buffer, qint64 read); + + inline quint32 getULong(const unsigned char* data, quint32 offset) const; + inline quint64 getULLong(const unsigned char* data, quint32 offset) const; + inline quint16 getUShort(const unsigned char* data, quint32 offset) const; + inline int decryptByte(quint32 key2) const; + inline void updateKeys(quint32* keys, int c) const; + inline void initKeys(const QString& pwd, quint32* keys) const; + + inline QDateTime convertDateTime(const unsigned char date[2], const unsigned char time[2]) const; + +private slots: + void deviceDestroyed(QObject*); + +private: + UnZip::ErrorCode extractStoredFile(const quint32 szComp, quint32** keys, + quint32& myCRC, QIODevice* outDev, UnZip::ExtractionOptions options); + UnZip::ErrorCode inflateFile(const quint32 szComp, quint32** keys, + quint32& myCRC, QIODevice* outDev, UnZip::ExtractionOptions options); + void do_closeArchive(); +}; + +OSDAB_END_NAMESPACE + +#endif // OSDAB_UNZIP_P__H diff --git a/oracle/src/zip/zip.cpp b/oracle/src/zip/zip.cpp new file mode 100755 index 000000000..5b0177293 --- /dev/null +++ b/oracle/src/zip/zip.cpp @@ -0,0 +1,1619 @@ +/**************************************************************************** +** Filename: zip.cpp +** Last updated [dd/mm/yyyy]: 01/02/2007 +** +** pkzip 2.0 file compression. +** +** Some of the code has been inspired by other open source projects, +** (mainly Info-Zip and Gilles Vollant's minizip). +** Compression and decompression actually uses the zlib library. +** +** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. +** +** This file is part of the OSDaB project (http://osdab.42cows.org/). +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See the file LICENSE.GPL that came with this software distribution or +** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. +** +**********************************************************************/ + +#include "zip.h" +#include "zip_p.h" +#include "zipentry_p.h" + +// we only use this to seed the random number generator +#include + +#include +#include +#include +#include +#include +#include +#include + +// You can remove this #include if you replace the qDebug() statements. +#include + + +/*! #define OSDAB_ZIP_NO_PNG_RLE to disable the use of Z_RLE compression strategy with + PNG files (achieves slightly better compression levels according to the authors). +*/ +// #define OSDAB_ZIP_NO_PNG_RLE + +#define OSDAB_ZIP_NO_DEBUG + +//! Local header size (including signature, excluding variable length fields) +#define ZIP_LOCAL_HEADER_SIZE 30 +//! Encryption header size +#define ZIP_LOCAL_ENC_HEADER_SIZE 12 +//! Data descriptor size (signature included) +#define ZIP_DD_SIZE_WS 16 +//! Central Directory record size (signature included) +#define ZIP_CD_SIZE 46 +//! End of Central Directory record size (signature included) +#define ZIP_EOCD_SIZE 22 + +// Some offsets inside a local header record (signature included) +#define ZIP_LH_OFF_VERS 4 +#define ZIP_LH_OFF_GPFLAG 6 +#define ZIP_LH_OFF_CMET 8 +#define ZIP_LH_OFF_MODT 10 +#define ZIP_LH_OFF_MODD 12 +#define ZIP_LH_OFF_CRC 14 +#define ZIP_LH_OFF_CSIZE 18 +#define ZIP_LH_OFF_USIZE 22 +#define ZIP_LH_OFF_NAMELEN 26 +#define ZIP_LH_OFF_XLEN 28 + +// Some offsets inside a data descriptor record (including signature) +#define ZIP_DD_OFF_CRC32 4 +#define ZIP_DD_OFF_CSIZE 8 +#define ZIP_DD_OFF_USIZE 12 + +// Some offsets inside a Central Directory record (including signature) +#define ZIP_CD_OFF_MADEBY 4 +#define ZIP_CD_OFF_VERSION 6 +#define ZIP_CD_OFF_GPFLAG 8 +#define ZIP_CD_OFF_CMET 10 +#define ZIP_CD_OFF_MODT 12 +#define ZIP_CD_OFF_MODD 14 +#define ZIP_CD_OFF_CRC 16 +#define ZIP_CD_OFF_CSIZE 20 +#define ZIP_CD_OFF_USIZE 24 +#define ZIP_CD_OFF_NAMELEN 28 +#define ZIP_CD_OFF_XLEN 30 +#define ZIP_CD_OFF_COMMLEN 32 +#define ZIP_CD_OFF_DISKSTART 34 +#define ZIP_CD_OFF_IATTR 36 +#define ZIP_CD_OFF_EATTR 38 +#define ZIP_CD_OFF_LHOFF 42 + +// Some offsets inside a EOCD record (including signature) +#define ZIP_EOCD_OFF_DISKNUM 4 +#define ZIP_EOCD_OFF_CDDISKNUM 6 +#define ZIP_EOCD_OFF_ENTRIES 8 +#define ZIP_EOCD_OFF_CDENTRIES 10 +#define ZIP_EOCD_OFF_CDSIZE 12 +#define ZIP_EOCD_OFF_CDOFF 16 +#define ZIP_EOCD_OFF_COMMLEN 20 + +//! PKZip version for archives created by this API +#define ZIP_VERSION 0x14 + +//! Do not store very small files as the compression headers overhead would be to big +#define ZIP_COMPRESSION_THRESHOLD 60 + +/*! + \class Zip zip.h + + \brief Zip file compression. + + Some quick usage examples. + + \verbatim + Suppose you have this directory structure: + + /home/user/dir1/file1.1 + /home/user/dir1/file1.2 + /home/user/dir1/dir1.1/ + /home/user/dir1/dir1.2/file1.2.1 + + EXAMPLE 1: + myZipInstance.addDirectory("/home/user/dir1"); + + RESULT: + Beheaves like any common zip software and creates a zip file with this structure: + + dir1/file1.1 + dir1/file1.2 + dir1/dir1.1/ + dir1/dir1.2/file1.2.1 + + EXAMPLE 2: + myZipInstance.addDirectory("/home/user/dir1", "myRoot/myFolder"); + + RESULT: + Adds a custom root to the paths and creates a zip file with this structure: + + myRoot/myFolder/dir1/file1.1 + myRoot/myFolder/dir1/file1.2 + myRoot/myFolder/dir1/dir1.1/ + myRoot/myFolder/dir1/dir1.2/file1.2.1 + + EXAMPLE 3: + myZipInstance.addDirectory("/home/user/dir1", Zip::AbsolutePaths); + + NOTE: + Same as calling addDirectory(SOME_PATH, PARENT_PATH_of_SOME_PATH). + + RESULT: + Preserves absolute paths and creates a zip file with this structure: + + /home/user/dir1/file1.1 + /home/user/dir1/file1.2 + /home/user/dir1/dir1.1/ + /home/user/dir1/dir1.2/file1.2.1 + + EXAMPLE 4: + myZipInstance.setPassword("hellopass"); + myZipInstance.addDirectory("/home/user/dir1", "/"); + + RESULT: + Adds and encrypts the files in /home/user/dir1, creating the following zip structure: + + /dir1/file1.1 + /dir1/file1.2 + /dir1/dir1.1/ + /dir1/dir1.2/file1.2.1 + + EXAMPLE 5: + myZipInstance.addDirectory("/home/user/dir1", Zip::IgnoreRoot); + + RESULT: + Adds the files in /home/user/dir1 but doesn't create the top level + directory: + + file1.1 + file1.2 + dir1.1/ + dir1.2/file1.2.1 + + EXAMPLE 5: + myZipInstance.addDirectory("/home/user/dir1", "data/backup", Zip::IgnoreRoot); + + RESULT: + Adds the files in /home/user/dir1 but uses "data/backup" as top level + directory instead of "dir1": + + data/backup/file1.1 + data/backup/file1.2 + data/backup/dir1.1/ + data/backup/dir1.2/file1.2.1 + + \endverbatim +*/ + +/*! \enum Zip::ErrorCode The result of a compression operation. + \value Zip::Ok No error occurred. + \value Zip::ZlibInit Failed to init or load the zlib library. + \value Zip::ZlibError The zlib library returned some error. + \value Zip::FileExists The file already exists and will not be overwritten. + \value Zip::OpenFailed Unable to create or open a device. + \value Zip::NoOpenArchive CreateArchive() has not been called yet. + \value Zip::FileNotFound File or directory does not exist. + \value Zip::ReadFailed Reading of a file failed. + \value Zip::WriteFailed Writing of a file failed. + \value Zip::SeekFailed Seek failed. +*/ + +/*! \enum Zip::CompressionLevel Returns the result of a decompression operation. + \value Zip::Store No compression. + \value Zip::Deflate1 Deflate compression level 1(lowest compression). + \value Zip::Deflate1 Deflate compression level 2. + \value Zip::Deflate1 Deflate compression level 3. + \value Zip::Deflate1 Deflate compression level 4. + \value Zip::Deflate1 Deflate compression level 5. + \value Zip::Deflate1 Deflate compression level 6. + \value Zip::Deflate1 Deflate compression level 7. + \value Zip::Deflate1 Deflate compression level 8. + \value Zip::Deflate1 Deflate compression level 9 (maximum compression). + \value Zip::AutoCPU Adapt compression level to CPU speed (faster CPU => better compression). + \value Zip::AutoMIME Adapt compression level to MIME type of the file being compressed. + \value Zip::AutoFull Use both CPU and MIME type detection. +*/ + +namespace { + +struct ZippedDir { + bool init; + QString actualRoot; + int files; + ZippedDir() : init(false), actualRoot(), files(0) {} +}; + +void checkRootPath(QString& path) +{ + const bool isUnixRoot = path.length() == 1 && path.at(0) == QLatin1Char('/'); + if (!path.isEmpty() && !isUnixRoot) { + while (path.endsWith(QLatin1String("\\"))) + path.truncate(path.length() - 1); + + int sepCount = 0; + for (int i = path.length()-1; i >= 0; --i) { + if (path.at(i) == QLatin1Char('/')) + ++sepCount; + else break; + } + + if (sepCount > 1) + path.truncate(path.length() - (sepCount-1)); + else if (sepCount == 0) + path.append(QLatin1String("/")); + } +} + +} + +////////////////////////////////////////////////////////////////////////// + +OSDAB_BEGIN_NAMESPACE(Zip) + +/************************************************************************ + Private interface +*************************************************************************/ + +//! \internal +ZipPrivate::ZipPrivate() : + headers(0), + device(0), + file(0), + uBuffer(0), + crcTable(0), + comment(), + password() +{ + // keep an unsigned pointer so we avoid to over bloat the code with casts + uBuffer = (unsigned char*) buffer1; + crcTable = get_crc_table(); +} + +//! \internal +ZipPrivate::~ZipPrivate() +{ + closeArchive(); +} + +//! \internal +Zip::ErrorCode ZipPrivate::createArchive(QIODevice* dev) +{ + Q_ASSERT(dev); + + if (device) + closeArchive(); + + device = dev; + if (device != file) + connect(device, SIGNAL(destroyed(QObject*)), this, SLOT(deviceDestroyed(QObject*))); + + if (!device->isOpen()) { + if (!device->open(QIODevice::ReadOnly)) { + delete device; + device = 0; + qDebug() << "Unable to open device for writing."; + return Zip::OpenFailed; + } + } + + headers = new QMap; + return Zip::Ok; +} + +//! \internal +void ZipPrivate::deviceDestroyed(QObject*) +{ + qDebug("Unexpected device destruction detected."); + do_closeArchive(); +} + +/*! Returns true if an entry for \p info has already been added. + Uses file size and lower case absolute path to compare entries. +*/ +bool ZipPrivate::containsEntry(const QFileInfo& info) const +{ + if (!headers || headers->isEmpty()) + return false; + + const qint64 sz = info.size(); + const QString path = info.absoluteFilePath().toLower(); + + QMap::ConstIterator b = headers->constBegin(); + const QMap::ConstIterator e = headers->constEnd(); + while (b != e) { + const ZipEntryP* e = b.value(); + if (e->fileSize == sz && e->absolutePath == path) + return true; + ++b; + } + + return false; +} + +//! \internal Actual implementation of the addDirectory* methods. +Zip::ErrorCode ZipPrivate::addDirectory(const QString& path, const QString& root, + Zip::CompressionOptions options, Zip::CompressionLevel level, int hierarchyLevel, + int* addedFiles) +{ + if (addedFiles) + ++(*addedFiles); + + // Bad boy didn't call createArchive() yet :) + if (!device) + return Zip::NoOpenArchive; + + QDir dir(path); + if (!dir.exists()) + return Zip::FileNotFound; + + // Remove any trailing separator + QString actualRoot = root.trimmed(); + + // Preserve Unix root but make sure the path ends only with a single + // unix like separator + ::checkRootPath(actualRoot); + + // QDir::cleanPath() fixes some issues with QDir::dirName() + QFileInfo current(QDir::cleanPath(path)); + + const bool path_absolute = options.testFlag(Zip::AbsolutePaths); + const bool path_ignore = options.testFlag(Zip::IgnorePaths); + const bool path_noroot = options.testFlag(Zip::IgnoreRoot); + + if (path_absolute && !path_ignore && !path_noroot) { + QString absolutePath = extractRoot(path, options); + if (!absolutePath.isEmpty() && absolutePath != QLatin1String("/")) + absolutePath.append(QLatin1String("/")); + actualRoot.append(absolutePath); + } + + const bool skipDirName = !hierarchyLevel && path_noroot; + if (!path_ignore && !skipDirName) { + actualRoot.append(QDir(current.absoluteFilePath()).dirName()); + actualRoot.append(QLatin1String("/")); + } + + // actualRoot now contains the path of the file relative to the zip archive + // with a trailing / + + const bool skipBad = options & Zip::SkipBadFiles; + const bool noDups = options & Zip::CheckForDuplicates; + + const QDir::Filters dir_filter = + QDir::Files | + QDir::Dirs | + QDir::NoDotAndDotDot | + QDir::NoSymLinks; + const QDir::SortFlags dir_sort = + QDir::DirsFirst; + QFileInfoList list = dir.entryInfoList(dir_filter, dir_sort); + + Zip::ErrorCode ec = Zip::Ok; + bool filesAdded = false; + + Zip::CompressionOptions recursionOptions; + if (path_ignore) + recursionOptions |= Zip::IgnorePaths; + else recursionOptions |= Zip::RelativePaths; + + for (int i = 0; i < list.size(); ++i) { + QFileInfo info = list.at(i); + const QString absPath = info.absoluteFilePath(); + if (noDups && containsEntry(info)) + continue; + if (info.isDir()) { + // Recursion + ec = addDirectory(absPath, actualRoot, recursionOptions, + level, hierarchyLevel + 1, addedFiles); + } else { + ec = createEntry(info, actualRoot, level); + if (ec == Zip::Ok) { + filesAdded = true; + if (addedFiles) + ++(*addedFiles); + } + } + + if (ec != Zip::Ok && !skipBad) { + break; + } + } + + // We need an explicit record for this dir + // Non-empty directories don't need it because they have a path component in the filename + if (!filesAdded && !path_ignore) + ec = createEntry(current, actualRoot, level); + + return ec; +} + +//! \internal Actual implementation of the addFile methods. +Zip::ErrorCode ZipPrivate::addFiles(const QStringList& files, const QString& root, + Zip::CompressionOptions options, Zip::CompressionLevel level, + int* addedFiles) +{ + if (addedFiles) + *addedFiles = 0; + + const bool skipBad = options & Zip::SkipBadFiles; + const bool noDups = options & Zip::CheckForDuplicates; + + // Bad boy didn't call createArchive() yet :) + if (!device) + return Zip::NoOpenArchive; + + QFileInfoList paths; + paths.reserve(files.size()); + for (int i = 0; i < files.size(); ++i) { + QFileInfo info(files.at(i)); + if (noDups && (paths.contains(info) || containsEntry(info))) + continue; + if (!info.exists() || !info.isReadable()) { + if (skipBad) { + continue; + } else { + return Zip::FileNotFound; + } + } + paths.append(info); + } + + if (paths.isEmpty()) + return Zip::Ok; + + // Remove any trailing separator + QString actualRoot = root.trimmed(); + + // Preserve Unix root but make sure the path ends only with a single + // unix like separator + ::checkRootPath(actualRoot); + + const bool path_absolute = options.testFlag(Zip::AbsolutePaths); + const bool path_ignore = options.testFlag(Zip::IgnorePaths); + const bool path_noroot = options.testFlag(Zip::IgnoreRoot); + + Zip::ErrorCode ec = Zip::Ok; + QHash dirMap; + + for (int i = 0; i < paths.size(); ++i) { + const QFileInfo& info = paths.at(i); + const QString path = QFileInfo(QDir::cleanPath(info.absolutePath())).absolutePath(); + + ZippedDir& zd = dirMap[path]; + if (!zd.init) { + zd.init = true; + zd.actualRoot = actualRoot; + if (path_absolute && !path_ignore && !path_noroot) { + QString absolutePath = extractRoot(path, options); + if (!absolutePath.isEmpty() && absolutePath != QLatin1String("/")) + absolutePath.append(QLatin1String("/")); + zd.actualRoot.append(absolutePath); + } + + if (!path_ignore && !path_noroot) { + zd.actualRoot.append(QDir(path).dirName()); + zd.actualRoot.append(QLatin1String("/")); + } + } + + // zd.actualRoot now contains the path of the file relative to the zip archive + // with a trailing / + + if (info.isDir()) { + // Recursion + ec = addDirectory(info.absoluteFilePath(), actualRoot, options, + level, 1, addedFiles); + } else { + ec = createEntry(info, actualRoot, level); + if (ec == Zip::Ok) { + ++zd.files; + if (addedFiles) + ++(*addedFiles); + } + } + + if (ec != Zip::Ok && !skipBad) { + break; + } + } + + // Create explicit records for empty directories + if (!path_ignore) { + QHash::ConstIterator b = dirMap.constBegin(); + const QHash::ConstIterator e = dirMap.constEnd(); + while (b != e) { + const ZippedDir& zd = b.value(); + if (zd.files <= 0) { + ec = createEntry(b.key(), zd.actualRoot, level); + } + ++b; + } + } + + return ec; +} + +//! \internal \p file must be a file and not a directory. +Zip::ErrorCode ZipPrivate::deflateFile(const QFileInfo& fileInfo, + quint32& crc, qint64& written, const Zip::CompressionLevel& level, quint32** keys) +{ + const QString path = fileInfo.absoluteFilePath(); + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + qDebug() << QString("An error occurred while opening %1").arg(path); + return Zip::OpenFailed; + } + + const Zip::ErrorCode ec = (level == Zip::Store) + ? storeFile(path, file, crc, written, keys) + : compressFile(path, file, crc, written, level, keys); + + file.close(); + return ec; +} + +//! \internal +Zip::ErrorCode ZipPrivate::storeFile(const QString& path, QIODevice& file, + quint32& crc, qint64& totalWritten, quint32** keys) +{ + Q_UNUSED(path); + + qint64 read = 0; + qint64 written = 0; + + const bool encrypt = keys != 0; + + totalWritten = 0; + crc = crc32(0L, Z_NULL, 0); + + while ( (read = file.read(buffer1, ZIP_READ_BUFFER)) > 0 ) { + crc = crc32(crc, uBuffer, read); + if (encrypt) + encryptBytes(*keys, buffer1, read); + written = device->write(buffer1, read); + totalWritten += written; + if (written != read) { + return Zip::WriteFailed; + } + } + + return Zip::Ok; +} + +//! \internal +int ZipPrivate::compressionStrategy(const QString& path, QIODevice& file) const +{ + Q_UNUSED(file); + +#ifndef OSDAB_ZIP_NO_PNG_RLE + return Z_DEFAULT_STRATEGY; +#endif + const bool isPng = path.endsWith(QLatin1String("png"), Qt::CaseInsensitive); + return isPng ? Z_RLE : Z_DEFAULT_STRATEGY; +} + +//! \internal +Zip::ErrorCode ZipPrivate::compressFile(const QString& path, QIODevice& file, + quint32& crc, qint64& totalWritten, const Zip::CompressionLevel& level, quint32** keys) +{ + qint64 read = 0; + qint64 written = 0; + + qint64 totRead = 0; + qint64 toRead = file.size(); + + const bool encrypt = keys != 0; + const int strategy = compressionStrategy(path, file); + + totalWritten = 0; + crc = crc32(0L, Z_NULL, 0); + + z_stream zstr; + + // Initialize zalloc, zfree and opaque before calling the init function + zstr.zalloc = Z_NULL; + zstr.zfree = Z_NULL; + zstr.opaque = Z_NULL; + + int zret; + + // Use deflateInit2 with negative windowBits to get raw compression + if ((zret = deflateInit2_( + &zstr, + (int)level, // compression level + Z_DEFLATED, // method + -MAX_WBITS, // windowBits + 8, // memLevel + strategy, + ZLIB_VERSION, + sizeof(z_stream) + )) != Z_OK ) { + qDebug() << "Could not initialize zlib for compression"; + return Zip::ZlibError; + } + + qint64 compressed; + int flush = Z_NO_FLUSH; + do { + read = file.read(buffer1, ZIP_READ_BUFFER); + totRead += read; + if (!read) + break; + + if (read < 0) { + deflateEnd(&zstr); + qDebug() << QString("Error while reading %1").arg(path); + return Zip::ReadFailed; + } + + crc = crc32(crc, uBuffer, read); + + zstr.next_in = (Bytef*) buffer1; + zstr.avail_in = (uInt)read; + + // Tell zlib if this is the last chunk we want to encode + // by setting the flush parameter to Z_FINISH + flush = (totRead == toRead) ? Z_FINISH : Z_NO_FLUSH; + + // Run deflate() on input until output buffer not full + // finish compression if all of source has been read in + do { + zstr.next_out = (Bytef*) buffer2; + zstr.avail_out = ZIP_READ_BUFFER; + + zret = deflate(&zstr, flush); + // State not clobbered + Q_ASSERT(zret != Z_STREAM_ERROR); + + // Write compressed data to file and empty buffer + compressed = ZIP_READ_BUFFER - zstr.avail_out; + + if (encrypt) + encryptBytes(*keys, buffer2, compressed); + + written = device->write(buffer2, compressed); + totalWritten += written; + + if (written != compressed) { + deflateEnd(&zstr); + qDebug() << QString("Error while writing %1").arg(path); + return Zip::WriteFailed; + } + + } while (zstr.avail_out == 0); + + // All input will be used + Q_ASSERT(zstr.avail_in == 0); + + } while (flush != Z_FINISH); + + // Stream will be complete + Q_ASSERT(zret == Z_STREAM_END); + deflateEnd(&zstr); + + return Zip::Ok; +} + +//! \internal Writes a new entry in the zip file. +Zip::ErrorCode ZipPrivate::createEntry(const QFileInfo& file, const QString& root, + Zip::CompressionLevel level) +{ + const bool dirOnly = file.isDir(); + + // entryName contains the path as it should be written + // in the zip file records + const QString entryName = dirOnly + ? root + : root + file.fileName(); + + // Directory entry + if (dirOnly || file.size() < ZIP_COMPRESSION_THRESHOLD) { + level = Zip::Store; + } else { + switch (level) { + case Zip::AutoCPU: + level = Zip::Deflate5; +#ifndef OSDAB_ZIP_NO_DEBUG + qDebug("Compression level for '%s': %d", entryName.toLatin1().constData(), (int)level); +#endif + break; + case Zip::AutoMIME: + level = detectCompressionByMime(file.completeSuffix().toLower()); +#ifndef OSDAB_ZIP_NO_DEBUG + qDebug("Compression level for '%s': %d", entryName.toLatin1().constData(), (int)level); +#endif + break; + case Zip::AutoFull: + level = detectCompressionByMime(file.completeSuffix().toLower()); +#ifndef OSDAB_ZIP_NO_DEBUG + qDebug("Compression level for '%s': %d", entryName.toLatin1().constData(), (int)level); +#endif + break; + default: ; + } + } + + + + // create header and store it to write a central directory later + QScopedPointer h(new ZipEntryP); + h->absolutePath = file.absoluteFilePath().toLower(); + h->fileSize = file.size(); + + // Set encryption bit and set the data descriptor bit + // so we can use mod time instead of crc for password check + bool encrypt = !dirOnly && !password.isEmpty(); + if (encrypt) + h->gpFlag[0] |= 9; + + QDateTime dt = file.lastModified(); + dt = OSDAB_ZIP_MANGLE(fromFileTimestamp)(dt); + QDate d = dt.date(); + h->modDate[1] = ((d.year() - 1980) << 1) & 254; + h->modDate[1] |= ((d.month() >> 3) & 1); + h->modDate[0] = ((d.month() & 7) << 5) & 224; + h->modDate[0] |= d.day(); + + QTime t = dt.time(); + h->modTime[1] = (t.hour() << 3) & 248; + h->modTime[1] |= ((t.minute() >> 3) & 7); + h->modTime[0] = ((t.minute() & 7) << 5) & 224; + h->modTime[0] |= t.second() / 2; + + h->szUncomp = dirOnly ? 0 : file.size(); + + h->compMethod = (level == Zip::Store) ? 0 : 0x0008; + + // **** Write local file header **** + + // signature + buffer1[0] = 'P'; buffer1[1] = 'K'; + buffer1[2] = 0x3; buffer1[3] = 0x4; + + // version needed to extract + buffer1[ZIP_LH_OFF_VERS] = ZIP_VERSION; + buffer1[ZIP_LH_OFF_VERS + 1] = 0; + + // general purpose flag + buffer1[ZIP_LH_OFF_GPFLAG] = h->gpFlag[0]; + buffer1[ZIP_LH_OFF_GPFLAG + 1] = h->gpFlag[1]; + + // compression method + buffer1[ZIP_LH_OFF_CMET] = h->compMethod & 0xFF; + buffer1[ZIP_LH_OFF_CMET + 1] = (h->compMethod>>8) & 0xFF; + + // last mod file time + buffer1[ZIP_LH_OFF_MODT] = h->modTime[0]; + buffer1[ZIP_LH_OFF_MODT + 1] = h->modTime[1]; + + // last mod file date + buffer1[ZIP_LH_OFF_MODD] = h->modDate[0]; + buffer1[ZIP_LH_OFF_MODD + 1] = h->modDate[1]; + + // skip crc (4bytes) [14,15,16,17] + + // skip compressed size but include evtl. encryption header (4bytes: [18,19,20,21]) + buffer1[ZIP_LH_OFF_CSIZE] = + buffer1[ZIP_LH_OFF_CSIZE + 1] = + buffer1[ZIP_LH_OFF_CSIZE + 2] = + buffer1[ZIP_LH_OFF_CSIZE + 3] = 0; + + h->szComp = encrypt ? ZIP_LOCAL_ENC_HEADER_SIZE : 0; + + // uncompressed size [22,23,24,25] + setULong(h->szUncomp, buffer1, ZIP_LH_OFF_USIZE); + + // filename length + QByteArray entryNameBytes = entryName.toLatin1(); + int sz = entryNameBytes.size(); + + buffer1[ZIP_LH_OFF_NAMELEN] = sz & 0xFF; + buffer1[ZIP_LH_OFF_NAMELEN + 1] = (sz >> 8) & 0xFF; + + // extra field length + buffer1[ZIP_LH_OFF_XLEN] = buffer1[ZIP_LH_OFF_XLEN + 1] = 0; + + // Store offset to write crc and compressed size + h->lhOffset = device->pos(); + quint32 crcOffset = h->lhOffset + ZIP_LH_OFF_CRC; + + if (device->write(buffer1, ZIP_LOCAL_HEADER_SIZE) != ZIP_LOCAL_HEADER_SIZE) { + return Zip::WriteFailed; + } + + // Write out filename + if (device->write(entryNameBytes) != sz) { + return Zip::WriteFailed; + } + + // Encryption keys + quint32 keys[3] = { 0, 0, 0 }; + + if (encrypt) { + // **** encryption header **** + + // XOR with PI to ensure better random numbers + // with poorly implemented rand() as suggested by Info-Zip + srand(time(NULL) ^ 3141592654UL); + int randByte; + + initKeys(keys); + for (int i = 0; i < 10; ++i) { + randByte = (rand() >> 7) & 0xff; + buffer1[i] = decryptByte(keys[2]) ^ randByte; + updateKeys(keys, randByte); + } + + // Encrypt encryption header + initKeys(keys); + for (int i = 0; i < 10; ++i) { + randByte = decryptByte(keys[2]); + updateKeys(keys, buffer1[i]); + buffer1[i] ^= randByte; + } + + // We don't know the CRC at this time, so we use the modification time + // as the last two bytes + randByte = decryptByte(keys[2]); + updateKeys(keys, h->modTime[0]); + buffer1[10] ^= randByte; + + randByte = decryptByte(keys[2]); + updateKeys(keys, h->modTime[1]); + buffer1[11] ^= randByte; + + // Write out encryption header + if (device->write(buffer1, ZIP_LOCAL_ENC_HEADER_SIZE) != ZIP_LOCAL_ENC_HEADER_SIZE) { + return Zip::WriteFailed; + } + } + + quint32 crc = 0; + qint64 written = 0; + + if (!dirOnly) { + quint32* k = keys; + const Zip::ErrorCode ec = deflateFile(file, crc, written, level, encrypt ? &k : 0); + if (ec != Zip::Ok) + return ec; + Q_ASSERT(!h.isNull()); + } + + // Store end of entry offset + quint32 current = device->pos(); + + // Update crc and compressed size in local header + if (!device->seek(crcOffset)) { + return Zip::SeekFailed; + } + + h->crc = dirOnly ? 0 : crc; + h->szComp += written; + + setULong(h->crc, buffer1, 0); + setULong(h->szComp, buffer1, 4); + if ( device->write(buffer1, 8) != 8) { + return Zip::WriteFailed; + } + + // Seek to end of entry + if (!device->seek(current)) { + return Zip::SeekFailed; + } + + if ((h->gpFlag[0] & 8) == 8) { + // Write data descriptor + + // Signature: PK\7\8 + buffer1[0] = 'P'; + buffer1[1] = 'K'; + buffer1[2] = 0x07; + buffer1[3] = 0x08; + + // CRC + setULong(h->crc, buffer1, ZIP_DD_OFF_CRC32); + + // Compressed size + setULong(h->szComp, buffer1, ZIP_DD_OFF_CSIZE); + + // Uncompressed size + setULong(h->szUncomp, buffer1, ZIP_DD_OFF_USIZE); + + if (device->write(buffer1, ZIP_DD_SIZE_WS) != ZIP_DD_SIZE_WS) { + return Zip::WriteFailed; + } + } + + headers->insert(entryName, h.take()); + return Zip::Ok; +} + +//! \internal +int ZipPrivate::decryptByte(quint32 key2) const +{ + quint16 temp = ((quint16)(key2) & 0xffff) | 2; + return (int)(((temp * (temp ^ 1)) >> 8) & 0xff); +} + +//! \internal Writes an quint32 (4 bytes) to a byte array at given offset. +void ZipPrivate::setULong(quint32 v, char* buffer, unsigned int offset) +{ + buffer[offset+3] = ((v >> 24) & 0xFF); + buffer[offset+2] = ((v >> 16) & 0xFF); + buffer[offset+1] = ((v >> 8) & 0xFF); + buffer[offset] = (v & 0xFF); +} + +//! \internal Initializes decryption keys using a password. +void ZipPrivate::initKeys(quint32* keys) const +{ + // Encryption keys initialization constants are taken from the + // PKZip file format specification docs + keys[0] = 305419896L; + keys[1] = 591751049L; + keys[2] = 878082192L; + + QByteArray pwdBytes = password.toLatin1(); + int sz = pwdBytes.size(); + const char* ascii = pwdBytes.data(); + + for (int i = 0; i < sz; ++i) + updateKeys(keys, (int)ascii[i]); +} + +//! Updates a one-char-only CRC; it's the Info-Zip macro re-adapted. +quint32 ZipPrivate::updateChecksum(const quint32& crc, const quint32& val) const +{ + return quint32(crcTable[quint32(crc^val) & 0xff] ^ crc_t(crc >> 8)); +} + +//! \internal Updates encryption keys. +void ZipPrivate::updateKeys(quint32* keys, int c) const +{ + keys[0] = updateChecksum(keys[0], c); + keys[1] += keys[0] & 0xff; + keys[1] = keys[1] * 134775813L + 1; + keys[2] = updateChecksum(keys[2], ((int)keys[1]) >> 24); +} + +//! \internal Encrypts a byte array. +void ZipPrivate::encryptBytes(quint32* keys, char* buffer, qint64 read) +{ + char t; + + for (qint64 i = 0; i < read; ++i) { + t = buffer[i]; + buffer[i] ^= decryptByte(keys[2]); + updateKeys(keys, t); + } +} + +namespace { +struct KeywordHelper { + const QString needle; + inline KeywordHelper(const QString& keyword) : needle(keyword) {} +}; + +bool operator<(const KeywordHelper& helper, const char* keyword) { + return helper.needle.compare(QLatin1String(keyword)) < 0; +} + +bool operator<(const char* keyword, const KeywordHelper& helper) { + return helper.needle.compare(QLatin1String(keyword)) > 0; +} + +bool hasExtension(const QString& ext, const char* const* map, int max) { + const char* const* start = &map[0]; + const char* const* end = &map[max - 1]; + const char* const* kw = qBinaryFind(start, end, KeywordHelper(ext)); + return kw != end; +} +} + +//! \internal Detects the best compression level for a given file extension. +Zip::CompressionLevel ZipPrivate::detectCompressionByMime(const QString& ext) +{ + // NOTE: Keep the MAX_* and the number of strings in the map up to date. + // NOTE: Alphabetically sort the strings in the map -- we use a binary search! + + // Archives or files that will hardly compress + const int MAX_EXT1 = 14; + const char* const ext1[MAX_EXT1] = { + "7z", "bin", "deb", "exe", "gz", "gz2", "jar", "rar", "rpm", "tar", "tgz", "z", "zip", + 0 // # MAX_EXT1 + }; + + // Slow or usually large files that we should not spend to much time with + const int MAX_EXT2 = 24; + const char* const ext2[MAX_EXT2] = { + "asf", + "avi", + "divx", + "doc", + "docx", + "flv", + "gif", + "iso", + "jpg", + "jpeg", + "mka", + "mkv", + "mp3", + "mp4", + "mpeg", + "mpg", + "odt", + "ogg", + "ogm", + "ra", + "rm", + "wma", + "wmv", + 0 // # MAX_EXT2 + }; + + // Files with high compression ratio + const int MAX_EXT3 = 28; + const char* const ext3[MAX_EXT3] = { + "asp", "bat", "c", "conf", "cpp", "cpp", "css", "csv", "cxx", "h", "hpp", "htm", "html", "hxx", + "ini", "js", "php", "pl", "py", "rtf", "sh", "tsv", "txt", "vb", "vbs", "xml", "xst", + 0 // # MAX_EXT3 + }; + + const char* const* map = ext1; + if (hasExtension(ext, map, MAX_EXT1)) + return Zip::Store; + + map = ext2; + if (hasExtension(ext, map, MAX_EXT2)) + return Zip::Deflate2; + + map = ext3; + if (hasExtension(ext, map, MAX_EXT3)) + return Zip::Deflate9; + + return Zip::Deflate5; +} + +/*! + Closes the current archive and writes out pending data. +*/ +Zip::ErrorCode ZipPrivate::closeArchive() +{ + if (!device) { + Q_ASSERT(!file); + return Zip::Ok; + } + + if (device != file) + disconnect(device, 0, this, 0); + + return do_closeArchive(); +} + +//! \internal +Zip::ErrorCode ZipPrivate::do_closeArchive() +{ + // Close current archive by writing out central directory + // and free up resources + + if (!device && !headers) + return Zip::Ok; + + quint32 szCentralDir = 0; + quint32 offCentralDir = device->pos(); + Zip::ErrorCode c = Zip::Ok; + + if (headers && device) { + for (QMap::ConstIterator itr = headers->constBegin(); + itr != headers->constEnd(); ++itr) { + const QString fileName = itr.key(); + const ZipEntryP* h = itr.value(); + c = writeEntry(fileName, h, szCentralDir); + } + } + + if (c == Zip::Ok) + c = writeCentralDir(offCentralDir, szCentralDir); + + if (c != Zip::Ok) { + if (file) { + file->close(); + if (!file->remove()) { + qDebug() << "Failed to delete corrupt archive."; + } + } + } + + return c; +} + +//! \internal +Zip::ErrorCode ZipPrivate::writeEntry(const QString& fileName, const ZipEntryP* h, quint32& szCentralDir) +{ + unsigned int sz; + + Q_ASSERT(h && device && headers); + + // signature + buffer1[0] = 'P'; + buffer1[1] = 'K'; + buffer1[2] = 0x01; + buffer1[3] = 0x02; + + // version made by (currently only MS-DOS/FAT - no symlinks or other stuff supported) + buffer1[ZIP_CD_OFF_MADEBY] = buffer1[ZIP_CD_OFF_MADEBY + 1] = 0; + + // version needed to extract + buffer1[ZIP_CD_OFF_VERSION] = ZIP_VERSION; + buffer1[ZIP_CD_OFF_VERSION + 1] = 0; + + // general purpose flag + buffer1[ZIP_CD_OFF_GPFLAG] = h->gpFlag[0]; + buffer1[ZIP_CD_OFF_GPFLAG + 1] = h->gpFlag[1]; + + // compression method + buffer1[ZIP_CD_OFF_CMET] = h->compMethod & 0xFF; + buffer1[ZIP_CD_OFF_CMET + 1] = (h->compMethod >> 8) & 0xFF; + + // last mod file time + buffer1[ZIP_CD_OFF_MODT] = h->modTime[0]; + buffer1[ZIP_CD_OFF_MODT + 1] = h->modTime[1]; + + // last mod file date + buffer1[ZIP_CD_OFF_MODD] = h->modDate[0]; + buffer1[ZIP_CD_OFF_MODD + 1] = h->modDate[1]; + + // crc (4bytes) [16,17,18,19] + setULong(h->crc, buffer1, ZIP_CD_OFF_CRC); + + // compressed size (4bytes: [20,21,22,23]) + setULong(h->szComp, buffer1, ZIP_CD_OFF_CSIZE); + + // uncompressed size [24,25,26,27] + setULong(h->szUncomp, buffer1, ZIP_CD_OFF_USIZE); + + // filename + QByteArray fileNameBytes = fileName.toLatin1(); + sz = fileNameBytes.size(); + buffer1[ZIP_CD_OFF_NAMELEN] = sz & 0xFF; + buffer1[ZIP_CD_OFF_NAMELEN + 1] = (sz >> 8) & 0xFF; + + // extra field length + buffer1[ZIP_CD_OFF_XLEN] = buffer1[ZIP_CD_OFF_XLEN + 1] = 0; + + // file comment length + buffer1[ZIP_CD_OFF_COMMLEN] = buffer1[ZIP_CD_OFF_COMMLEN + 1] = 0; + + // disk number start + buffer1[ZIP_CD_OFF_DISKSTART] = buffer1[ZIP_CD_OFF_DISKSTART + 1] = 0; + + // internal file attributes + buffer1[ZIP_CD_OFF_IATTR] = buffer1[ZIP_CD_OFF_IATTR + 1] = 0; + + // external file attributes + buffer1[ZIP_CD_OFF_EATTR] = + buffer1[ZIP_CD_OFF_EATTR + 1] = + buffer1[ZIP_CD_OFF_EATTR + 2] = + buffer1[ZIP_CD_OFF_EATTR + 3] = 0; + + // relative offset of local header [42->45] + setULong(h->lhOffset, buffer1, ZIP_CD_OFF_LHOFF); + + if (device->write(buffer1, ZIP_CD_SIZE) != ZIP_CD_SIZE) { + return Zip::WriteFailed; + } + + // Write out filename + if ((unsigned int)device->write(fileNameBytes) != sz) { + return Zip::WriteFailed; + } + + szCentralDir += (ZIP_CD_SIZE + sz); + + return Zip::Ok; +} + +//! \internal +Zip::ErrorCode ZipPrivate::writeCentralDir(quint32 offCentralDir, quint32 szCentralDir) +{ + Q_ASSERT(device && headers); + + unsigned int sz; + + // signature + buffer1[0] = 'P'; + buffer1[1] = 'K'; + buffer1[2] = 0x05; + buffer1[3] = 0x06; + + // number of this disk + buffer1[ZIP_EOCD_OFF_DISKNUM] = buffer1[ZIP_EOCD_OFF_DISKNUM + 1] = 0; + + // number of disk with central directory + buffer1[ZIP_EOCD_OFF_CDDISKNUM] = buffer1[ZIP_EOCD_OFF_CDDISKNUM + 1] = 0; + + // number of entries in this disk + sz = headers->count(); + buffer1[ZIP_EOCD_OFF_ENTRIES] = sz & 0xFF; + buffer1[ZIP_EOCD_OFF_ENTRIES + 1] = (sz >> 8) & 0xFF; + + // total number of entries + buffer1[ZIP_EOCD_OFF_CDENTRIES] = buffer1[ZIP_EOCD_OFF_ENTRIES]; + buffer1[ZIP_EOCD_OFF_CDENTRIES + 1] = buffer1[ZIP_EOCD_OFF_ENTRIES + 1]; + + // size of central directory [12->15] + setULong(szCentralDir, buffer1, ZIP_EOCD_OFF_CDSIZE); + + // central dir offset [16->19] + setULong(offCentralDir, buffer1, ZIP_EOCD_OFF_CDOFF); + + // ZIP file comment length + QByteArray commentBytes = comment.toLatin1(); + quint16 commentLength = commentBytes.size(); + + if (commentLength == 0) { + buffer1[ZIP_EOCD_OFF_COMMLEN] = buffer1[ZIP_EOCD_OFF_COMMLEN + 1] = 0; + } else { + buffer1[ZIP_EOCD_OFF_COMMLEN] = commentLength & 0xFF; + buffer1[ZIP_EOCD_OFF_COMMLEN + 1] = (commentLength >> 8) & 0xFF; + } + + if (device->write(buffer1, ZIP_EOCD_SIZE) != ZIP_EOCD_SIZE) { + return Zip::WriteFailed; + } + + if (commentLength != 0) { + if ((unsigned int)device->write(commentBytes) != commentLength) { + return Zip::WriteFailed; + } + } + + return Zip::Ok; +} + +//! \internal +void ZipPrivate::reset() +{ + comment.clear(); + + if (headers) { + qDeleteAll(*headers); + delete headers; + headers = 0; + } + + device = 0; + + if (file) + delete file; + file = 0; +} + +//! \internal Returns the path of the parent directory +QString ZipPrivate::extractRoot(const QString& p, Zip::CompressionOptions o) +{ + Q_UNUSED(o); + QDir d(QDir::cleanPath(p)); + if (!d.exists()) + return QString(); + + if (!d.cdUp()) + return QString(); + + return d.absolutePath(); +} + + +/************************************************************************ + Public interface +*************************************************************************/ + +/*! + Creates a new Zip file compressor. +*/ +Zip::Zip() : d(new ZipPrivate) +{ +} + +/*! + Closes any open archive and releases used resources. +*/ +Zip::~Zip() +{ + closeArchive(); + delete d; +} + +/*! + Returns true if there is an open archive. +*/ +bool Zip::isOpen() const +{ + return d->device; +} + +/*! + Sets the password to be used for the next files being added! + Files added before calling this method will use the previously + set password (if any). + Closing the archive won't clear the password! +*/ +void Zip::setPassword(const QString& pwd) +{ + d->password = pwd; +} + +//! Convenience method, clears the current password. +void Zip::clearPassword() +{ + d->password.clear(); +} + +//! Returns the currently used password. +QString Zip::password() const +{ + return d->password; +} + +/*! + Attempts to create a new Zip archive. If \p overwrite is true and the file + already exist it will be overwritten. + Any open archive will be closed. + */ +Zip::ErrorCode Zip::createArchive(const QString& filename, bool overwrite) +{ + closeArchive(); + Q_ASSERT(!d->device && !d->file); + + if (filename.isEmpty()) + return Zip::FileNotFound; + + d->file = new QFile(filename); + + if (d->file->exists() && !overwrite) { + delete d->file; + d->file = 0; + return Zip::FileExists; + } + + if (!d->file->open(QIODevice::WriteOnly)) { + delete d->file; + d->file = 0; + return Zip::OpenFailed; + } + + const Zip::ErrorCode ec = createArchive(d->file); + if (ec != Zip::Ok) { + closeArchive(); + } + + return ec; +} + +/*! + Attempts to create a new Zip archive. If there is another open archive this will be closed. + \warning The class takes ownership of the device! + */ +Zip::ErrorCode Zip::createArchive(QIODevice* device) +{ + if (!device) { + qDebug() << "Invalid device."; + return Zip::OpenFailed; + } + + return d->createArchive(device); +} + +/*! + Returns the current archive comment. +*/ +QString Zip::archiveComment() const +{ + return d->comment; +} + +/*! + Sets the comment for this archive. Note: createArchive() should have been + called before. +*/ +void Zip::setArchiveComment(const QString& comment) +{ + d->comment = comment; +} + +/*! + Convenience method, same as calling Zip::addDirectory(const QString&,const QString&,CompressionOptions,CompressionLevel) + with the Zip::IgnorePaths flag as compression option and an empty \p root parameter. + + The result is that all files found in \p path (and in subdirectories) are + added to the zip file without a directory entry. +*/ +Zip::ErrorCode Zip::addDirectoryContents(const QString& path, CompressionLevel level) +{ + return addDirectory(path, QString(), IgnorePaths, level); +} + +/*! + Convenience method, same as calling Zip::addDirectory(const QString&,const QString&,CompressionOptions,CompressionLevel) + with the Zip::IgnorePaths flag as compression option. + + The result is that all files found in \p path (and in subdirectories) are + added to the zip file without a directory entry (or within a directory + structure specified by \p root). +*/ +Zip::ErrorCode Zip::addDirectoryContents(const QString& path, const QString& root, CompressionLevel level) +{ + return addDirectory(path, root, IgnorePaths, level); +} + +/*! + Convenience method, same as calling + Zip::addDirectory(const QString&,const QString&,CompressionLevel) + with an empty \p root parameter and Zip::RelativePaths flag as compression option. + */ +Zip::ErrorCode Zip::addDirectory(const QString& path, CompressionLevel level) +{ + return addDirectory(path, QString(), Zip::RelativePaths, level); +} + +/*! + Convenience method, same as calling Zip::addDirectory(const QString&,const QString&,CompressionOptions,CompressionLevel) + with the Zip::RelativePaths flag as compression option. + */ +Zip::ErrorCode Zip::addDirectory(const QString& path, const QString& root, CompressionLevel level) +{ + return addDirectory(path, root, Zip::RelativePaths, level); +} + +/*! + Recursively adds files contained in \p dir to the archive, using \p root as name for the root folder. + Stops adding files if some error occurs. + + The ExtractionOptions are checked in the order they are defined in the zip.h heaser file. + This means that the last one overwrites the previous one (if some conflict occurs), i.e. + Zip::IgnorePaths | Zip::AbsolutePaths would be interpreted as Zip::IgnorePaths. + + The \p root parameter is ignored with the Zip::IgnorePaths parameter and used as path prefix (a trailing / + is always added as directory separator!) otherwise (even with Zip::AbsolutePaths set!). + + If \p addedFiles is not null it is set to the number of successfully added + files. +*/ +Zip::ErrorCode Zip::addDirectory(const QString& path, const QString& root, + CompressionOptions options, CompressionLevel level, int* addedFiles) +{ + const int hierarchyLev = 0; + return d->addDirectory(path, root, options, level, hierarchyLev, addedFiles); +} + +/*! + Convenience method, same as calling Zip::addFile(const QString&,const QString&,CompressionOptions,CompressionLevel) + with an empty \p root parameter and Zip::RelativePaths as compression option. + */ +Zip::ErrorCode Zip::addFile(const QString& path, CompressionLevel level) +{ + return addFile(path, QString(), Zip::RelativePaths, level); +} + +/*! + Convenience method, same as calling Zip::addFile(const QString&,const QString&,CompressionOptions,CompressionLevel) + with the Zip::RelativePaths flag as compression option. + */ +Zip::ErrorCode Zip::addFile(const QString& path, const QString& root, + CompressionLevel level) +{ + return addFile(path, root, Zip::RelativePaths, level); +} + +/*! + Adds the file at \p path to the archive, using \p root as name for the root folder. + If \p path points to a directory the behaviour is basically the same as + addDirectory(). + + The ExtractionOptions are checked in the order they are defined in the zip.h heaser file. + This means that the last one overwrites the previous one (if some conflict occurs), i.e. + Zip::IgnorePaths | Zip::AbsolutePaths would be interpreted as Zip::IgnorePaths. + + The \p root parameter is ignored with the Zip::IgnorePaths parameter and used as path prefix (a trailing / + is always added as directory separator!) otherwise (even with Zip::AbsolutePaths set!). +*/ +Zip::ErrorCode Zip::addFile(const QString& path, const QString& root, + CompressionOptions options, CompressionLevel level) +{ + if (path.isEmpty()) + return Zip::Ok; + return addFiles(QStringList() << path, root, options, level); +} + +/*! + Convenience method, same as calling Zip::addFiles(const QStringList&,const QString&,CompressionOptions,CompressionLevel) + with an empty \p root parameter and Zip::RelativePaths as compression option. + */ +Zip::ErrorCode Zip::addFiles(const QStringList& paths, CompressionLevel level) +{ + return addFiles(paths, QString(), Zip::RelativePaths, level); +} + +/*! + Convenience method, same as calling Zip::addFiles(const QStringList&,const QString&,CompressionOptions,CompressionLevel) + with the Zip::RelativePaths flag as compression option. + */ +Zip::ErrorCode Zip::addFiles(const QStringList& paths, const QString& root, + CompressionLevel level) +{ + return addFiles(paths, root, Zip::RelativePaths, level); +} + +/*! + Adds the files or directories in \p paths to the archive, using \p root as + name for the root folder. + This is similar to calling addFile or addDirectory for all the entries in + \p paths, except it is slightly faster. + + The ExtractionOptions are checked in the order they are defined in the zip.h heaser file. + This means that the last one overwrites the previous one (if some conflict occurs), i.e. + Zip::IgnorePaths | Zip::AbsolutePaths would be interpreted as Zip::IgnorePaths. + + The \p root parameter is ignored with the Zip::IgnorePaths parameter and used as path prefix (a trailing / + is always added as directory separator!) otherwise (even with Zip::AbsolutePaths set!). + + If \p addedFiles is not null it is set to the number of successfully added + files. +*/ +Zip::ErrorCode Zip::addFiles(const QStringList& paths, const QString& root, + CompressionOptions options, CompressionLevel level, int* addedFiles) +{ + return d->addFiles(paths, root, options, level, addedFiles); +} + +/*! + Closes the archive and writes any pending data. +*/ +Zip::ErrorCode Zip::closeArchive() +{ + Zip::ErrorCode ec = d->closeArchive(); + d->reset(); + return ec; +} + +/*! + Returns a locale translated error string for a given error code. +*/ +QString Zip::formatError(Zip::ErrorCode c) const +{ + switch (c) + { + case Ok: return QCoreApplication::translate("Zip", "ZIP operation completed successfully."); break; + case ZlibInit: return QCoreApplication::translate("Zip", "Failed to initialize or load zlib library."); break; + case ZlibError: return QCoreApplication::translate("Zip", "zlib library error."); break; + case OpenFailed: return QCoreApplication::translate("Zip", "Unable to create or open file."); break; + case NoOpenArchive: return QCoreApplication::translate("Zip", "No archive has been created yet."); break; + case FileNotFound: return QCoreApplication::translate("Zip", "File or directory does not exist."); break; + case ReadFailed: return QCoreApplication::translate("Zip", "File read error."); break; + case WriteFailed: return QCoreApplication::translate("Zip", "File write error."); break; + case SeekFailed: return QCoreApplication::translate("Zip", "File seek error."); break; + default: ; + } + + return QCoreApplication::translate("Zip", "Unknown error."); +} + +OSDAB_END_NAMESPACE diff --git a/oracle/src/zip/zip.h b/oracle/src/zip/zip.h new file mode 100755 index 000000000..79f32ada5 --- /dev/null +++ b/oracle/src/zip/zip.h @@ -0,0 +1,158 @@ +/**************************************************************************** +** Filename: zip.h +** Last updated [dd/mm/yyyy]: 27/03/2011 +** +** pkzip 2.0 file compression. +** +** Some of the code has been inspired by other open source projects, +** (mainly Info-Zip and Gilles Vollant's minizip). +** Compression and decompression actually uses the zlib library. +** +** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. +** +** This file is part of the OSDaB project (http://osdab.42cows.org/). +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See the file LICENSE.GPL that came with this software distribution or +** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. +** +**********************************************************************/ + +#ifndef OSDAB_ZIP__H +#define OSDAB_ZIP__H + +#include "zipglobal.h" + +#include +#include + +#include + +class QIODevice; +class QFile; +class QDir; +class QStringList; +class QString; + +OSDAB_BEGIN_NAMESPACE(Zip) + +class ZipPrivate; + +class OSDAB_ZIP_EXPORT Zip +{ +public: + enum ErrorCode + { + Ok, + ZlibInit, + ZlibError, + FileExists, + OpenFailed, + NoOpenArchive, + FileNotFound, + ReadFailed, + WriteFailed, + SeekFailed, + InternalError + }; + + enum CompressionLevel + { + Store, + Deflate1 = 1, Deflate2, Deflate3, Deflate4, + Deflate5, Deflate6, Deflate7, Deflate8, Deflate9, + AutoCPU, AutoMIME, AutoFull + }; + + enum CompressionOption + { + /*! Does not preserve absolute paths in the zip file when adding a + file or directory (default) */ + RelativePaths = 0x0001, + /*! Preserve absolute paths */ + AbsolutePaths = 0x0002, + /*! Do not store paths. All the files are put in the (evtl. user defined) + root of the zip file */ + IgnorePaths = 0x0004, + /*! Works only with addDirectory(). Adds the directory's contents, + including subdirectories, but does not add an entry for the root + directory itself. */ + IgnoreRoot = 0x0008, + /*! Used only when compressing a directory or multiple files. + If set invalid or unreadable files are simply skipped. + */ + SkipBadFiles = 0x0020, + /*! Makes sure a file is never added twice to the same zip archive. + This check is only necessary in certain usage scenarios and given + that it slows down processing you need to enable it explicitly with + this flag. + */ + CheckForDuplicates = 0x0040 + }; + Q_DECLARE_FLAGS(CompressionOptions, CompressionOption) + + Zip(); + virtual ~Zip(); + + bool isOpen() const; + + void setPassword(const QString& pwd); + void clearPassword(); + QString password() const; + + ErrorCode createArchive(const QString& file, bool overwrite = true); + ErrorCode createArchive(QIODevice* device); + + QString archiveComment() const; + void setArchiveComment(const QString& comment); + + ErrorCode addDirectoryContents(const QString& path, + CompressionLevel level = AutoFull); + ErrorCode addDirectoryContents(const QString& path, const QString& root, + CompressionLevel level = AutoFull); + + ErrorCode addDirectory(const QString& path, + CompressionLevel level = AutoFull); + ErrorCode addDirectory(const QString& path, const QString& root, + CompressionLevel level = AutoFull); + ErrorCode addDirectory(const QString& path, const QString& root, + CompressionOptions options, CompressionLevel level = AutoFull, + int* addedFiles = 0); + + ErrorCode addFile(const QString& path, + CompressionLevel level = AutoFull); + ErrorCode addFile(const QString& path, const QString& root, + CompressionLevel level = AutoFull); + ErrorCode addFile(const QString& path, const QString& root, + CompressionOptions options, + CompressionLevel level = AutoFull); + + ErrorCode addFiles(const QStringList& paths, + CompressionLevel level = AutoFull); + ErrorCode addFiles(const QStringList& paths, const QString& root, + CompressionLevel level = AutoFull); + ErrorCode addFiles(const QStringList& paths, const QString& root, + CompressionOptions options, + CompressionLevel level = AutoFull, + int* addedFiles = 0); + + ErrorCode closeArchive(); + + QString formatError(ErrorCode c) const; + +private: + ZipPrivate* d; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(Zip::CompressionOptions) + +OSDAB_END_NAMESPACE + +#endif // OSDAB_ZIP__H diff --git a/oracle/src/zip/zip_p.h b/oracle/src/zip/zip_p.h new file mode 100755 index 000000000..b8ec6564b --- /dev/null +++ b/oracle/src/zip/zip_p.h @@ -0,0 +1,133 @@ +/**************************************************************************** +** Filename: zip_p.h +** Last updated [dd/mm/yyyy]: 27/03/2011 +** +** pkzip 2.0 file compression. +** +** Some of the code has been inspired by other open source projects, +** (mainly Info-Zip and Gilles Vollant's minizip). +** Compression and decompression actually uses the zlib library. +** +** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. +** +** This file is part of the OSDaB project (http://osdab.42cows.org/). +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See the file LICENSE.GPL that came with this software distribution or +** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. +** +**********************************************************************/ + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Zip/UnZip API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#ifndef OSDAB_ZIP_P__H +#define OSDAB_ZIP_P__H + +#include "zip.h" +#include "zipentry_p.h" + +#include +#include +#include + +#include + +/*! + zLib authors suggest using larger buffers (128K or 256K) for (de)compression (especially for inflate()) + we use a 256K buffer here - if you want to use this code on a pre-iceage mainframe please change it ;) +*/ +#define ZIP_READ_BUFFER (256*1024) + +OSDAB_BEGIN_NAMESPACE(Zip) + +class ZipPrivate : public QObject +{ + Q_OBJECT + +public: + // uLongf from zconf.h + typedef uLongf crc_t; + + ZipPrivate(); + virtual ~ZipPrivate(); + + QMap* headers; + + QIODevice* device; + QFile* file; + + char buffer1[ZIP_READ_BUFFER]; + char buffer2[ZIP_READ_BUFFER]; + + unsigned char* uBuffer; + + const crc_t* crcTable; + + QString comment; + QString password; + + Zip::ErrorCode createArchive(QIODevice* device); + Zip::ErrorCode closeArchive(); + void reset(); + + bool zLibInit(); + + bool containsEntry(const QFileInfo& info) const; + + Zip::ErrorCode addDirectory(const QString& path, const QString& root, + Zip::CompressionOptions options, Zip::CompressionLevel level, + int hierarchyLevel, int* addedFiles = 0); + Zip::ErrorCode addFiles(const QStringList& paths, const QString& root, + Zip::CompressionOptions options, Zip::CompressionLevel level, + int* addedFiles); + + Zip::ErrorCode createEntry(const QFileInfo& file, const QString& root, + Zip::CompressionLevel level); + Zip::CompressionLevel detectCompressionByMime(const QString& ext); + + inline quint32 updateChecksum(const quint32& crc, const quint32& val) const; + + inline void encryptBytes(quint32* keys, char* buffer, qint64 read); + + inline void setULong(quint32 v, char* buffer, unsigned int offset); + inline void updateKeys(quint32* keys, int c) const; + inline void initKeys(quint32* keys) const; + inline int decryptByte(quint32 key2) const; + + inline QString extractRoot(const QString& p, Zip::CompressionOptions o); + +private slots: + void deviceDestroyed(QObject*); + +private: + int compressionStrategy(const QString& path, QIODevice& file) const; + Zip::ErrorCode deflateFile(const QFileInfo& fileInfo, + quint32& crc, qint64& written, const Zip::CompressionLevel& level, quint32** keys); + Zip::ErrorCode storeFile(const QString& path, QIODevice& file, + quint32& crc, qint64& written, quint32** keys); + Zip::ErrorCode compressFile(const QString& path, QIODevice& file, + quint32& crc, qint64& written, const Zip::CompressionLevel& level, quint32** keys); + Zip::ErrorCode do_closeArchive(); + Zip::ErrorCode writeEntry(const QString& fileName, const ZipEntryP* h, quint32& szCentralDir); + Zip::ErrorCode writeCentralDir(quint32 offCentralDir, quint32 szCentralDir); +}; + +OSDAB_END_NAMESPACE + +#endif // OSDAB_ZIP_P__H diff --git a/oracle/src/zip/zipentry_p.h b/oracle/src/zip/zipentry_p.h new file mode 100755 index 000000000..f0675b6b6 --- /dev/null +++ b/oracle/src/zip/zipentry_p.h @@ -0,0 +1,91 @@ +/**************************************************************************** +** Filename: ZipEntryP.h +** Last updated [dd/mm/yyyy]: 27/03/2011 +** +** Wrapper for a ZIP local header. +** +** Some of the code has been inspired by other open source projects, +** (mainly Info-Zip and Gilles Vollant's minizip). +** Compression and decompression actually uses the zlib library. +** +** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. +** +** This file is part of the OSDaB project (http://osdab.42cows.org/). +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See the file LICENSE.GPL that came with this software distribution or +** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. +** +**********************************************************************/ + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Zip/UnZip API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#ifndef OSDAB_ZIPENTRY_P__H +#define OSDAB_ZIPENTRY_P__H + +#include +#include + +OSDAB_BEGIN_NAMESPACE(Zip) + +class ZipEntryP +{ +public: + ZipEntryP() : + lhOffset(0), + dataOffset(0), + gpFlag(), + compMethod(0), + modTime(), + modDate(), + crc(0), + szComp(0), + szUncomp(0), + absolutePath(), + fileSize(0), + lhEntryChecked(false) + { + gpFlag[0] = gpFlag[1] = 0; + modTime[0] = modTime[1] = 0; + modDate[0] = modDate[1] = 0; + } + + quint32 lhOffset; // Offset of the local header record for this entry + mutable quint32 dataOffset; // Offset of the file data for this entry + unsigned char gpFlag[2]; // General purpose flag + quint16 compMethod; // Compression method + unsigned char modTime[2]; // Last modified time + unsigned char modDate[2]; // Last modified date + quint32 crc; // CRC32 + quint32 szComp; // Compressed file size + quint32 szUncomp; // Uncompressed file size + QString comment; // File comment + + QString absolutePath; // Internal use + qint64 fileSize; // Internal use + + mutable bool lhEntryChecked; // Is true if the local header record for this entry has been parsed + + inline bool isEncrypted() const { return gpFlag[0] & 0x01; } + inline bool hasDataDescriptor() const { return gpFlag[0] & 0x08; } +}; + +OSDAB_END_NAMESPACE + +#endif // OSDAB_ZIPENTRY_P__H diff --git a/oracle/src/zip/zipglobal.cpp b/oracle/src/zip/zipglobal.cpp new file mode 100644 index 000000000..e972005f5 --- /dev/null +++ b/oracle/src/zip/zipglobal.cpp @@ -0,0 +1,150 @@ +/**************************************************************************** +** Filename: zipglobal.cpp +** Last updated [dd/mm/yyyy]: 06/02/2011 +** +** pkzip 2.0 file compression. +** +** Some of the code has been inspired by other open source projects, +** (mainly Info-Zip and Gilles Vollant's minizip). +** Compression and decompression actually uses the zlib library. +** +** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. +** +** This file is part of the OSDaB project (http://osdab.42cows.org/). +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See the file LICENSE.GPL that came with this software distribution or +** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. +** +**********************************************************************/ + +#include "zipglobal.h" + +#if defined(Q_OS_WIN) || defined(Q_OS_WINCE) || defined(Q_OS_LINUX) || defined(Q_OS_MACOS) +#define OSDAB_ZIP_HAS_UTC +#include +#else +#undef OSDAB_ZIP_HAS_UTC +#endif + +#if defined(Q_OS_WIN) +#include +#elif defined(Q_OS_LINUX) || defined(Q_OS_MACOS) +#include +#endif + +OSDAB_BEGIN_NAMESPACE(Zip) + +/*! Returns the current UTC offset in seconds unless OSDAB_ZIP_NO_UTC is defined + and method is implemented for the current platform and 0 otherwise. +*/ +int OSDAB_ZIP_MANGLE(currentUtcOffset)() +{ +#if !(!defined OSDAB_ZIP_NO_UTC && defined OSDAB_ZIP_HAS_UTC) + return 0; +#else + time_t curr_time_t; + time(&curr_time_t); + +#if defined Q_OS_WIN + struct tm _tm_struct; + struct tm *tm_struct = &_tm_struct; +#else + struct tm *tm_struct = 0; +#endif + +#if !defined(QT_NO_THREAD) && defined(_POSIX_THREAD_SAFE_FUNCTIONS) + // use the reentrant version of localtime() where available + tzset(); + tm res; + tm_struct = gmtime_r(&curr_time_t, &res); +#elif defined Q_OS_WIN && !defined Q_CC_MINGW + if (gmtime_s(tm_struct, &curr_time_t)) + return 0; +#else + tm_struct = gmtime(&curr_time_t); +#endif + + if (!tm_struct) + return 0; + + const time_t global_time_t = mktime(tm_struct); + +#if !defined(QT_NO_THREAD) && defined(_POSIX_THREAD_SAFE_FUNCTIONS) + // use the reentrant version of localtime() where available + tm_struct = localtime_r(&curr_time_t, &res); +#elif defined Q_OS_WIN && !defined Q_CC_MINGW + if (localtime_s(tm_struct, &curr_time_t)) + return 0; +#else + tm_struct = localtime(&curr_time_t); +#endif + + if (!tm_struct) + return 0; + + const time_t local_time_t = mktime(tm_struct); + + const int utcOffset = -qRound(difftime(global_time_t, local_time_t)); + return tm_struct->tm_isdst > 0 ? utcOffset + 3600 : utcOffset; +#endif // No UTC +} + +QDateTime OSDAB_ZIP_MANGLE(fromFileTimestamp)(const QDateTime &dateTime) +{ +#if !defined OSDAB_ZIP_NO_UTC && defined OSDAB_ZIP_HAS_UTC + const int utc = OSDAB_ZIP_MANGLE(currentUtcOffset)(); + return dateTime.toUTC().addSecs(utc); +#else + return dateTime; +#endif // OSDAB_ZIP_NO_UTC +} + +bool OSDAB_ZIP_MANGLE(setFileTimestamp)(const QString &fileName, const QDateTime &dateTime) +{ + if (fileName.isEmpty()) + return true; + +#ifdef Q_OS_WIN + HANDLE hFile = + CreateFileW(fileName.toStdWString().c_str(), GENERIC_WRITE, FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0); + if (hFile == INVALID_HANDLE_VALUE) { + return false; + } + + SYSTEMTIME st; + FILETIME ft, ftLastMod; + const QDate date = dateTime.date(); + const QTime time = dateTime.time(); + st.wYear = date.year(); + st.wMonth = date.month(); + st.wDay = date.day(); + st.wHour = time.hour(); + st.wMinute = time.minute(); + st.wSecond = time.second(); + st.wMilliseconds = time.msec(); + + SystemTimeToFileTime(&st, &ft); + LocalFileTimeToFileTime(&ft, &ftLastMod); + + const bool success = SetFileTime(hFile, NULL, NULL, &ftLastMod); + CloseHandle(hFile); + return success; + +#elif defined(Q_OS_LINUX) || defined(Q_OS_MACOS) + + struct utimbuf t_buffer; + t_buffer.actime = t_buffer.modtime = dateTime.toSecsSinceEpoch(); + return utime(fileName.toLocal8Bit().constData(), &t_buffer) == 0; +#endif + + return true; +} +OSDAB_END_NAMESPACE diff --git a/oracle/src/zip/zipglobal.h b/oracle/src/zip/zipglobal.h new file mode 100755 index 000000000..e7ff33105 --- /dev/null +++ b/oracle/src/zip/zipglobal.h @@ -0,0 +1,77 @@ +/**************************************************************************** +** Filename: zipglobal.h +** Last updated [dd/mm/yyyy]: 27/03/2011 +** +** pkzip 2.0 file compression. +** +** Some of the code has been inspired by other open source projects, +** (mainly Info-Zip and Gilles Vollant's minizip). +** Compression and decompression actually uses the zlib library. +** +** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. +** +** This file is part of the OSDaB project (http://osdab.42cows.org/). +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See the file LICENSE.GPL that came with this software distribution or +** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. +** +**********************************************************************/ + +#ifndef OSDAB_ZIPGLOBAL__H +#define OSDAB_ZIPGLOBAL__H + +#include +#include + +/* If you want to build the OSDaB Zip code as + a library, define OSDAB_ZIP_LIB in the library's .pro file and + in the libraries using it OR remove the #ifndef OSDAB_ZIP_LIB + define below and leave the #else body. Also remember to define + OSDAB_ZIP_BUILD_LIB in the library's project). +*/ + +#ifndef OSDAB_ZIP_LIB +# define OSDAB_ZIP_EXPORT +#else +# if defined(OSDAB_ZIP_BUILD_LIB) +# define OSDAB_ZIP_EXPORT Q_DECL_EXPORT +# else +# define OSDAB_ZIP_EXPORT Q_DECL_IMPORT +# endif +#endif + +#ifdef OSDAB_NAMESPACE +#define OSDAB_BEGIN_NAMESPACE(ModuleName) namespace Osdab { namespace ModuleName { +#else +#define OSDAB_BEGIN_NAMESPACE(ModuleName) +#endif + +#ifdef OSDAB_NAMESPACE +#define OSDAB_END_NAMESPACE } } +#else +#define OSDAB_END_NAMESPACE +#endif + +#ifndef OSDAB_NAMESPACE +#define OSDAB_ZIP_MANGLE(x) zip_##x +#else +#define OSDAB_ZIP_MANGLE(x) x +#endif + +OSDAB_BEGIN_NAMESPACE(Zip) + +OSDAB_ZIP_EXPORT int OSDAB_ZIP_MANGLE(currentUtcOffset)(); +OSDAB_ZIP_EXPORT QDateTime OSDAB_ZIP_MANGLE(fromFileTimestamp)(const QDateTime& dateTime); +OSDAB_ZIP_EXPORT bool OSDAB_ZIP_MANGLE(setFileTimestamp)(const QString& fileName, const QDateTime& dateTime); + +OSDAB_END_NAMESPACE + +#endif // OSDAB_ZIPGLOBAL__H diff --git a/oracle/translations/oracle_cs.ts b/oracle/translations/oracle_cs.ts new file mode 100644 index 000000000..1578b52cb --- /dev/null +++ b/oracle/translations/oracle_cs.ts @@ -0,0 +1,550 @@ + + + IntroPage + + + Introduction + + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + + + + + Interface language: + + + + + Version: + + + + + LoadSetsPage + + + Source selection + + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + + + + + Download URL: + + + + + Local file: + + + + + Restore default URL + + + + + Choose file... + + + + + Load sets file + + + + + Sets JSON file (%1) + + + + + + + + + + + Error + Chyba + + + + The provided URL is not valid. + + + + + Downloading (0MB) + + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + + + + + Network error: %1. + + + + + Parsing file + + + + + Xz extraction failed. + + + + + Sorry, this version of Oracle does not support xz compressed files. + + + + + Failed to open Zip archive: %1. + + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + + + + + Zip extraction failed: %1. + + + + + Sorry, this version of Oracle does not support zipped files. + + + + + Failed to interpret downloaded data. + + + + + Do you want to download the uncompressed file instead? + + + + + The file was retrieved successfully, but it does not contain any sets data. + + + + + LoadSpoilersPage + + + Save spoiler database + + + + + XML; spoiler database (*.xml) + + + + + Spoilers import + + + + + Please specify a compatible source for spoiler data. + + + + + Download URL: + + + + + Restore default URL + + + + + The spoiler database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + LoadTokensPage + + + Save token database + + + + + XML; token database (*.xml) + + + + + Tokens import + + + + + Please specify a compatible source for token data. + + + + + Download URL: + + + + + Restore default URL + + + + + The token database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + OracleImporter + + + Dummy set containing tokens + + + + + OracleWizard + + + Oracle Importer + + + + + OutroPage + + + Finished + + + + + The wizard has finished. + + + + + You can now start using Cockatrice with the newly updated cards. + + + + + If the card databases don't reload automatically, restart the Cockatrice client. + + + + + SaveSetsPage + + + + Error + Chyba + + + + No set has been imported. + + + + + Sets imported + + + + + A cockatrice database file of %1 MB has been downloaded. + + + + + The following sets have been found: + + + + + Press "Save" to store the imported cards in the Cockatrice database. + + + + + The card database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + &Save + + + + + Import finished: %1 cards. + + + + + %1: %2 cards imported + + + + + Save card database + + + + + XML; card database (*.xml) + + + + + The file could not be saved to %1 + + + + + SimpleDownloadFilePage + + + + + Error + + + + + The provided URL is not valid. + + + + + Downloading (0MB) + + + + + Downloading (%1MB) + + + + + Network error: %1. + + + + + The file could not be saved to %1 + + + + + UnZip + + + ZIP operation completed successfully. + + + + + Failed to initialize or load zlib library. + + + + + zlib library error. + + + + + Unable to create or open file. + + + + + Partially corrupted archive. Some files might be extracted. + + + + + Corrupted archive. + + + + + Wrong password. + + + + + No archive has been created yet. + + + + + File or directory does not exist. + + + + + File read error. + + + + + File write error. + + + + + File seek error. + + + + + Unable to create a directory. + + + + + Invalid device. + + + + + Invalid or incompatible zip archive. + + + + + Inconsistent headers. Archive might be corrupted. + + + + + Unknown error. + + + + + Zip + + + ZIP operation completed successfully. + + + + + Failed to initialize or load zlib library. + + + + + zlib library error. + + + + + Unable to create or open file. + + + + + No archive has been created yet. + + + + + File or directory does not exist. + + + + + File read error. + + + + + File write error. + + + + + File seek error. + + + + + Unknown error. + + + + + i18n + + + English + Česky (Czech) + + + + main + + + Only run in spoiler mode + + + + \ No newline at end of file diff --git a/oracle/translations/oracle_de.ts b/oracle/translations/oracle_de.ts new file mode 100644 index 000000000..df12f40f4 --- /dev/null +++ b/oracle/translations/oracle_de.ts @@ -0,0 +1,609 @@ + + + IntroPage + + + Introduction + Einführung + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + Dieser Assistent wird eine Liste aller Editionen, Karten und Spielsteine importieren, die von Cockatrice genutzt werden. + + + + Interface language: + Interfacesprache: + + + + Version: + Version: + + + + LoadSetsPage + + + Source selection + Quellenauswahl + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + Bitte geben Sie eine kompatible Quelle für die Liste der Editionen und Karten an. Sie können eine URL-Adresse zum Herunterladen angeben oder eine existierende Datei von Ihrem Computer verwenden. + + + + Download URL: + Download URL: + + + + Local file: + Lokale Datei: + + + + Restore default URL + Standard-URL wiederherstellen + + + + Choose file... + Datei auswählen... + + + + Load sets file + Editionsdatei wird geladen + + + + Sets file (%1) + Sets JSON file (%1) + Sets Datei (%1) + + + + + + + + + + Error + Fehler + + + + The provided URL is not valid. + Die eingegebene URL ist nicht gültig. + + + + Downloading (0MB) + Herunterladen (0MB) + + + + Please choose a file. + Bitte wähle eine Datei aus. + + + + Cannot open file '%1'. + Datei '%1' kann nicht geöffnet werden. + + + + Downloading (%1MB) + Herunterladen (%1MB) + + + + Network error: %1. + Netzwerkfehler: %1. + + + + Parsing file + Datei wird verarbeitet + + + + Xz extraction failed. + Fehler beim Extrahieren der xz Datei. + + + + Sorry, this version of Oracle does not support xz compressed files. + Es tut uns Leid, diese Version von Oracle unterstützt keine xz komprimierten Dateien. + + + + Failed to open Zip archive: %1. + Fehler beim Öffnen des Zip Archivs: %1. + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + Fehler beim Extrahieren: Das Zip Archiv enthält mehr als eine Datei. + + + + Zip extraction failed: %1. + Fehler beim Extrahieren: %1. + + + + Sorry, this version of Oracle does not support zipped files. + Es tut uns Leid, diese Version von Oracle unterstützt keine Zip Archive. + + + + Failed to interpret downloaded data. + Interpretation der heruntergeladenen Daten fehlgeschlagen. + + + + Do you want to download the uncompressed file instead? + Möchten Sie stattdessen die unkomprimierte Datei herunterladen? + + + + The file was retrieved successfully, but it does not contain any sets data. + Die Datei wurde erfolgreich abgerufen, sie enthält aber keine Editionsdaten. + + + + LoadSpoilersPage + + + Save spoiler database + Speichere Spoilerdatenbank + + + + XML; spoiler database (*.xml) + XML; Spoilerdatenbank (*.xml) + + + + spoiler + Spoiler + + + + Spoilers import + Spoilerimport + + + + Please specify a compatible source for spoiler data. + Bitte geben Sie eine kompatible Quelle für Spoilerdaten an. + + + + Download URL: + Download URL: + + + + Local file: + Lokale Datei: + + + + Restore default URL + Standard-URL wiederherstellen + + + + Choose file... + Datei auswählen... + + + + The spoiler database will be saved at the following location: + Die Spoilerdatenbank wird in folgendem Pfad gespeichert: + + + + Save to a custom path (not recommended) + Speichere in benutzerdefiniertem Pfad (nicht empfohlen) + + + + LoadTokensPage + + + Save token database + Speichere Spielsteindatenbank + + + + XML; token database (*.xml) + XML; Spielsteindatenbank (*.xml) + + + + tokens + Spielsteine + + + + Tokens import + Spielsteinimport + + + + Please specify a compatible source for token data. + Bitte geben Sie eine kompatible Quelle für Spielsteindaten an. + + + + Download URL: + Download URL: + + + + Local file: + Lokale Datei: + + + + Restore default URL + Standard-URL wiederherstellen + + + + Choose file... + Datei auswählen... + + + + The token database will be saved at the following location: + Die Spielsteindatenbank wird in folgendem Pfad gespeichert: + + + + Save to a custom path (not recommended) + Speichere in benutzerdefiniertem Pfad (nicht empfohlen) + + + + OracleImporter + + + Dummy set containing tokens + Platzhalter Edition mit Spielsteinen + + + + OracleWizard + + + Oracle Importer + Oracle Importer + + + + OutroPage + + + Finished + Fertig + + + + The wizard has finished. + Der Wizard ist fertig. + + + + You can now start using Cockatrice with the newly updated cards. + Sie können nun Cockatrice mit den aktuellen Karten verwenden. + + + + If the card databases don't reload automatically, restart the Cockatrice client. + Falls die Datenbanken nicht automatisch neu geladen werden, starten Sie bitte Cockatrice neu. + + + + SaveSetsPage + + + + Error + Fehler + + + + No set has been imported. + Es wurden keine Editionen importiert. + + + + Sets imported + Editionen wurden importiert + + + + A cockatrice database file of %1 MB has been downloaded. + Eine Cockatrice-Datenbankdatei der Größe %1 MB wurde heruntergeladen. + + + + The following sets have been found: + Die folgenden Sets wurden gefunden: + + + + Press "Save" to store the imported cards in the Cockatrice database. + Drücken Sie "Speichern", um die importierten Karten in der Datenbank zu speichern. + + + + The card database will be saved at the following location: + Die Kartendatenbank wird in folgendem Pfad gespeichert: + + + + Save to a custom path (not recommended) + Speichere in benutzerdefiniertem Pfad (nicht empfohlen) + + + + &Save + &Speichern + + + + Import finished: %1 cards. + Importieren abgeschlossen: %1 Karten. + + + + %1: %2 cards imported + %1: %2 Karten importiert. + + + + Save card database + Kartendatenbank speichern + + + + XML; card database (*.xml) + XML; Kartendatenbank (*.xml) + + + + The file could not be saved to %1 + Die Datei konnte nicht gespeichert werden: +%1 + + + + SimpleDownloadFilePage + + + Load %1 file + Lade %1 Datei + + + + %1 file (%1) + %1 Datei (%1) + + + + + + + + Error + Fehler + + + + The provided URL is not valid: + Die bereitgestellte URL ist nicht gültig: + + + + Downloading (0MB) + Herunterladen (0MB) + + + + Please choose a file. + Bitte wähle eine Datei aus. + + + + Cannot open file '%1'. + Datei '%1' kann nicht geöffnet werden. + + + + Downloading (%1MB) + Herunterladen (%1MB) + + + + Network error: %1. + Netzwerkfehler: %1. + + + + The file could not be saved to %1 + Die Datei konnte nicht in %1 gespeichert werden + + + + UnZip + + + ZIP operation completed successfully. + ZIP-Operation erfolgreich abgeschlossen. + + + + Failed to initialize or load zlib library. + Fehler beim Initialisieren oder Laden der zlib Bibliothek. + + + + zlib library error. + zlib Bibliotheksfehler. + + + + Unable to create or open file. + Fehler beim Erstellen oder Öffnen der Datei. + + + + Partially corrupted archive. Some files might be extracted. + Teilweise fehlerhaftes Archiv. Manche Dateien wurden möglicherweise extrahiert. + + + + Corrupted archive. + Fehlerhaftes Archiv. + + + + Wrong password. + Falsches Passwort. + + + + No archive has been created yet. + Es wurde noch kein Archiv erstellt. + + + + File or directory does not exist. + Datei oder Verzeichnis existiert nicht. + + + + File read error. + Dateilesefehler. + + + + File write error. + Dateischreibfehler. + + + + File seek error. + Dateisuchfehler. + + + + Unable to create a directory. + Erstellen eines Verzeichnisses nicht möglich. + + + + Invalid device. + Ungültiges Gerät. + + + + Invalid or incompatible zip archive. + Ungültiges oder inkompatibles Zip Archiv. + + + + Inconsistent headers. Archive might be corrupted. + Unstimmiger Dateikopf. Das Archiv ist möglicherweise fehlerhaft. + + + + Unknown error. + Unbekannter Fehler. + + + + Zip + + + ZIP operation completed successfully. + ZIP Operation erfolgreich abgeschlossen. + + + + Failed to initialize or load zlib library. + Fehler beim Initialisieren oder Laden der zlib Bibliothek. + + + + zlib library error. + zlib Bibliotheksfehler. + + + + Unable to create or open file. + Fehler beim Erstellen oder Öffnen der Datei. + + + + No archive has been created yet. + Es wurde noch kein Archiv erstellt. + + + + File or directory does not exist. + Datei oder Verzeichnis existiert nicht. + + + + File read error. + Dateilesefehler. + + + + File write error. + Dateischreibfehler. + + + + File seek error. + Dateisuchfehler. + + + + Unknown error. + Unbekannter Fehler. + + + + i18n + + + English + Deutsch (German) + + + + main + + + Only run in spoiler mode + Nur im Spoiler Modus ausführen + + + + Run in no-confirm background mode + Ausführen im Hintergrund-Modus ohne Bestätigung + + + \ No newline at end of file diff --git a/oracle/translations/oracle_el.ts b/oracle/translations/oracle_el.ts new file mode 100644 index 000000000..7934997cf --- /dev/null +++ b/oracle/translations/oracle_el.ts @@ -0,0 +1,608 @@ + + + IntroPage + + + Introduction + Εισαγωγή + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + Αυτός ο αυτόματος οδηγός θα εισάγει τη λίστα των σετ, καρτών και δειγμάτων (tokens) που θα χρησιμοποιηθούν από το Cockatrice. + + + + Interface language: + + + + + Version: + Έκδοση: + + + + LoadSetsPage + + + Source selection + Επιλογή πηγής + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + + + + + Download URL: + URL λήψης: + + + + Local file: + Τοπικό αρχείο: + + + + Restore default URL + Επαναφορά προκαθορισμένης διεύθυνσης URL + + + + Choose file... + Επιλέξτε αρχείο... + + + + Load sets file + Φόρτωση αρχείων + + + + Sets file (%1) + Sets JSON file (%1) + + + + + + + + + + + Error + Σφάλμα + + + + The provided URL is not valid. + Η παρεχόμενη διεύθυνση URL δεν είναι έγκυρη. + + + + Downloading (0MB) + Λήψη (0MB) + + + + Please choose a file. + Παρακαλώ διαλέξτε έναν αρχείο. + + + + Cannot open file '%1'. + Δεν είναι δυνατό να ανοίξει το αρχείο '% 1'. + + + + Downloading (%1MB) + Λήψη (% 1MB) + + + + Network error: %1. + Σφάλμα δικτύου: % 1. + + + + Parsing file + Ανάλυση αρχείου + + + + Xz extraction failed. + + + + + Sorry, this version of Oracle does not support xz compressed files. + + + + + Failed to open Zip archive: %1. + Αποτυχία ανοίγματος αρχείου Zip:% 1. + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + Η εξαγωγή zip απέτυχε: το αρχείο Zip δεν περιέχει ακριβώς ένα αρχείο. + + + + Zip extraction failed: %1. + Η εξαγωγή zip απέτυχε: % 1. + + + + Sorry, this version of Oracle does not support zipped files. + Λυπούμαστε, αυτή η έκδοση του Oracle δεν υποστηρίζει αρχεία τύπου zipped. + + + + Failed to interpret downloaded data. + + + + + Do you want to download the uncompressed file instead? + + + + + The file was retrieved successfully, but it does not contain any sets data. + Το αρχείο ανακτήθηκε με επιτυχία, αλλά δεν περιέχει σύνολα δεδομένων. + + + + LoadSpoilersPage + + + Save spoiler database + + + + + XML; spoiler database (*.xml) + + + + + spoiler + + + + + Spoilers import + + + + + Please specify a compatible source for spoiler data. + + + + + Download URL: + + + + + Local file: + + + + + Restore default URL + + + + + Choose file... + + + + + The spoiler database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + LoadTokensPage + + + Save token database + + + + + XML; token database (*.xml) + + + + + tokens + + + + + Tokens import + + + + + Please specify a compatible source for token data. + + + + + Download URL: + URL λήψης: + + + + Local file: + + + + + Restore default URL + Επαναφορά προκαθορισμένης διεύθυνσης URL + + + + Choose file... + + + + + The token database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + OracleImporter + + + Dummy set containing tokens + Εικονικό σετ που περιέχει tokens + + + + OracleWizard + + + Oracle Importer + Εισαγωγέας Oracle + + + + OutroPage + + + Finished + + + + + The wizard has finished. + + + + + You can now start using Cockatrice with the newly updated cards. + + + + + If the card databases don't reload automatically, restart the Cockatrice client. + + + + + SaveSetsPage + + + + Error + Σφάλμα + + + + No set has been imported. + Δεν έχει εισαχθεί κανένα σετ. + + + + Sets imported + Εισαγόμενα σετς + + + + A cockatrice database file of %1 MB has been downloaded. + + + + + The following sets have been found: + + + + + Press "Save" to store the imported cards in the Cockatrice database. + + + + + The card database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + &Save + + + + + Import finished: %1 cards. + Η εισαγωγή ολοκληρώθηκε: % 1 κάρτες. + + + + %1: %2 cards imported + %1: %2 κάρτες εισήχθησαν + + + + Save card database + Αποθήκευση κάρτας βάσης δεδομένων + + + + XML; card database (*.xml) + XML; κάρτα βάσης δεδομένων (* .xml) + + + + The file could not be saved to %1 + Δεν ήταν δυνατή η αποθήκευση του αρχείου στο %1 + + + + SimpleDownloadFilePage + + + Load %1 file + + + + + %1 file (%1) + + + + + + + + + Error + + + + + The provided URL is not valid: + + + + + Downloading (0MB) + + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + + + + + Network error: %1. + + + + + The file could not be saved to %1 + + + + + UnZip + + + ZIP operation completed successfully. + Η λειτουργία ZIP ολοκληρώθηκε με επιτυχία. + + + + Failed to initialize or load zlib library. + Δεν ήταν δυνατή η προετοιμασία ή η φόρτωση της βιβλιοθήκης zlib. + + + + zlib library error. + Σφάλμα βιβλιοθήκης zlib. + + + + Unable to create or open file. + Δεν είναι δυνατή η δημιουργία ή το άνοιγμα αρχείου. + + + + Partially corrupted archive. Some files might be extracted. + Μερικώς κατεστραμμένο αρχείο. Κάποια αρχεία ενδέχεται να εξαχθούν. + + + + Corrupted archive. + Κατεστραμμένο αρχείο. + + + + Wrong password. + Λάθος κωδικός. + + + + No archive has been created yet. + Δεν έχει δημιουργηθεί κανένα αρχείο ακόμα. + + + + File or directory does not exist. + Το αρχείο ή ο κατάλογος δεν υπάρχει. + + + + File read error. + Σφάλμα ανάγνωσης αρχείου. + + + + File write error. + Σφάλμα εγγραφής αρχείου. + + + + File seek error. + Σφάλμα αναζήτησης αρχείου. + + + + Unable to create a directory. + Δεν είναι δυνατή η δημιουργία ενός καταλόγου. + + + + Invalid device. + Μη έγκυρη συσκευή. + + + + Invalid or incompatible zip archive. + Μη έγκυρο ή ασυμβίβαστο zip αρχείο. + + + + Inconsistent headers. Archive might be corrupted. + Αντιφατικές κεφαλίδες. Το αρχείο μπορεί να είναι κατεστραμμένο. + + + + Unknown error. + Άγνωστο σφάλμα. + + + + Zip + + + ZIP operation completed successfully. + Η λειτουργία ZIP ολοκληρώθηκε με επιτυχία. + + + + Failed to initialize or load zlib library. + Δεν ήταν δυνατή η προετοιμασία ή η φόρτωση της βιβλιοθήκης zlib. + + + + zlib library error. + Σφάλμα βιβλιοθήκης zlib. + + + + Unable to create or open file. + Δεν είναι δυνατή η δημιουργία ή το άνοιγμα αρχείου. + + + + No archive has been created yet. + Δεν έχει δημιουργηθεί αρχείο ακόμα. + + + + File or directory does not exist. + Το αρχείο ή ο κατάλογος δεν υπάρχει. + + + + File read error. + Σφάλμα ανάγνωσης αρχείου. + + + + File write error. + Σφάλμα εγγραφής αρχείου. + + + + File seek error. + Σφάλμα αναζήτησης αρχείου. + + + + Unknown error. + Άγνωστο σφάλμα. + + + + i18n + + + English + Ελληνικά (Greek) + + + + main + + + Only run in spoiler mode + + + + + Run in no-confirm background mode + + + + \ No newline at end of file diff --git a/oracle/translations/oracle_en@pirate.ts b/oracle/translations/oracle_en@pirate.ts new file mode 100644 index 000000000..13a57750b --- /dev/null +++ b/oracle/translations/oracle_en@pirate.ts @@ -0,0 +1,556 @@ + + + IntroPage + + + Introduction + + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + + + + + Interface language: + + + + + Version: + + + + + LoadSetsPage + + + Source selection + + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + + + + + Download URL: + + + + + Local file: + + + + + Restore default URL + + + + + Choose file... + + + + + Load sets file + + + + + Sets file (%1) + Sets JSON file (%1) + + + + + + + + + + + Error + Cap'n? Thar be a problem + + + + The provided URL is not valid. + + + + + Downloading (0MB) + + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + + + + + Network error: %1. + Cap'n? Thar be a problem wi' t' smoke signals: %1. + + + + Parsing file + + + + + Xz extraction failed. + + + + + Sorry, this version of Oracle does not support xz compressed files. + + + + + Failed to open Zip archive: %1. + + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + + + + + Zip extraction failed: %1. + + + + + Sorry, this version of Oracle does not support zipped files. + + + + + Failed to interpret downloaded data. + + + + + Do you want to download the uncompressed file instead? + + + + + The file was retrieved successfully, but it does not contain any sets data. + + + + + LoadSpoilersPage + + + Save spoiler database + + + + + XML; spoiler database (*.xml) + + + + + Spoilers import + + + + + Please specify a compatible source for spoiler data. + + + + + Download URL: + + + + + Restore default URL + + + + + The spoiler database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + LoadTokensPage + + + Save token database + + + + + XML; token database (*.xml) + + + + + Tokens import + + + + + Please specify a compatible source for token data. + + + + + Download URL: + + + + + Restore default URL + + + + + The token database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + OracleImporter + + + Dummy set containing tokens + + + + + OracleWizard + + + Oracle Importer + + + + + OutroPage + + + Finished + + + + + The wizard has finished. + + + + + You can now start using Cockatrice with the newly updated cards. + + + + + If the card databases don't reload automatically, restart the Cockatrice client. + + + + + SaveSetsPage + + + + Error + Cap'n? Thar be a problem + + + + No set has been imported. + + + + + Sets imported + + + + + A cockatrice database file of %1 MB has been downloaded. + + + + + The following sets have been found: + + + + + Press "Save" to store the imported cards in the Cockatrice database. + + + + + The card database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + &Save + + + + + Import finished: %1 cards. + + + + + %1: %2 cards imported + + + + + Save card database + + + + + XML; card database (*.xml) + + + + + The file could not be saved to %1 + + + + + SimpleDownloadFilePage + + + + + Error + + + + + The provided URL is not valid: + + + + + Downloading (0MB) + + + + + Downloading (%1MB) + + + + + Network error: %1. + + + + + The file could not be saved to %1 + + + + + UnZip + + + ZIP operation completed successfully. + + + + + Failed to initialize or load zlib library. + + + + + zlib library error. + Cap'n? Thar be a problem wi' t' zlib library. + + + + Unable to create or open file. + + + + + Partially corrupted archive. Some files might be extracted. + + + + + Corrupted archive. + + + + + Wrong password. + + + + + No archive has been created yet. + + + + + File or directory does not exist. + + + + + File read error. + + + + + File write error. + + + + + File seek error. + + + + + Unable to create a directory. + + + + + Invalid device. + + + + + Invalid or incompatible zip archive. + + + + + Inconsistent headers. Archive might be corrupted. + + + + + Unknown error. + Cap'n? Thar be a unknown problem wi' t' ship. + + + + Zip + + + ZIP operation completed successfully. + + + + + Failed to initialize or load zlib library. + + + + + zlib library error. + Cap'n? Thar be a problem wi' t' zlib library. + + + + Unable to create or open file. + + + + + No archive has been created yet. + + + + + File or directory does not exist. + + + + + File read error. + + + + + File write error. + + + + + File seek error. + + + + + Unknown error. + Cap'n? Thar be a unknown problem wi' t' ship. + + + + i18n + + + English + English, arr! (Pirate English) + + + + main + + + Only run in spoiler mode + + + + + Run in no-confirm background mode + + + + \ No newline at end of file diff --git a/oracle/translations/oracle_en_US.ts b/oracle/translations/oracle_en_US.ts new file mode 100644 index 000000000..4b664bf57 --- /dev/null +++ b/oracle/translations/oracle_en_US.ts @@ -0,0 +1,608 @@ + + + IntroPage + + + Introduction + Introduction + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + + + + Interface language: + Interface language: + + + + Version: + Version: + + + + LoadSetsPage + + + Source selection + Source selection + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + + + + Download URL: + Download URL: + + + + Local file: + Local file: + + + + Restore default URL + Restore default URL + + + + Choose file... + Choose file... + + + + Load sets file + Load sets file + + + + Sets file (%1) + Sets JSON file (%1) + Sets file (%1) + + + + + + + + + + Error + Error + + + + The provided URL is not valid. + The provided URL is not valid. + + + + Downloading (0MB) + Downloading (0MB) + + + + Please choose a file. + Please choose a file. + + + + Cannot open file '%1'. + Cannot open file '%1'. + + + + Downloading (%1MB) + Downloading (%1MB) + + + + Network error: %1. + Network error: %1. + + + + Parsing file + Parsing file + + + + Xz extraction failed. + Xz extraction failed. + + + + Sorry, this version of Oracle does not support xz compressed files. + Sorry, this version of Oracle does not support xz compressed files. + + + + Failed to open Zip archive: %1. + Failed to open Zip archive: %1. + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + Zip extraction failed: the Zip archive doesn't contain exactly one file. + + + + Zip extraction failed: %1. + Zip extraction failed: %1. + + + + Sorry, this version of Oracle does not support zipped files. + Sorry, this version of Oracle does not support zipped files. + + + + Failed to interpret downloaded data. + Failed to interpret downloaded data. + + + + Do you want to download the uncompressed file instead? + Do you want to download the uncompressed file instead? + + + + The file was retrieved successfully, but it does not contain any sets data. + The file was retrieved successfully, but it does not contain any sets data. + + + + LoadSpoilersPage + + + Save spoiler database + Save spoiler database + + + + XML; spoiler database (*.xml) + XML; spoiler database (*.xml) + + + + spoiler + spoiler + + + + Spoilers import + Spoilers import + + + + Please specify a compatible source for spoiler data. + Please specify a compatible source for spoiler data. + + + + Download URL: + Download URL: + + + + Local file: + Local file: + + + + Restore default URL + Restore default URL + + + + Choose file... + Choose file... + + + + The spoiler database will be saved at the following location: + The spoiler database will be saved at the following location: + + + + Save to a custom path (not recommended) + Save to a custom path (not recommended) + + + + LoadTokensPage + + + Save token database + Save token database + + + + XML; token database (*.xml) + XML; token database (*.xml) + + + + tokens + tokens + + + + Tokens import + Tokens import + + + + Please specify a compatible source for token data. + Please specify a compatible source for token data. + + + + Download URL: + Download URL: + + + + Local file: + Local file: + + + + Restore default URL + Restore default URL + + + + Choose file... + Choose file... + + + + The token database will be saved at the following location: + The token database will be saved at the following location: + + + + Save to a custom path (not recommended) + Save to a custom path (not recommended) + + + + OracleImporter + + + Dummy set containing tokens + Dummy set containing tokens + + + + OracleWizard + + + Oracle Importer + Oracle Importer + + + + OutroPage + + + Finished + Finished + + + + The wizard has finished. + The wizard has finished. + + + + You can now start using Cockatrice with the newly updated cards. + You can now start using Cockatrice with the newly updated cards. + + + + If the card databases don't reload automatically, restart the Cockatrice client. + If the card databases don't reload automatically, restart the Cockatrice client. + + + + SaveSetsPage + + + + Error + Error + + + + No set has been imported. + No set has been imported. + + + + Sets imported + Sets imported + + + + A cockatrice database file of %1 MB has been downloaded. + A cockatrice database file of %1 MB has been downloaded. + + + + The following sets have been found: + The following sets have been found: + + + + Press "Save" to store the imported cards in the Cockatrice database. + Press "Save" to store the imported cards in the Cockatrice database. + + + + The card database will be saved at the following location: + The card database will be saved at the following location: + + + + Save to a custom path (not recommended) + Save to a custom path (not recommended) + + + + &Save + &Save + + + + Import finished: %1 cards. + Import finished: %1 cards. + + + + %1: %2 cards imported + %1: %2 cards imported + + + + Save card database + Save card database + + + + XML; card database (*.xml) + XML; card database (*.xml) + + + + The file could not be saved to %1 + The file could not be saved to %1 + + + + SimpleDownloadFilePage + + + Load %1 file + Load %1 file + + + + %1 file (%1) + %1 file (%1) + + + + + + + + Error + Error + + + + The provided URL is not valid: + The provided URL is not valid: + + + + Downloading (0MB) + Downloading (0MB) + + + + Please choose a file. + Please choose a file. + + + + Cannot open file '%1'. + Cannot open file '%1'. + + + + Downloading (%1MB) + Downloading (%1MB) + + + + Network error: %1. + Network error: %1. + + + + The file could not be saved to %1 + The file could not be saved to %1 + + + + UnZip + + + ZIP operation completed successfully. + ZIP operation completed successfully. + + + + Failed to initialize or load zlib library. + Failed to initialize or load zlib library. + + + + zlib library error. + zlib library error. + + + + Unable to create or open file. + Unable to create or open file. + + + + Partially corrupted archive. Some files might be extracted. + Partially corrupted archive. Some files might be extracted. + + + + Corrupted archive. + Corrupted archive. + + + + Wrong password. + Wrong password. + + + + No archive has been created yet. + No archive has been created yet. + + + + File or directory does not exist. + File or directory does not exist. + + + + File read error. + File read error. + + + + File write error. + File write error. + + + + File seek error. + File seek error. + + + + Unable to create a directory. + Unable to create a directory. + + + + Invalid device. + Invalid device. + + + + Invalid or incompatible zip archive. + Invalid or incompatible zip archive. + + + + Inconsistent headers. Archive might be corrupted. + Inconsistent headers. Archive might be corrupted. + + + + Unknown error. + Unknown error. + + + + Zip + + + ZIP operation completed successfully. + ZIP operation completed successfully. + + + + Failed to initialize or load zlib library. + Failed to initialize or load zlib library. + + + + zlib library error. + zlib library error. + + + + Unable to create or open file. + Unable to create or open file. + + + + No archive has been created yet. + No archive has been created yet. + + + + File or directory does not exist. + File or directory does not exist. + + + + File read error. + File read error. + + + + File write error. + File write error. + + + + File seek error. + File seek error. + + + + Unknown error. + Unknown error. + + + + i18n + + + English + English + + + + main + + + Only run in spoiler mode + Only run in spoiler mode + + + + Run in no-confirm background mode + Run in no-confirm background mode + + + \ No newline at end of file diff --git a/oracle/translations/oracle_es.ts b/oracle/translations/oracle_es.ts new file mode 100644 index 000000000..eeb9f71bd --- /dev/null +++ b/oracle/translations/oracle_es.ts @@ -0,0 +1,608 @@ + + + IntroPage + + + Introduction + Introducción + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + Este instalador importará la lista de ediciones, cartas y fichas que va a usar Cockatrice. + + + + Interface language: + Idioma de la interfaz: + + + + Version: + Versión: + + + + LoadSetsPage + + + Source selection + Seleccionar origen + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + Por favor especifica un origen compatible para la lista de sets y cartas. Puedes especificar la URL de donde descargarla o usar un archivo existente de tu ordenador. + + + + Download URL: + URL de descarga: + + + + Local file: + Archivo local: + + + + Restore default URL + Restablecer URL predeterminada + + + + Choose file... + Elegir archivo... + + + + Load sets file + Cargar archivo de ediciones + + + + Sets file (%1) + Sets JSON file (%1) + + + + + + + + + + + Error + Error + + + + The provided URL is not valid. + La URL suministrada no es válida. + + + + Downloading (0MB) + Descargando (0MB) + + + + Please choose a file. + Por favor elija un archivo. + + + + Cannot open file '%1'. + No se puede abrir el archivo '%1' + + + + Downloading (%1MB) + Descargando (%1MB) + + + + Network error: %1. + Error de red: %1 + + + + Parsing file + Procesando archivo + + + + Xz extraction failed. + Extracción de Xz fallida + + + + Sorry, this version of Oracle does not support xz compressed files. + Lo sentimos, esta versión de Oracle no soporta archivos comprimidos xz + + + + Failed to open Zip archive: %1. + Error al abrir el archivo Zip: %1. + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + Fallo al extraer el contenido: el Zip contiene más de un archivo. + + + + Zip extraction failed: %1. + Error al extraer el contenido del Zip: %1. + + + + Sorry, this version of Oracle does not support zipped files. + Lo sentimos, esta versión de Oracle no soporta archivos comprimidos. + + + + Failed to interpret downloaded data. + No se pudieron interpretar los datos descargados. + + + + Do you want to download the uncompressed file instead? + ¿Prefieres descargar el archivo sin comprimir? + + + + The file was retrieved successfully, but it does not contain any sets data. + El archivo fue cargado correctamente pero no contiene datos sobre ningún set. + + + + LoadSpoilersPage + + + Save spoiler database + Guardar la base de datos de spoilers + + + + XML; spoiler database (*.xml) + XML; base de datos de spoilers (*.xml) + + + + spoiler + + + + + Spoilers import + Importar spoilers + + + + Please specify a compatible source for spoiler data. + Por favor elija un origen compatible para los datos de los spoilers. + + + + Download URL: + URL de descarga: + + + + Local file: + + + + + Restore default URL + Restaurar URL por defecto + + + + Choose file... + + + + + The spoiler database will be saved at the following location: + La base de datos de los spoilers será guardada en la siguiente ubicación: + + + + Save to a custom path (not recommended) + Guardar en una ruta personalizada (no recomendado) + + + + LoadTokensPage + + + Save token database + Guardar base de datos de fichas + + + + XML; token database (*.xml) + XML; base de datos de fichas (*.xml) + + + + tokens + + + + + Tokens import + Importar tokens + + + + Please specify a compatible source for token data. + Por favor elija un origen compatible para los datos de las fichas. + + + + Download URL: + URL de descarga: + + + + Local file: + + + + + Restore default URL + Restablecer URL predeterminada + + + + Choose file... + + + + + The token database will be saved at the following location: + La base de datos de las fichas será guardada en la siguiente ubicación: + + + + Save to a custom path (not recommended) + Guardar en una ruta personalizada (no recomendado) + + + + OracleImporter + + + Dummy set containing tokens + Set dedicado para tokens + + + + OracleWizard + + + Oracle Importer + Importador de Oracle + + + + OutroPage + + + Finished + Completado + + + + The wizard has finished. + El asistente ha terminado. + + + + You can now start using Cockatrice with the newly updated cards. + Ahora puedes usar Cockatrice con las cartas actualizadas. + + + + If the card databases don't reload automatically, restart the Cockatrice client. + Si las bases de datos de cartas no se recargan automáticamente, reinicia el cliente de Cockatrice. + + + + SaveSetsPage + + + + Error + Error + + + + No set has been imported. + Ningún set ha sido importado. + + + + Sets imported + Sets importados + + + + A cockatrice database file of %1 MB has been downloaded. + Se ha descargado un archivo de base de datos cockatrice de %1 MB. + + + + The following sets have been found: + Las siguientes ediciones han sido encontradas: + + + + Press "Save" to store the imported cards in the Cockatrice database. + Pulsa "Guardar" para guardar las cartas importadas en la base de datos de Cockatrice. + + + + The card database will be saved at the following location: + La base de datos de cartas ha sido guardada en la siguiente ubicación: + + + + Save to a custom path (not recommended) + Guardar en una ruta personalizada (no recomendado) + + + + &Save + &Guardar + + + + Import finished: %1 cards. + Importación terminada: %1 cartas. + + + + %1: %2 cards imported + %1: %2 cartas importadas + + + + Save card database + Guardar base de datos de cartas + + + + XML; card database (*.xml) + XML; base de datos de cartas (*.xml) + + + + The file could not be saved to %1 + El archivo no ha podido ser guardado en %1 + + + + SimpleDownloadFilePage + + + Load %1 file + + + + + %1 file (%1) + + + + + + + + + Error + Error + + + + The provided URL is not valid: + La URL proporcionada no es válida: + + + + Downloading (0MB) + Descargando (0MB) + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + Descargando (%1MB) + + + + Network error: %1. + Error de red: %1. + + + + The file could not be saved to %1 + El archivo no ha podido ser guardado en %1 + + + + UnZip + + + ZIP operation completed successfully. + La operación ZIP se completó con éxito + + + + Failed to initialize or load zlib library. + No se pudo inicializar o cargar la biblioteca zlib + + + + zlib library error. + Error de biblioteca zlib. + + + + Unable to create or open file. + No se puede crear o abrir el archivo. + + + + Partially corrupted archive. Some files might be extracted. + Archivo parcialmente dañado. Es posible que se extraigan algunos archivos. + + + + Corrupted archive. + Archivo corrupto. + + + + Wrong password. + Contraseña incorrecta. + + + + No archive has been created yet. + Aún no se ha creado ningún archivo. + + + + File or directory does not exist. + El archivo o directorio no existe. + + + + File read error. + error de lectura de archivo + + + + File write error. + Error de escritura del archivo. + + + + File seek error. + Error de búsqueda de archivo. + + + + Unable to create a directory. + No se puede crear un directorio + + + + Invalid device. + Dispositivo no válido. + + + + Invalid or incompatible zip archive. + Archivo zip no válido o incompatible + + + + Inconsistent headers. Archive might be corrupted. + Encabezados inconsistentes. Es posible que el archivo esté dañado. + + + + Unknown error. + Encabezados inconsistentes. Es posible que el archivo esté dañado. + + + + Zip + + + ZIP operation completed successfully. + La operación ZIP se completó con éxito. + + + + Failed to initialize or load zlib library. + Fallo al iniciar o cargar la librería zlib. + + + + zlib library error. + Error en la librería zlib. + + + + Unable to create or open file. + No es posible crear o abrir el archivo. + + + + No archive has been created yet. + No se ha creado ningún archivo todavía. + + + + File or directory does not exist. + El archivo o directorio no existe. + + + + File read error. + Error al leer el archivo. + + + + File write error. + Error al escribir en archivo. + + + + File seek error. + Error al buscar en el archivo. + + + + Unknown error. + Error desconocido. + + + + i18n + + + English + Español (Spanish) + + + + main + + + Only run in spoiler mode + Sólo ejecutar en modo spoiler + + + + Run in no-confirm background mode + + + + \ No newline at end of file diff --git a/oracle/translations/oracle_et.ts b/oracle/translations/oracle_et.ts new file mode 100644 index 000000000..9f2c560cd --- /dev/null +++ b/oracle/translations/oracle_et.ts @@ -0,0 +1,608 @@ + + + IntroPage + + + Introduction + Sissejuhatus + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + See võlur impordib Cockatrice’is kasutatava nimekirja komplektidest, kaartidest ja märgistustest. + + + + Interface language: + + + + + Version: + Versioon: + + + + LoadSetsPage + + + Source selection + Allika valik + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + Palun täpsusta ühilduv allikas komplektide ja kaartide nimekirja jaoks. Võid täpsustada URL-i aadressi, mis laaditakse alla või kasutada olemasolevat faili oma arvutist. + + + + Download URL: + Allalaadimise URL: + + + + Local file: + Fail arvutis: + + + + Restore default URL + Taasta tavaURL + + + + Choose file... + Valige fail.... + + + + Load sets file + Lae komplektide kaust + + + + Sets file (%1) + Sets JSON file (%1) + + + + + + + + + + + Error + Viga + + + + The provided URL is not valid. + Antud URl pole kehtiv. + + + + Downloading (0MB) + Allalaadimine (0MB) + + + + Please choose a file. + Palun valige fail. + + + + Cannot open file '%1'. + Ei suudeta avada '%1'. + + + + Downloading (%1MB) + Allalaadimine (%1MB) + + + + Network error: %1. + Võrgu viga: %1. + + + + Parsing file + Faili hankimine + + + + Xz extraction failed. + Xzi lahtipakkimine nurjus. + + + + Sorry, this version of Oracle does not support xz compressed files. + Vabandame, aga antud Oracle’i versioon ei toeta xz kokkupakitud faile. + + + + Failed to open Zip archive: %1. + Zip-arhiivi avamine ebaõnnestus: %1. + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + Zip-i lahtipakkimine ebaõnnestus: Zip-arhiiv sisaldab rohkem faile kui üks. + + + + Zip extraction failed: %1. + Zipi lahtipakkimine ebaõnnestus: %1. + + + + Sorry, this version of Oracle does not support zipped files. + Vabandame, aga antud Oracle versioon ei toeta kokkupakitud faile. + + + + Failed to interpret downloaded data. + + + + + Do you want to download the uncompressed file instead? + Soovid alla laadida hoopis pakkimata faili? + + + + The file was retrieved successfully, but it does not contain any sets data. + Fail on edukalt alla laetud, ent ei sisalda andmeid. + + + + LoadSpoilersPage + + + Save spoiler database + Salvesta spoileri andmebaas + + + + XML; spoiler database (*.xml) + XML; spoileri andmebaas (*.xml) + + + + spoiler + + + + + Spoilers import + Spoilerite import + + + + Please specify a compatible source for spoiler data. + Täpsusta spoileri andmetega ühilduv allikas. + + + + Download URL: + Allalaadimise URL: + + + + Local file: + + + + + Restore default URL + Taasta tavaURL + + + + Choose file... + + + + + The spoiler database will be saved at the following location: + Spoileri andmebaas salvestatakse järgmisesse asukohta: + + + + Save to a custom path (not recommended) + Salvesta enda määratud asukohta (pole soovitatav) + + + + LoadTokensPage + + + Save token database + Salvesta märgistuste andmebaas + + + + XML; token database (*.xml) + XML; märgistuste andmebaas (*.xml) + + + + tokens + + + + + Tokens import + Märgistuste importimine + + + + Please specify a compatible source for token data. + Täpsusta märgistuste andmetega ühilduv allikas. + + + + Download URL: + Allalaadimise URL: + + + + Local file: + + + + + Restore default URL + Taasta tavaURL + + + + Choose file... + + + + + The token database will be saved at the following location: + Märgistuste andmebaas salvestatakse järgmisesse asukohta: + + + + Save to a custom path (not recommended) + Salvesta enda määratud asukohta (pole soovitatav) + + + + OracleImporter + + + Dummy set containing tokens + Nukk-komplekt mis sisaldab märgistusi + + + + OracleWizard + + + Oracle Importer + Oracle sissetooja + + + + OutroPage + + + Finished + Valmis + + + + The wizard has finished. + Võlur on lõpetanud. + + + + You can now start using Cockatrice with the newly updated cards. + Nüüd saad Cockatrice’i kasutada uhiuute kaartidega. + + + + If the card databases don't reload automatically, restart the Cockatrice client. + Kui kaardi andmebaasid ei lae ise, taaskäivita Cockatrice’i klient. + + + + SaveSetsPage + + + + Error + Viga + + + + No set has been imported. + Komplekti pole sisse toodud. + + + + Sets imported + Allalaetud komplektid + + + + A cockatrice database file of %1 MB has been downloaded. + + + + + The following sets have been found: + Leiti järgmised komplektid: + + + + Press "Save" to store the imported cards in the Cockatrice database. + Vajuta „Salvesta“, et salvestada imporditud kaardid Cockatrice’i andmebaasi. + + + + The card database will be saved at the following location: + Kaardi andmebaas salvestatakse järgmisesse asukohta: + + + + Save to a custom path (not recommended) + Salvesta enda määratud asukohta (pole soovitatav) + + + + &Save + &Salvesta + + + + Import finished: %1 cards. + %1 kaarti imporditi edukalt. + + + + %1: %2 cards imported + %1: imporditi %2 kaarti + + + + Save card database + Salvesta kaartide andmebaas + + + + XML; card database (*.xml) + XML; kaartide andmebaas (*.xml) + + + + The file could not be saved to %1 + Faili salvestamine asukohta %1 ebaõnnestus + + + + SimpleDownloadFilePage + + + Load %1 file + + + + + %1 file (%1) + + + + + + + + + Error + Viga + + + + The provided URL is not valid: + + + + + Downloading (0MB) + Allalaadimine (0MB) + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + Allalaadimine (%1MB) + + + + Network error: %1. + Võrgu viga: %1. + + + + The file could not be saved to %1 + Faili salvestamine asukohta %1 nurjus + + + + UnZip + + + ZIP operation completed successfully. + Zip-i tegevus oli edukas. + + + + Failed to initialize or load zlib library. + Zlibi kogu ei suudetud ette valmistada või laadida. + + + + zlib library error. + zlibi kogu viga. + + + + Unable to create or open file. + Faili ei suudetud luua või avada. + + + + Partially corrupted archive. Some files might be extracted. + Osaliselt vigane arhiiv. Ainult osa faile võidakse lahti pakkida. + + + + Corrupted archive. + Kahjustunud arhiiv. + + + + Wrong password. + Vale parool. + + + + No archive has been created yet. + Loodud arhiivid puuduvad. + + + + File or directory does not exist. + Faili või asukohta pole olemas. + + + + File read error. + Faili lugemise viga. + + + + File write error. + Faili kirjutamise viga. + + + + File seek error. + Faili otsimise viga + + + + Unable to create a directory. + Asukoha loomine ebaõnnestus. + + + + Invalid device. + Vigane seade. + + + + Invalid or incompatible zip archive. + Vigane või mittetoetatav zip-arhiiv. + + + + Inconsistent headers. Archive might be corrupted. + Ebajäriekindlad päised. Arhiiv võib olla rikutud. + + + + Unknown error. + Tundmatu viga. + + + + Zip + + + ZIP operation completed successfully. + Zip-tegevus on valmis. + + + + Failed to initialize or load zlib library. + Zlib kogu ei suudetud ette valmistada või laadida. + + + + zlib library error. + zlibi kogu viga. + + + + Unable to create or open file. + Faili loomine või avamine ebaõnnestus. + + + + No archive has been created yet. + Loodud arhiivid puuduvad. + + + + File or directory does not exist. + Faili või asukohta pole olemas. + + + + File read error. + Faili lugemise viga. + + + + File write error. + Faili kirjutamise viga. + + + + File seek error. + Faili otsimise viga. + + + + Unknown error. + Tundmatu viga. + + + + i18n + + + English + Eesti Keel (Estonian) + + + + main + + + Only run in spoiler mode + Käivita vaid spoileri režiimis + + + + Run in no-confirm background mode + + + + \ No newline at end of file diff --git a/oracle/translations/oracle_fi.ts b/oracle/translations/oracle_fi.ts new file mode 100644 index 000000000..c7657cf7a --- /dev/null +++ b/oracle/translations/oracle_fi.ts @@ -0,0 +1,608 @@ + + + IntroPage + + + Introduction + Johdanto + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + Tämä työkalu asentaa Cockatricessa käytettävät setit, kortit ja tokenit. + + + + Interface language: + Käyttöliittymän kieli: + + + + Version: + Versio: + + + + LoadSetsPage + + + Source selection + Lähteen määritys + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + Ole hyvä ja määritä lähde setti- ja korttilistalle. Voit määrittää ladattavan URL-osoitteen tai käyttää tietokoneellasi olevaa tiedostoa. + + + + Download URL: + Ladattavan tiedoston URL-osoite: + + + + Local file: + Paikallinen tiedosto: + + + + Restore default URL + Palauta oletus-URL + + + + Choose file... + Valitse tiedosto... + + + + Load sets file + Ladatta ekspansiotiedosto + + + + Sets file (%1) + Sets JSON file (%1) + + + + + + + + + + + Error + Virhe + + + + The provided URL is not valid. + Määritetty URL on virheellinen. + + + + Downloading (0MB) + Ladataan (0MB) + + + + Please choose a file. + Valitse tiedosto. + + + + Cannot open file '%1'. + Ei voida avata tiedostoa '%1'. + + + + Downloading (%1MB) + Ladataan (%1MB) + + + + Network error: %1. + Yhteysvirhe: %1. + + + + Parsing file + Jäsennellään tiedostoa + + + + Xz extraction failed. + Xz purku epäonnistui. + + + + Sorry, this version of Oracle does not support xz compressed files. + Pahoittelut. Tämä versio Oraclesta ei tue xz-pakattuja tiedostoja. + + + + Failed to open Zip archive: %1. + Zip-tiedoston avaaminen epäonnistui: %1. + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + Zip-tiedoston purkaminen epäonnistui: Zip-tiedosto ei sisällä tarkalleen yhtä tiedostoa. + + + + Zip extraction failed: %1. + Zip-tiedoston purkaminen epäonnistuie: %1. + + + + Sorry, this version of Oracle does not support zipped files. + Pahoittelut. Tämä versio Oraclesta ei tue Zip-pakattuja tiedostoja. + + + + Failed to interpret downloaded data. + Ladattujen tietojen tulkitseminen epäonnistui. + + + + Do you want to download the uncompressed file instead? + Haluatko sensijaan ladata pakkaamattoman tiedoston? + + + + The file was retrieved successfully, but it does not contain any sets data. + Tiedosto palautettiin onnistuneesti, mutta se ei sisällä yhdenkään ekspansion dataa. + + + + LoadSpoilersPage + + + Save spoiler database + Tallenna spoileritietokanta + + + + XML; spoiler database (*.xml) + XML; spoileritietokanta (*.xml) + + + + spoiler + + + + + Spoilers import + Spoilerien lataus + + + + Please specify a compatible source for spoiler data. + Ole hyvä ja osoita yhteensopiva spoileridatan lähde. + + + + Download URL: + Ladattavan tiedoston URL-osoite: + + + + Local file: + + + + + Restore default URL + Palauta oletus-URL + + + + Choose file... + + + + + The spoiler database will be saved at the following location: + Spoileritietokanta tallennetaan seuraavaan sijaintiin: + + + + Save to a custom path (not recommended) + Tallenna mukautettuun sijaintiin (ei-suositeltu) + + + + LoadTokensPage + + + Save token database + Tallenna tokenitietokanta + + + + XML; token database (*.xml) + XML; tokenitietokanta (*.xml) + + + + tokens + + + + + Tokens import + Tokenien lataus + + + + Please specify a compatible source for token data. + Ole hyvä ja osoita yhteensopiva tokenidatan lähde. + + + + Download URL: + Ladattavan tiedoston URL-osoite: + + + + Local file: + + + + + Restore default URL + Palauta oletus-URL + + + + Choose file... + + + + + The token database will be saved at the following location: + Tokenitietokanta tallennetaan seuraavaan sijaintiin: + + + + Save to a custom path (not recommended) + Tallenna mukautettuun sijaintiin (ei-suositeltu) + + + + OracleImporter + + + Dummy set containing tokens + Tokeneja sisältävä mallisetti + + + + OracleWizard + + + Oracle Importer + Oracle-lataaja + + + + OutroPage + + + Finished + Valmis + + + + The wizard has finished. + Asennusohjelma on valmis. + + + + You can now start using Cockatrice with the newly updated cards. + Voit nyt käyttää Cockatricea juuri päivitetyillä korteilla. + + + + If the card databases don't reload automatically, restart the Cockatrice client. + Jos korttitietokannat eivät päivity automaattisesti, käynnistä Cockatrice uudelleen. + + + + SaveSetsPage + + + + Error + Virhe + + + + No set has been imported. + Yhtään settiä ei ladattu. + + + + Sets imported + Ladatut ekspansiot + + + + A cockatrice database file of %1 MB has been downloaded. + Cockatrice tietokantatiedosto kooltaan %1 MB on ladattu. + + + + The following sets have been found: + Seuraavat setit löydettiin: + + + + Press "Save" to store the imported cards in the Cockatrice database. + Paina "Tallenna" varastoidaksesi ladatut kortit Cockatricen tietokantaan. + + + + The card database will be saved at the following location: + Korttitietokanta tallennetaan seuraavaan sijaintiin: + + + + Save to a custom path (not recommended) + Tallenna mukautettuun sijaintiin (ei-suositeltu) + + + + &Save + &Tallenna + + + + Import finished: %1 cards. + Lataaminen valmis: %1 kortit. + + + + %1: %2 cards imported + %1: %2 kortit ladattu + + + + Save card database + Tallenna korttitietokanta + + + + XML; card database (*.xml) + XML; korttitietokanta (*.xml) + + + + The file could not be saved to %1 + Tiedostoa ei voitu tallentaa osoitteeseen %1 + + + + SimpleDownloadFilePage + + + Load %1 file + + + + + %1 file (%1) + + + + + + + + + Error + Virhe + + + + The provided URL is not valid: + + + + + Downloading (0MB) + Ladataan (0 MB) + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + Ladataan (%1 MB) + + + + Network error: %1. + Yhteysvirhe: %1. + + + + The file could not be saved to %1 + Tiedostoa ei voitu tallentaa osoitteeseen %1 + + + + UnZip + + + ZIP operation completed successfully. + ZIP-operaatio suoritettiin onnistuneesti. + + + + Failed to initialize or load zlib library. + zlib-kirjaston alustaminen tai lataaminen epäonnistui + + + + zlib library error. + zlib-kirjastovirhe. + + + + Unable to create or open file. + Tiedostoa ei voitu luoda tai avata. + + + + Partially corrupted archive. Some files might be extracted. + Osittain korruptoitunut tiedosto. Osia zip-tiedostosta saatettiin purkaa. + + + + Corrupted archive. + Korruptoitunut tiedosto. + + + + Wrong password. + Väärä salasana. + + + + No archive has been created yet. + Yhtään arkistoa ei olla vielä luotu. + + + + File or directory does not exist. + Tiedostoa tai hakemistoa ei ole olemassa. + + + + File read error. + Tiedostonlukuvirhe. + + + + File write error. + Tiedostonkirjoitusvirhe. + + + + File seek error. + Tiedostonetsimisvirhe. + + + + Unable to create a directory. + Hakemistoa ei voitu luoda. + + + + Invalid device. + Virheellinen laite. + + + + Invalid or incompatible zip archive. + Virheellinen tai yhteensopimaton zip-tiedosto. + + + + Inconsistent headers. Archive might be corrupted. + Epäjohdonmukaiset otsikot. Tiedosto saattaa olla korruptoitunut. + + + + Unknown error. + Tuntematon virhe. + + + + Zip + + + ZIP operation completed successfully. + ZIP-operaatio suoritettiin onnistuneesti. + + + + Failed to initialize or load zlib library. + zlib-kirjaston alustaminen tai lataaminen epäonnistui. + + + + zlib library error. + zlib-kirjastovirhe. + + + + Unable to create or open file. + Tiedostoa ei voitu luoda tai avata. + + + + No archive has been created yet. + Yhtään arkistoa ei olla vielä luotu. + + + + File or directory does not exist. + Tiedostoa tai hakemistoa ei ole olemassa. + + + + File read error. + Tiedostonlukuvirhe. + + + + File write error. + Tiedostonkirjoitusvirhe. + + + + File seek error. + Tiedostonetsimisvirhe. + + + + Unknown error. + Tuntematon virhe. + + + + i18n + + + English + Suomi (Finnish) + + + + main + + + Only run in spoiler mode + Suorita vain spoileritilassa + + + + Run in no-confirm background mode + + + + \ No newline at end of file diff --git a/oracle/translations/oracle_fr.ts b/oracle/translations/oracle_fr.ts new file mode 100644 index 000000000..47ab5d0fa --- /dev/null +++ b/oracle/translations/oracle_fr.ts @@ -0,0 +1,608 @@ + + + IntroPage + + + Introduction + Introduction + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + Cet assistant va importer la liste des éditions, cartes et jetons qui seront utilisés par Cockatrice. + + + + Interface language: + Langage de l'interface : + + + + Version: + Version : + + + + LoadSetsPage + + + Source selection + Choix du fichier source + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + Veuillez spécifier une source compatible pour la liste d'éditions et de cartes. Vous pouvez spécifier une URL qui sera utilisée pour télécharger un fichier ou utiliser un fichier existant sur votre ordinateur. + + + + Download URL: + URL de téléchargement : + + + + Local file: + Fichier local : + + + + Restore default URL + Restaurer l'URL par défaut + + + + Choose file... + Choisissez un fichier... + + + + Load sets file + Charger une liste d'éditions + + + + Sets file (%1) + Sets JSON file (%1) + Choisir le fichier (%1) + + + + + + + + + + Error + Erreur + + + + The provided URL is not valid. + L'URL fournie n'est pas valable + + + + Downloading (0MB) + Téléchargement en cours (0MB) + + + + Please choose a file. + Choisissez un fichier. + + + + Cannot open file '%1'. + Impossible d'ouvrir le fichier '%1'. + + + + Downloading (%1MB) + Téléchargement (%1MB) + + + + Network error: %1. + Erreur réseau : %1. + + + + Parsing file + Traitement du fichier. + + + + Xz extraction failed. + L'extraction du zip à échoué. + + + + Sorry, this version of Oracle does not support xz compressed files. + Désolé, cette version d'Oracle ne supporte pas les fichiers zip. + + + + Failed to open Zip archive: %1. + Impossible d'ouvrir l'archive zip: %1. + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + Extraction zip échouée: l'archive zip contient plus qu'un fichier. + + + + Zip extraction failed: %1. + L'extraction du zip a échoué : %1. + + + + Sorry, this version of Oracle does not support zipped files. + Désolé, cette version d'Oracle ne supporte pas les fichiers zip. + + + + Failed to interpret downloaded data. + Échec lors de l'interprétation des données téléchargées. + + + + Do you want to download the uncompressed file instead? + Voulez-vous télécharger le fichier non compressé à la place ? + + + + The file was retrieved successfully, but it does not contain any sets data. + Le fichier a été trouvé, mais ne contient aucune éditions. + + + + LoadSpoilersPage + + + Save spoiler database + Enregistrer la base de données des spoilers + + + + XML; spoiler database (*.xml) + XML ; base de données des spoilers (*.xml) + + + + spoiler + spoiler + + + + Spoilers import + Importation des spoilers + + + + Please specify a compatible source for spoiler data. + Veuillez spécifier une source compatible pour les spoilers. + + + + Download URL: + URL de téléchargement : + + + + Local file: + Fichier local: + + + + Restore default URL + Restaurer l'URL par défaut + + + + Choose file... + Choisissez un fichier... + + + + The spoiler database will be saved at the following location: + La base de données des spoilers sera enregistrée à l'emplacement suivant : + + + + Save to a custom path (not recommended) + Sauvegarder à un emplacement personnalisé (non recommandé) + + + + LoadTokensPage + + + Save token database + Enregistrer la base de données des jetons + + + + XML; token database (*.xml) + XML ; base de données des jetons (*.xml) + + + + tokens + jetons + + + + Tokens import + Importation des jetons + + + + Please specify a compatible source for token data. + Veuillez spécifier une source compatible pour les jetons. + + + + Download URL: + URL de téléchargement: + + + + Local file: + Ficher local: + + + + Restore default URL + Restaurer l'URL par défaut + + + + Choose file... + Choisissez un ficher... + + + + The token database will be saved at the following location: + La base de données des jetons sera enregistrée à l'emplacement suivant : + + + + Save to a custom path (not recommended) + Sauvegarder à un emplacement personnalisé (non recommandé) + + + + OracleImporter + + + Dummy set containing tokens + Fausse édition contenant les jetons + + + + OracleWizard + + + Oracle Importer + Importateur Oracle + + + + OutroPage + + + Finished + Terminé + + + + The wizard has finished. + L'assistant a terminé. + + + + You can now start using Cockatrice with the newly updated cards. + Vous pouvez maintenant commencer à utiliser Cockatrice avec les cartes mises à jour. + + + + If the card databases don't reload automatically, restart the Cockatrice client. + Si les bases de données de cartes ne rechargent pas automatiquement, redémarrez le client Cockatrice. + + + + SaveSetsPage + + + + Error + Erreur + + + + No set has been imported. + Aucune édition n'a été importé. + + + + Sets imported + Éditions importées + + + + A cockatrice database file of %1 MB has been downloaded. + Un fichier de base de données Cockatrice de %1 MB a été téléchargé. + + + + The following sets have been found: + Les éditions suivantes ont été trouvées : + + + + Press "Save" to store the imported cards in the Cockatrice database. + Cliquez sur « Enregistrer » pour enregistrer les cartes importées dans la base de données de Cockatrice. + + + + The card database will be saved at the following location: + La base de données des cartes sera enregistrée à l'emplacement suivant : + + + + Save to a custom path (not recommended) + Sauvegarder à un emplacement personnalisé (non recommandé) + + + + &Save + Sauvegarder + + + + Import finished: %1 cards. + Import terminé: %1 cartes. + + + + %1: %2 cards imported + %1: %2 cartes ajoutées. + + + + Save card database + Sauvegarder la base de carte + + + + XML; card database (*.xml) + XML ; base de données de cartes (*.xml) + + + + The file could not be saved to %1 + Le fichier n'a pu être sauvegarder au chemin '%1' + + + + SimpleDownloadFilePage + + + Load %1 file + Chargez le fichier %1 + + + + %1 file (%1) + fichier %1 (%1) + + + + + + + + Error + Erreur + + + + The provided URL is not valid: + L'URL fournie n'est pas valide : + + + + Downloading (0MB) + Téléchargement (0 Mo) + + + + Please choose a file. + Merci de choisir un fichier. + + + + Cannot open file '%1'. + Le fichier '%1' ne peut pas être ouvert. + + + + Downloading (%1MB) + Téléchargement (%1 Mo) + + + + Network error: %1. + Erreur réseau : %1. + + + + The file could not be saved to %1 + Le fichier n'a pas pu être enregistré dans %1 + + + + UnZip + + + ZIP operation completed successfully. + Opération ZIP complétée avec succès. + + + + Failed to initialize or load zlib library. + Impossible d'initialiser ou de charger la bibliothèque zlib. + + + + zlib library error. + Erreur avec la bibliothèque zlib. + + + + Unable to create or open file. + Impossible de créer ou ouvrir le fichier. + + + + Partially corrupted archive. Some files might be extracted. + Archive partiellement corrompue. Certains fichiers ont pu être extraits. + + + + Corrupted archive. + Archive corrompue. + + + + Wrong password. + Mauvais mot de passe. + + + + No archive has been created yet. + Aucune archive n'a été créée pour l'instant. + + + + File or directory does not exist. + Le fichier ou le dossier n'existe pas. + + + + File read error. + Erreur lors de la lecture du fichier. + + + + File write error. + Erreur lors de l'écriture du fichier. + + + + File seek error. + Erreur lors de la recherche dans le fichier. + + + + Unable to create a directory. + Impossible de créer un répertoire. + + + + Invalid device. + Périphérique non valide. + + + + Invalid or incompatible zip archive. + Archive zip non valide ou incompatible. + + + + Inconsistent headers. Archive might be corrupted. + En-têtes inconsistants. L'archive peut être corrompue. + + + + Unknown error. + Erreur inconnue. + + + + Zip + + + ZIP operation completed successfully. + Opération ZIP complétée avec succès. + + + + Failed to initialize or load zlib library. + Impossible d'initialiser ou de charger la bibliothèque zlib. + + + + zlib library error. + Erreur avec la bibliothèque zlib. + + + + Unable to create or open file. + Impossible de créer ou d'ouvrir le fichier. + + + + No archive has been created yet. + Aucune archive n'a été créée pour l'instant. + + + + File or directory does not exist. + Le fichier ou le dossier n'existe pas. + + + + File read error. + Erreur lors de la lecture du fichier. + + + + File write error. + Erreur lors de l'écriture du fichier. + + + + File seek error. + Erreur lors de la recherche dans le fichier. + + + + Unknown error. + Erreur inconnue. + + + + i18n + + + English + Français (French) + + + + main + + + Only run in spoiler mode + Ne fonctionne qu'en mode spoiler + + + + Run in no-confirm background mode + S'exécuter en mode tâche de fond sans demande de confirmation + + + \ No newline at end of file diff --git a/oracle/translations/oracle_hu.ts b/oracle/translations/oracle_hu.ts new file mode 100644 index 000000000..aed051595 --- /dev/null +++ b/oracle/translations/oracle_hu.ts @@ -0,0 +1,398 @@ + + + IntroPage + + + Introduction + + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + + + + + Interface language: + + + + + Version: + + + + + LoadSetsPage + + + Source selection + + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + + + + + Download URL: + + + + + Local file: + + + + + Restore default URL + + + + + Choose file... + + + + + Load sets file + + + + + Sets JSON file (%1) + + + + + + + + + + Error + + + + + The provided URL is not valid. + + + + + Downloading (0MB) + + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + + + + + Network error: %1. + + + + + Parsing file + + + + + Xz extraction failed. + + + + + Sorry, this version of Oracle does not support xz compressed files. + + + + + Failed to open Zip archive: %1. + + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + + + + + Zip extraction failed: %1. + + + + + Sorry, this version of Oracle does not support zipped files. + + + + + Do you want to download the uncompressed file instead? + + + + + The file was retrieved successfully, but it does not contain any sets data. + + + + + LoadSpoilersPage + + + Save spoiler database + + + + + XML; spoiler database (*.xml) + + + + + Spoilers import + + + + + Please specify a compatible source for spoiler data. + + + + + Download URL: + + + + + Restore default URL + + + + + The spoiler database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + LoadTokensPage + + + Save token database + + + + + XML; token database (*.xml) + + + + + Tokens import + + + + + Please specify a compatible source for token data. + + + + + Download URL: + + + + + Restore default URL + + + + + The token database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + OracleImporter + + + Dummy set containing tokens + + + + + OracleWizard + + + Oracle Importer + + + + + OutroPage + + + Finished + + + + + The wizard has finished. + + + + + You can now start using Cockatrice with the newly updated cards. + + + + + If the card databases don't reload automatically, restart the Cockatrice client. + + + + + SaveSetsPage + + + + Error + + + + + No set has been imported. + + + + + Sets imported + + + + + The following sets have been found: + + + + + Press "Save" to store the imported cards in the Cockatrice database. + + + + + The card database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + &Save + + + + + Import finished: %1 cards. + + + + + %1: %2 cards imported + + + + + Save card database + + + + + XML; card database (*.xml) + + + + + The file could not be saved to %1 + + + + + SimpleDownloadFilePage + + + + + Error + + + + + The provided URL is not valid. + + + + + Downloading (0MB) + + + + + Downloading (%1MB) + + + + + Network error: %1. + + + + + The file could not be saved to %1 + + + + + i18n + + + English + Magyar (Hungarian) + + + + main + + + Only run in spoiler mode + + + + \ No newline at end of file diff --git a/oracle/translations/oracle_it.ts b/oracle/translations/oracle_it.ts new file mode 100644 index 000000000..34bcb3cbc --- /dev/null +++ b/oracle/translations/oracle_it.ts @@ -0,0 +1,609 @@ + + + IntroPage + + + Introduction + Introduzione + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + Questo wizard importerà la lista di set, carte +e pedine che verranno usate da Cockatrice. + + + + Interface language: + Lingua dell'interfaccia: + + + + Version: + Versione: + + + + LoadSetsPage + + + Source selection + Selezione sorgente + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + Specifica una sorgente compatibile per la lista dei set e delle carte. Puoi specificare un indirizzo URL da cui scaricare il file o selezionare un file già presente nel tuo computer. + + + + Download URL: + Indirizzo download: + + + + Local file: + File nel pc: + + + + Restore default URL + Usa l'indirizzo predefinito + + + + Choose file... + Scegli file... + + + + Load sets file + Carica file dei set + + + + Sets file (%1) + Sets JSON file (%1) + File dei set (%1) + + + + + + + + + + Error + Errore + + + + The provided URL is not valid. + L'indirizzo specificato non è valido. + + + + Downloading (0MB) + Scaricamento (0MB) + + + + Please choose a file. + Seleziona un file. + + + + Cannot open file '%1'. + Impossibile aprire il file '%1'. + + + + Downloading (%1MB) + Scaricamento (%1MB) + + + + Network error: %1. + Errore di rete: %1 + + + + Parsing file + Analisi dei file + + + + Xz extraction failed. + Estrazione da file xz fallita. + + + + Sorry, this version of Oracle does not support xz compressed files. + Spiacente, ma questa versione di Oracle non supporta i file xz. + + + + Failed to open Zip archive: %1. + Impossibile aprire il file Zip: %1 + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + Estrazione file Zip fallita: lo Zip non contiene un solo file. + + + + Zip extraction failed: %1. + Estrazione file Zip fallita: %1 + + + + Sorry, this version of Oracle does not support zipped files. + Spiacente, ma questa versione di Oracle non supporta i file zip. + + + + Failed to interpret downloaded data. + Impossibile interpretare i dati scaricati. + + + + Do you want to download the uncompressed file instead? + Vuoi provare a scaricare il file non compresso? + + + + The file was retrieved successfully, but it does not contain any sets data. + Il file è stato analizzato correttamente, ma non contiene i dati di nessun set. + + + + LoadSpoilersPage + + + Save spoiler database + Salva archivio spoiler + + + + XML; spoiler database (*.xml) + XML; archivio spoiler (*.xml) + + + + spoiler + spoiler + + + + Spoilers import + Importazione spoiler + + + + Please specify a compatible source for spoiler data. + Specifica una sorgente compatibile per gli spoiler. + + + + Download URL: + Indirizzo download: + + + + Local file: + File nel pc: + + + + Restore default URL + Usa l'indirizzo predefinito + + + + Choose file... + Scegli file... + + + + The spoiler database will be saved at the following location: + L'archivio degli spoiler verrà salvato nel seguente percorso: + + + + Save to a custom path (not recommended) + Salva in un percorso diverso (sconsigliato) + + + + LoadTokensPage + + + Save token database + Salva archivio pedine + + + + XML; token database (*.xml) + XML; archivio pedine (*.xml) + + + + tokens + pedine + + + + Tokens import + Importazione pedine + + + + Please specify a compatible source for token data. + Specifica una sorgente compatibile per le pedine. + + + + Download URL: + Indirizzo download: + + + + Local file: + File nel pc: + + + + Restore default URL + Usa l'indirizzo predefinito + + + + Choose file... + Scegli file... + + + + The token database will be saved at the following location: + L'archivio delle pedine verrà salvato nel seguente percorso: + + + + Save to a custom path (not recommended) + Salva in un percorso diverso (sconsigliato) + + + + OracleImporter + + + Dummy set containing tokens + Set finto contenente i token + + + + OracleWizard + + + Oracle Importer + Oracle Importer + + + + OutroPage + + + Finished + Finito + + + + The wizard has finished. + Il wizard è completato. + + + + You can now start using Cockatrice with the newly updated cards. + Adesso puoi iniziare ad usare Cockatrice con le nuove carte aggiornate. + + + + If the card databases don't reload automatically, restart the Cockatrice client. + Se il database delle carte non si ricarica in automatico, riavvia il programma Cockatrice. + + + + SaveSetsPage + + + + Error + Errore + + + + No set has been imported. + Nessun set importato. + + + + Sets imported + Set importati + + + + A cockatrice database file of %1 MB has been downloaded. + È stato scaricato un file database di Cockatrice di %1 MB. + + + + The following sets have been found: + Sono stati trovati i seguenti set: + + + + Press "Save" to store the imported cards in the Cockatrice database. + Premi "Salva" per salvare le carte importate nel database di Cockatrice. + + + + The card database will be saved at the following location: + L'archivio delle carte verrà salvato nel seguente percorso: + + + + Save to a custom path (not recommended) + Salva in un percorso diverso (sconsigliato) + + + + &Save + &Salva + + + + Import finished: %1 cards. + Importazione conclusa: %1 carte. + + + + %1: %2 cards imported + %1: %2 carte importate + + + + Save card database + Salva archivio carte + + + + XML; card database (*.xml) + XML; archivio carte (*.xml) + + + + The file could not be saved to %1 + Impossibile salvare il file su %1 + + + + SimpleDownloadFilePage + + + Load %1 file + Carica %1 file + + + + %1 file (%1) + %1 file (%1) + + + + + + + + Error + Errore + + + + The provided URL is not valid: + L'indirizzo fornito non è valido: + + + + Downloading (0MB) + Scaricamento (0MB) + + + + Please choose a file. + Seleziona un file. + + + + Cannot open file '%1'. + Impossibile aprire il file '%1'. + + + + Downloading (%1MB) + Scaricamento (%1MB) + + + + Network error: %1. + Errore di rete: %1. + + + + The file could not be saved to %1 + Impossibile salvare il file su %1 + + + + UnZip + + + ZIP operation completed successfully. + Operazione ZIP completata con successo. + + + + Failed to initialize or load zlib library. + Impossibile inizializzare o caricare libreria zlib. + + + + zlib library error. + Errore libreria zlib. + + + + Unable to create or open file. + Impossibile creare o aprile il file. + + + + Partially corrupted archive. Some files might be extracted. + Archivio parzialmente corrotto. Alcuni file potrebbero essere estratti. + + + + Corrupted archive. + Archivio corrotto. + + + + Wrong password. + Password errata. + + + + No archive has been created yet. + Nessun archivio è stato ancora creato. + + + + File or directory does not exist. + Il file o il percorso non esistono. + + + + File read error. + Errore di lettura file. + + + + File write error. + Errore di scrittura file. + + + + File seek error. + Errore di ricerca file. + + + + Unable to create a directory. + Impossibile creare il percorso specificato. + + + + Invalid device. + Dispositivo invalido. + + + + Invalid or incompatible zip archive. + Archivio ZIP invalido o incompatibile. + + + + Inconsistent headers. Archive might be corrupted. + Header inconsistenti. L'archivio potrebbe essere corrotto. + + + + Unknown error. + Errore sconosciuto. + + + + Zip + + + ZIP operation completed successfully. + Operazione ZIP completata con successo. + + + + Failed to initialize or load zlib library. + Inizializzazione o caricamento libreria zlib non riusciti. + + + + zlib library error. + Errore libreria zlib. + + + + Unable to create or open file. + Impossibile creare o aprire il file. + + + + No archive has been created yet. + Nessun archivio è stato ancora creato. + + + + File or directory does not exist. + Il file o il percorso non esistono. + + + + File read error. + Errore di lettura file. + + + + File write error. + Errore di scrittura file. + + + + File seek error. + Errore di ricerca file. + + + + Unknown error. + Errore sconosciuto. + + + + i18n + + + English + Italiano (Italian) + + + + main + + + Only run in spoiler mode + Avvia solo in modalità spoiler + + + + Run in no-confirm background mode + Esegui in background senza conferma + + + \ No newline at end of file diff --git a/oracle/translations/oracle_ja.ts b/oracle/translations/oracle_ja.ts new file mode 100644 index 000000000..8319f37a2 --- /dev/null +++ b/oracle/translations/oracle_ja.ts @@ -0,0 +1,608 @@ + + + IntroPage + + + Introduction + はじめに + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + このウィザードでは、Cockatriceで使用されるカードやトークン、セットのリストをインポートします。 + + + + Interface language: + インターフェース言語: + + + + Version: + バージョン: + + + + LoadSetsPage + + + Source selection + ソース選択 + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + セットとカードのリストの互換性のあるソースを指定してください。ダウンロードするURLアドレスを指定するか、コンピューターから既存のファイルを使用できます。 + + + + Download URL: + ダウンロードURL: + + + + Local file: + ローカルファイル: + + + + Restore default URL + デフォルトのURLを復元 + + + + Choose file... + ファイルを選択... + + + + Load sets file + カードセットファイルを開く + + + + Sets file (%1) + Sets JSON file (%1) + + + + + + + + + + + Error + エラー + + + + The provided URL is not valid. + 指定されたURLは無効です。 + + + + Downloading (0MB) + ダウンロード中 (0MB) + + + + Please choose a file. + ファイルを選択してください。 + + + + Cannot open file '%1'. + '%1'を開けませんでした。 + + + + Downloading (%1MB) + ダウンロード中 (%1MB) + + + + Network error: %1. + ネットワークエラー: %1。 + + + + Parsing file + ファイルの解析 + + + + Xz extraction failed. + Xz展開に失敗。 + + + + Sorry, this version of Oracle does not support xz compressed files. + このバージョンのOracleはxz圧縮ファイルをサポートしていません。 + + + + Failed to open Zip archive: %1. + ZIPアーカイブの展開に失敗: %1。 + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + ZIP展開に失敗:Zipアーカイブに含まれるファイルが1つだけではありません。 + + + + Zip extraction failed: %1. + ZIP展開に失敗: %1。 + + + + Sorry, this version of Oracle does not support zipped files. + 申し訳ありませんが現バージョンのOracleはzip形式のファイルをサポートしていません。 + + + + Failed to interpret downloaded data. + + + + + Do you want to download the uncompressed file instead? + 代わりに非圧縮ファイルをダウンロードしますか? + + + + The file was retrieved successfully, but it does not contain any sets data. + ファイルは正常に取得されましたが、カードセットのデータが含まれていませんでした。 + + + + LoadSpoilersPage + + + Save spoiler database + スポイラーデータベースを保存 + + + + XML; spoiler database (*.xml) + XML; スポイラーデータベース (*.xml) + + + + spoiler + + + + + Spoilers import + スポイラーインポート + + + + Please specify a compatible source for spoiler data. + 互換性のあるスポイラーデータのソースを指定してください。 + + + + Download URL: + ダウンロードURL: + + + + Local file: + + + + + Restore default URL + デフォルトのURLを復元 + + + + Choose file... + + + + + The spoiler database will be saved at the following location: + スポイラーデータベースは、以下の場所に保存されます: + + + + Save to a custom path (not recommended) + 別のパスに保存(非推奨) + + + + LoadTokensPage + + + Save token database + トークンデータベースを保存 + + + + XML; token database (*.xml) + XML; トークンデータベース (*.xml) + + + + tokens + + + + + Tokens import + トークンのインポート + + + + Please specify a compatible source for token data. + トークンデータのソースを選択してください。 + + + + Download URL: + ダウンロードURL: + + + + Local file: + + + + + Restore default URL + デフォルトのURLを復元 + + + + Choose file... + + + + + The token database will be saved at the following location: + トークンデータベースは以下の場所に保存されます。 + + + + Save to a custom path (not recommended) + 別のパスに保存(非推奨) + + + + OracleImporter + + + Dummy set containing tokens + ダミーセットを含むトークン + + + + OracleWizard + + + Oracle Importer + Oracle Importer - オラクル・インポーター + + + + OutroPage + + + Finished + 完了しました! + + + + The wizard has finished. + ウィザードが完了しました。 + + + + You can now start using Cockatrice with the newly updated cards. + Cockatriceで新しく更新されたカードを使うことが出来ます。 + + + + If the card databases don't reload automatically, restart the Cockatrice client. + カードデータベースが自動的に再読込されない場合は、Cockatriceを再起動して下さい。 + + + + SaveSetsPage + + + + Error + エラー + + + + No set has been imported. + セットはインポートされませんでした。 + + + + Sets imported + カードセットインポート + + + + A cockatrice database file of %1 MB has been downloaded. + + + + + The following sets have been found: + 次のセットが見つかりました: + + + + Press "Save" to store the imported cards in the Cockatrice database. + ”保存”をクリックするとインポートしたカードをCockatriceデータベースに保存します。 + + + + The card database will be saved at the following location: + カードデータベースは以下の場所に保存されます: + + + + Save to a custom path (not recommended) + 別のパスに保存(非推奨) + + + + &Save + 保存 + + + + Import finished: %1 cards. + %1枚のカードがインポートされました。 + + + + %1: %2 cards imported + %1: %2枚のカードがインポートされました。 + + + + Save card database + カードデータベースを保存 + + + + XML; card database (*.xml) + XML; card database (*.xml) + + + + The file could not be saved to %1 + %1に保存できませんでした。 + + + + SimpleDownloadFilePage + + + Load %1 file + + + + + %1 file (%1) + + + + + + + + + Error + エラー + + + + The provided URL is not valid: + + + + + Downloading (0MB) + ダウンロード中 (0MB) + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + ダウンロード中 (%1MB) + + + + Network error: %1. + ネットワークエラー: %1。 + + + + The file could not be saved to %1 + %1に保存できませんでした。 + + + + UnZip + + + ZIP operation completed successfully. + 正常に完了しました。 + + + + Failed to initialize or load zlib library. + 初期化またはzlibライブラリのロードに失敗しました。 + + + + zlib library error. + zlibライブラリエラー。 + + + + Unable to create or open file. + ファイルが作成または開けませんでした。 + + + + Partially corrupted archive. Some files might be extracted. + アーカイブが部分的に破損しています。いくつかのファイルが抽出されることがあります。 + + + + Corrupted archive. + 破損したアーカイブ。 + + + + Wrong password. + パスワードが間違っています。 + + + + No archive has been created yet. + アーカイブは作成されませんでした。 + + + + File or directory does not exist. + ファイルまたはフォルダが存在しません。 + + + + File read error. + ファイル読み込みエラー。 + + + + File write error. + ファイル書き込みエラー。 + + + + File seek error. + シーク エラー。 + + + + Unable to create a directory. + フォルダが作成できませんでした。 + + + + Invalid device. + 無効なデバイス。 + + + + Invalid or incompatible zip archive. + 無効なまたは互換性のないzipアーカイブ。 + + + + Inconsistent headers. Archive might be corrupted. + 一貫性のないヘッダー。アーカイブが壊れている可能性があります。 + + + + Unknown error. + 不明なエラー。 + + + + Zip + + + ZIP operation completed successfully. + 正常に完了しました。 + + + + Failed to initialize or load zlib library. + 初期化またはzlibライブラリのロードに失敗しました。 + + + + zlib library error. + zlibライブラリエラー。 + + + + Unable to create or open file. + ファイルが作成または開けませんでした。 + + + + No archive has been created yet. + アーカイブは作成されませんでした。 + + + + File or directory does not exist. + ファイルまたはフォルダが存在しません。 + + + + File read error. + ファイル読み込みエラー。 + + + + File write error. + ファイル書き込みエラー。 + + + + File seek error. + シーク エラー。 + + + + Unknown error. + 不明なエラー。 + + + + i18n + + + English + 日本語 (Japanese) + + + + main + + + Only run in spoiler mode + スポイラーモードでのみ起動 + + + + Run in no-confirm background mode + + + + \ No newline at end of file diff --git a/oracle/translations/oracle_ko.ts b/oracle/translations/oracle_ko.ts new file mode 100644 index 000000000..c5c24583f --- /dev/null +++ b/oracle/translations/oracle_ko.ts @@ -0,0 +1,608 @@ + + + IntroPage + + + Introduction + 개요 + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + + + + + Interface language: + + + + + Version: + + + + + LoadSetsPage + + + Source selection + 확장판 목록 파일 주소 입력 + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + + + + + Download URL: + 다운로드 주소: + + + + Local file: + 파일 위치: + + + + Restore default URL + 기본 주소로 복원 + + + + Choose file... + 파일 선택... + + + + Load sets file + 확장판 목록 파일 불러오기 + + + + Sets file (%1) + Sets JSON file (%1) + + + + + + + + + + + Error + 오류 + + + + The provided URL is not valid. + 잘못된 주소를 입력하셨습니다. + + + + Downloading (0MB) + 다운로드 중 (0MB) + + + + Please choose a file. + 확장판 목록 파일을 선택해 주세요. + + + + Cannot open file '%1'. + 파일 '%1'을(를) 열 수 없습니다. + + + + Downloading (%1MB) + 다운로드 중 (%1MB) + + + + Network error: %1. + 네트워크 오류 : %1. + + + + Parsing file + 목록 파싱중 + + + + Xz extraction failed. + + + + + Sorry, this version of Oracle does not support xz compressed files. + + + + + Failed to open Zip archive: %1. + 압축파일 열기 실패 : %1. + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + 압축 풀기 실패 : 압축 파일에 확장판 목록 파일 이외의 파일이 있습니다. + + + + Zip extraction failed: %1. + 압축 풀기 실패 : %1. + + + + Sorry, this version of Oracle does not support zipped files. + 죄송합니다. 본 버전에서는 압축 파일을 지원하지 않습니다. + + + + Failed to interpret downloaded data. + + + + + Do you want to download the uncompressed file instead? + + + + + The file was retrieved successfully, but it does not contain any sets data. + 파일을 성공적으로 다운로드 하였으나 확장판 정보가 들어있지 않습니다. + + + + LoadSpoilersPage + + + Save spoiler database + + + + + XML; spoiler database (*.xml) + + + + + spoiler + + + + + Spoilers import + + + + + Please specify a compatible source for spoiler data. + + + + + Download URL: + + + + + Local file: + + + + + Restore default URL + + + + + Choose file... + + + + + The spoiler database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + LoadTokensPage + + + Save token database + + + + + XML; token database (*.xml) + + + + + tokens + + + + + Tokens import + + + + + Please specify a compatible source for token data. + + + + + Download URL: + 웹 주소: + + + + Local file: + + + + + Restore default URL + 기본 주소로 복원 + + + + Choose file... + + + + + The token database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + OracleImporter + + + Dummy set containing tokens + 토큰 정보가 들어있는 더미 확장판 + + + + OracleWizard + + + Oracle Importer + 오라클 + + + + OutroPage + + + Finished + + + + + The wizard has finished. + + + + + You can now start using Cockatrice with the newly updated cards. + + + + + If the card databases don't reload automatically, restart the Cockatrice client. + + + + + SaveSetsPage + + + + Error + 오류 + + + + No set has been imported. + 아무 확장판도 불러오지 못했습니다. + + + + Sets imported + 확장판 불러오기 완료 + + + + A cockatrice database file of %1 MB has been downloaded. + + + + + The following sets have been found: + + + + + Press "Save" to store the imported cards in the Cockatrice database. + + + + + The card database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + &Save + + + + + Import finished: %1 cards. + 총 %1장의 카드 불러오기 완료 + + + + %1: %2 cards imported + %1에서 %2장의 카드 불러옴 + + + + Save card database + 카드 데이터베이스 저장 + + + + XML; card database (*.xml) + 카드 데이터베이스 XML 파일 (*.xml) + + + + The file could not be saved to %1 + 파일을 %1에 저장 할 수 없습니다. + + + + SimpleDownloadFilePage + + + Load %1 file + + + + + %1 file (%1) + + + + + + + + + Error + + + + + The provided URL is not valid: + + + + + Downloading (0MB) + + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + + + + + Network error: %1. + + + + + The file could not be saved to %1 + + + + + UnZip + + + ZIP operation completed successfully. + 압축파일 작업을 성공적으로 완료하였습니다. + + + + Failed to initialize or load zlib library. + zlib 라이브러리를 초기화하거나 불러올 수 없습니다. + + + + zlib library error. + zlib 라이브러리 오류. + + + + Unable to create or open file. + 파일을 만들거나 열 수 없습니다. + + + + Partially corrupted archive. Some files might be extracted. + 압축파일의 일부가 손상되었습니다. 몇몇 파일은 압축이 풀렸을 수도 있습니다. + + + + Corrupted archive. + 압축파일이 손상되었습니다. + + + + Wrong password. + 비밀번호가 틀렸습니다. + + + + No archive has been created yet. + 압축파일이 아직 생성되지 않았습니다. + + + + File or directory does not exist. + 파일이나 디렉토리가 존재하지 않습니다. + + + + File read error. + 파일 읽기 오류. + + + + File write error. + 파일 쓰기 오류. + + + + File seek error. + 파일 찾기 오류. + + + + Unable to create a directory. + 디렉토리를 생성할 수 없었습니다. + + + + Invalid device. + 잘못된 장치입니다. + + + + Invalid or incompatible zip archive. + 잘못되거나 지원하지 않는 압축파일입니다. + + + + Inconsistent headers. Archive might be corrupted. + 헤더가 손상되었습니다. 압축 파일이 손상됐을 가능성이 있습니다. + + + + Unknown error. + 알 수 없는 오류. + + + + Zip + + + ZIP operation completed successfully. + 압축파일 작업을 성공적으로 완료하였습니다. + + + + Failed to initialize or load zlib library. + zlib 라이브러리를 초기화하거나 불러올 수 없습니다. + + + + zlib library error. + zlib 라이브러리 오류. + + + + Unable to create or open file. + 파일을 만들거나 열 수 없습니다. + + + + No archive has been created yet. + 압축파일이 아직 생성되지 않았습니다. + + + + File or directory does not exist. + 파일이나 디렉토리가 존재하지 않습니다. + + + + File read error. + 파일 읽기 오류. + + + + File write error. + 파일 쓰기 오류. + + + + File seek error. + 파일 찾기 오류. + + + + Unknown error. + 알 수 없는 오류. + + + + i18n + + + English + 한국어 (Korean) + + + + main + + + Only run in spoiler mode + + + + + Run in no-confirm background mode + + + + \ No newline at end of file diff --git a/oracle/translations/oracle_nb.ts b/oracle/translations/oracle_nb.ts new file mode 100644 index 000000000..b40868cc1 --- /dev/null +++ b/oracle/translations/oracle_nb.ts @@ -0,0 +1,608 @@ + + + IntroPage + + + Introduction + Introduksjon + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + + + + + Interface language: + + + + + Version: + + + + + LoadSetsPage + + + Source selection + Kilde valg + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + + + + + Download URL: + Nedlastings URL + + + + Local file: + Lokal fil + + + + Restore default URL + Gjenopprett standard URL + + + + Choose file... + Velg fil... + + + + Load sets file + Last set fil... + + + + Sets file (%1) + Sets JSON file (%1) + + + + + + + + + + + Error + Feil + + + + The provided URL is not valid. + URL du anga er ikke gyldig. + + + + Downloading (0MB) + Laster ned (0MB) + + + + Please choose a file. + Vennligst velg en fil + + + + Cannot open file '%1'. + Kunne ikke åpme '%1' + + + + Downloading (%1MB) + Laster ned (%1MB) + + + + Network error: %1. + Nettverks feil: %1 + + + + Parsing file + Tolker fil. + + + + Xz extraction failed. + + + + + Sorry, this version of Oracle does not support xz compressed files. + + + + + Failed to open Zip archive: %1. + Kunne ikke åpne Zip Arkiv: %1 + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + Zip ekstraksjon feilet: Zip arkivet inneholder ikke nøyaktig en fil + + + + Zip extraction failed: %1. + Zip ekstraksjon feilet: %1 + + + + Sorry, this version of Oracle does not support zipped files. + Beklager, denne versjonen av Oracle støtter ikke Zip filer. + + + + Failed to interpret downloaded data. + + + + + Do you want to download the uncompressed file instead? + + + + + The file was retrieved successfully, but it does not contain any sets data. + Filen ble hentet riktig, men den inneholder ikke noe set data + + + + LoadSpoilersPage + + + Save spoiler database + + + + + XML; spoiler database (*.xml) + + + + + spoiler + + + + + Spoilers import + + + + + Please specify a compatible source for spoiler data. + + + + + Download URL: + + + + + Local file: + + + + + Restore default URL + + + + + Choose file... + + + + + The spoiler database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + LoadTokensPage + + + Save token database + + + + + XML; token database (*.xml) + + + + + tokens + + + + + Tokens import + + + + + Please specify a compatible source for token data. + + + + + Download URL: + Nedlastings URL + + + + Local file: + + + + + Restore default URL + Gjenopprett standard URL + + + + Choose file... + + + + + The token database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + OracleImporter + + + Dummy set containing tokens + Dummy sett som inneholder tokens + + + + OracleWizard + + + Oracle Importer + Oracle importerer + + + + OutroPage + + + Finished + + + + + The wizard has finished. + + + + + You can now start using Cockatrice with the newly updated cards. + + + + + If the card databases don't reload automatically, restart the Cockatrice client. + + + + + SaveSetsPage + + + + Error + Feil + + + + No set has been imported. + Ingen set har blitt importert. + + + + Sets imported + Set importert + + + + A cockatrice database file of %1 MB has been downloaded. + + + + + The following sets have been found: + + + + + Press "Save" to store the imported cards in the Cockatrice database. + + + + + The card database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + &Save + + + + + Import finished: %1 cards. + Importering fullført: %1 kort. + + + + %1: %2 cards imported + %1: %2 kort importert. + + + + Save card database + Lagre kort database. + + + + XML; card database (*.xml) + XML; kort database (*.xml) + + + + The file could not be saved to %1 + Filen kunne ikke lagres til %1 + + + + SimpleDownloadFilePage + + + Load %1 file + + + + + %1 file (%1) + + + + + + + + + Error + + + + + The provided URL is not valid: + + + + + Downloading (0MB) + + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + + + + + Network error: %1. + + + + + The file could not be saved to %1 + + + + + UnZip + + + ZIP operation completed successfully. + Zip operasjonen fullført. + + + + Failed to initialize or load zlib library. + Kunne ikke initialisere eller laste zlib mappen. + + + + zlib library error. + Feil med zlib mappen + + + + Unable to create or open file. + Kunne ikke lage eller åpne fil. + + + + Partially corrupted archive. Some files might be extracted. + Delvis korrupt arkiv. Noen filer kunne ikke bli ekstraktert. + + + + Corrupted archive. + Korrupt arkiv. + + + + Wrong password. + Feil passord + + + + No archive has been created yet. + Det har ikke blitt laget noe arkiv enda. + + + + File or directory does not exist. + Fil eller katalog eksisterer ikke. + + + + File read error. + Kunne ikke lese filen. + + + + File write error. + Kunne ikke skrive fil. + + + + File seek error. + Kunne ikke finne filen. + + + + Unable to create a directory. + Kunne ikke lage katalog. + + + + Invalid device. + Ugyldig apparat. + + + + Invalid or incompatible zip archive. + Ugyldig eller inkompatibelt zip akriv. + + + + Inconsistent headers. Archive might be corrupted. + Ukonsistente overskrifter. Arkivet kan være korrupt. + + + + Unknown error. + Ukjent feil. + + + + Zip + + + ZIP operation completed successfully. + Zip operasjonen fullført. + + + + Failed to initialize or load zlib library. + Kunne ikke initialisere eller laste zlib biblioteket. + + + + zlib library error. + Feil med zlib mappen + + + + Unable to create or open file. + Kunne ikke lage eller åpne fil. + + + + No archive has been created yet. + Det har ikke blitt laget noe arkiv enda. + + + + File or directory does not exist. + Fil eller katalog eksisterer ikke. + + + + File read error. + Kunne ikke lese filen. + + + + File write error. + Kunne ikke skrive fil. + + + + File seek error. + Kunne ikke finne filen. + + + + Unknown error. + Ukjent feil. + + + + i18n + + + English + Norsk Bokmål (Norwegian Bokmål) + + + + main + + + Only run in spoiler mode + + + + + Run in no-confirm background mode + + + + \ No newline at end of file diff --git a/oracle/translations/oracle_nl.ts b/oracle/translations/oracle_nl.ts new file mode 100644 index 000000000..ed9a5fae9 --- /dev/null +++ b/oracle/translations/oracle_nl.ts @@ -0,0 +1,608 @@ + + + IntroPage + + + Introduction + Introductie + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + Deze wizard importeert de lijst met sets, kaarten en tokens die door Cockatrice zullen worden gebruikt. + + + + Interface language: + Interface taal: + + + + Version: + Versie: + + + + LoadSetsPage + + + Source selection + Bron selectie + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + Gelieve een compatibele bron te vermelden voor de lijst van sets en kaarten. U kunt een URL-adres opgeven dat wordt gedownload of een bestaand bestand van uw computer gebruiken. + + + + Download URL: + Download URL: + + + + Local file: + Lokaal bestand + + + + Restore default URL + Herstel standaard URL + + + + Choose file... + Bestand kiezen... + + + + Load sets file + Bestand laden + + + + Sets file (%1) + Sets JSON file (%1) + Sets bestand (%1) + + + + + + + + + + Error + Fout + + + + The provided URL is not valid. + De ingevoerde URL is niet geldig. + + + + Downloading (0MB) + Downloaden (0MB) + + + + Please choose a file. + Gelieve een bestand te kiezen. + + + + Cannot open file '%1'. + Bestand '%1' kan niet geopend worden. + + + + Downloading (%1MB) + Downloaden (%1MB) + + + + Network error: %1. + Netwerk fout: %1. + + + + Parsing file + Parsen van bestand + + + + Xz extraction failed. + Xz extractie mislukt. + + + + Sorry, this version of Oracle does not support xz compressed files. + Sorry, deze versie van Oracle ondersteunt geen xz gecomprimeerde bestanden. + + + + Failed to open Zip archive: %1. + Zip archief kan niet geopend worden: %1. + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + Uitpakken van Zip niet gelukt: het archief moet precies één bestand bevatten. + + + + Zip extraction failed: %1. + Uitpakken van Zip niet gelukt: %1. + + + + Sorry, this version of Oracle does not support zipped files. + Sorry, deze versie van Oracle ondersteunt geen gecomprimeerde bestanden. + + + + Failed to interpret downloaded data. + Lezen van gedownloade data mislukt. + + + + Do you want to download the uncompressed file instead? + Wilt u het ongecomprimeerde bestand downloaden? + + + + The file was retrieved successfully, but it does not contain any sets data. + Het bestand is succesvol binnengehaald, maar bevat geen set data. + + + + LoadSpoilersPage + + + Save spoiler database + Opslaan spoiler database + + + + XML; spoiler database (*.xml) + XML; spoiler database (*.xml) + + + + spoiler + + + + + Spoilers import + Spoilers import + + + + Please specify a compatible source for spoiler data. + Gelieve een compatibele bron voor spoilergegevens te specificeren. + + + + Download URL: + Download URL: + + + + Local file: + + + + + Restore default URL + Herstel standaard URL + + + + Choose file... + + + + + The spoiler database will be saved at the following location: + De spoiler database wordt op de volgende locatie opgeslagen: + + + + Save to a custom path (not recommended) + Opslaan naar een aangepaste locatie (niet aanbevolen) + + + + LoadTokensPage + + + Save token database + token database opslaan + + + + XML; token database (*.xml) + XML; token database (*.xml) + + + + tokens + + + + + Tokens import + Tokens import + + + + Please specify a compatible source for token data. + Gelieve een compatibele bron voor tokengegevens op te geven. + + + + Download URL: + Download URL: + + + + Local file: + + + + + Restore default URL + Herstel standaard URL + + + + Choose file... + + + + + The token database will be saved at the following location: + De token database wordt op de volgende locatie opgeslagen: + + + + Save to a custom path (not recommended) + Opslaan naar een aangepaste locatie (niet aanbevolen) + + + + OracleImporter + + + Dummy set containing tokens + Token voorbeeldset + + + + OracleWizard + + + Oracle Importer + Oracle importer + + + + OutroPage + + + Finished + Klaar + + + + The wizard has finished. + De wizard is klaar. + + + + You can now start using Cockatrice with the newly updated cards. + U kunt nu beginnen met het gebruik van Cockatrice met de nieuw bijgewerkte kaarten. + + + + If the card databases don't reload automatically, restart the Cockatrice client. + Als de kaartendatabases niet automatisch herladen, start dan de Cockatrice client opnieuw op. + + + + SaveSetsPage + + + + Error + Fout + + + + No set has been imported. + Er zijn geen sets geïmporteerd. + + + + Sets imported + Sets geïmporteerd + + + + A cockatrice database file of %1 MB has been downloaded. + Een cockatrice database bestand van %1 MB is gedownload. + + + + The following sets have been found: + De volgende sets zijn gevonden: + + + + Press "Save" to store the imported cards in the Cockatrice database. + Druk op "Opslaan" om de geïmporteerde kaarten op te slaan in de Cockatrice database. + + + + The card database will be saved at the following location: + De kaartendatabase wordt op de volgende locatie opgeslagen: + + + + Save to a custom path (not recommended) + Opslaan naar een aangepaste locatie (niet aanbevolen) + + + + &Save + &Opslaan + + + + Import finished: %1 cards. + Import klaar: %1 kaarten. + + + + %1: %2 cards imported + %1: %2 kaarten geïmporteerd + + + + Save card database + Kaartendatabase opslaan + + + + XML; card database (*.xml) + XML; kaart database (*.xml) + + + + The file could not be saved to %1 + Het bestand kon niet worden opgeslagen in %1 + + + + SimpleDownloadFilePage + + + Load %1 file + + + + + %1 file (%1) + + + + + + + + + Error + Fout + + + + The provided URL is not valid: + De ingevoerde URL is niet geldig: + + + + Downloading (0MB) + Downloaden (0MB) + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + Downloaden (%1MB) + + + + Network error: %1. + Netwerk fout: %1. + + + + The file could not be saved to %1 + Het bestand kon niet worden opgeslagen naar %1 + + + + UnZip + + + ZIP operation completed successfully. + ZIP taak succesvol voltooid. + + + + Failed to initialize or load zlib library. + Zlib library kon niet geladen of geïnitialiseerd worden. + + + + zlib library error. + Zlib library fout. + + + + Unable to create or open file. + Kan het bestand niet openen of aanmaken. + + + + Partially corrupted archive. Some files might be extracted. + Gedeeltelijk beschadigd archief. Sommige bestanden kunnen mogelijk uitgepakt worden. + + + + Corrupted archive. + Beschadigd archief. + + + + Wrong password. + Verkeerd wachtwoord. + + + + No archive has been created yet. + Er is nog geen archief gemaakt. + + + + File or directory does not exist. + Bestand of map bestaat niet. + + + + File read error. + Bestand leesfout. + + + + File write error. + Bestand schrijffout. + + + + File seek error. + Bestand zoekfout. + + + + Unable to create a directory. + Map kon niet aangemaakt worden. + + + + Invalid device. + Ongeldig apparaat. + + + + Invalid or incompatible zip archive. + Ongeldig of incompatibel zip archief. + + + + Inconsistent headers. Archive might be corrupted. + Inconsistente headers. Archief is mogelijk beschadigd. + + + + Unknown error. + Onbekende fout. + + + + Zip + + + ZIP operation completed successfully. + Zip handeling succesvol voltooid. + + + + Failed to initialize or load zlib library. + Initialisatie van zlib library niet gelukt. + + + + zlib library error. + Zlib library fout. + + + + Unable to create or open file. + Kon geen bestand openen of aanmaken. + + + + No archive has been created yet. + Er is nog geen archief gemaakt. + + + + File or directory does not exist. + Bestand of map bestaat niet. + + + + File read error. + Bestand leesfout. + + + + File write error. + Bestand schrijffout. + + + + File seek error. + Bestand zoekfout. + + + + Unknown error. + Onbekende fout. + + + + i18n + + + English + Nederlands (Dutch) + + + + main + + + Only run in spoiler mode + Alleen in spoiler-modus werken + + + + Run in no-confirm background mode + Uitvoeren in achtergrondmodus zonder bevestiging + + + \ No newline at end of file diff --git a/oracle/translations/oracle_pl.ts b/oracle/translations/oracle_pl.ts new file mode 100644 index 000000000..d5d632f70 --- /dev/null +++ b/oracle/translations/oracle_pl.ts @@ -0,0 +1,608 @@ + + + IntroPage + + + Introduction + Wprowadzenie + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + Ten kreator zaimportuje listę dodatków, kart oraz tokenów które zostaną użyte przez Cockatrice. + + + + Interface language: + Język interfejsu: + + + + Version: + Wersja: + + + + LoadSetsPage + + + Source selection + Wybór źródła + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + Proszę podać źródło listy edycji i kart. Można podać adres URL, z którego zostanie pobrana, lub istniejący plik na komputerze. + + + + Download URL: + Pobierz URL + + + + Local file: + Plik lokalny: + + + + Restore default URL + Przywróć domyślny URL + + + + Choose file... + Wybierz plik… + + + + Load sets file + Wczytaj listę dodatków + + + + Sets file (%1) + Sets JSON file (%1) + + + + + + + + + + + Error + Błąd + + + + The provided URL is not valid. + Podano nieprawidłowy URL. + + + + Downloading (0MB) + Pobieranie (0MB) + + + + Please choose a file. + Proszę wybrać plik. + + + + Cannot open file '%1'. + Nie można otworzyć pliku '%1'. + + + + Downloading (%1MB) + Pobieranie (%1MB) + + + + Network error: %1. + Błąd sieci: %1. + + + + Parsing file + Analizowanie pliku + + + + Xz extraction failed. + Rozpakowanie XZ nie udało się. + + + + Sorry, this version of Oracle does not support xz compressed files. + Przepraszamy, ta wersja Oracle nie obsługuje plików spakowanych w archiwum xz. + + + + Failed to open Zip archive: %1. + Otwieranie archiwum Zip zakończone niepowodzeniem: %1. + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + Rozpakowanie pliku Zip nieudane: archiwum nie zawiera dokładnie jednego pliku. + + + + Zip extraction failed: %1. + Rozpakowanie Zip nieudane: %1. + + + + Sorry, this version of Oracle does not support zipped files. + Przepraszamy, ta wersja Oracle nie obsługuje plików spakowanych w archiwum Zip. + + + + Failed to interpret downloaded data. + Interpretacja pobranych danych nie udała się. + + + + Do you want to download the uncompressed file instead? + Czy chcesz zamiast tego pobrać plik nieskompresowany? + + + + The file was retrieved successfully, but it does not contain any sets data. + Plik został pobrany z powodzeniem, ale nie zawiera informacji o dodatkach. + + + + LoadSpoilersPage + + + Save spoiler database + Zapisz bazę danych spoilerów. + + + + XML; spoiler database (*.xml) + XML; baza danych spoilerów (*.xml) + + + + spoiler + + + + + Spoilers import + Import spoilerów + + + + Please specify a compatible source for spoiler data. + Proszę wybrać kompatybilne źródło danych ze spoilerami. + + + + Download URL: + URL pobierania: + + + + Local file: + + + + + Restore default URL + Przywróć domyślny URL + + + + Choose file... + + + + + The spoiler database will be saved at the following location: + Baza danych spoilerów zostanie zapisana w tym miejscu: + + + + Save to a custom path (not recommended) + Zapisz do niestandardowej ścieżki (niezalecane) + + + + LoadTokensPage + + + Save token database + Zapisz bazę danych tokenów. + + + + XML; token database (*.xml) + XML; baza danych tokenów (*.xml) + + + + tokens + + + + + Tokens import + Import tokenów + + + + Please specify a compatible source for token data. + Proszę wybrać kompatybilne źródło danych z tokenami. + + + + Download URL: + Pobierz odnośnik: + + + + Local file: + + + + + Restore default URL + Przywróć domyślny URL + + + + Choose file... + + + + + The token database will be saved at the following location: + Baza danych tokenów zostanie zapisana w tym miejscu: + + + + Save to a custom path (not recommended) + Zapisz do niestandardowej ścieżki (niezalecane) + + + + OracleImporter + + + Dummy set containing tokens + Dodatek-atrapa, zawierający tokeny. + + + + OracleWizard + + + Oracle Importer + Oracle – kreator importu + + + + OutroPage + + + Finished + Zakończone + + + + The wizard has finished. + Kreator zakończył swoją pracę. + + + + You can now start using Cockatrice with the newly updated cards. + Możesz teraz korzystać z Cockatrice z nowo zaktualizowanymi kartami. + + + + If the card databases don't reload automatically, restart the Cockatrice client. + Jeżeli bazy kart nie przeładują się automatycznie, zrestartuj klienta Cockatrice. + + + + SaveSetsPage + + + + Error + Błąd + + + + No set has been imported. + Nie zaimportowano żadnego dodatku. + + + + Sets imported + Zaimportowane dodatki + + + + A cockatrice database file of %1 MB has been downloaded. + Baza danych Cockatrice mająca %1 MB została pobrana. + + + + The following sets have been found: + Następujące dodatki zostały znalezione: + + + + Press "Save" to store the imported cards in the Cockatrice database. + Kliknij "Zapisz" aby zapisać zaimportowane karty w bazie danych Cockatrice. + + + + The card database will be saved at the following location: + Baza danych kart zostanie zapisana w tym miejscu: + + + + Save to a custom path (not recommended) + Zapisz do niestandardowej ścieżki (niezalecane) + + + + &Save + Zapisz + + + + Import finished: %1 cards. + Import zakończony: %1 kart. + + + + %1: %2 cards imported + %1: zaimportowano %2 kart + + + + Save card database + Zapisz bazę kart + + + + XML; card database (*.xml) + XML, baza kart (*.xml) + + + + The file could not be saved to %1 + Plik nie mógł zostać zapisany do %1 + + + + SimpleDownloadFilePage + + + Load %1 file + + + + + %1 file (%1) + + + + + + + + + Error + Błąd + + + + The provided URL is not valid: + + + + + Downloading (0MB) + Pobieranie (0MB) + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + Pobieranie (%1MB) + + + + Network error: %1. + Błąd sieci: %1. + + + + The file could not be saved to %1 + Plik nie mógł zostać zapisany do %1 + + + + UnZip + + + ZIP operation completed successfully. + Operacja ZIP zakończona powodzeniem. + + + + Failed to initialize or load zlib library. + Nieudana inicjalizacja lub załadowanie biblioteki zlib. + + + + zlib library error. + Bląd biblioteki zlib. + + + + Unable to create or open file. + Nie można utworzyć lub otworzyć pliku. + + + + Partially corrupted archive. Some files might be extracted. + Archiwum częściowo uszkodzone. Część plików mogła zostać wypakowana. + + + + Corrupted archive. + Archiwum uszkodzone. + + + + Wrong password. + Nieprawidłowe hasło. + + + + No archive has been created yet. + Nie utworzono jeszcze archiwum. + + + + File or directory does not exist. + Katalog lub plik nie istnieją. + + + + File read error. + Błąd odczytu pliku. + + + + File write error. + Błąd zapisu pliku. + + + + File seek error. + Błąd wyszukania w czasie odczytu pliku. + + + + Unable to create a directory. + Nie można utworzyć katalogu. + + + + Invalid device. + Nieprawidłowe urządzenie. + + + + Invalid or incompatible zip archive. + Nieprawidłowe lub niezgodne archiwum zip. + + + + Inconsistent headers. Archive might be corrupted. + Niespójne nagłówki. Archiwum może być uszkodzone. + + + + Unknown error. + Nieznany błąd. + + + + Zip + + + ZIP operation completed successfully. + Operacja ZIP zakończona powodzeniem. + + + + Failed to initialize or load zlib library. + Nieudana inicjalizacja lub załadowanie biblioteki zlib. + + + + zlib library error. + Błąd biblioteki zlib. + + + + Unable to create or open file. + Nie można utworzyć lub otworzyć pliku. + + + + No archive has been created yet. + Nie utworzono jeszcze archiwum. + + + + File or directory does not exist. + Katalog lub plik nie istnieją. + + + + File read error. + Błąd odczytu pliku. + + + + File write error. + Błąd zapisu pliku. + + + + File seek error. + Błąd wyszukania w czasie odczytu pliku. + + + + Unknown error. + Nieznany błąd. + + + + i18n + + + English + Polski (Polish) + + + + main + + + Only run in spoiler mode + Tylko działaj w trybie spoilerów + + + + Run in no-confirm background mode + + + + \ No newline at end of file diff --git a/oracle/translations/oracle_pt.ts b/oracle/translations/oracle_pt.ts new file mode 100644 index 000000000..c578a0365 --- /dev/null +++ b/oracle/translations/oracle_pt.ts @@ -0,0 +1,608 @@ + + + IntroPage + + + Introduction + Introdução + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + + + + + Interface language: + + + + + Version: + + + + + LoadSetsPage + + + Source selection + Selecção da fonte + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + + + + + Download URL: + URL de download: + + + + Local file: + Ficheiro local: + + + + Restore default URL + URL para repor definições de origem + + + + Choose file... + Escolher ficheiro... + + + + Load sets file + Carregar ficheiro das edições + + + + Sets file (%1) + Sets JSON file (%1) + + + + + + + + + + + Error + Erro + + + + The provided URL is not valid. + O URL fornecido não é válido. + + + + Downloading (0MB) + A efectuar download (0MB) + + + + Please choose a file. + Por favor escolha um ficheiro. + + + + Cannot open file '%1'. + Impossível abrir ficheiro '%1'. + + + + Downloading (%1MB) + A efectuar download (%1MB) + + + + Network error: %1. + Erro da rede: %1. + + + + Parsing file + Ficheiro de análise + + + + Xz extraction failed. + + + + + Sorry, this version of Oracle does not support xz compressed files. + + + + + Failed to open Zip archive: %1. + Abrir archivo zip falhou: %1. + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + Extracção do ZIP falhada: o arquivo ZIP não contem exactamente um ficheiro. + + + + Zip extraction failed: %1. + Extração do Zip falhada: %1. + + + + Sorry, this version of Oracle does not support zipped files. + Pedimos desculpa, mas esta versão do Oracle não suporta ficheiros zipados. + + + + Failed to interpret downloaded data. + + + + + Do you want to download the uncompressed file instead? + + + + + The file was retrieved successfully, but it does not contain any sets data. + O ficheiro foi recuperado com sucesso, mas não contem nenhum dado sobre expansões. + + + + LoadSpoilersPage + + + Save spoiler database + + + + + XML; spoiler database (*.xml) + + + + + spoiler + + + + + Spoilers import + + + + + Please specify a compatible source for spoiler data. + + + + + Download URL: + + + + + Local file: + + + + + Restore default URL + + + + + Choose file... + + + + + The spoiler database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + LoadTokensPage + + + Save token database + + + + + XML; token database (*.xml) + + + + + tokens + + + + + Tokens import + + + + + Please specify a compatible source for token data. + + + + + Download URL: + URL de download: + + + + Local file: + + + + + Restore default URL + URL para repor definições de origem + + + + Choose file... + + + + + The token database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + OracleImporter + + + Dummy set containing tokens + Set básico contendo fichas + + + + OracleWizard + + + Oracle Importer + Importar Oracle + + + + OutroPage + + + Finished + + + + + The wizard has finished. + + + + + You can now start using Cockatrice with the newly updated cards. + + + + + If the card databases don't reload automatically, restart the Cockatrice client. + + + + + SaveSetsPage + + + + Error + Erro + + + + No set has been imported. + Nenhuma expansão foi importada. + + + + Sets imported + Edições importadas + + + + A cockatrice database file of %1 MB has been downloaded. + + + + + The following sets have been found: + + + + + Press "Save" to store the imported cards in the Cockatrice database. + + + + + The card database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + &Save + + + + + Import finished: %1 cards. + Importação terminada: %1 cartas. + + + + %1: %2 cards imported + %1. %2 cartas importadas + + + + Save card database + Guardar base de dados das cartas + + + + XML; card database (*.xml) + XML; Base de dados de cartas (*.xml) + + + + The file could not be saved to %1 + O ficheiro não pode ser gravado em %1 + + + + SimpleDownloadFilePage + + + Load %1 file + + + + + %1 file (%1) + + + + + + + + + Error + + + + + The provided URL is not valid: + + + + + Downloading (0MB) + + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + + + + + Network error: %1. + + + + + The file could not be saved to %1 + + + + + UnZip + + + ZIP operation completed successfully. + Operação ZIP completa com sucesso. + + + + Failed to initialize or load zlib library. + Falha na inicialização ou carregamento da biblioteca zlib + + + + zlib library error. + Erro da biblioteca zlib + + + + Unable to create or open file. + Incapaz de criar ou abrir ficheiro + + + + Partially corrupted archive. Some files might be extracted. + Arquivo parcialmente corrompido. Alguns ficheiros possivelmente serão extraídos. + + + + Corrupted archive. + Archivo corrompido. + + + + Wrong password. + Senha incorrecta. + + + + No archive has been created yet. + Ainda não foi criado nenhum arquivo + + + + File or directory does not exist. + O ficheiro ou directório não existe. + + + + File read error. + Erro na leitura do ficheiro. + + + + File write error. + erro na escrita do ficheiro. + + + + File seek error. + Erro na procura do ficheiro. + + + + Unable to create a directory. + Incapaz de criar directório. + + + + Invalid device. + Dispositivo inválido. + + + + Invalid or incompatible zip archive. + Arquivo ZIP inválido ou incompatível + + + + Inconsistent headers. Archive might be corrupted. + Cabeçalhos inconsistentes. O arquivo pode estar corrompido, + + + + Unknown error. + Erro desconhecido. + + + + Zip + + + ZIP operation completed successfully. + Operação ZIP completa com sucesso. + + + + Failed to initialize or load zlib library. + Falha na inicialização ou carregamento da biblioteca zlib + + + + zlib library error. + Erro da biblioteca zlib + + + + Unable to create or open file. + Incapaz de criar ou abrir ficheiro + + + + No archive has been created yet. + Ainda não foi criado nenhum arquivo + + + + File or directory does not exist. + O ficheiro ou directório não existe. + + + + File read error. + Erro na leitura do ficheiro. + + + + File write error. + erro na escrita do ficheiro. + + + + File seek error. + Erro na procura do ficheiro. + + + + Unknown error. + Erro desconhecido. + + + + i18n + + + English + Português (Portuguese) + + + + main + + + Only run in spoiler mode + + + + + Run in no-confirm background mode + + + + \ No newline at end of file diff --git a/oracle/translations/oracle_pt_BR.ts b/oracle/translations/oracle_pt_BR.ts new file mode 100644 index 000000000..96a6382ec --- /dev/null +++ b/oracle/translations/oracle_pt_BR.ts @@ -0,0 +1,608 @@ + + + IntroPage + + + Introduction + Introdução + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + Esse recurso irá importar a lista de coleções, cartas e fichas que serão utilizadas pelo Cokatrice. + + + + Interface language: + Idioma da interface: + + + + Version: + Versão: + + + + LoadSetsPage + + + Source selection + Selecione a origem + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + Por favor especifique uma fonte compatível de expansões e cartas. Você pode especificar um endereço URL que será baixado ou usar um arquivo existente no seu computador. + + + + Download URL: + Endereço para download: + + + + Local file: + Arquivo local: + + + + Restore default URL + Restaurar URL padrão + + + + Choose file... + Escolha o arquivo... + + + + Load sets file + Carregar arquivos de expansão + + + + Sets file (%1) + Sets JSON file (%1) + Arquivo de expansões (%1) + + + + + + + + + + Error + Erro + + + + The provided URL is not valid. + A URL escolhida não é valida + + + + Downloading (0MB) + Baixando (0MB) + + + + Please choose a file. + Por favor, escolha um arquivo; + + + + Cannot open file '%1'. + Não pode abrir arquivo '%1' + + + + Downloading (%1MB) + Baixando (%1MB) + + + + Network error: %1. + Erro na conexão com a internet: %1. + + + + Parsing file + Analisando arquivo + + + + Xz extraction failed. + Extração de arquivo .xz falhou + + + + Sorry, this version of Oracle does not support xz compressed files. + Desculpe, esta versão do Oracle não suporta arquivos comprimidos no formato .xz + + + + Failed to open Zip archive: %1. + Falhou em abrir o arquivo Zip: %1. + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + A extração do arquivo ZIP falhou: o arquivo ZIP não contém exatamente um arquivo. + + + + Zip extraction failed: %1. + Extração do arquivo Zip falhou: %1. + + + + Sorry, this version of Oracle does not support zipped files. + Desculpe, esta versão do Oracle não suporta arquivos compactados. + + + + Failed to interpret downloaded data. + Falha ao interpretar dados baixados + + + + Do you want to download the uncompressed file instead? + Gostaria de baixar os arquivos descomprimidos, em vez disso? + + + + The file was retrieved successfully, but it does not contain any sets data. + O arquivo foi recebido com sucesso, mas não contém qualquer informação de expansão. + + + + LoadSpoilersPage + + + Save spoiler database + Salvar base de dados de spoiler + + + + XML; spoiler database (*.xml) + XML; base de dados de spoilers (*.xml) + + + + spoiler + + + + + Spoilers import + Importar spoilers + + + + Please specify a compatible source for spoiler data. + Por favor, selecione uma fonte compatível para os dados de spoilers. + + + + Download URL: + Endereço para download: + + + + Local file: + + + + + Restore default URL + Restaurar URL padrão + + + + Choose file... + + + + + The spoiler database will be saved at the following location: + A base de dados de spoilers será salva no seguinte diretório: + + + + Save to a custom path (not recommended) + Salvar em diretório customizado (não recomendado) + + + + LoadTokensPage + + + Save token database + Salvar base de dados de tokens + + + + XML; token database (*.xml) + XML; base de dados de token (*.xml) + + + + tokens + + + + + Tokens import + Importar tokens + + + + Please specify a compatible source for token data. + Por favor especifique uma fonte compatível para os dados de tokens. + + + + Download URL: + Endereço para download: + + + + Local file: + + + + + Restore default URL + Restaurar URL padrão + + + + Choose file... + + + + + The token database will be saved at the following location: + A base de dados de tokens será salva no seguinte diretório: + + + + Save to a custom path (not recommended) + Salvar em diretório customizado (não recomendado) + + + + OracleImporter + + + Dummy set containing tokens + Esta expansão contém fichas. + + + + OracleWizard + + + Oracle Importer + Importador Oracle + + + + OutroPage + + + Finished + Finalizado + + + + The wizard has finished. + O recurso terminou. + + + + You can now start using Cockatrice with the newly updated cards. + Agora você pode começar a usar o Cockatrice com as novas cartas atualizadas. + + + + If the card databases don't reload automatically, restart the Cockatrice client. + Se a base de dados das cartas não for atualizada automaticamente, reinicie o cliente do Cockatrice. + + + + SaveSetsPage + + + + Error + Erro + + + + No set has been imported. + O set não foi importado. + + + + Sets imported + Expansões importadas + + + + A cockatrice database file of %1 MB has been downloaded. + Um arquivo de %1 MB de banco de dados do cockatrice foi baixado. + + + + The following sets have been found: + As seguintes expansões foram encontradas: + + + + Press "Save" to store the imported cards in the Cockatrice database. + Aperte "Salvar" para guardar as cartas importadas na base de dados do Cockatrice. + + + + The card database will be saved at the following location: + A base de dados de cartas será salva no seguinte diretório: + + + + Save to a custom path (not recommended) + Salvar em diretório customizado (não recomendado) + + + + &Save + &Salvar + + + + Import finished: %1 cards. + Importação finalizada: %1 cartas. + + + + %1: %2 cards imported + %1: %2 cartas importadas + + + + Save card database + Carta salva no banco de dados + + + + XML; card database (*.xml) + XML; banco de dados de cartas (*.xml) + + + + The file could not be saved to %1 + O arquivo não pode ser salvo para %1 + + + + SimpleDownloadFilePage + + + Load %1 file + + + + + %1 file (%1) + + + + + + + + + Error + Erro + + + + The provided URL is not valid: + + + + + Downloading (0MB) + Baixando (0MB) + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + Baixando (%1MB) + + + + Network error: %1. + Erro de rede: %1 + + + + The file could not be saved to %1 + O arquivo não pode ser salvo em %1 + + + + UnZip + + + ZIP operation completed successfully. + Operação de compactação concluída. + + + + Failed to initialize or load zlib library. + Falha ao inicializar ou carregar a biblioteca zlib. + + + + zlib library error. + Erro na biblioteca zlib. + + + + Unable to create or open file. + Não foi possível criar ou abrir o arquivo. + + + + Partially corrupted archive. Some files might be extracted. + Arquivo parcialmente corrompido. Alguns arquivos podem ter sido extraídos. + + + + Corrupted archive. + Arquivo corrompido. + + + + Wrong password. + Senha incorreta. + + + + No archive has been created yet. + Nenhum arquivo foi criado ainda. + + + + File or directory does not exist. + Arquivo ou diretório não existe. + + + + File read error. + Erro de leitura do arquivo. + + + + File write error. + Erro de gravação do arquivo. + + + + File seek error. + Erro ao buscar no arquivo + + + + Unable to create a directory. + Não foi possível criar um diretório. + + + + Invalid device. + Dispositivo inválido. + + + + Invalid or incompatible zip archive. + Arquivo ZIP inválido ou incompatível. + + + + Inconsistent headers. Archive might be corrupted. + Cabeçalhos inconsistentes. O arquivo pode estar corrompido. + + + + Unknown error. + Erro desconhecido. + + + + Zip + + + ZIP operation completed successfully. + Operação de compactação concluída com sucesso. + + + + Failed to initialize or load zlib library. + Falha ao inicializar ou carregar a biblioteca zlib. + + + + zlib library error. + Erro na biblioteca zlib. + + + + Unable to create or open file. + Não foi possível criar ou abrir o arquivo. + + + + No archive has been created yet. + Nenhum arquivo foi criado ainda. + + + + File or directory does not exist. + Arquivo ou diretório não existe. + + + + File read error. + Erro de leitura do arquivo. + + + + File write error. + Erro de gravação do arquivo. + + + + File seek error. + Erro ao buscar no arquivo + + + + Unknown error. + Erro desconhecido. + + + + i18n + + + English + Português do Brasil (Brazilian Portuguese) + + + + main + + + Only run in spoiler mode + Rodar apenas em modo de 'spoiler' + + + + Run in no-confirm background mode + + + + \ No newline at end of file diff --git a/oracle/translations/oracle_ru.ts b/oracle/translations/oracle_ru.ts new file mode 100644 index 000000000..e2ff5b5a0 --- /dev/null +++ b/oracle/translations/oracle_ru.ts @@ -0,0 +1,608 @@ + + + IntroPage + + + Introduction + Введение + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + Эта программа импортирует перечень выпусков, карт и фишек, которые будут использоваться в Cockatrice. + + + + Interface language: + Язык интерфейса: + + + + Version: + Версия + + + + LoadSetsPage + + + Source selection + Выбор источника + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + Пожалуйста, укажите источник для перечня сетов и карт. Вы можете использовать ссылку или уже существующий локальный файл. + + + + Download URL: + Ссылка на скачивание: + + + + Local file: + Локальный файл: + + + + Restore default URL + Восстановить ссылку по умолчанию + + + + Choose file... + Выбрать файл.. + + + + Load sets file + Загрузить файл сетов + + + + Sets file (%1) + Sets JSON file (%1) + Файл (%1) + + + + + + + + + + Error + Ошибка + + + + The provided URL is not valid. + Предоставленная ссылка некорректна. + + + + Downloading (0MB) + Загрузка (0MB) + + + + Please choose a file. + Пожалуйста, выберите файл. + + + + Cannot open file '%1'. + Не удалось открыть файл '%1'. + + + + Downloading (%1MB) + Загрузка (%1MB) + + + + Network error: %1. + Ошибка сети: %1. + + + + Parsing file + Ожидание файла + + + + Xz extraction failed. + Ошибка распаковки архива + + + + Sorry, this version of Oracle does not support xz compressed files. + Данная версия Oracle не поддерживает архивы xz + + + + Failed to open Zip archive: %1. + Не удалось загрузить zip-архив: %1. + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + Ошибка распаковки: zip-архив содержит больше одного файла. + + + + Zip extraction failed: %1. + Ошибка распаковки zip-архива: %1. + + + + Sorry, this version of Oracle does not support zipped files. + Данная версия Oracle не поддерживает zip-архивы. + + + + Failed to interpret downloaded data. + Ошибка в обработке загруженных данных + + + + Do you want to download the uncompressed file instead? + Хотите вместо этого загрузить новую несжатую копию? + + + + The file was retrieved successfully, but it does not contain any sets data. + Файл успешно получен, но в нем не содержится данных о сетах. + + + + LoadSpoilersPage + + + Save spoiler database + Сохранить базу карт + + + + XML; spoiler database (*.xml) + XML; база карт (*.xml) + + + + spoiler + + + + + Spoilers import + Импорт карт + + + + Please specify a compatible source for spoiler data. + Укажите совместимый ресурс для импорта базы карт + + + + Download URL: + Ссылка на скачивание: + + + + Local file: + + + + + Restore default URL + Восстановить ссылку по умолчанию + + + + Choose file... + + + + + The spoiler database will be saved at the following location: + База карт будет сохранена в следующей директории: + + + + Save to a custom path (not recommended) + + + + + LoadTokensPage + + + Save token database + + + + + XML; token database (*.xml) + + + + + tokens + + + + + Tokens import + + + + + Please specify a compatible source for token data. + + + + + Download URL: + Ссылка на скачивание: + + + + Local file: + + + + + Restore default URL + Восстановить ссылку по умолчанию + + + + Choose file... + + + + + The token database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + OracleImporter + + + Dummy set containing tokens + Пример сета с фишками + + + + OracleWizard + + + Oracle Importer + Импортер Oracle + + + + OutroPage + + + Finished + + + + + The wizard has finished. + + + + + You can now start using Cockatrice with the newly updated cards. + + + + + If the card databases don't reload automatically, restart the Cockatrice client. + + + + + SaveSetsPage + + + + Error + Ошибка + + + + No set has been imported. + Не было импортировано ни одного сета. + + + + Sets imported + Импортировано сетов + + + + A cockatrice database file of %1 MB has been downloaded. + + + + + The following sets have been found: + + + + + Press "Save" to store the imported cards in the Cockatrice database. + + + + + The card database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + &Save + Сохранить + + + + Import finished: %1 cards. + Импорт завершен: %1 карт. + + + + %1: %2 cards imported + %1: %2 карт импортировано + + + + Save card database + Сохранить базу карт + + + + XML; card database (*.xml) + База карт (*.xml) + + + + The file could not be saved to %1 + Не удалось сохранить файл в %1 + + + + SimpleDownloadFilePage + + + Load %1 file + + + + + %1 file (%1) + + + + + + + + + Error + + + + + The provided URL is not valid: + + + + + Downloading (0MB) + + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + + + + + Network error: %1. + + + + + The file could not be saved to %1 + + + + + UnZip + + + ZIP operation completed successfully. + Распаковка успешно завершена. + + + + Failed to initialize or load zlib library. + Не удалось загрузить библиотеку .zlib. + + + + zlib library error. + ошибка библиотеки .zlib + + + + Unable to create or open file. + Не удалось создать или открыть файл. + + + + Partially corrupted archive. Some files might be extracted. + Архив частично поврежден. Некоторые файлы могут быть извлечены. + + + + Corrupted archive. + Архив поврежден. + + + + Wrong password. + Неверный пароль. + + + + No archive has been created yet. + Не было создано ни одного архива. + + + + File or directory does not exist. + Файл или директория не существуют. + + + + File read error. + Ошибка чтения файла. + + + + File write error. + Ошибка записи файла. + + + + File seek error. + Не удалось найти файл. + + + + Unable to create a directory. + Не удалось создать директорию. + + + + Invalid device. + Неверное устройство. + + + + Invalid or incompatible zip archive. + Неверный или несовместимый zip-архив. + + + + Inconsistent headers. Archive might be corrupted. + Несовместимые заголовки. Архив может быть поврежден. + + + + Unknown error. + Неизвестная ошибка. + + + + Zip + + + ZIP operation completed successfully. + Распаковка успешно завершена. + + + + Failed to initialize or load zlib library. + Не удалось загрузить библиотеку .zlib. + + + + zlib library error. + ошибка библиотеки .zlib + + + + Unable to create or open file. + Не удалось создать или открыть файл. + + + + No archive has been created yet. + Не было создано ни одного архива. + + + + File or directory does not exist. + Файл или директория не существуют. + + + + File read error. + Ошибка чтения файла. + + + + File write error. + Ошибка записи файла. + + + + File seek error. + Не удалось найти файл. + + + + Unknown error. + Неизвестная ошибка. + + + + i18n + + + English + Русский (Russian) + + + + main + + + Only run in spoiler mode + Запускать только в режиме сетов + + + + Run in no-confirm background mode + + + + \ No newline at end of file diff --git a/oracle/translations/oracle_sr.ts b/oracle/translations/oracle_sr.ts new file mode 100644 index 000000000..04d48688a --- /dev/null +++ b/oracle/translations/oracle_sr.ts @@ -0,0 +1,608 @@ + + + IntroPage + + + Introduction + Uvod + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + + + + + Interface language: + + + + + Version: + + + + + LoadSetsPage + + + Source selection + Odabir izvora + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + + + + + Download URL: + URL za preuzimanje: + + + + Local file: + Lokalni fajl: + + + + Restore default URL + Povrati uobičajeni URL + + + + Choose file... + Izaberite fajl... + + + + Load sets file + + + + + Sets file (%1) + Sets JSON file (%1) + + + + + + + + + + + Error + Greška + + + + The provided URL is not valid. + Dati URL nije važeći. + + + + Downloading (0MB) + Preuzimanje (0MB) + + + + Please choose a file. + Molimo Vas izaberite fajl. + + + + Cannot open file '%1'. + Nemoguće otvori fajl '%1'. + + + + Downloading (%1MB) + Preuzimanje (%1MB) + + + + Network error: %1. + Mrežna greška: %1. + + + + Parsing file + Parsiranje fajla + + + + Xz extraction failed. + + + + + Sorry, this version of Oracle does not support xz compressed files. + + + + + Failed to open Zip archive: %1. + Neuspeh u otvaranju Zip arhive: %1. + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + + + + + Zip extraction failed: %1. + + + + + Sorry, this version of Oracle does not support zipped files. + Izvinite, ova verzija Oracle-a ne podržava zip fajlove. + + + + Failed to interpret downloaded data. + + + + + Do you want to download the uncompressed file instead? + + + + + The file was retrieved successfully, but it does not contain any sets data. + + + + + LoadSpoilersPage + + + Save spoiler database + + + + + XML; spoiler database (*.xml) + + + + + spoiler + + + + + Spoilers import + + + + + Please specify a compatible source for spoiler data. + + + + + Download URL: + + + + + Local file: + + + + + Restore default URL + + + + + Choose file... + + + + + The spoiler database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + LoadTokensPage + + + Save token database + + + + + XML; token database (*.xml) + + + + + tokens + + + + + Tokens import + + + + + Please specify a compatible source for token data. + + + + + Download URL: + URL za preuzimanje: + + + + Local file: + + + + + Restore default URL + + + + + Choose file... + + + + + The token database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + OracleImporter + + + Dummy set containing tokens + + + + + OracleWizard + + + Oracle Importer + + + + + OutroPage + + + Finished + + + + + The wizard has finished. + + + + + You can now start using Cockatrice with the newly updated cards. + + + + + If the card databases don't reload automatically, restart the Cockatrice client. + + + + + SaveSetsPage + + + + Error + Greška + + + + No set has been imported. + + + + + Sets imported + + + + + A cockatrice database file of %1 MB has been downloaded. + + + + + The following sets have been found: + + + + + Press "Save" to store the imported cards in the Cockatrice database. + + + + + The card database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + &Save + + + + + Import finished: %1 cards. + + + + + %1: %2 cards imported + + + + + Save card database + + + + + XML; card database (*.xml) + + + + + The file could not be saved to %1 + + + + + SimpleDownloadFilePage + + + Load %1 file + + + + + %1 file (%1) + + + + + + + + + Error + + + + + The provided URL is not valid: + + + + + Downloading (0MB) + + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + + + + + Network error: %1. + + + + + The file could not be saved to %1 + + + + + UnZip + + + ZIP operation completed successfully. + + + + + Failed to initialize or load zlib library. + + + + + zlib library error. + + + + + Unable to create or open file. + + + + + Partially corrupted archive. Some files might be extracted. + + + + + Corrupted archive. + + + + + Wrong password. + Pogrešna lozinka. + + + + No archive has been created yet. + + + + + File or directory does not exist. + + + + + File read error. + Greška u čitanju fajla. + + + + File write error. + Greška u pisanju fajla. + + + + File seek error. + Greška u traženju fajla. + + + + Unable to create a directory. + + + + + Invalid device. + + + + + Invalid or incompatible zip archive. + + + + + Inconsistent headers. Archive might be corrupted. + + + + + Unknown error. + Nepoznata greška. + + + + Zip + + + ZIP operation completed successfully. + + + + + Failed to initialize or load zlib library. + + + + + zlib library error. + + + + + Unable to create or open file. + Nemoguće napraviti ili otvoriti fajl. + + + + No archive has been created yet. + + + + + File or directory does not exist. + + + + + File read error. + Greška u čitanju fajla. + + + + File write error. + Greška u pisanju fajla. + + + + File seek error. + Greška u traženju fajla. + + + + Unknown error. + Nepoznata greška. + + + + i18n + + + English + Srpski (Serbian) + + + + main + + + Only run in spoiler mode + + + + + Run in no-confirm background mode + + + + \ No newline at end of file diff --git a/oracle/translations/oracle_sv.ts b/oracle/translations/oracle_sv.ts new file mode 100644 index 000000000..89979872d --- /dev/null +++ b/oracle/translations/oracle_sv.ts @@ -0,0 +1,398 @@ + + + IntroPage + + + Introduction + + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + + + + + Interface language: + + + + + Version: + Version: + + + + LoadSetsPage + + + Source selection + + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + + + + + Download URL: + + + + + Local file: + + + + + Restore default URL + + + + + Choose file... + + + + + Load sets file + + + + + Sets JSON file (%1) + + + + + + + + + + Error + + + + + The provided URL is not valid. + + + + + Downloading (0MB) + + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + + + + + Network error: %1. + + + + + Parsing file + + + + + Xz extraction failed. + + + + + Sorry, this version of Oracle does not support xz compressed files. + + + + + Failed to open Zip archive: %1. + + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + + + + + Zip extraction failed: %1. + + + + + Sorry, this version of Oracle does not support zipped files. + + + + + Do you want to download the uncompressed file instead? + + + + + The file was retrieved successfully, but it does not contain any sets data. + + + + + LoadSpoilersPage + + + Save spoiler database + + + + + XML; spoiler database (*.xml) + + + + + Spoilers import + + + + + Please specify a compatible source for spoiler data. + + + + + Download URL: + + + + + Restore default URL + + + + + The spoiler database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + LoadTokensPage + + + Save token database + + + + + XML; token database (*.xml) + + + + + Tokens import + + + + + Please specify a compatible source for token data. + + + + + Download URL: + + + + + Restore default URL + + + + + The token database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + OracleImporter + + + Dummy set containing tokens + + + + + OracleWizard + + + Oracle Importer + + + + + OutroPage + + + Finished + + + + + The wizard has finished. + + + + + You can now start using Cockatrice with the newly updated cards. + + + + + If the card databases don't reload automatically, restart the Cockatrice client. + + + + + SaveSetsPage + + + + Error + + + + + No set has been imported. + + + + + Sets imported + + + + + The following sets have been found: + + + + + Press "Save" to store the imported cards in the Cockatrice database. + + + + + The card database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + &Save + + + + + Import finished: %1 cards. + + + + + %1: %2 cards imported + + + + + Save card database + + + + + XML; card database (*.xml) + + + + + The file could not be saved to %1 + + + + + SimpleDownloadFilePage + + + + + Error + + + + + The provided URL is not valid. + + + + + Downloading (0MB) + + + + + Downloading (%1MB) + + + + + Network error: %1. + + + + + The file could not be saved to %1 + + + + + i18n + + + English + Svenska (Swedish) + + + + main + + + Only run in spoiler mode + + + + \ No newline at end of file diff --git a/oracle/translations/oracle_tr.ts b/oracle/translations/oracle_tr.ts new file mode 100644 index 000000000..517f9248e --- /dev/null +++ b/oracle/translations/oracle_tr.ts @@ -0,0 +1,608 @@ + + + IntroPage + + + Introduction + Açıklama + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + Bu sihirbaz Cockatrice tarafından kullanılacak setlerin, kartların ve tokenlerin listesini içe aktaracaktır. + + + + Interface language: + Arayüz dili: + + + + Version: + Sürüm: + + + + LoadSetsPage + + + Source selection + Kaynak seçimi + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + Lütfen set ve kart listesi için uyumlu bir kaynak belirtin. İndirilecek bir URL adresi belirtebilir veya bilgisayarınızdaki mevcut bir dosyayı kullanabilirsiniz. + + + + Download URL: + İndirme Bağlantısı: + + + + Local file: + Yerel dosya: + + + + Restore default URL + Varsayılan URL'yi geri yükle + + + + Choose file... + Dosya seç... + + + + Load sets file + Set dosyasını yükle + + + + Sets file (%1) + Sets JSON file (%1) + + + + + + + + + + + Error + Hata + + + + The provided URL is not valid. + Paylaşılan bağlantı geçerli değil. + + + + Downloading (0MB) + İndiriliyor (0MB) + + + + Please choose a file. + Lütfen dosya seçin. + + + + Cannot open file '%1'. + Dosya açılamıyor '1%'. + + + + Downloading (%1MB) + İndiriliyor (%1MB) + + + + Network error: %1. + Bağlantı hatası: %1 + + + + Parsing file + Dosya ayrıştırılıyor + + + + Xz extraction failed. + Xz çıkarma işlemi başarısız oldu. + + + + Sorry, this version of Oracle does not support xz compressed files. + Maalesef, Oracle'ın bu sürümü xz sıkıştırılmış dosyaları desteklemiyor. + + + + Failed to open Zip archive: %1. + Zip dosyası açılamadı: %1. + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + Zip çıkarma işlemi başarısız oldu: Zip arşivi tam olarak bir dosya içermiyor. + + + + Zip extraction failed: %1. + Zip çıkarma işlemi başarısız oldu: %1. + + + + Sorry, this version of Oracle does not support zipped files. + Maalesef, Oracle'ın bu sürümü zip dosyalarını desteklemiyor. + + + + Failed to interpret downloaded data. + İndirilen veriler işlenemedi. + + + + Do you want to download the uncompressed file instead? + Bunun yerine sıkıştırılmamış dosyayı indirmek ister misiniz? + + + + The file was retrieved successfully, but it does not contain any sets data. + Dosya başarıyla alındı, ancak herhangi bir set verisi içermiyor. + + + + LoadSpoilersPage + + + Save spoiler database + Spoiler veritabanını kaydet + + + + XML; spoiler database (*.xml) + XML; spoiler veritabanı (*.xml) + + + + spoiler + + + + + Spoilers import + Spoilers içe aktar + + + + Please specify a compatible source for spoiler data. + Lütfen spoiler verileri için uyumlu bir kaynak belirtin. + + + + Download URL: + İndirme Bağlantısı: + + + + Local file: + + + + + Restore default URL + Varsayılan bağlantıyı geri yükle + + + + Choose file... + + + + + The spoiler database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + LoadTokensPage + + + Save token database + + + + + XML; token database (*.xml) + + + + + tokens + + + + + Tokens import + + + + + Please specify a compatible source for token data. + + + + + Download URL: + + + + + Local file: + + + + + Restore default URL + + + + + Choose file... + + + + + The token database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + OracleImporter + + + Dummy set containing tokens + + + + + OracleWizard + + + Oracle Importer + + + + + OutroPage + + + Finished + + + + + The wizard has finished. + + + + + You can now start using Cockatrice with the newly updated cards. + + + + + If the card databases don't reload automatically, restart the Cockatrice client. + + + + + SaveSetsPage + + + + Error + + + + + No set has been imported. + + + + + Sets imported + + + + + A cockatrice database file of %1 MB has been downloaded. + + + + + The following sets have been found: + + + + + Press "Save" to store the imported cards in the Cockatrice database. + + + + + The card database will be saved at the following location: + + + + + Save to a custom path (not recommended) + + + + + &Save + + + + + Import finished: %1 cards. + + + + + %1: %2 cards imported + + + + + Save card database + + + + + XML; card database (*.xml) + + + + + The file could not be saved to %1 + + + + + SimpleDownloadFilePage + + + Load %1 file + + + + + %1 file (%1) + + + + + + + + + Error + + + + + The provided URL is not valid: + + + + + Downloading (0MB) + + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + + + + + Network error: %1. + + + + + The file could not be saved to %1 + + + + + UnZip + + + ZIP operation completed successfully. + + + + + Failed to initialize or load zlib library. + + + + + zlib library error. + + + + + Unable to create or open file. + + + + + Partially corrupted archive. Some files might be extracted. + + + + + Corrupted archive. + + + + + Wrong password. + + + + + No archive has been created yet. + + + + + File or directory does not exist. + + + + + File read error. + + + + + File write error. + + + + + File seek error. + + + + + Unable to create a directory. + + + + + Invalid device. + + + + + Invalid or incompatible zip archive. + + + + + Inconsistent headers. Archive might be corrupted. + + + + + Unknown error. + + + + + Zip + + + ZIP operation completed successfully. + + + + + Failed to initialize or load zlib library. + + + + + zlib library error. + + + + + Unable to create or open file. + + + + + No archive has been created yet. + + + + + File or directory does not exist. + + + + + File read error. + + + + + File write error. + + + + + File seek error. + + + + + Unknown error. + + + + + i18n + + + English + Türkçe (Turkish) + + + + main + + + Only run in spoiler mode + + + + + Run in no-confirm background mode + + + + \ No newline at end of file diff --git a/oracle/translations/oracle_yue.ts b/oracle/translations/oracle_yue.ts new file mode 100644 index 000000000..13a621b9e --- /dev/null +++ b/oracle/translations/oracle_yue.ts @@ -0,0 +1,608 @@ + + + IntroPage + + + Introduction + 介绍 + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + 此嚮導將會導入Cockatrice中用到的系列,卡牌和衍生物列表。 + + + + Interface language: + 介面語言: + + + + Version: + 版本: + + + + LoadSetsPage + + + Source selection + 選擇來源 + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + 請選擇合適的牌組和卡片清單來源。您可以輸入下載URL或使用電腦中已有的檔案。 + + + + Download URL: + 下载URL: + + + + Local file: + 本地檔案: + + + + Restore default URL + 恢復常用URL + + + + Choose file... + 選擇檔案... + + + + Load sets file + 载入牌组檔案 + + + + Sets file (%1) + Sets JSON file (%1) + + + + + + + + + + + Error + 出現錯誤 + + + + The provided URL is not valid. + 所提供的url無效. + + + + Downloading (0MB) + 下載中 (0MB) + + + + Please choose a file. + 請選擇檔案 + + + + Cannot open file '%1'. + 不能開啟檔案 '%1 + + + + Downloading (%1MB) + 下載中 (%1MB) + + + + Network error: %1. + 網路出錯: %1. + + + + Parsing file + 分析檔案中 + + + + Xz extraction failed. + xz解壓縮失敗. + + + + Sorry, this version of Oracle does not support xz compressed files. + 很抱歉,現前Oracle版本不支援xc壓縮檔案 + + + + Failed to open Zip archive: %1. + 未能開啟壓縮文件: %1. + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + 解壓縮失敗:這壓縮文件擁有超過一個檔案. + + + + Zip extraction failed: %1. + 解壓縮失敗: %1. + + + + Sorry, this version of Oracle does not support zipped files. + 很抱歉,現前Oracle版本不支援壓縮檔案. + + + + Failed to interpret downloaded data. + 不能分析下載資料. + + + + Do you want to download the uncompressed file instead? + 你是否想另再下載未經壓縮的檔案? + + + + The file was retrieved successfully, but it does not contain any sets data. + 雖然檔案成功取回, 但是檔案並未含有任何牌組資料. + + + + LoadSpoilersPage + + + Save spoiler database + 儲存預覽資料庫 + + + + XML; spoiler database (*.xml) + XML ; 預覽資料庫(*.XML) + + + + spoiler + + + + + Spoilers import + 導入預覽卡牌 + + + + Please specify a compatible source for spoiler data. + 請指定預覽資料合用來源. + + + + Download URL: + 下载URL: + + + + Local file: + + + + + Restore default URL + 恢復常用URL + + + + Choose file... + + + + + The spoiler database will be saved at the following location: + 預覽資料庫將會儲藏於以下位置 + + + + Save to a custom path (not recommended) + 儲存到自訂路徑(不建議) + + + + LoadTokensPage + + + Save token database + 儲存衍生物資料庫 + + + + XML; token database (*.xml) + XML; 衍生物資料庫 (*.xml) + + + + tokens + + + + + Tokens import + 導入衍生物 + + + + Please specify a compatible source for token data. + 請指定衍生物資料合用來源. + + + + Download URL: + 下载URL: + + + + Local file: + + + + + Restore default URL + 恢復常用URL + + + + Choose file... + + + + + The token database will be saved at the following location: + 衍生物資料庫將會儲藏於以下位置: + + + + Save to a custom path (not recommended) + 儲存到自訂路徑(不建議) + + + + OracleImporter + + + Dummy set containing tokens + 包含衍生物的虚拟牌組 + + + + OracleWizard + + + Oracle Importer + Oracle導入器 + + + + OutroPage + + + Finished + 完成 + + + + The wizard has finished. + 嚮導程序工作完成 + + + + You can now start using Cockatrice with the newly updated cards. + Cockatrice已載入更新了卡牌, 現在已經可以使用. + + + + If the card databases don't reload automatically, restart the Cockatrice client. + 如果卡牌資料庫尚未能自動更新,請把cockatrice用戶端重新開啟. + + + + SaveSetsPage + + + + Error + 出現錯誤 + + + + No set has been imported. + 沒有牌組被導入。 + + + + Sets imported + 已導入牌組 + + + + A cockatrice database file of %1 MB has been downloaded. + 有%1MB大小的Cockatrice資料庫檔案已被下載 + + + + The following sets have been found: + 已找到以下牌组: + + + + Press "Save" to store the imported cards in the Cockatrice database. + 要將導入卡牌保存於Cockatrice資料庫內請按"儲藏". + + + + The card database will be saved at the following location: + 卡牌資料庫將被儲藏於以下位置 + + + + Save to a custom path (not recommended) + 儲存到自訂路徑(不建議) + + + + &Save + &儲存 + + + + Import finished: %1 cards. + 導入過程完成:成功導入%1張卡牌. + + + + %1: %2 cards imported + %1:成功導入%2張卡牌. + + + + Save card database + 儲存卡牌資料庫 + + + + XML; card database (*.xml) + XML; 卡牌資料庫 (*.xml) + + + + The file could not be saved to %1 + 檔案未能儲藏於%1。 + + + + SimpleDownloadFilePage + + + Load %1 file + + + + + %1 file (%1) + + + + + + + + + Error + 出現錯誤 + + + + The provided URL is not valid: + + + + + Downloading (0MB) + 下載中(0MB) + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + 下載中 (%1MB) + + + + Network error: %1. + 網路出錯: %1. + + + + The file could not be saved to %1 + 檔案未能儲藏於%1 + + + + UnZip + + + ZIP operation completed successfully. + 檔案壓縮成功 + + + + Failed to initialize or load zlib library. + zlib函式庫初始化或導入失敗 + + + + zlib library error. + zlib函式庫出現錯誤 + + + + Unable to create or open file. + 無法創建或開啟檔案。 + + + + Partially corrupted archive. Some files might be extracted. + 本存檔已受到局部損壞,部分被壓縮檔案可能已被解壓. + + + + Corrupted archive. + 存檔已受損壞. + + + + Wrong password. + 密碼錯誤. + + + + No archive has been created yet. + 存檔尚未創建. + + + + File or directory does not exist. + 檔案或目錄並不存在. + + + + File read error. + 檔案讀取出錯 + + + + File write error. + 檔案寫入出錯 + + + + File seek error. + 檔案尋找出錯 + + + + Unable to create a directory. + 無法創建目錄 + + + + Invalid device. + 裝置無效. + + + + Invalid or incompatible zip archive. + 無效或不配合的壓縮存檔. + + + + Inconsistent headers. Archive might be corrupted. + 本檔案標題並不一致.本存檔可能以受損壞. + + + + Unknown error. + 不知名錯誤 + + + + Zip + + + ZIP operation completed successfully. + 檔案壓縮成功 + + + + Failed to initialize or load zlib library. + zlib函式庫初始化或導入失敗 + + + + zlib library error. + zlib函式庫出現錯誤 + + + + Unable to create or open file. + 無法創建或開啟檔案。 + + + + No archive has been created yet. + 存檔尚未創建. + + + + File or directory does not exist. + 檔案或目錄並不存在. + + + + File read error. + 檔案讀取出錯. + + + + File write error. + 檔案寫入出錯. + + + + File seek error. + 檔案尋找出錯. + + + + Unknown error. + 不知名錯誤. + + + + i18n + + + English + + + + + main + + + Only run in spoiler mode + 限於在預覽模式運行 + + + + Run in no-confirm background mode + + + + \ No newline at end of file diff --git a/oracle/translations/oracle_zh-Hans.ts b/oracle/translations/oracle_zh-Hans.ts new file mode 100644 index 000000000..ff19ab463 --- /dev/null +++ b/oracle/translations/oracle_zh-Hans.ts @@ -0,0 +1,608 @@ + + + IntroPage + + + Introduction + 介绍 + + + + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. + 此向导将会导入Cockatrice中用到的系列,卡牌和衍生物列表. + + + + Interface language: + 界面语言: + + + + Version: + 版本: + + + + LoadSetsPage + + + Source selection + 选择来源 + + + + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. + 请选择合适的牌组和卡片清单来源。您可以输入下载URL或使用电脑中已有的档案。 + + + + Download URL: + 下载URL: + + + + Local file: + 本地文件: + + + + Restore default URL + 恢复常用URL + + + + Choose file... + 选择档案... + + + + Load sets file + 载入牌组档案 + + + + Sets file (%1) + Sets JSON file (%1) + + + + + + + + + + + Error + 出现错误 + + + + The provided URL is not valid. + 所提供的url无效. + + + + Downloading (0MB) + 下载中(0MB) + + + + Please choose a file. + 请选择档案. + + + + Cannot open file '%1'. + 不能开启档案 '%1 + + + + Downloading (%1MB) + 下载中(%1MB) + + + + Network error: %1. + 网路出错: %1. + + + + Parsing file + 分析档案中 + + + + Xz extraction failed. + xz解压缩失败。 + + + + Sorry, this version of Oracle does not support xz compressed files. + 很抱歉,现前Oracle版本不支援xc压缩档案 + + + + Failed to open Zip archive: %1. + 未能开启压缩文件: %1. + + + + Zip extraction failed: the Zip archive doesn't contain exactly one file. + 解压缩失败:这压缩文件拥有超过一个档案. + + + + Zip extraction failed: %1. + 解压缩失败:%1。 + + + + Sorry, this version of Oracle does not support zipped files. + 很抱歉,现前Oracle版本不支援压缩档案. + + + + Failed to interpret downloaded data. + 不能分析下载资料. + + + + Do you want to download the uncompressed file instead? + 你是否想另再下载未经压缩的档案? + + + + The file was retrieved successfully, but it does not contain any sets data. + 虽然档案成功取回, 但是档案并未含有任何牌组资料. + + + + LoadSpoilersPage + + + Save spoiler database + 储存预览资料库 + + + + XML; spoiler database (*.xml) + XML ; 预览资料库(*.XML) + + + + spoiler + + + + + Spoilers import + 导入预览卡牌 + + + + Please specify a compatible source for spoiler data. + 请指定预览资料合用来源. + + + + Download URL: + 下载URL: + + + + Local file: + + + + + Restore default URL + 恢复常用URL + + + + Choose file... + + + + + The spoiler database will be saved at the following location: + 预览资料库将会储藏于以下位置: + + + + Save to a custom path (not recommended) + 保存到自定义路径(不推荐) + + + + LoadTokensPage + + + Save token database + 储存衍生物资料库 + + + + XML; token database (*.xml) + XML; 衍生物资料库 (*.xml) + + + + tokens + + + + + Tokens import + 导入衍生物 + + + + Please specify a compatible source for token data. + 请指定衍生物资料合用来源. + + + + Download URL: + 下载URL: + + + + Local file: + + + + + Restore default URL + 恢复常用URL + + + + Choose file... + + + + + The token database will be saved at the following location: + 衍生物资料库将会储藏于以下位置: + + + + Save to a custom path (not recommended) + 保存到自定义路径(不推荐) + + + + OracleImporter + + + Dummy set containing tokens + 包含衍生物的虚拟牌组 + + + + OracleWizard + + + Oracle Importer + Oracle导入器 + + + + OutroPage + + + Finished + 完成 + + + + The wizard has finished. + 向导程式工作完成. + + + + You can now start using Cockatrice with the newly updated cards. + Cockatrice已载入更新了卡牌, 现在已经可以使用. + + + + If the card databases don't reload automatically, restart the Cockatrice client. + 如果卡牌资料库尚未能自动更新,请把cockatrice用户端重新开启. + + + + SaveSetsPage + + + + Error + 出现错误 + + + + No set has been imported. + 没有牌组被导入。 + + + + Sets imported + 已导入牌组 + + + + A cockatrice database file of %1 MB has been downloaded. + 有%1MB大小的Cockatrice资料库档案已被下载. + + + + The following sets have been found: + 已找到以下牌组: + + + + Press "Save" to store the imported cards in the Cockatrice database. + 要将导入卡牌保存于Cockatrice资料库内请按"储藏". + + + + The card database will be saved at the following location: + 卡牌资料库将被储藏于以下位置 + + + + Save to a custom path (not recommended) + 保存到自定义路径(不推荐) + + + + &Save + &储存 + + + + Import finished: %1 cards. + 导入过程完成:成功导入%1张卡牌. + + + + %1: %2 cards imported + %1:成功导入%2张卡牌. + + + + Save card database + 储存卡牌资料库 + + + + XML; card database (*.xml) + XML; 卡牌资料库 (*.xml) + + + + The file could not be saved to %1 + 档案未能储藏于%1。 + + + + SimpleDownloadFilePage + + + Load %1 file + + + + + %1 file (%1) + + + + + + + + + Error + 出现错误 + + + + The provided URL is not valid: + + + + + Downloading (0MB) + 下载中(0MB) + + + + Please choose a file. + + + + + Cannot open file '%1'. + + + + + Downloading (%1MB) + 下载中(%1MB) + + + + Network error: %1. + 网路出错: %1. + + + + The file could not be saved to %1 + 档案未能储藏于%1 + + + + UnZip + + + ZIP operation completed successfully. + 档案压缩成功. + + + + Failed to initialize or load zlib library. + zlib函式库初始化或导入失败. + + + + zlib library error. + zlib函式库出现错误. + + + + Unable to create or open file. + 无法创建或开启档案。 + + + + Partially corrupted archive. Some files might be extracted. + 本存档已受到局部损坏,部分被压缩档案可能已被解压. + + + + Corrupted archive. + 存档已受损坏. + + + + Wrong password. + 密码错误. + + + + No archive has been created yet. + 存档尚未创建. + + + + File or directory does not exist. + 档案或目录并不存在. + + + + File read error. + 档案读取出错. + + + + File write error. + 档案写入出错. + + + + File seek error. + 檔案查找出錯. + + + + Unable to create a directory. + 无法创建目录. + + + + Invalid device. + 装置无效. + + + + Invalid or incompatible zip archive. + 无效或不配合的压缩存档. + + + + Inconsistent headers. Archive might be corrupted. + 本档案标题并不一致.本存档可能以受损坏. + + + + Unknown error. + 不知名错误. + + + + Zip + + + ZIP operation completed successfully. + 档案压缩成功. + + + + Failed to initialize or load zlib library. + zlib函式库初始化或导入失败. + + + + zlib library error. + zlib函式库出现错误. + + + + Unable to create or open file. + 无法创建或开启档案。 + + + + No archive has been created yet. + 存档尚未创建. + + + + File or directory does not exist. + 档案或目录并不存在. + + + + File read error. + 档案读取出错. + + + + File write error. + 档案写入出错. + + + + File seek error. + 档案寻找出错. + + + + Unknown error. + 无法创建目录. + + + + i18n + + + English + 简体中文 (Chinese Simplified) + + + + main + + + Only run in spoiler mode + 限于在预览模式运行 + + + + Run in no-confirm background mode + + + + \ No newline at end of file diff --git a/servatrice/CMakeLists.txt b/servatrice/CMakeLists.txt index c1aefed1c..6e4191beb 100644 --- a/servatrice/CMakeLists.txt +++ b/servatrice/CMakeLists.txt @@ -2,86 +2,229 @@ # # provides the servatrice binary -PROJECT(servatrice) +project(Servatrice VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") -# cmake module for libgcrypt is included in current directory -SET(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}) -FIND_PACKAGE(Libgcrypt REQUIRED) - -SET(servatrice_SOURCES +set(servatrice_SOURCES + src/email_parser.cpp src/main.cpp - src/passwordhasher.cpp src/servatrice.cpp src/servatrice_connection_pool.cpp src/servatrice_database_interface.cpp src/server_logger.cpp src/serversocketinterface.cpp + src/settingscache.cpp src/isl_interface.cpp - ${CMAKE_CURRENT_BINARY_DIR}/version_string.cpp + src/signalhandler.cpp + ${VERSION_STRING_CPP} + src/smtpclient.cpp + src/smtp/qxthmac.cpp + src/smtp/qxtmailattachment.cpp + src/smtp/qxtmailmessage.cpp + src/smtp/qxtsmtp.cpp ) -SET(QT_DONTUSE_QTGUI) -SET(QT_USE_QTNETWORK TRUE) -SET(QT_USE_QTSQL TRUE) +set(servatrice_RESOURCES servatrice.qrc) -# Include directories -INCLUDE(${QT_USE_FILE}) -INCLUDE_DIRECTORIES(../common) -INCLUDE_DIRECTORIES(${LIBGCRYPT_INCLUDE_DIR}) -INCLUDE_DIRECTORIES(${PROTOBUF_INCLUDE_DIR}) -INCLUDE_DIRECTORIES(${CMAKE_CURRENT_BINARY_DIR}/../common) -INCLUDE_DIRECTORIES(${CMAKE_CURRENT_BINARY_DIR}) +if(WIN32) + set(servatrice_SOURCES ${servatrice_SOURCES} servatrice.rc) +endif(WIN32) -# Build servatrice binary and link it -ADD_EXECUTABLE(servatrice MACOSX_BUNDLE ${servatrice_SOURCES} ${servatrice_MOC_SRCS}) -TARGET_LINK_LIBRARIES(servatrice cockatrice_common ${QT_LIBRARIES} ${LIBGCRYPT_LIBRARY} ${CMAKE_THREAD_LIBS_INIT}) - -#add_custom_target(versionheader ALL DEPENDS version_header) -add_custom_command( - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/version_string.h ${CMAKE_CURRENT_BINARY_DIR}/version_string.cpp - COMMAND ${CMAKE_COMMAND} -DSOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR} -P ${CMAKE_CURRENT_SOURCE_DIR}/../common/getversion.cmake -) - -# install rules -if(UNIX) - if(APPLE) - INSTALL(TARGETS servatrice BUNDLE DESTINATION ./) - else() - # Assume linux - INSTALL(TARGETS servatrice RUNTIME DESTINATION bin/) - endif() -elseif(WIN32) - INSTALL(TARGETS servatrice RUNTIME DESTINATION ./) +# Under FreeBSD we need libexecinfo to use backtrace_symbols_fd() +if(CMAKE_HOST_SYSTEM MATCHES "FreeBSD") + find_package(Libexecinfo REQUIRED) + set(SYSTEM_LIBRARIES ${EXECINFO_LIBRARY} ${SYSTEM_LIBRARIES}) endif() if(APPLE) - # these needs to be relative to CMAKE_INSTALL_PREFIX - set(plugin_dest_dir servatrice.app/Contents/Plugins) - set(qtconf_dest_dir servatrice.app/Contents/Resources) + set(MACOSX_BUNDLE_ICON_FILE appicon.icns) + set_source_files_properties( + ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources + ) + set(servatrice_SOURCES ${servatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns) +endif(APPLE) - # note: no codecs in qt5 - # note: phonon_backend => mediaservice - # note: needs platform on osx +if(Qt6_FOUND) + qt6_add_resources(servatrice_RESOURCES_RCC ${servatrice_RESOURCES}) +elseif(Qt5_FOUND) + qt5_add_resources(servatrice_RESOURCES_RCC ${servatrice_RESOURCES}) +endif() - if (CMAKE_BUILD_TYPE STREQUAL "Debug") - install(DIRECTORY "${QT_PLUGINS_DIR}/" DESTINATION ${plugin_dest_dir} COMPONENT Runtime - FILES_MATCHING REGEX "(codecs|iconengines|imageformats|mediaservice|phonon_backend|platforms)/.*_debug\\.dylib") - else() - install(DIRECTORY "${QT_PLUGINS_DIR}/" DESTINATION ${plugin_dest_dir} COMPONENT Runtime - FILES_MATCHING REGEX "(codecs|iconengines|imageformats|mediaservice|phonon_backend|platforms)/[^_]*\\.dylib") - endif() +set(QT_DONT_USE_QTGUI TRUE) - install(CODE " +# Mysql connector +if(UNIX) + if(APPLE) + set(MYSQLCLIENT_DEFAULT_PATHS "/usr/local/lib" "/opt/local/lib/mysql55/mysql/" "/opt/local/lib/mysql56/mysql/") + else() + set(MYSQLCLIENT_DEFAULT_PATHS "/usr/lib64" "/usr/local/lib64" "/usr/lib" "/usr/local/lib") + endif() +elseif(WIN32) + set(MYSQLCLIENT_DEFAULT_PATHS "C:\\Program Files\\MySQL\\MySQL Server 5.7\\lib" + "C:\\Program Files (x86)\\MySQL\\MySQL Server 5.7\\lib" + ) +endif() + +find_library( + MYSQL_CLIENT_LIBRARIES + NAMES mysqlclient + PATHS ${MYSQLCLIENT_DEFAULT_PATHS} + PATH_SUFFIXES mysql mariadb +) +if(${MYSQL_CLIENT_LIBRARIES} MATCHES "NOTFOUND") + set(MYSQLCLIENT_FOUND + FALSE + CACHE INTERNAL "" + ) + message(STATUS "MySQL connector NOT FOUND: Servatrice won't be able to connect to a MySQL server") + unset(MYSQL_CLIENT_LIBRARIES) +else() + set(MYSQLCLIENT_FOUND + TRUE + CACHE INTERNAL "" + ) + get_filename_component(MYSQLCLIENT_LIBRARY_DIR ${MYSQL_CLIENT_LIBRARIES} PATH) + message(STATUS "Found MySQL connector at: ${MYSQL_CLIENT_LIBRARIES}") +endif() + +# Declare path variables +set(ICONDIR + share/icons + CACHE STRING "icon dir" +) +set(DESKTOPDIR + share/applications + CACHE STRING "desktop file destination" +) + +# Build servatrice binary and link it +add_executable(servatrice MACOSX_BUNDLE ${servatrice_MOC_SRCS} ${servatrice_RESOURCES_RCC} ${servatrice_SOURCES}) + +if(CMAKE_HOST_SYSTEM MATCHES "FreeBSD") + target_link_libraries( + servatrice libcockatrice_deck_list libcockatrice_network_server_remote Threads::Threads ${SERVATRICE_QT_MODULES} + ${LIBEXECINFO_LIBRARY} + ) +else() + target_link_libraries( + servatrice libcockatrice_deck_list libcockatrice_network_server_remote Threads::Threads ${SERVATRICE_QT_MODULES} + ) +endif() + +# install rules +if(UNIX) + if(APPLE) + set(MACOSX_BUNDLE_INFO_STRING "${PROJECT_NAME}") + set(MACOSX_BUNDLE_GUI_IDENTIFIER "com.cockatrice.${PROJECT_NAME}") + set(MACOSX_BUNDLE_LONG_VERSION_STRING "${PROJECT_NAME}-${PROJECT_VERSION}") + set(MACOSX_BUNDLE_BUNDLE_NAME ${PROJECT_NAME}) + set(MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION}) + set(MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}) + + install(TARGETS servatrice BUNDLE DESTINATION ./) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/servatrice.ini.example DESTINATION ./servatrice.app/Contents/Resources/) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/servatrice.sql DESTINATION ./servatrice.app/Contents/Resources/) + else() + # Assume linux + install(TARGETS servatrice RUNTIME DESTINATION bin/) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/servatrice.ini.example DESTINATION share/servatrice/) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/servatrice.sql DESTINATION share/servatrice/) + + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/servatrice.png DESTINATION ${ICONDIR}/hicolor/48x48/apps) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/servatrice.svg DESTINATION ${ICONDIR}/hicolor/scalable/apps) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/servatrice.desktop DESTINATION ${DESKTOPDIR}) + endif() +elseif(WIN32) + install(TARGETS servatrice RUNTIME DESTINATION ./) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/servatrice.ini.example DESTINATION ./) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/servatrice.sql DESTINATION ./) +endif() + +if(APPLE) + # these needs to be relative to CMAKE_INSTALL_PREFIX + set(plugin_dest_dir servatrice.app/Contents/Plugins) + set(qtconf_dest_dir servatrice.app/Contents/Resources) + + # Qt plugins: platforms, sqldrivers/mysql, tls (Qt6) + install( + DIRECTORY "${QT_PLUGINS_DIR}/" + DESTINATION ${plugin_dest_dir} + COMPONENT Runtime + FILES_MATCHING + PATTERN "*.dSYM" EXCLUDE + PATTERN "*_debug.dylib" EXCLUDE + PATTERN "platforms/*.dylib" + PATTERN "sqldrivers/libqsqlmysql*.dylib" + PATTERN "tls/*.dylib" + ) + + install( + CODE " file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${qtconf_dest_dir}/qt.conf\" \"[Paths] Plugins = Plugins Translations = Resources/translations\") - " COMPONENT Runtime) + " + COMPONENT Runtime + ) - install(CODE " + install( + CODE " file(GLOB_RECURSE QTPLUGINS \"\${CMAKE_INSTALL_PREFIX}/${plugin_dest_dir}/*.dylib\") set(BU_CHMOD_BUNDLE_ITEMS ON) include(BundleUtilities) - fixup_bundle(\"\${CMAKE_INSTALL_PREFIX}/servatrice.app\" \"\${QTPLUGINS}\" \"${QT_LIBRARY_DIR}\") - " COMPONENT Runtime) + fixup_bundle(\"\${CMAKE_INSTALL_PREFIX}/servatrice.app\" \"\${QTPLUGINS}\" \"${QT_LIBRARY_DIR};${MYSQLCLIENT_LIBRARY_DIR}\") + " + COMPONENT Runtime + ) +endif() + +if(WIN32) + # these needs to be relative to CMAKE_INSTALL_PREFIX + set(plugin_dest_dir Plugins) + set(qtconf_dest_dir .) + + install( + DIRECTORY "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/${CMAKE_BUILD_TYPE}/" + DESTINATION ./ + FILES_MATCHING + PATTERN "*.dll" + ) + + # Qt plugins: platforms, sqldrivers, tls (Qt6) + install( + DIRECTORY "${QT_PLUGINS_DIR}/" + DESTINATION ${plugin_dest_dir} + COMPONENT Runtime + FILES_MATCHING + PATTERN "platforms/qdirect2d.dll" + PATTERN "platforms/qminimal.dll" + PATTERN "platforms/qoffscreen.dll" + PATTERN "platforms/qwindows.dll" + PATTERN "tls/qcertonlybackend.dll" + PATTERN "tls/qopensslbackend.dll" + PATTERN "tls/qschannelbackend.dll" + PATTERN "sqldrivers/qsqlite.dll" + PATTERN "sqldrivers/qsqlodbc.dll" + PATTERN "sqldrivers/qsqlpsql.dll" + ) + + install( + CODE " + file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${qtconf_dest_dir}/qt.conf\" \"[Paths] +Plugins = Plugins +Translations = Resources/translations\") + " + COMPONENT Runtime + ) + + install( + CODE " + file(GLOB_RECURSE QTPLUGINS + \"\${CMAKE_INSTALL_PREFIX}/${plugin_dest_dir}/*.dll\") + set(BU_CHMOD_BUNDLE_ITEMS ON) + include(BundleUtilities) + fixup_bundle(\"\${CMAKE_INSTALL_PREFIX}/Servatrice.exe\" \"\${QTPLUGINS}\" \"${QT_LIBRARY_DIR};${MYSQLCLIENT_LIBRARY_DIR}\") + " + COMPONENT Runtime + ) endif() diff --git a/servatrice/FindLibgcrypt.cmake b/servatrice/FindLibgcrypt.cmake deleted file mode 100644 index 880a5d69e..000000000 --- a/servatrice/FindLibgcrypt.cmake +++ /dev/null @@ -1,46 +0,0 @@ -# -*- cmake -*- - -# Copied from http://code.google.com/p/emeraldviewer/ - -# - Find libgcrypt -# Find the libgcrypt includes and library -# This module defines -# LIBGCRYPT_INCLUDE_DIR, where to find gcrypt.h, etc. -# LIBGCRYPT_LIBRARIES, the libraries needed to use libgcrypt. -# LIBGCRYPT_FOUND, If false, do not try to use libgcrypt. -# also defined, but not for general use are -# LIBGCRYPT_LIBRARY, where to find the libgcrypt library. - -FIND_PATH(LIBGCRYPT_INCLUDE_DIR gcrypt.h) - -SET(LIBGCRYPT_NAMES ${LIBGCRYPT_NAMES} gcrypt) -FIND_LIBRARY(LIBGCRYPT_LIBRARY - NAMES ${LIBGCRYPT_NAMES} - ) - -IF (LIBGCRYPT_LIBRARY AND LIBGCRYPT_INCLUDE_DIR) - SET(LIBGCRYPT_LIBRARIES ${LIBGCRYPT_LIBRARY}) - SET(LIBGCRYPT_FOUND "YES") -ELSE (LIBGCRYPT_LIBRARY AND LIBGCRYPT_INCLUDE_DIR) - SET(LIBGCRYPT_FOUND "NO") -ENDIF (LIBGCRYPT_LIBRARY AND LIBGCRYPT_INCLUDE_DIR) - - -IF (LIBGCRYPT_FOUND) - IF (NOT LIBGCRYPT_FIND_QUIETLY) - MESSAGE(STATUS "Found libgcrypt: '${LIBGCRYPT_LIBRARIES}' and header in '${LIBGCRYPT_INCLUDE_DIR}'") - ENDIF (NOT LIBGCRYPT_FIND_QUIETLY) -ELSE (LIBGCRYPT_FOUND) - IF (LIBGCRYPT_FIND_REQUIRED) - MESSAGE(FATAL_ERROR "Could not find libgcrypt library") - ENDIF (LIBGCRYPT_FIND_REQUIRED) -ENDIF (LIBGCRYPT_FOUND) - -# Deprecated declarations. -SET (NATIVE_LIBGCRYPT_INCLUDE_PATH ${LIBGCRYPT_INCLUDE_DIR} ) -GET_FILENAME_COMPONENT (NATIVE_LIBGCRYPT_LIB_PATH ${LIBGCRYPT_LIBRARY} PATH) - -MARK_AS_ADVANCED( - LIBGCRYPT_LIBRARY - LIBGCRYPT_INCLUDE_DIR - ) diff --git a/servatrice/check_schema_version.sh b/servatrice/check_schema_version.sh new file mode 100755 index 000000000..c4aadf356 --- /dev/null +++ b/servatrice/check_schema_version.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +set -e + +version_line="$(grep 'INSERT INTO cockatrice_schema_version' servatrice/servatrice.sql)" +version_line="${version_line#*VALUES(}" +declare -i schema_ver="${version_line%%)*}" + +# shellcheck disable=2012 +latest_migration="$(ls -1 servatrice/migrations/ | tail -n1)" +xtoysql="${latest_migration#servatrice_}" +xtoy="${xtoysql%.sql}" +declare -i old_ver="10#${xtoy%_to_*}" #declare as integer with base 10, numbers with a leading 0 are normally interpreted as base 16 +declare -i new_ver="10#${xtoy#*_to_}" + +if ((old_ver >= new_ver)); then + echo "New version $new_ver is not newer than $old_ver" + exit 1 +fi + +if ((schema_ver != new_ver)); then + echo "Schema version $schema_ver does not equal new version $new_ver" + exit 1 +fi + +expected_sql="^UPDATE cockatrice_schema_version SET version=${new_ver} WHERE version=${old_ver};$" +if ! grep -q "$expected_sql" "servatrice/migrations/$latest_migration"; then + echo "$latest_migration does not contain expected sql: $expected_sql" + exit 1 +fi + +expected_define="^#define DATABASE_SCHEMA_VERSION $new_ver$" +if ! grep -q "$expected_define" servatrice/src/servatrice_database_interface.h; then + echo "servatrice_database_interface.h does not contain expected #define: $expected_define" + exit 1 +fi diff --git a/servatrice/docker/servatrice-docker.ini b/servatrice/docker/servatrice-docker.ini new file mode 100644 index 000000000..59af1caf9 --- /dev/null +++ b/servatrice/docker/servatrice-docker.ini @@ -0,0 +1,7 @@ +[database] +type=mysql +prefix=cockatrice +hostname=mysql +database=servatrice +user=servatrice +password=password diff --git a/servatrice/migrations/servatrice_0000_to_0001.sql b/servatrice/migrations/servatrice_0000_to_0001.sql new file mode 100644 index 000000000..a59f9afc4 --- /dev/null +++ b/servatrice/migrations/servatrice_0000_to_0001.sql @@ -0,0 +1,13 @@ +-- Servatrice db migration from version 0 to version 1 + +-- FIX #153 +CREATE TABLE IF NOT EXISTS `cockatrice_schema_version` ( + `version` int(7) unsigned NOT NULL, + PRIMARY KEY (`version`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO cockatrice_schema_version VALUES(1); + +-- FIX #1119 +ALTER TABLE `cockatrice_rooms_gametypes` DROP PRIMARY KEY; +ALTER TABLE `cockatrice_rooms_gametypes` ADD KEY (`id_room`); diff --git a/servatrice/migrations/servatrice_0001_to_0002.sql b/servatrice/migrations/servatrice_0001_to_0002.sql new file mode 100644 index 000000000..fc30b4a11 --- /dev/null +++ b/servatrice/migrations/servatrice_0001_to_0002.sql @@ -0,0 +1,8 @@ +-- Servatrice db migration from version 1 to version 2 + +-- FIX #1281 +CREATE TABLE IF NOT EXISTS `cockatrice_activation_emails` ( + `name` varchar(35) NOT NULL +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +UPDATE cockatrice_schema_version SET version=2 WHERE version=1; diff --git a/servatrice/migrations/servatrice_0002_to_0003.sql b/servatrice/migrations/servatrice_0002_to_0003.sql new file mode 100644 index 000000000..4a87bea41 --- /dev/null +++ b/servatrice/migrations/servatrice_0002_to_0003.sql @@ -0,0 +1,5 @@ +-- Servatrice db migration from version 2 to version 3 + +alter table cockatrice_users add clientid varchar(15) not null; + +UPDATE cockatrice_schema_version SET version=3 WHERE version=2; diff --git a/servatrice/migrations/servatrice_0003_to_0004.sql b/servatrice/migrations/servatrice_0003_to_0004.sql new file mode 100644 index 000000000..bbfce7502 --- /dev/null +++ b/servatrice/migrations/servatrice_0003_to_0004.sql @@ -0,0 +1,5 @@ +-- Servatrice db migration from version 3 to version 4 + +alter table cockatrice_sessions add clientid varchar(15) not null; + +UPDATE cockatrice_schema_version SET version=4 WHERE version=3; diff --git a/servatrice/migrations/servatrice_0004_to_0005.sql b/servatrice/migrations/servatrice_0004_to_0005.sql new file mode 100644 index 000000000..2d4ecf00e --- /dev/null +++ b/servatrice/migrations/servatrice_0004_to_0005.sql @@ -0,0 +1,5 @@ +-- Servatrice db migration from version 4 to version 5 + +alter table cockatrice_bans add clientid varchar(15) not null; + +UPDATE cockatrice_schema_version SET version=5 WHERE version=4; diff --git a/servatrice/migrations/servatrice_0005_to_0006.sql b/servatrice/migrations/servatrice_0005_to_0006.sql new file mode 100644 index 000000000..3f9552b27 --- /dev/null +++ b/servatrice/migrations/servatrice_0005_to_0006.sql @@ -0,0 +1,5 @@ +-- Servatrice db migration from version 5 to version 6 + +alter table cockatrice_users add last_login datetime not null; + +UPDATE cockatrice_schema_version SET version=6 WHERE version=5; diff --git a/servatrice/migrations/servatrice_0006_to_0007.sql b/servatrice/migrations/servatrice_0006_to_0007.sql new file mode 100644 index 000000000..651105a87 --- /dev/null +++ b/servatrice/migrations/servatrice_0006_to_0007.sql @@ -0,0 +1,5 @@ +-- Servatrice db migration from version 6 to version 7 + +alter table cockatrice_rooms add permissionlevel varchar(20) not null after descr; + +UPDATE cockatrice_schema_version SET version=7 WHERE version=6; diff --git a/servatrice/migrations/servatrice_0007_to_0008.sql b/servatrice/migrations/servatrice_0007_to_0008.sql new file mode 100644 index 000000000..0b2c15665 --- /dev/null +++ b/servatrice/migrations/servatrice_0007_to_0008.sql @@ -0,0 +1,16 @@ +-- Servatrice db migration from version 7 to version 8 + +CREATE TABLE IF NOT EXISTS `cockatrice_user_analytics` ( + `id` int(7) unsigned zerofill NOT NULL, + `client_ver` varchar(35) NOT NULL, + `last_login` datetime NOT NULL, + `notes` varchar(255) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `cockatrice_user_analytics` (id, last_login) SELECT id, last_login FROM `cockatrice_users` WHERE last_login != ''; + +ALTER TABLE `cockatrice_users` + DROP COLUMN last_login; + +UPDATE cockatrice_schema_version SET version=8 WHERE version=7; diff --git a/servatrice/migrations/servatrice_0008_to_0009.sql b/servatrice/migrations/servatrice_0008_to_0009.sql new file mode 100644 index 000000000..3874b9fcc --- /dev/null +++ b/servatrice/migrations/servatrice_0008_to_0009.sql @@ -0,0 +1,6 @@ +-- Servatrice db migration from version 8 to version 9 + +alter table cockatrice_rooms add chat_history_size int(4) not null after join_message; +update cockatrice_rooms set chat_history_size = 100; + +UPDATE cockatrice_schema_version SET version=9 WHERE version=8; diff --git a/servatrice/migrations/servatrice_0009_to_0010.sql b/servatrice/migrations/servatrice_0009_to_0010.sql new file mode 100644 index 000000000..3502d1b8f --- /dev/null +++ b/servatrice/migrations/servatrice_0009_to_0010.sql @@ -0,0 +1,13 @@ +-- Servatrice db migration from version 9 to version 10 + +CREATE TABLE IF NOT EXISTS `cockatrice_warnings` ( + `id` int(7) unsigned NOT NULL, + `user_name` varchar(255) NOT NULL, + `mod_name` varchar(255) NOT NULL, + `reason` text NOT NULL, + `time_of` datetime NOT NULL, + `clientid` varchar(15) NOT NULL, + PRIMARY KEY (`user_name`,`time_of`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +UPDATE cockatrice_schema_version SET version=10 WHERE version=9; diff --git a/servatrice/migrations/servatrice_0010_to_0011.sql b/servatrice/migrations/servatrice_0010_to_0011.sql new file mode 100644 index 000000000..cad12bd3d --- /dev/null +++ b/servatrice/migrations/servatrice_0010_to_0011.sql @@ -0,0 +1,6 @@ +-- Servatrice db migration from version 10 to version 11 + +alter table cockatrice_warnings change id user_id int(7) unsigned NOT NULL; +alter table cockatrice_warnings drop primary key, add primary key(user_id,time_of); + +UPDATE cockatrice_schema_version SET version=11 WHERE version=10; diff --git a/servatrice/migrations/servatrice_0011_to_0012.sql b/servatrice/migrations/servatrice_0011_to_0012.sql new file mode 100644 index 000000000..373363204 --- /dev/null +++ b/servatrice/migrations/servatrice_0011_to_0012.sql @@ -0,0 +1,5 @@ +-- Servatrice db migration from version 11 to version 12 + +alter table cockatrice_users modify token binary(16) NULL; + +UPDATE cockatrice_schema_version SET version=12 WHERE version=11; diff --git a/servatrice/migrations/servatrice_0012_to_0013.sql b/servatrice/migrations/servatrice_0012_to_0013.sql new file mode 100644 index 000000000..788932810 --- /dev/null +++ b/servatrice/migrations/servatrice_0012_to_0013.sql @@ -0,0 +1,86 @@ +-- Servatrice db migration from version 12 to version 13 + +-- WARNING: this is quite a big change, so you really, REALLY should +-- backup your database before attempting to execute this migration. + +-- First move all the tables to the InnoDB engine +ALTER TABLE `cockatrice_schema_version` ENGINE=InnoDB; +ALTER TABLE `cockatrice_decklist_files` ENGINE=InnoDB; +ALTER TABLE `cockatrice_decklist_folders` ENGINE=InnoDB; +ALTER TABLE `cockatrice_games` ENGINE=InnoDB; +ALTER TABLE `cockatrice_games_players` ENGINE=InnoDB; +ALTER TABLE `cockatrice_news` ENGINE=InnoDB; +ALTER TABLE `cockatrice_users` ENGINE=InnoDB; +ALTER TABLE `cockatrice_uptime` ENGINE=InnoDB; +ALTER TABLE `cockatrice_servermessages` ENGINE=InnoDB; +ALTER TABLE `cockatrice_ignorelist` ENGINE=InnoDB; +ALTER TABLE `cockatrice_buddylist` ENGINE=InnoDB; +ALTER TABLE `cockatrice_bans` ENGINE=InnoDB; +ALTER TABLE `cockatrice_warnings` ENGINE=InnoDB; +ALTER TABLE `cockatrice_sessions` ENGINE=InnoDB; +ALTER TABLE `cockatrice_servers` ENGINE=InnoDB; +ALTER TABLE `cockatrice_replays` ENGINE=InnoDB; +ALTER TABLE `cockatrice_replays_access` ENGINE=InnoDB; +ALTER TABLE `cockatrice_rooms` ENGINE=InnoDB; +ALTER TABLE `cockatrice_rooms_gametypes` ENGINE=InnoDB; +ALTER TABLE `cockatrice_log` ENGINE=InnoDB; +ALTER TABLE `cockatrice_activation_emails` ENGINE=InnoDB; +ALTER TABLE `cockatrice_user_analytics` ENGINE=InnoDB; + +-- Fix the replays tables not using unsigned values for id_game and id_player +ALTER TABLE `cockatrice_replays` MODIFY COLUMN `id_game` int(7) unsigned NULL; +ALTER TABLE `cockatrice_replays_access` MODIFY COLUMN `id_game` int(7) unsigned NOT NULL; +ALTER TABLE `cockatrice_replays_access` MODIFY COLUMN `id_player` int(7) unsigned NOT NULL; + +-- Now add some foreign keys between tables. Since there was no constaint before, +-- we need to ensure no leftover record (eg. a user deck without an user) exists +-- before adding the FK, or the query will fail. + +DELETE FROM `cockatrice_decklist_files` WHERE `id_user` NOT IN (SELECT `id` FROM `cockatrice_users`); +ALTER TABLE `cockatrice_decklist_files` ADD FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +DELETE FROM `cockatrice_decklist_folders` WHERE `id_user` NOT IN (SELECT `id` FROM `cockatrice_users`); +ALTER TABLE `cockatrice_decklist_folders` ADD FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +DELETE FROM `cockatrice_news` WHERE `id_user` NOT IN (SELECT `id` FROM `cockatrice_users`); +ALTER TABLE `cockatrice_news` ADD FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +DELETE FROM `cockatrice_ignorelist` WHERE `id_user1` NOT IN (SELECT `id` FROM `cockatrice_users`); +DELETE FROM `cockatrice_ignorelist` WHERE `id_user2` NOT IN (SELECT `id` FROM `cockatrice_users`); +ALTER TABLE `cockatrice_ignorelist` ADD FOREIGN KEY(`id_user1`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE `cockatrice_ignorelist` ADD FOREIGN KEY(`id_user2`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +DELETE FROM `cockatrice_buddylist` WHERE `id_user1` NOT IN (SELECT `id` FROM `cockatrice_users`); +DELETE FROM `cockatrice_buddylist` WHERE `id_user2` NOT IN (SELECT `id` FROM `cockatrice_users`); +ALTER TABLE `cockatrice_buddylist` ADD FOREIGN KEY(`id_user1`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE `cockatrice_buddylist` ADD FOREIGN KEY(`id_user2`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +DELETE FROM `cockatrice_user_analytics` WHERE `id` NOT IN (SELECT `id` FROM `cockatrice_users`); +ALTER TABLE `cockatrice_user_analytics` ADD FOREIGN KEY(`id`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +DELETE FROM `cockatrice_log` WHERE `sender_id` NOT IN (SELECT `id` FROM `cockatrice_users`); +ALTER TABLE `cockatrice_log` ADD FOREIGN KEY(`sender_id`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +DELETE FROM `cockatrice_activation_emails` WHERE `name` NOT IN (SELECT `name` FROM `cockatrice_users`); +ALTER TABLE `cockatrice_activation_emails` ADD FOREIGN KEY(`name`) REFERENCES `cockatrice_users`(`name`) ON DELETE CASCADE ON UPDATE CASCADE; + +DELETE FROM `cockatrice_rooms_gametypes` WHERE `id_room` NOT IN (SELECT `id` FROM `cockatrice_rooms`); +ALTER TABLE `cockatrice_rooms_gametypes` ADD FOREIGN KEY(`id_room`) REFERENCES `cockatrice_rooms`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +DELETE FROM `cockatrice_games_players` WHERE `id_game` NOT IN (SELECT `id` FROM `cockatrice_games`); +ALTER TABLE `cockatrice_games_players` ADD FOREIGN KEY(`id_game`) REFERENCES `cockatrice_games`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +DELETE FROM `cockatrice_replays` WHERE `id_game` NOT IN (SELECT `id` FROM `cockatrice_games`); +ALTER TABLE `cockatrice_replays` ADD FOREIGN KEY(`id_game`) REFERENCES `cockatrice_games`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +DELETE FROM `cockatrice_replays_access` WHERE `id_game` NOT IN (SELECT `id` FROM `cockatrice_games`); +ALTER TABLE `cockatrice_replays_access` ADD FOREIGN KEY(`id_game`) REFERENCES `cockatrice_games`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +DELETE FROM `cockatrice_replays_access` WHERE `id_player` NOT IN (SELECT `id` FROM `cockatrice_users`); +ALTER TABLE `cockatrice_replays_access` ADD FOREIGN KEY(`id_player`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +DELETE FROM `cockatrice_bans` WHERE `id_admin` NOT IN (SELECT `id` FROM `cockatrice_users`); +ALTER TABLE `cockatrice_bans` ADD FOREIGN KEY(`id_admin`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- Last: update schema version +UPDATE cockatrice_schema_version SET version=13 WHERE version=12; diff --git a/servatrice/migrations/servatrice_0013_to_0014.sql b/servatrice/migrations/servatrice_0013_to_0014.sql new file mode 100644 index 000000000..9b8be3f2a --- /dev/null +++ b/servatrice/migrations/servatrice_0013_to_0014.sql @@ -0,0 +1,6 @@ +-- Servatrice db migration from version 13 to version 14 + +alter table cockatrice_sessions add `connection_type` ENUM('tcp', 'websocket'); +UPDATE cockatrice_sessions SET connection_type = 'tcp'; + +UPDATE cockatrice_schema_version SET version=14 WHERE version=13; diff --git a/servatrice/migrations/servatrice_0014_to_0015.sql b/servatrice/migrations/servatrice_0014_to_0015.sql new file mode 100644 index 000000000..bb83d5836 --- /dev/null +++ b/servatrice/migrations/servatrice_0014_to_0015.sql @@ -0,0 +1,6 @@ +-- Servatrice db migration from version 14 to version 15 + +alter table cockatrice_rooms add `id_server` tinyint(3) not null default 0; +alter table cockatrice_rooms_gametypes add `id_server` tinyint(3) not null default 0; + +UPDATE cockatrice_schema_version SET version=15 WHERE version=14; diff --git a/servatrice/migrations/servatrice_0015_to_0016.sql b/servatrice/migrations/servatrice_0015_to_0016.sql new file mode 100644 index 000000000..d6029c50a --- /dev/null +++ b/servatrice/migrations/servatrice_0015_to_0016.sql @@ -0,0 +1,5 @@ +-- Servatrice db migration from version 15 to version 16 + +drop table cockatrice_news; + +UPDATE cockatrice_schema_version SET version=16 WHERE version=15; diff --git a/servatrice/migrations/servatrice_0016_to_0017.sql b/servatrice/migrations/servatrice_0016_to_0017.sql new file mode 100644 index 000000000..c351c4c7a --- /dev/null +++ b/servatrice/migrations/servatrice_0016_to_0017.sql @@ -0,0 +1,7 @@ +-- Servatrice db migration from version 16 to version 17 + +alter table cockatrice_rooms modify column `id_server` tinyint(3) not null default 1; +alter table cockatrice_rooms_gametypes modify column `id_server` tinyint(3) not null default 1; +alter table cockatrice_servermessages modify column `id_server` tinyint(3) not null default 1; + +UPDATE cockatrice_schema_version SET version=17 WHERE version=16; diff --git a/servatrice/migrations/servatrice_0017_to_0018.sql b/servatrice/migrations/servatrice_0017_to_0018.sql new file mode 100644 index 000000000..5b61f9bbc --- /dev/null +++ b/servatrice/migrations/servatrice_0017_to_0018.sql @@ -0,0 +1,5 @@ +-- Servatrice db migration from version 17 to version 18 + +alter table cockatrice_users add column privlevel enum("NONE","VIP","DONATOR") NOT NULL; + +UPDATE cockatrice_schema_version SET version=18 WHERE version=17; diff --git a/servatrice/migrations/servatrice_0018_to_0019.sql b/servatrice/migrations/servatrice_0018_to_0019.sql new file mode 100644 index 000000000..2c936307d --- /dev/null +++ b/servatrice/migrations/servatrice_0018_to_0019.sql @@ -0,0 +1,6 @@ +-- Servatrice db migration from version 18 to version 19 + +alter table cockatrice_sessions modify column `user_name` varchar(35) NOT NULL; +alter table cockatrice_sessions modify column `ip_address` varchar(255) NOT NULL; + +UPDATE cockatrice_schema_version SET version=19 WHERE version=18; diff --git a/servatrice/migrations/servatrice_0019_to_0020.sql b/servatrice/migrations/servatrice_0019_to_0020.sql new file mode 100644 index 000000000..337e27fb2 --- /dev/null +++ b/servatrice/migrations/servatrice_0019_to_0020.sql @@ -0,0 +1,20 @@ +-- Servatrice db migration from version 19 to version 20 + +alter table cockatrice_users add column privlevelStartDate datetime NOT NULL; +alter table cockatrice_users add column privlevelEndDate datetime NOT NULL; +update cockatrice_users set privlevelStartDate = NOW() where privlevel != 'NONE'; +update cockatrice_users set privlevelEndDate = DATE_ADD(NOW() , INTERVAL 30 DAY) where privlevel != 'NONE'; + +CREATE TABLE IF NOT EXISTS `cockatrice_donations` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `username` varchar(255) DEFAULT NULL, + `email` varchar(255) DEFAULT NULL, + `payment_pre_fee` double DEFAULT NULL, + `payment_post_fee` double DEFAULT NULL, + `term_length` int(11) DEFAULT NULL, + `date` varchar(255) DEFAULT NULL, + `pp_type` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +UPDATE cockatrice_schema_version SET version=20 WHERE version=19; diff --git a/servatrice/migrations/servatrice_0020_to_0021.sql b/servatrice/migrations/servatrice_0020_to_0021.sql new file mode 100644 index 000000000..97372253b --- /dev/null +++ b/servatrice/migrations/servatrice_0020_to_0021.sql @@ -0,0 +1,12 @@ + + +CREATE TABLE IF NOT EXISTS `cockatrice_forgot_password` ( + `id` int(7) unsigned zerofill NOT NULL auto_increment, + `name` varchar(35) NOT NULL, + `requestDate` datetime NOT NULL default '0000-00-00 00:00:00', + `emailed` tinyint(1) NOT NULL default 0, + PRIMARY KEY (`id`), + KEY `user_name` (`name`) +) ENGINE=INNODB DEFAULT CHARSET=utf8; + +UPDATE cockatrice_schema_version SET version=21 WHERE version=20; \ No newline at end of file diff --git a/servatrice/migrations/servatrice_0021_to_0022.sql b/servatrice/migrations/servatrice_0021_to_0022.sql new file mode 100644 index 000000000..10ed30785 --- /dev/null +++ b/servatrice/migrations/servatrice_0021_to_0022.sql @@ -0,0 +1,16 @@ + +CREATE TABLE IF NOT EXISTS `cockatrice_audit` ( + `id` int(7) unsigned zerofill NOT NULL auto_increment, + `id_server` tinyint(3) NOT NULL, + `name` varchar(35) NOT NULL, + `ip_address` varchar(255) NOT NULL, + `clientid` varchar(15) NOT NULL, + `incidentDate` datetime NOT NULL default '0000-00-00 00:00:00', + `action` varchar(35) NOT NULL, + `results` ENUM('fail', 'success') NOT NULL DEFAULT 'fail', + `details` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `user_name` (`name`) +) ENGINE=INNODB DEFAULT CHARSET=utf8; + +UPDATE cockatrice_schema_version SET version=22 WHERE version=21; \ No newline at end of file diff --git a/servatrice/migrations/servatrice_0022_to_0023.sql b/servatrice/migrations/servatrice_0022_to_0023.sql new file mode 100644 index 000000000..12109afad --- /dev/null +++ b/servatrice/migrations/servatrice_0022_to_0023.sql @@ -0,0 +1,6 @@ +-- Servatrice db migration from version 22 to version 23 + +alter table cockatrice_rooms modify column permissionlevel enum('NONE','REGISTERED','MODERATOR','ADMINISTRATOR'); +alter table cockatrice_rooms add column privlevel enum('NONE','PRIVILEGED','VIP','DONATOR') NOT NULL; + +UPDATE cockatrice_schema_version SET version=23 WHERE version=22; diff --git a/servatrice/migrations/servatrice_0023_to_0024.sql b/servatrice/migrations/servatrice_0023_to_0024.sql new file mode 100644 index 000000000..5d60d8541 --- /dev/null +++ b/servatrice/migrations/servatrice_0023_to_0024.sql @@ -0,0 +1,62 @@ +-- Servatrice db migration from version 23 to version 24 + +SET FOREIGN_KEY_CHECKS=0; + +-- short the "ip address" columns to 45 chars (max length of an ipv6 address) +-- to ensure the field can be used as a key on mysql < 5.7 +-- (not all fields are actually keys, but better keep them uniform) +ALTER TABLE `cockatrice_sessions` MODIFY COLUMN `ip_address` varchar(45) NOT NULL; +ALTER TABLE `cockatrice_bans` MODIFY COLUMN `ip_address` varchar(45) NOT NULL; +ALTER TABLE `cockatrice_log` MODIFY COLUMN `sender_ip` varchar(45) NOT NULL; +ALTER TABLE `cockatrice_audit` MODIFY COLUMN `ip_address` varchar(45) NOT NULL; + +-- short the "user name" columns to 35 chars (current max length in servatrice) +-- to ensure the field can be used as a key on mysql < 5.7 +-- (not all fields are actually keys, but better keep them uniform) +ALTER TABLE `cockatrice_bans` MODIFY COLUMN `user_name` varchar(35) NOT NULL; +ALTER TABLE `cockatrice_warnings` MODIFY COLUMN `user_name` varchar(35) NOT NULL; +ALTER TABLE `cockatrice_warnings` MODIFY COLUMN `mod_name` varchar(35) NOT NULL; +ALTER TABLE `cockatrice_games` MODIFY COLUMN `creator_name` varchar(35) NOT NULL; +ALTER TABLE `cockatrice_games_players` MODIFY COLUMN `player_name` varchar(35) NOT NULL; +ALTER TABLE `cockatrice_donations` MODIFY COLUMN `username` varchar(35) NOT NULL; + +-- remove the FK from cockatrice_activation_emails (it will be created again later) +-- the key name should end with _1, but multiple run of the 0012_to_003 migration +-- can lead to a different name or even multiple keys. In this case you must remove +-- all of them before continue. +-- Use "show create table cockatrice_activation_emails" to see the key names. +ALTER TABLE cockatrice_activation_emails DROP FOREIGN KEY `cockatrice_activation_emails_ibfk_1`; + +-- unify tables and columns collation to utf8mb4_unicode_ci +ALTER TABLE `cockatrice_schema_version` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_users` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_decklist_files` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_decklist_folders` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_ignorelist` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_buddylist` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_rooms` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_rooms_gametypes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_games` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_games_players` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_replays` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_replays_access` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_servers` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_uptime` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_servermessages` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_sessions` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_bans` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_warnings` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_log` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_activation_emails` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_user_analytics` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_donations` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_forgot_password` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `cockatrice_audit` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- re-add the FK constraint on cockatrice_activation_emails +ALTER TABLE `cockatrice_activation_emails` ADD FOREIGN KEY(`name`) REFERENCES `cockatrice_users`(`name`) ON DELETE CASCADE ON UPDATE CASCADE; + +SET FOREIGN_KEY_CHECKS=1; + +-- update schema version +UPDATE cockatrice_schema_version SET version=24 WHERE version=23; diff --git a/servatrice/migrations/servatrice_0024_to_0025.sql b/servatrice/migrations/servatrice_0024_to_0025.sql new file mode 100644 index 000000000..290f15008 --- /dev/null +++ b/servatrice/migrations/servatrice_0024_to_0025.sql @@ -0,0 +1,6 @@ +-- Servatrice db migration from version 24 to version 25 + +ALTER TABLE cockatrice_uptime ADD COLUMN mods_count int(11) NOT NULL DEFAULT 0; +ALTER TABLE cockatrice_uptime ADD COLUMN mods_list TEXT; + +UPDATE cockatrice_schema_version SET version=25 WHERE version=24; diff --git a/servatrice/migrations/servatrice_0025_to_0026.sql b/servatrice/migrations/servatrice_0025_to_0026.sql new file mode 100644 index 000000000..fbbd3ee78 --- /dev/null +++ b/servatrice/migrations/servatrice_0025_to_0026.sql @@ -0,0 +1,171 @@ +-- Servatrice db migration from version 25 to version 26 + +-- Some previous migrations didn't care about column ordering, +-- meaning select could return info in a different order depending on the +-- age of the database. + +-- This migration ensures a consistent column ordering across all databases, +-- regardless of age. Future migrations should take care to ensure this +-- ordering stays consistent. + +-- cockatrice_users.id cannot be modified because its used in a foreign key constraint +-- it should be first anyway, so this isnt a big deal +-- ALTER TABLE cockatrice_users MODIFY `id` int(7) unsigned zerofill NOT NULL FIRST; +ALTER TABLE cockatrice_users MODIFY COLUMN `admin` tinyint(1) NOT NULL AFTER `id`; +ALTER TABLE cockatrice_users MODIFY COLUMN `name` varchar(35) NOT NULL AFTER `admin`; +ALTER TABLE cockatrice_users MODIFY COLUMN `realname` varchar(255) NOT NULL AFTER `name`; +ALTER TABLE cockatrice_users MODIFY COLUMN `gender` char(1) NOT NULL AFTER `realname`; +ALTER TABLE cockatrice_users MODIFY COLUMN `password_sha512` char(120) NOT NULL AFTER `gender`; +ALTER TABLE cockatrice_users MODIFY COLUMN `email` varchar(255) NOT NULL AFTER `password_sha512`; +ALTER TABLE cockatrice_users MODIFY COLUMN `country` char(2) NOT NULL AFTER `email`; +ALTER TABLE cockatrice_users MODIFY COLUMN `avatar_bmp` blob NOT NULL AFTER `country`; +ALTER TABLE cockatrice_users MODIFY COLUMN `registrationDate` datetime NOT NULL AFTER `avatar_bmp`; +ALTER TABLE cockatrice_users MODIFY COLUMN `active` tinyint(1) NOT NULL AFTER `registrationDate`; +ALTER TABLE cockatrice_users MODIFY COLUMN `token` binary(16) AFTER `active`; +ALTER TABLE cockatrice_users MODIFY COLUMN `clientid` varchar(15) NOT NULL AFTER `token`; +ALTER TABLE cockatrice_users MODIFY COLUMN `privlevel` enum("NONE","VIP","DONATOR") NOT NULL AFTER `clientid`; +ALTER TABLE cockatrice_users MODIFY COLUMN `privlevelStartDate` datetime NOT NULL AFTER `privlevel`; +ALTER TABLE cockatrice_users MODIFY COLUMN `privlevelEndDate` datetime NOT NULL AFTER `privlevelStartDate`; + +ALTER TABLE cockatrice_decklist_files MODIFY COLUMN `id` int(7) unsigned zerofill NOT NULL auto_increment FIRST; +ALTER TABLE cockatrice_decklist_files MODIFY COLUMN `id_folder` int(7) unsigned zerofill NOT NULL AFTER `id`; +ALTER TABLE cockatrice_decklist_files MODIFY COLUMN `id_user` int(7) unsigned NULL AFTER `id_folder`; +ALTER TABLE cockatrice_decklist_files MODIFY COLUMN `name` varchar(50) NOT NULL AFTER `id_user`; +ALTER TABLE cockatrice_decklist_files MODIFY COLUMN `upload_time` datetime NOT NULL AFTER `name`; +ALTER TABLE cockatrice_decklist_files MODIFY COLUMN `content` text NOT NULL AFTER `upload_time`; + +ALTER TABLE cockatrice_decklist_folders MODIFY COLUMN `id` int(7) unsigned zerofill NOT NULL auto_increment FIRST; +ALTER TABLE cockatrice_decklist_folders MODIFY COLUMN `id_parent` int(7) unsigned zerofill NOT NULL AFTER `id`; +ALTER TABLE cockatrice_decklist_folders MODIFY COLUMN `id_user` int(7) unsigned NULL AFTER `id_parent`; +ALTER TABLE cockatrice_decklist_folders MODIFY COLUMN `name` varchar(30) NOT NULL AFTER `id_user`; + +ALTER TABLE cockatrice_ignorelist MODIFY COLUMN `id_user1` int(7) unsigned NOT NULL FIRST; +ALTER TABLE cockatrice_ignorelist MODIFY COLUMN `id_user2` int(7) unsigned NOT NULL AFTER `id_user1`; + +ALTER TABLE cockatrice_buddylist MODIFY COLUMN `id_user1` int(7) unsigned NOT NULL FIRST; +ALTER TABLE cockatrice_buddylist MODIFY COLUMN `id_user2` int(7) unsigned NOT NULL AFTER `id_user1`; + +ALTER TABLE cockatrice_rooms MODIFY COLUMN `id` int(7) unsigned NOT NULL auto_increment FIRST; +ALTER TABLE cockatrice_rooms MODIFY COLUMN `name` varchar(50) NOT NULL AFTER `id`; +ALTER TABLE cockatrice_rooms MODIFY COLUMN `descr` varchar(255) NOT NULL AFTER `name`; +ALTER TABLE cockatrice_rooms MODIFY COLUMN `permissionlevel` enum('NONE','REGISTERED','MODERATOR','ADMINISTRATOR') NOT NULL AFTER `descr`; +ALTER TABLE cockatrice_rooms MODIFY COLUMN `privlevel` enum('NONE','PRIVILEGED','VIP','DONATOR') NOT NULL AFTER `permissionlevel`; +ALTER TABLE cockatrice_rooms MODIFY COLUMN `auto_join` tinyint(1) default 0 AFTER `privlevel`; +ALTER TABLE cockatrice_rooms MODIFY COLUMN `join_message` varchar(255) NOT NULL AFTER `auto_join`; +ALTER TABLE cockatrice_rooms MODIFY COLUMN `chat_history_size` int(4) NOT NULL AFTER `join_message`; +ALTER TABLE cockatrice_rooms MODIFY COLUMN `id_server` tinyint(3) NOT NULL DEFAULT 1 AFTER `chat_history_size`; + +ALTER TABLE cockatrice_rooms_gametypes MODIFY COLUMN `id_room` int(7) unsigned NOT NULL FIRST; +ALTER TABLE cockatrice_rooms_gametypes MODIFY COLUMN `name` varchar(50) NOT NULL AFTER `id_room`; +ALTER TABLE cockatrice_rooms_gametypes MODIFY COLUMN `id_server` tinyint(3) NOT NULL DEFAULT 1 AFTER `name`; + +ALTER TABLE cockatrice_games MODIFY COLUMN `room_name` varchar(255) NOT NULL FIRST; +ALTER TABLE cockatrice_games MODIFY COLUMN `id` int(7) unsigned NOT NULL auto_increment AFTER `room_name`; +ALTER TABLE cockatrice_games MODIFY COLUMN `descr` varchar(50) default NULL AFTER `id`; +ALTER TABLE cockatrice_games MODIFY COLUMN `creator_name` varchar(35) NOT NULL AFTER `descr`; +ALTER TABLE cockatrice_games MODIFY COLUMN `password` tinyint(1) NOT NULL AFTER `creator_name`; +ALTER TABLE cockatrice_games MODIFY COLUMN `game_types` varchar(255) NOT NULL AFTER `password`; +ALTER TABLE cockatrice_games MODIFY COLUMN `player_count` tinyint(3) NOT NULL AFTER `game_types`; +ALTER TABLE cockatrice_games MODIFY COLUMN `time_started` datetime default NULL AFTER `player_count`; +ALTER TABLE cockatrice_games MODIFY COLUMN `time_finished` datetime default NULL AFTER `time_started`; + +ALTER TABLE cockatrice_games_players MODIFY COLUMN `id_game` int(7) unsigned zerofill NOT NULL FIRST; +ALTER TABLE cockatrice_games_players MODIFY COLUMN `player_name` varchar(35) NOT NULL AFTER `id_game`; + +ALTER TABLE cockatrice_replays MODIFY COLUMN `id` int(7) NOT NULL AUTO_INCREMENT FIRST; +ALTER TABLE cockatrice_replays MODIFY COLUMN `id_game` int(7) unsigned NULL AFTER `id`; +ALTER TABLE cockatrice_replays MODIFY COLUMN `duration` int(7) NOT NULL AFTER `id_game`; +ALTER TABLE cockatrice_replays MODIFY COLUMN `replay` mediumblob NOT NULL AFTER `duration`; + +ALTER TABLE cockatrice_replays_access MODIFY COLUMN `id_game` int(7) unsigned NOT NULL FIRST; +ALTER TABLE cockatrice_replays_access MODIFY COLUMN `id_player` int(7) unsigned NOT NULL AFTER `id_game`; +ALTER TABLE cockatrice_replays_access MODIFY COLUMN `replay_name` varchar(255) NOT NULL AFTER `id_player`; +ALTER TABLE cockatrice_replays_access MODIFY COLUMN `do_not_hide` tinyint(1) NOT NULL AFTER `replay_name`; + +ALTER TABLE cockatrice_servers MODIFY COLUMN `id` mediumint(8) unsigned NOT NULL FIRST; +ALTER TABLE cockatrice_servers MODIFY COLUMN `ssl_cert` text NOT NULL AFTER `id`; +ALTER TABLE cockatrice_servers MODIFY COLUMN `hostname` varchar(255) NOT NULL AFTER `ssl_cert`; +ALTER TABLE cockatrice_servers MODIFY COLUMN `address` varchar(255) NOT NULL AFTER `hostname`; +ALTER TABLE cockatrice_servers MODIFY COLUMN `game_port` mediumint(8) unsigned NOT NULL AFTER `address`; +ALTER TABLE cockatrice_servers MODIFY COLUMN `control_port` mediumint(9) NOT NULL AFTER `game_port`; + +ALTER TABLE cockatrice_uptime MODIFY COLUMN `id_server` tinyint(3) NOT NULL FIRST; +ALTER TABLE cockatrice_uptime MODIFY COLUMN `timest` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' AFTER `id_server`; +ALTER TABLE cockatrice_uptime MODIFY COLUMN `uptime` int(11) NOT NULL AFTER `timest`; +ALTER TABLE cockatrice_uptime MODIFY COLUMN `users_count` int(11) NOT NULL AFTER `uptime`; +ALTER TABLE cockatrice_uptime MODIFY COLUMN `mods_count` int(11) NOT NULL DEFAULT 0 AFTER `users_count`; +ALTER TABLE cockatrice_uptime MODIFY COLUMN `mods_list` TEXT AFTER `mods_count`; +ALTER TABLE cockatrice_uptime MODIFY COLUMN `games_count` int(11) NOT NULL AFTER `mods_list`; +ALTER TABLE cockatrice_uptime MODIFY COLUMN `rx_bytes` int(11) NOT NULL AFTER `games_count`; +ALTER TABLE cockatrice_uptime MODIFY COLUMN `tx_bytes` int(11) NOT NULL AFTER `rx_bytes`; + +ALTER TABLE cockatrice_servermessages MODIFY COLUMN `id_server` tinyint(3) not null default 1 FIRST; +ALTER TABLE cockatrice_servermessages MODIFY COLUMN `timest` datetime NOT NULL default '0000-00-00 00:00:00' AFTER `id_server`; +ALTER TABLE cockatrice_servermessages MODIFY COLUMN `message` text AFTER `timest`; + +ALTER TABLE cockatrice_sessions MODIFY COLUMN `id` int(9) NOT NULL AUTO_INCREMENT FIRST; +ALTER TABLE cockatrice_sessions MODIFY COLUMN `user_name` varchar(35) NOT NULL AFTER `id`; +ALTER TABLE cockatrice_sessions MODIFY COLUMN `id_server` tinyint(3) NOT NULL AFTER `user_name`; +ALTER TABLE cockatrice_sessions MODIFY COLUMN `ip_address` varchar(45) NOT NULL AFTER `id_server`; +ALTER TABLE cockatrice_sessions MODIFY COLUMN `start_time` datetime NOT NULL AFTER `ip_address`; +ALTER TABLE cockatrice_sessions MODIFY COLUMN `end_time` datetime DEFAULT NULL AFTER `start_time`; +ALTER TABLE cockatrice_sessions MODIFY COLUMN `clientid` varchar(15) NOT NULL AFTER `end_time`; +ALTER TABLE cockatrice_sessions MODIFY COLUMN `connection_type` ENUM('tcp', 'websocket') AFTER `clientid`; + +ALTER TABLE cockatrice_bans MODIFY COLUMN `user_name` varchar(35) NOT NULL FIRST; +ALTER TABLE cockatrice_bans MODIFY COLUMN `ip_address` varchar(45) NOT NULL AFTER `user_name`; +ALTER TABLE cockatrice_bans MODIFY COLUMN `id_admin` int(7) unsigned zerofill NOT NULL AFTER `ip_address`; +ALTER TABLE cockatrice_bans MODIFY COLUMN `time_from` datetime NOT NULL AFTER `id_admin`; +ALTER TABLE cockatrice_bans MODIFY COLUMN `minutes` int(6) NOT NULL AFTER `time_from`; +ALTER TABLE cockatrice_bans MODIFY COLUMN `reason` text NOT NULL AFTER `minutes`; +ALTER TABLE cockatrice_bans MODIFY COLUMN `visible_reason` text NOT NULL AFTER `reason`; +ALTER TABLE cockatrice_bans MODIFY COLUMN `clientid` varchar(15) NOT NULL AFTER `visible_reason`; + +ALTER TABLE cockatrice_warnings MODIFY COLUMN `user_id` int(7) unsigned NOT NULL FIRST; +ALTER TABLE cockatrice_warnings MODIFY COLUMN `user_name` varchar(35) NOT NULL AFTER `user_id`; +ALTER TABLE cockatrice_warnings MODIFY COLUMN `mod_name` varchar(35) NOT NULL AFTER `user_name`; +ALTER TABLE cockatrice_warnings MODIFY COLUMN `reason` text NOT NULL AFTER `mod_name`; +ALTER TABLE cockatrice_warnings MODIFY COLUMN `time_of` datetime NOT NULL AFTER `reason`; +ALTER TABLE cockatrice_warnings MODIFY COLUMN `clientid` varchar(15) NOT NULL AFTER `time_of`; + +ALTER TABLE cockatrice_log MODIFY COLUMN `log_time` datetime NOT NULL FIRST; +ALTER TABLE cockatrice_log MODIFY COLUMN `sender_id` int(7) unsigned NULL AFTER `log_time`; +ALTER TABLE cockatrice_log MODIFY COLUMN `sender_name` varchar(35) NOT NULL AFTER `sender_id`; +ALTER TABLE cockatrice_log MODIFY COLUMN `sender_ip` varchar(45) NOT NULL AFTER `sender_name`; +ALTER TABLE cockatrice_log MODIFY COLUMN `log_message` text NOT NULL AFTER `sender_ip`; +ALTER TABLE cockatrice_log MODIFY COLUMN `target_type` ENUM('room', 'game', 'chat') AFTER `log_message`; +ALTER TABLE cockatrice_log MODIFY COLUMN `target_id` int(7) NULL AFTER `target_type`; +ALTER TABLE cockatrice_log MODIFY COLUMN `target_name` varchar(50) NOT NULL AFTER `target_id`; + +-- cockatrice_activation_emails has only 1 column so we skip it + +ALTER TABLE cockatrice_user_analytics MODIFY COLUMN `id` int(7) unsigned zerofill NOT NULL FIRST; +ALTER TABLE cockatrice_user_analytics MODIFY COLUMN `client_ver` varchar(35) NOT NULL AFTER `id`; +ALTER TABLE cockatrice_user_analytics MODIFY COLUMN `last_login` datetime NOT NULL AFTER `client_ver`; +ALTER TABLE cockatrice_user_analytics MODIFY COLUMN `notes` varchar(255) NOT NULL AFTER `last_login`; + +ALTER TABLE cockatrice_donations MODIFY COLUMN `id` int(11) unsigned NOT NULL AUTO_INCREMENT FIRST; +ALTER TABLE cockatrice_donations MODIFY COLUMN `username` varchar(35) DEFAULT NULL AFTER `id`; +ALTER TABLE cockatrice_donations MODIFY COLUMN `email` varchar(255) DEFAULT NULL AFTER `username`; +ALTER TABLE cockatrice_donations MODIFY COLUMN `payment_pre_fee` double DEFAULT NULL AFTER `email`; +ALTER TABLE cockatrice_donations MODIFY COLUMN `payment_post_fee` double DEFAULT NULL AFTER `payment_pre_fee`; +ALTER TABLE cockatrice_donations MODIFY COLUMN `term_length` int(11) DEFAULT NULL AFTER `payment_post_fee`; +ALTER TABLE cockatrice_donations MODIFY COLUMN `date` varchar(255) DEFAULT NULL AFTER `term_length`; +ALTER TABLE cockatrice_donations MODIFY COLUMN `pp_type` varchar(255) DEFAULT NULL AFTER `date`; + +ALTER TABLE cockatrice_forgot_password MODIFY COLUMN `id` int(7) unsigned zerofill NOT NULL auto_increment FIRST; +ALTER TABLE cockatrice_forgot_password MODIFY COLUMN `name` varchar(35) NOT NULL AFTER `id`; +ALTER TABLE cockatrice_forgot_password MODIFY COLUMN `requestDate` datetime NOT NULL default '0000-00-00 00:00:00' AFTER `name`; +ALTER TABLE cockatrice_forgot_password MODIFY COLUMN `emailed` tinyint(1) NOT NULL default 0 AFTER `requestDate`; + +ALTER TABLE cockatrice_audit MODIFY COLUMN `id` int(7) unsigned zerofill NOT NULL auto_increment FIRST; +ALTER TABLE cockatrice_audit MODIFY COLUMN `id_server` tinyint(3) NOT NULL AFTER `id`; +ALTER TABLE cockatrice_audit MODIFY COLUMN `name` varchar(35) NOT NULL AFTER `id_server`; +ALTER TABLE cockatrice_audit MODIFY COLUMN `ip_address` varchar(45) NOT NULL AFTER `name`; +ALTER TABLE cockatrice_audit MODIFY COLUMN `clientid` varchar(15) NOT NULL AFTER `ip_address`; +ALTER TABLE cockatrice_audit MODIFY COLUMN `incidentDate` datetime NOT NULL default '0000-00-00 00:00:00' AFTER `clientid`; +ALTER TABLE cockatrice_audit MODIFY COLUMN `action` varchar(35) NOT NULL AFTER `incidentDate`; +ALTER TABLE cockatrice_audit MODIFY COLUMN `results` ENUM('fail', 'success') NOT NULL DEFAULT 'fail' AFTER `action`; +ALTER TABLE cockatrice_audit MODIFY COLUMN `details` varchar(255) NOT NULL AFTER `results`; + +UPDATE cockatrice_schema_version SET version=26 WHERE version=25; diff --git a/servatrice/migrations/servatrice_0026_to_0027.sql b/servatrice/migrations/servatrice_0026_to_0027.sql new file mode 100644 index 000000000..bed0c488b --- /dev/null +++ b/servatrice/migrations/servatrice_0026_to_0027.sql @@ -0,0 +1,5 @@ +-- Servatrice db migration from version 26 to version 27 + +ALTER TABLE cockatrice_users ADD COLUMN passwordLastChangedDate datetime NOT NULL DEFAULT '0000-00-00 00:00:00'; + +UPDATE cockatrice_schema_version SET version=27 WHERE version=26; diff --git a/servatrice/migrations/servatrice_0027_to_0028.sql b/servatrice/migrations/servatrice_0027_to_0028.sql new file mode 100644 index 000000000..70bec3607 --- /dev/null +++ b/servatrice/migrations/servatrice_0027_to_0028.sql @@ -0,0 +1,5 @@ +-- Servatrice db migration from version 27 to version 28 + +ALTER TABLE cockatrice_users DROP COLUMN gender; + +UPDATE cockatrice_schema_version SET version=28 WHERE version=27; diff --git a/servatrice/migrations/servatrice_0028_to_0029.sql b/servatrice/migrations/servatrice_0028_to_0029.sql new file mode 100644 index 000000000..03cfcfa71 --- /dev/null +++ b/servatrice/migrations/servatrice_0028_to_0029.sql @@ -0,0 +1,5 @@ +-- Servatrice db migration from version 28 to version 29 + +ALTER TABLE cockatrice_users MODIFY COLUMN avatar_bmp mediumblob NOT NULL; + +UPDATE cockatrice_schema_version SET version=29 WHERE version=28; diff --git a/servatrice/migrations/servatrice_0029_to_0030.sql b/servatrice/migrations/servatrice_0029_to_0030.sql new file mode 100644 index 000000000..37f32ccb0 --- /dev/null +++ b/servatrice/migrations/servatrice_0029_to_0030.sql @@ -0,0 +1,5 @@ +-- Servatrice db migration from version 29 to version 30 + +ALTER TABLE cockatrice_users ADD COLUMN adminnotes mediumtext NOT NULL; + +UPDATE cockatrice_schema_version SET version=30 WHERE version=29; diff --git a/servatrice/migrations/servatrice_0030_to_0031.sql b/servatrice/migrations/servatrice_0030_to_0031.sql new file mode 100644 index 000000000..963336ea0 --- /dev/null +++ b/servatrice/migrations/servatrice_0030_to_0031.sql @@ -0,0 +1,11 @@ +-- Servatrice db migration from version 30 to version 31 + +ALTER TABLE cockatrice_log DROP INDEX `target_type`; + +ALTER TABLE cockatrice_forgot_password ADD INDEX idx_emailed (`emailed`); +ALTER TABLE cockatrice_sessions ADD INDEX idx_start_time (`start_time`); +ALTER TABLE cockatrice_users ADD INDEX idx_admin (`admin`); +ALTER TABLE cockatrice_users ADD INDEX idx_active (`active`); +ALTER TABLE cockatrice_users ADD INDEX idx_privlevel (`privlevel`); + +UPDATE cockatrice_schema_version SET version=31 WHERE version=30; diff --git a/servatrice/migrations/servatrice_0031_to_0032.sql b/servatrice/migrations/servatrice_0031_to_0032.sql new file mode 100644 index 000000000..dd3a1334c --- /dev/null +++ b/servatrice/migrations/servatrice_0031_to_0032.sql @@ -0,0 +1,11 @@ +-- Servatrice db migration from version 31 to version 32 + +ALTER TABLE cockatrice_users ADD INDEX `idx_clientid` (`clientid`); +ALTER TABLE cockatrice_sessions ADD INDEX `idx_clientid` (`clientid`); +ALTER TABLE cockatrice_sessions ADD INDEX `idx_ip_address` (`ip_address`); +ALTER TABLE cockatrice_bans ADD INDEX `idx_user_name` (`user_name`); +ALTER TABLE cockatrice_warnings ADD INDEX `idx_time_of` (`time_of`); +ALTER TABLE cockatrice_warnings ADD INDEX `idx_user_name` (`user_name`); +ALTER TABLE cockatrice_log ADD INDEX `idx_log_time` (`log_time`); + +UPDATE cockatrice_schema_version SET version=32 WHERE version=31; diff --git a/servatrice/migrations/servatrice_0032_to_0033.sql b/servatrice/migrations/servatrice_0032_to_0033.sql new file mode 100644 index 000000000..5706c01f8 --- /dev/null +++ b/servatrice/migrations/servatrice_0032_to_0033.sql @@ -0,0 +1,5 @@ +-- Servatrice db migration from version 32 to version 33 + +ALTER TABLE cockatrice_user_analytics ADD INDEX `idx_last_login` (`last_login`); + +UPDATE cockatrice_schema_version SET version=33 WHERE version=32; diff --git a/servatrice/migrations/servatrice_0033_to_0034.sql b/servatrice/migrations/servatrice_0033_to_0034.sql new file mode 100644 index 000000000..19c33c2dc --- /dev/null +++ b/servatrice/migrations/servatrice_0033_to_0034.sql @@ -0,0 +1,8 @@ +-- Servatrice db migration from version 33 to version 34 + +ALTER TABLE cockatrice_users ADD COLUMN leftPawnColorOverride varchar(255); +ALTER TABLE cockatrice_users ADD COLUMN rightPawnColorOverride varchar(255); + +ALTER TABLE cockatrice_users ADD INDEX `idx_pawnColorOverrides` (`leftPawnColorOverride`, `rightPawnColorOverride`); + +UPDATE cockatrice_schema_version SET version=34 WHERE version=33; diff --git a/servatrice/mysql-storage/.gitignore b/servatrice/mysql-storage/.gitignore new file mode 100644 index 000000000..86d0cb272 --- /dev/null +++ b/servatrice/mysql-storage/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/servatrice/resources/appicon.icns b/servatrice/resources/appicon.icns new file mode 100644 index 000000000..1f3b5c207 Binary files /dev/null and b/servatrice/resources/appicon.icns differ diff --git a/servatrice/resources/appicon.ico b/servatrice/resources/appicon.ico new file mode 100644 index 000000000..1ef0f78a4 Binary files /dev/null and b/servatrice/resources/appicon.ico differ diff --git a/servatrice/resources/servatrice-original.svg b/servatrice/resources/servatrice-original.svg new file mode 100644 index 000000000..7be223288 --- /dev/null +++ b/servatrice/resources/servatrice-original.svg @@ -0,0 +1,320 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/servatrice/resources/servatrice.png b/servatrice/resources/servatrice.png new file mode 100644 index 000000000..723c01b1e Binary files /dev/null and b/servatrice/resources/servatrice.png differ diff --git a/servatrice/resources/servatrice.svg b/servatrice/resources/servatrice.svg new file mode 100644 index 000000000..f4c9f4927 --- /dev/null +++ b/servatrice/resources/servatrice.svg @@ -0,0 +1,369 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/servatrice/scripts/.gitignore b/servatrice/scripts/.gitignore new file mode 100644 index 000000000..eb47a0821 --- /dev/null +++ b/servatrice/scripts/.gitignore @@ -0,0 +1 @@ +pypb/ diff --git a/servatrice/scripts/File.History b/servatrice/scripts/File.History new file mode 100644 index 000000000..8ea21e2ca Binary files /dev/null and b/servatrice/scripts/File.History differ diff --git a/servatrice/scripts/linux/db_backup_all b/servatrice/scripts/linux/db_backup_all new file mode 100644 index 000000000..e97b91cc5 --- /dev/null +++ b/servatrice/scripts/linux/db_backup_all @@ -0,0 +1,46 @@ +#!/bin/bash +set -u +set -e +SLEEPTIME=5 +SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file +LOGAPPENDDATE=`date +%m%d%Y` +EXPIRATION=`date +%m%d%Y -d "-3 days"` +DBNAME="servatrice" +APPNAME="servatrice" +ROOTFOLDER="./backups" #set this to the root path you want backups to be stored in +BACKUPDIR="$ROOTFOLDER/$LOGAPPENDDATE/db/$APPNAME" +TABLES=( + "cockatrice_users" + "cockatrice_decklist_files" + "cockatrice_replays" + "cockatrice_buddylist" + "cockatrice_ignorelist" + "cockatrice_bans" + "cockatrice_sessions" + "cockatrice_decklist_folders" + "cockatrice_replays_access" + "cockatrice_games" + "cockatrice_games_players" + "cockatrice_uptime" + "cockatrice_schema_version" + "cockatrice_servermessages" + "cockatrice_servers" + "cockatrice_rooms" + "cockatrice_rooms_gametypes" + ) + +PROCESSNAME="mysqldump" +if [ "$(pgrep $PROCESSNAME)" == "" ]; +then + [ ! -d $BACKUPDIR ] && mkdir -p $BACKUPDIR/ + for TABLENAME in "${TABLES[@]}" + do + BACKUPFILE="$BACKUPDIR/$APPNAME.$TABLENAME.sql.$LOGAPPENDDATE" + echo "Backing up DB Table [$TABLENAME]" + ionice -c3 nice -n19 mysqldump --defaults-file=$SQLCONFFILE $DBNAME $TABLENAME > $BACKUPFILE + sleep $SLEEPTIME + done + rm -rf "$ROOTFOLDER/$EXPIRATION/" +else + echo "Backup in progress, aborting" +fi diff --git a/servatrice/scripts/linux/db_restore_all b/servatrice/scripts/linux/db_restore_all new file mode 100644 index 000000000..376a9fc00 --- /dev/null +++ b/servatrice/scripts/linux/db_restore_all @@ -0,0 +1,52 @@ +#!/bin/bash +set -u +set -e +SLEEPTIME=5 +SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file +LOGAPPENDDATE=`date +%m%d%Y` +EXPIRATION=`date +%m%d%Y -d "-3 days"` +DBNAME="servatrice" +APPNAME="servatrice" +ROOTFOLDER="./backups" #set this to the root path that contains the backup files +BACKUPDIR="$ROOTFOLDER/$LOGAPPENDDATE/db/$APPNAME" +TABLES=( + "cockatrice_users" + "cockatrice_decklist_files" + "cockatrice_replays" + "cockatrice_buddylist" + "cockatrice_ignorelist" + "cockatrice_bans" + "cockatrice_sessions" + "cockatrice_decklist_folders" + "cockatrice_replays_access" + "cockatrice_games" + "cockatrice_games_players" + "cockatrice_uptime" + "cockatrice_schema_version" + "cockatrice_servermessages" + "cockatrice_servers" + "cockatrice_rooms" + "cockatrice_rooms_gametypes" + ) + +PROCESSNAME="mysqldump" +if [ "$(pgrep $PROCESSNAME)" == "" ]; +then + [ ! -d $BACKUPDIR ] && mkdir -p $BACKUPDIR/ + for TABLENAME in "${TABLES[@]}" + do + BACKUPFILE="$BACKUPDIR/$APPNAME.$TABLENAME.sql.$LOGAPPENDDATE" + if [ -f "$BACKUPFILE" ] + then + echo "Restoring up DB Table [$TABLENAME]" + ionice -c3 nice -n19 mysql --defaults-file=$SQLCONFFILE $DBNAME < $BACKUPFILE + sleep $SLEEPTIME + else + echo "Missing backup file [$$TABLENAME]" + sleep $SLEEPTIME + fi + done + rm -rf "$ROOTFOLDER/$EXPIRATION/" +else + echo "Restore in progress, aborting" +fi diff --git a/servatrice/scripts/linux/info_db_tablesize b/servatrice/scripts/linux/info_db_tablesize new file mode 100644 index 000000000..29b5e1b99 --- /dev/null +++ b/servatrice/scripts/linux/info_db_tablesize @@ -0,0 +1,8 @@ +#!/bin/bash + +#USE THIS SCRIPT TO IDENTIFY THE SIZE OF YOUR TABLES IN THE DATABASE + +DBNAME="servatrice" #set this to the database name used +TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _) +SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file +mysql --defaults-file=$SQLCONFFILE -e 'SELECT table_name AS "Tables", round(((data_length + index_length) / 1024 / 1024), 2) "Size in MB" FROM information_schema.TABLES WHERE table_schema = "'$DBNAME'" ORDER BY (data_length + index_length) DESC;' diff --git a/servatrice/scripts/linux/maint_countrycodes b/servatrice/scripts/linux/maint_countrycodes new file mode 100644 index 000000000..32d78628e --- /dev/null +++ b/servatrice/scripts/linux/maint_countrycodes @@ -0,0 +1,36 @@ +#!/bin/bash + +# THIS SCRIPT EXPECTS TO BE EXECUTED FROM THE GITHUB SOURCE FOLDER PATH STRUCTURE +# OTHERWISE, UPDATE THE 'COUNTRYCODEIMAGEPATH' TO POINT TO THE FOLDER CONTAINING THE COUNTRY CODE IMAGES +# USE THIS SCRIPT TO COMPARE EXISTING USER ACCOUNTS TO VALID COUNTRY CODES AND CLEAR INVALID COUNTRY CODE DATA + +MODE="report" #set this to correct to fix invalid country codes, otherwise it only reports +DBNAME="servatrice" #set this to the database name used +TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _) +SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file +COUNTRYCODEIMAGEPATH='../../../cockatrice/resources/countries' +VALIDCOUNT=0 +INVALIDCOUNT=0 + +for i in `mysql --defaults-file=$SQLCONFFILE -h localhost -e "select distinct(country) from ""$DBNAME"".""$TABLEPREFIX""_users;"` +do + if [ "$i" != "country" ]; then + if [ -f "$COUNTRYCODEIMAGEPATH/$i.svg" ]; then + ((VALIDCOUNT++)) + else + ((INVALIDCOUNT++)) + + if [ "$MODE" == "correct" ]; then + echo "$i COUNTRY CODE INVALID, ATTEMPTING TO CORRECT" + mysql --defaults-file=$SQLCONFFILE -h localhost -e "update ""$DBNAME"".""$TABLEPREFIX""_users set country = '' where country = '$i';" + fi + fi + fi +done + +if [ "$MODE" == "correct" ]; then + mysql --defaults-file=$SQLCONFFILE -h localhost -e "update ""$DBNAME"".""$TABLEPREFIX""_users set country = lower(country);" +fi + +echo "INVALID: $INVALIDCOUNT" +echo "VALID: $VALIDCOUNT" diff --git a/servatrice/scripts/linux/maint_inactiveaccounts b/servatrice/scripts/linux/maint_inactiveaccounts new file mode 100644 index 000000000..294463569 --- /dev/null +++ b/servatrice/scripts/linux/maint_inactiveaccounts @@ -0,0 +1,9 @@ +#!/bin/bash + +# SCHEDULE WITH CRONTAB ON A REGULAR BASIS. NUMBER OF DAYS IS THE AMOUNT OF DAYS TO KEEP INACTIVE ACCOUNTS (EX: 1 DAY REMOVES ALL INACTIVE ACCOUNTS OLDER THAN A SINGLE DAY). + +DBNAME="servatrice" #set this to the database name used +TABLEPREFIX="cockatrice" #set this to the prefix used for the table names with in the database +SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file +NUMBEROFDAYS=5 #set this to the number of days to search for +mysql --defaults-file=$SQLCONFFILE -h localhost -e "delete from ""$DBNAME"".""$TABLEPREFIX""_users where active = 0 AND registrationDate < DATE_SUB(now(), INTERVAL ""$NUMBEROFDAYS"" DAY);" diff --git a/servatrice/scripts/linux/maint_logs b/servatrice/scripts/linux/maint_logs new file mode 100644 index 000000000..4e5a43e48 --- /dev/null +++ b/servatrice/scripts/linux/maint_logs @@ -0,0 +1,9 @@ +#!/bin/bash + +#SCHEDULE WITH CRONTAB AND ADJUST THE INTERVALS FOR THE NUMBER OF DAYS OF LOGS TO KEEP IN THE DATABASE + +DBNAME="servatrice" #set this to the database name used +TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _) +SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file +NUMBEROFDAYS=10 #set this to the number of days desired +mysql --defaults-file=$SQLCONFFILE -h localhost -e 'delete from ""$DBNAME"".""$TABLEPREFIX""_log where log_time < DATE_SUB(now(), INTERVAL ""$NUMBEROFDAYS"" DAY)' diff --git a/servatrice/scripts/linux/maint_privlevel b/servatrice/scripts/linux/maint_privlevel new file mode 100644 index 000000000..cced056e3 --- /dev/null +++ b/servatrice/scripts/linux/maint_privlevel @@ -0,0 +1,8 @@ +#!/bin/bash + +# SCHEDULE WITH CRONTAB TO RUN ON A REGULAR BASIS + +DBNAME="servatrice" #set this to the database name used +TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _) +SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file +mysql --defaults-file=$SQLCONFFILE -h localhost -e "update ""$DBNAME"".""$TABLEPREFIX""_users set privlevel = 'NONE' where privelevel != 'NONE" AND privlevelEndDate < NOW()" diff --git a/servatrice/scripts/linux/maint_replays b/servatrice/scripts/linux/maint_replays new file mode 100644 index 000000000..98eb3c4e2 --- /dev/null +++ b/servatrice/scripts/linux/maint_replays @@ -0,0 +1,7 @@ +#!/bin/bash + +# SCHEDULE WITH CRONTAB DAILY +DBNAME="servatrice" #set this to the database name used +TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _) +SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file +mysql --defaults-file=$SQLCONFFILE -h localhost -e 'delete from servatrice.cockatrice_games where time_finished < DATE_SUB(now(), INTERVAL 8 DAY)' \ No newline at end of file diff --git a/servatrice/scripts/linux/maint_sessions b/servatrice/scripts/linux/maint_sessions new file mode 100644 index 000000000..28d49b03e --- /dev/null +++ b/servatrice/scripts/linux/maint_sessions @@ -0,0 +1,9 @@ +#!/bin/bash + +# SCHEDULE WITH CRONTAB TO RUN ON A REGULAR BASIS + +DBNAME="servatrice" #set this to the database name used +TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _) +SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file +NUMBEROFDAYS=10 #set this to the number of days desired +mysql --defaults-file=$SQLCONFFILE -h localhost -e "delete from ""$DBNAME"".""$TABLEPREFIX""_sessions where start_time < DATE_SUB(now(), INTERVAL ""$NUMBEROFDAYS"" DAY)" diff --git a/servatrice/scripts/linux/setup_addfirstadmin b/servatrice/scripts/linux/setup_addfirstadmin new file mode 100644 index 000000000..9ea2511c9 --- /dev/null +++ b/servatrice/scripts/linux/setup_addfirstadmin @@ -0,0 +1,8 @@ +#!/bin/bash + +# SCRIPT TO ADD THE FIRST ADMIN USER NAMED SERVATRICE WITH THE PASSWORD OF PASSWORD + +DBNAME="servatrice" #set this to the database name used +TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _) +SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file +mysql --defaults-file=$SQLCONFFILE -h localhost -e "insert into ""$DBNAME"".""$TABLEPREFIX""_users ((admin,name,password_sha512,active,realname,email,country,avatar_bmp,registrationDate,clientID,adminnotes,privlevelStartDate,privlevelEndDate) values (1,'servatrice','jbB4kSWDmjaVzMNdU13n73SpdBCJTCJ/JYm5ZBZvfxlzbISbXir+e/aSvMz86KzOoaBfidxO0s6GVd8t00qC0TNPl+udHfECaF7MsA==',1,'servatrice','servatrice@localhost','us','null.bmp','1970-01-01 10:00:00','','','1970-01-01 10:00:00','9999-01-01 10:00:00'); diff --git a/servatrice/scripts/mysql.cnf.example b/servatrice/scripts/mysql.cnf.example new file mode 100644 index 000000000..e5d901be6 --- /dev/null +++ b/servatrice/scripts/mysql.cnf.example @@ -0,0 +1,3 @@ +[client] +user={db_username} +password={db_password} diff --git a/servatrice/scripts/register.py b/servatrice/scripts/register.py new file mode 100755 index 000000000..d33db2e21 --- /dev/null +++ b/servatrice/scripts/register.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python + +import socket, sys, struct, time + +from pypb.server_message_pb2 import ServerMessage +from pypb.session_commands_pb2 import Command_Register as Reg +from pypb.commands_pb2 import CommandContainer as Cmd +from pypb.event_server_identification_pb2 import Event_ServerIdentification as ServerId +from pypb.response_pb2 import Response + +HOST = "localhost" +PORT = 4748 + +CMD_ID = 1 + +def build_reg(): + global CMD_ID + cmd = Cmd() + sc = cmd.session_command.add() + + reg = sc.Extensions[Reg.ext] + reg.user_name = "testUser" + reg.email = "test@example.com" + reg.password = "password" + + cmd.cmd_id = CMD_ID + CMD_ID += 1 + return cmd + +def send(msg): + packed = struct.pack('>I', len(msg)) + sock.sendall(packed) + sock.sendall(msg) + +def print_resp(resp): + print "<<<" + print repr(resp) + m = ServerMessage() + m.ParseFromString(bytes(resp)) + print m + +def recv(sock): + print "< header" + header = sock.recv(4) + msg_size = struct.unpack('>I', header)[0] + print "< ", msg_size + raw_msg = sock.recv(msg_size) + print_resp(raw_msg) + +if __name__ == "__main__": + address = (HOST, PORT) + sock = socket.socket() + + print "Connecting to server ", address + sock.connect(address) + + # hack for old xml clients - server expects this and discards first message + print ">>> xml hack" + xmlClientHack = Cmd().SerializeToString() + send(xmlClientHack) + print sock.recv(60) + + recv(sock) + + print ">>> register" + r = build_reg() + print r + msg = r.SerializeToString() + send(msg) + recv(sock) + + print "Done" + diff --git a/servatrice/servatrice.desktop b/servatrice/servatrice.desktop new file mode 100644 index 000000000..7149284bc --- /dev/null +++ b/servatrice/servatrice.desktop @@ -0,0 +1,10 @@ +#!/usr/bin/env xdg-open +[Desktop Entry] +Version=1.0 +Type=Application +Name=Servatrice +Exec=servatrice +Icon=servatrice +Categories=Game;CardGame; +Terminal=true +Comment=Game server for Cockatrice diff --git a/servatrice/servatrice.ini.example b/servatrice/servatrice.ini.example index bce913606..fac743c39 100644 --- a/servatrice/servatrice.ini.example +++ b/servatrice/servatrice.ini.example @@ -1,47 +1,432 @@ +; Servatrice configuration file +; +; This is the main configuration file for Servatrice; while using a configuration is not mandatory, +; you may want to customize some aspects of your servatrice instance, like its name, port or the way +; users can authenticate to the server. +; +; Can be passed to servatrice with --config /path/to/servatrice.ini + [server] -port=4747 -statusupdate=15000 -logfile=server.log + +; This is the name that servatrice exposes to the users; the default value is pretty boring name="My Cockatrice server" + +; Multiple servatrice servers can run on the same host using the same database; each server instance +; must have a different id; the default id is 1 id=1 + +; The IP address servatrice will listen on for clients; defaults to "any" to listen on all the interfaces, +; a specific IPv4/v6 addresses can be used, eg for localhost-only 127.0.0.1 for IPv4 or ::1 for IPv6. + +host=any + +; The TCP port number Servatrice will listen on for clients; default is 4747; +; Will be removed in the future, use websocket connection instead +port=4747 + +; Servatrice can scale up to serve big number of users using more than one parallel thread of execution; +; If your server is hosting a lot of players and they frequently report of being unable to login or +; long delays (lag), you may want to try increasing this value; default is 1. +; Set to 0 to disable the tcp server. number_pools=1 -[servernetwork] -active=0 -port=14747 -ssl_cert=ssl_cert.pem -ssl_key=ssl_key.pem +; Servatrice can listen for clients on websockets, too. Multiple connection pools are available but +; unfortunately, due to a Qt limitation, they must run in the same execution thread. +; Set to 0 to disable the websocket server. +websocket_number_pools=1 + +; The IP address servatrice will listen on for websockets clients; defaults to "any" +websocket_host=any + +; The TCP port number servatrice will listen on for websockets clients; default is 4748 +websocket_port=4748 + +; When database is enabled, servatrice writes the server status in the "update" database table; this +; setting defines every how many milliseconds servatrice will update its status; default is 15000 (15 secs) +statusupdate=15000 + +; Do you want servatrice to write important events and errors to a logfile? Default is 1 (yes). +writelog=1 + +; Choose a name for the log file, if enabled; you can specify an absolute path or a path relative to +; the servatrice executable; the default file name is server.log (in the same path as servatrice) +; Note: When running servatrice under windows you will need to use double backslashes between folder locations +; [ex: C:\\Temp\\server.log ] +logfile=server.log + +; You may want to log only certain messages in the logfile. The default log level is extremely verbose. +; This setting should contain a comma-separated list of strings that will be selectively logged. +; All other lines will be excluded from the log. Default is empty; example: "Registration,_Login,foobar" +logfilters="" + +; Set the time interval in seconds that servatrice will use to communicate with each connected client +; to verify the client has not timed out. Defaults is 1 seconds +clientkeepalive=1 + +; Maximum time in seconds a player can stay inactive with there client not even responding to pings, before is +; considered disconnected; default is 15 +max_player_inactivity_time=15 + +; More modern clients generate client IDs based on specific client side information. Enable this option to +; require that clients report the client ID in order to log into the server. Default is false +requireclientid=false + +; You can limit the types of clients that connect to the server by requiring different features be available +; on the client. This setting can contain a comma-seperated list of features. if any of the features +; listed in this line are not available on the client the client will be denied access to the server upon +; attempting to log in. Example: "client_id,client_ver,websocket" +requiredfeatures="" + +; You can define custom warnings that users are sent when the moderation staff uses the right client warn user +; menu option. This list is comma seperated that each item will appear in the drop down list for staff members +; to choose from. Example: "Flaming,Foul Language" +officialwarnings="Flaming,Spamming,Causing Drama,Abusive Language" + +; Maximum time in seconds a player can stay connected but idle. Default is 3600 (0 = disabled) +; Clients will be notified at the 90% time period of pending disconnection if they do not take action. +idleclienttimeout=3600 [authentication] + +; Servatrice can authenticate users connecting. It currently supports 3 different authentication methods: +; * none: no authentication, accept every user; +; * password: require users to specify a common password to log in; +; * sql: authenticate users against the "users" table of the database; +; Please note that only the "sql" method permits to have registered users and store their data on the server. method=none +; if the chosen authentication method is password, here you can define the password your users will use to log in +password=123456 + +; Accept only registered users? default is false (accept unregistered users) +regonly=false + +[users] + +; The minimum length a username can be +minnamelength=6 + +; The maximum length a username can be +maxnamelength=12 + +; If a username should be allowed to contain lowercase chars [a-z] +allowlowercase=true + +; If a username should be allowed to conatain uppercase chars [A-Z] +allowuppercase=true + +; If a username should be allowed to contain numbers [0-9] +allownumerics=true + +; Define punctuation allowed in usernames +allowedpunctuation=_.- + +; If a username can begin with punctuation defined in allowedpunctuation +allowpunctuationprefix=false + +; Disallow usernames containing these words. This list is comma seperated, e.g. +; "admin,user,name" +disallowedwords="admin" + +; Overwrite the words shown to the user when they enter a wrong username, +; use \n to start a new line. Neither the real wordlist nor the disallowed +; expressions will be sent to the user if this is set. +; In the old versions of the client this list will be prefaced with +; "can not contain any of the following words:" +; example: +;displaydisallowedwords="no attempts at impersonating staff\nno unparliamentary language\nno references to controversial figures\nstaff reserves the right to remove accounts deemed inappropriate" +; Setting it to nothing will simply hide the list: +;displaydisallowedwords= + +; Disallow usernames matching these regular expressions. This list is comma +; separated, e.g. "\\w+\\d+,\\d{2}user", hence you cannot use commas in your +; expressions. Backslashes must be escaped, so `\w+\d+` becomes `\\w+\\d+`. +; WARNING: Complex expressions can be harmful to performance. Please make sure +; your expressions are considered well formed. See this page for info: +; http://www.regular-expressions.info/catastrophic.html +disallowedregexp="" + +; Define minimum password length +; Default 6. +minpasswordlength = 6 + +[registration] + +; Servatrice can process registration requests to add new users on the fly. +; Enable this feature? Default false. +;enabled=false + +; Require users to provide an email address in order to register. Default true. +;requireemail=true + +; Require email activation. Newly registered users will receive an activation token by email, +; and will be required to input back this token on cockatrice at the first login to get their +; account activated. Default true. +;requireemailactivation=true + +; Set this number to the maximum number of accounts any one user can use to create new accounts +; using the same email address. 0 = Unlimited number of accounts (default). +;maxaccountsperemail=0 + +; You can prevent users from using certain mail domains for registration. This setting contains a +; comma-seperated list of email provider domains that you would like to prevent users from using +; during registration. Comparison's are implicit, so placing an entry such as mail.com will also +; prevent users from registering accounts with providers such as gmail.com and hotmail.com +; Example: "10minutemail.com,gmail.com" +;emailproviderblacklist="" + +; You can require users to only use certain email domains for registration. This setting is a +; comma-separated list of email provider domains that you have explicitly audited and require +; the use of in order to create an account. Comparison's are explicit, so you must specify the +; domain in completion, such as gmail.com and hotmail.com. Email whitelist is checked before +; Email blacklist is checked, so an email cannot be in both setting configurations. +; Example: "gmail.com,hotmail.com,icloud.com" +;emailproviderwhitelist="" + +[forgotpassword] + +; Servatrice can process reset password requests allowing users to reset their account +; passwords in the event they forget it. Should this feature be enabled? Default: false. +; enable=false + +; Reset password request should not be allowed to stay valid forever. This settings +; informs servatrice how long a players reset password reset token is valid for (in minutes). +; Default: 60 +; tokenlife=60 + +; Servatrice can challenge users that are making reset password requests to answer +; questions in regards to their account to help validate they are the true owner of the account. +; Should this feature be enabled? Default: false +; enablechallenge=false + +; Email subject for the reset password emails +; subject="Cockatrice reset password token" + +; Reset password email body. You can use these tags here: %username %token +; They will be substituted with the actual values in the email +; +; body="Hi %username,\r\nthanks for reaching out to us with your password reset request for our Cockatrice server.\r\nHere's your unique token in order to reset your account password in the app:\r\n\r\n%token\r\n\r\nHappy gaming!" + + +[smtp] + +; Enable the internal smtp client to send registration emails. If you would like to +; use some other method to send email activation tokens set this value to false. Otherwise +; setting it to true (default) the server will send canned generated emails containing +; activation tokens for you during update intervals. Setting this to false will require +; you to either manually activate user accounts or manually send users the activation token +; by whatever means. +enableinternalsmtpclient=true + +; Connectin type: currently supported method are "tcp" and "ssl"; tls is autodetected if available +connection=tcp + +; Accept all certificates: in ssl mode, enable this if your server is using an invalid/self signed certificate +acceptallcerts=false; + +; Hostname or IP addres of the smtp server +host=localhost + +; Smtp port number of the smtp server. Usual values are 25 or 587 for tcp, 465 for ssl +port=25 + +; Username: this typically matches the "from" email address +username=root@localhost + +; Password for the username +password=foobar + +; Sender email address: the "from" email address +email=root@localhost + +; Sender email name +name="Cockatrice server" + +; Email subject +subject="Cockatrice server account activation token" + +; Email body. You can use these tags here: %username %token +; They will be substituted with the actual values in the email +; +body="Hi %username, thank our for registering on our Cockatrice server\r\nHere's the activation token you need to supply for activating your account:\r\n\r\n%token\r\n\r\nHappy gaming!" + [database] + +; Database type. Valid values are: +; * none: no database; +; * mysql: mysql or compatible database; type=none + +; Prefix used in he database for table names; default is cockatrice prefix=cockatrice + +; Database connection parameter: server hostname or IP hostname=localhost + +; Database connection parameter: database name database=servatrice + +; Database connection parameter: database user user=servatrice + +; Database connection parameter: database user's password password=foobar [rooms] + +; A servatrice server can expose to the users different "rooms" to chat and create games. Rooms can be defined +; with two different methods: +; config: rooms are defined in this configuration (see the following example) +; sql: rooms are defined in the "rooms" table of the database method=config + +; Example configuration for a server with rooms configured in the configuration file. Number of rooms defined roomlist\size=1 + +; Room name for the room number 1 roomlist\1\name="General room" + +; Room description for the room number 1 roomlist\1\description="Play anything here." + +; Rooms can restrict the level of user that can join. Current supported options are none, registered, moderator, administrator. +; Default is none. +roomlist\1\permissionlevel=none + +; Rooms can restrict the permission level of users that can join, Currnetly supported options are none, privileged, vip, and donator. +; Default is none. +roomlist\1\privilegelevel=none + +; Wether to make users autojoin this room when connected to the server roomlist\1\autojoin=true + +; Message displayed to each user when he joins room number 1 roomlist\1\joinmessage="This message is only here to show that rooms can have a join message." + +; The number of chat history messages to save that gets presented to a user joining the room +roomlist\1\chathistorysize=100 + +; Number of game types allowed (defined) in the room number 1 roomlist\1\game_types\size=3 + +; Name of the three game types for the room number 1 roomlist\1\game_types\1\name="GameType1" roomlist\1\game_types\2\name="GameType2" roomlist\1\game_types\3\name="GameType3" + [game] + +; Maximum time in seconds all players in a game can stay inactive before the game is automatically closed; +; default is 120 max_game_inactivity_time=120 -max_player_inactivity_time=15 + +; All actions during a game are recorded and stored in the database as a replay that all participants of +; the game can go back to and review after the game is closed. This can require a fairly large amount of +; storage to save all the information. Disable this option to prevent the storing of replay data in +; the database. Default value is true. +store_replays=true + +; Allow users to create a new game and join it as a judge. The host will be able to execute any action on +; the cards of every player. This is needed in order to support some games (eg. Werewolf). +; Default off to prevent abuse on servers that are mostly running other games. +allow_create_as_judge=false [security] +; You may want to restrict the number of users that can connect to your server at any given time. +enable_max_user_limit=false + +; Maximum number of users that can connect to the server, default is 500. +max_users_total=500 + +; Maximum number of users that can connect to the server using a tcp connection, default is 500. +max_users_tcp=500 + +; Maximum number of users that can connect to the server using a websocket connection, default is 500. +max_users_websocket=500 + +; Maximum number of users that can connect from the same IP address; useful to avoid bots, default is 4 max_users_per_address=4 + +; You may want to allow an unlimited number of users from a trusted source. This setting can contain a +; comma-separed list of IP addresses which will allow an unlimited number of connections from each of the +; IP addresses listed (ignoring the max_users_per_address). Default is "127.0.0.1,::1"; example: "192.73.233.244,81.4.100.74" +trusted_sources="127.0.0.1,::1" + +; Servatrice can avoid users from flooding rooms with large number of messages in an interval of time. +; This setting defines the length in seconds of the considered interval; default is 10 message_counting_interval=10 + +; Maximum size in characters of all messages in an interval before new messages gets dropped; default is 1000 max_message_size_per_interval=1000 + +; Maximum number of messages in an interval before new messages gets dropped; default is 10 max_message_count_per_interval=10 + +; Maximum number of games a single user can create; default is 5; set to -1 to disable; 0 disallows game creation max_games_per_user=5 + +; Servatrice can avoid users from flooding games with large number of game commands in an interval of time. +; This setting defines the length in seconds of the considered interval; default is 10 +command_counting_interval=10 + +; Maximum number of game commands in an interval before new commands gets dropped; default is 20 +max_command_count_per_interval=20 + +[logging] +; Admin/Moderators can query the stored logs for information when looking up reports by various players. This +; option can allow or disallow them from doing so. +; !!NOTE!! Enabling this feature puts a very high CPU and DISK load on the server, enable with caution. +enablelogquery=false + +; Servatrice can log user messages to the database table cockatrice_log. +; These messages can come from different sources; each source can be enabled separately. + +; Log user messages inside chat rooms +log_user_msg_room=false + +; Log user messages inside games +log_user_msg_game=false + +; Log user messages in private chats +log_user_msg_chat=false + +; Log user messages coming from other servers in the network +log_user_msg_isl=false + +[audit] + +; Servatrice can record certain actions being performed in the database for server operators to better understand +; if some one may be abusing application functionality. Enabling auditing will allow servatrice to record any +; of the below enabled audit functionality to be recorded. +; Default: true +enable_audit=true + +; Servatrice can record when users attempt a new account registration. Should we enable auditing for this action? +; Default: true +enable_registration_audit=true + +; Servatrice can record when a users attempts to reset the account password. Should we enable auditing for this action? +; Default: true +enable_forgotpassword_audit=true + + +; EXPERIMENTAL - NOT WORKING YET +; The following settings are relative to the server network functionality, that is not yet complete. +; Avoid enabling it unless you are willing to test it and help its development. + +[servernetwork] + +; Servatrice servers can connect themselves and build a network. This settins enable the ability of servatrice +; of waiting for other server's connections and connect to other servers. Other servers can be defined in the +; "servers" table of the database. Default is 0 (disabled) +active=0 + +; The TCP port number Servatrice will listen on for other servers; default is 14747 +port=14747 + +; Server-to-server communication needs a valid certificate in PEM format. Enter its filename in this setting +ssl_cert=ssl_cert.pem + +; Filename of the private key for the server-to-server certificate +ssl_key=ssl_key.pem diff --git a/servatrice/servatrice.qrc b/servatrice/servatrice.qrc new file mode 100644 index 000000000..f9cf3ef98 --- /dev/null +++ b/servatrice/servatrice.qrc @@ -0,0 +1,5 @@ + + + resources/servatrice.svg + + diff --git a/servatrice/servatrice.rc b/servatrice/servatrice.rc new file mode 100644 index 000000000..cf949f313 --- /dev/null +++ b/servatrice/servatrice.rc @@ -0,0 +1 @@ +ID1_ICON1 ICON DISCARDABLE "resources/appicon.ico" diff --git a/servatrice/servatrice.sql b/servatrice/servatrice.sql index 6e83f1f2d..fa644dbc0 100644 --- a/servatrice/servatrice.sql +++ b/servatrice/servatrice.sql @@ -1,11 +1,7 @@ --- phpMyAdmin SQL Dump --- version 2.11.8.1deb1ubuntu0.2 --- http://www.phpmyadmin.net --- --- Host: localhost --- Erstellungszeit: 11. Oktober 2010 um 23:57 --- Server Version: 5.0.67 --- PHP-Version: 5.2.6-2ubuntu4.6 +-- Schema file for servatrice database. + +-- This schema file is using the default table prefix "cockatrice", +-- to match the "prefix=cockatrice" default setting in servatrice.ini SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; @@ -15,15 +11,48 @@ SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET NAMES utf8 */; --- --- Datenbank: `servatrice` --- +-- Every time the database schema changes, the schema version number +-- must be incremented. Also remember to update the corresponding +-- number in servatrice/src/servatrice_database_interface.h --- -------------------------------------------------------- +CREATE TABLE IF NOT EXISTS `cockatrice_schema_version` ( + `version` int(7) unsigned NOT NULL, + PRIMARY KEY (`version`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; --- --- Tabellenstruktur für Tabelle `decklist_files` --- +INSERT INTO cockatrice_schema_version VALUES(34); + +-- users and user data tables +CREATE TABLE IF NOT EXISTS `cockatrice_users` ( + `id` int(7) unsigned zerofill NOT NULL auto_increment, + `admin` tinyint(1) NOT NULL, + `name` varchar(35) NOT NULL, + `realname` varchar(255) NOT NULL, + `password_sha512` char(120) NOT NULL, + `email` varchar(255) NOT NULL, + `country` char(2) NOT NULL, + `avatar_bmp` mediumblob NOT NULL, + `registrationDate` datetime NOT NULL, + `active` tinyint(1) NOT NULL, + `token` binary(16), + `clientid` varchar(15) NOT NULL, + `adminnotes` mediumtext NOT NULL, + `privlevel` enum("NONE","VIP","DONATOR") NOT NULL, + `privlevelStartDate` datetime NOT NULL, + `privlevelEndDate` datetime NOT NULL, + `passwordLastChangedDate` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + `leftPawnColorOverride` varchar(255), + `rightPawnColorOverride` varchar(255), + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`), + KEY `token` (`token`), + KEY `email` (`email`), + INDEX `idx_admin` (`admin`), + INDEX `idx_active` (`active`), + INDEX `idx_privlevel` (`privlevel`), + INDEX `idx_clientid` (`clientid`), + INDEX `idx_pawnColorOverrides` (`leftPawnColorOverride`, `rightPawnColorOverride`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS `cockatrice_decklist_files` ( `id` int(7) unsigned zerofill NOT NULL auto_increment, @@ -33,14 +62,9 @@ CREATE TABLE IF NOT EXISTS `cockatrice_decklist_files` ( `upload_time` datetime NOT NULL, `content` text NOT NULL, PRIMARY KEY (`id`), - KEY `FolderPlusUser` (`id_folder`,`id_user`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; - --- -------------------------------------------------------- - --- --- Tabellenstruktur für Tabelle `decklist_folders` --- + KEY `FolderPlusUser` (`id_folder`,`id_user`), + FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS `cockatrice_decklist_folders` ( `id` int(7) unsigned zerofill NOT NULL auto_increment, @@ -48,163 +72,231 @@ CREATE TABLE IF NOT EXISTS `cockatrice_decklist_folders` ( `id_user` int(7) unsigned NULL, `name` varchar(30) NOT NULL, PRIMARY KEY (`id`), - KEY `ParentPlusUser` (`id_parent`,`id_user`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; + KEY `ParentPlusUser` (`id_parent`,`id_user`), + FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; --- -------------------------------------------------------- +CREATE TABLE IF NOT EXISTS `cockatrice_ignorelist` ( + `id_user1` int(7) unsigned NOT NULL, + `id_user2` int(7) unsigned NOT NULL, + UNIQUE KEY `key` (`id_user1`, `id_user2`), + FOREIGN KEY(`id_user1`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY(`id_user2`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; --- --- Tabellenstruktur für Tabelle `games` --- +CREATE TABLE IF NOT EXISTS `cockatrice_buddylist` ( + `id_user1` int(7) unsigned NOT NULL, + `id_user2` int(7) unsigned NOT NULL, + UNIQUE KEY `key` (`id_user1`, `id_user2`), + FOREIGN KEY(`id_user1`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY(`id_user2`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; +-- rooms +CREATE TABLE IF NOT EXISTS `cockatrice_rooms` ( + `id` int(7) unsigned NOT NULL auto_increment, + `name` varchar(50) NOT NULL, + `descr` varchar(255) NOT NULL, + `permissionlevel` enum('NONE','REGISTERED','MODERATOR','ADMINISTRATOR') NOT NULL, + `privlevel` enum('NONE','PRIVILEGED','VIP','DONATOR') NOT NULL, + `auto_join` tinyint(1) default 0, + `join_message` varchar(255) NOT NULL, + `chat_history_size` int(4) NOT NULL, + `id_server` tinyint(3) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `cockatrice_rooms_gametypes` ( + `id_room` int(7) unsigned NOT NULL, + `name` varchar(50) NOT NULL, + `id_server` tinyint(3) NOT NULL DEFAULT 1, + FOREIGN KEY(`id_room`) REFERENCES `cockatrice_rooms`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; + +-- games CREATE TABLE IF NOT EXISTS `cockatrice_games` ( `room_name` varchar(255) NOT NULL, `id` int(7) unsigned NOT NULL auto_increment, `descr` varchar(50) default NULL, - `creator_name` varchar(255) NOT NULL, + `creator_name` varchar(35) NOT NULL, `password` tinyint(1) NOT NULL, `game_types` varchar(255) NOT NULL, `player_count` tinyint(3) NOT NULL, `time_started` datetime default NULL, `time_finished` datetime default NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; - --- -------------------------------------------------------- - --- --- Tabellenstruktur für Tabelle `games_players` --- +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS `cockatrice_games_players` ( `id_game` int(7) unsigned zerofill NOT NULL, - `player_name` varchar(255) NOT NULL, - KEY `id_game` (`id_game`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; + `player_name` varchar(35) NOT NULL, + FOREIGN KEY(`id_game`) REFERENCES `cockatrice_games`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; --- -------------------------------------------------------- +-- Note: an empty row with id_game = NULL is created when the game is created, +-- and then updated when the game ends with the full replay data. +CREATE TABLE IF NOT EXISTS `cockatrice_replays` ( + `id` int(7) NOT NULL AUTO_INCREMENT, + `id_game` int(7) unsigned NULL, + `duration` int(7) NOT NULL, + `replay` mediumblob NOT NULL, + PRIMARY KEY (`id`), + FOREIGN KEY(`id_game`) REFERENCES `cockatrice_games`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; --- --- Tabellenstruktur für Tabelle `news` --- +CREATE TABLE IF NOT EXISTS `cockatrice_replays_access` ( + `id_game` int(7) unsigned NOT NULL, + `id_player` int(7) unsigned NOT NULL, + `replay_name` varchar(255) NOT NULL, + `do_not_hide` tinyint(1) NOT NULL, + KEY `id_player` (`id_player`), + FOREIGN KEY(`id_game`) REFERENCES `cockatrice_games`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY(`id_player`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; -CREATE TABLE IF NOT EXISTS `cockatrice_news` ( - `id` int(7) unsigned zerofill NOT NULL auto_increment, - `id_user` int(7) unsigned zerofill NOT NULL, - `news_date` datetime NOT NULL, - `subject` varchar(255) NOT NULL, - `content` text NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +-- server administration --- -------------------------------------------------------- +-- Note: unused table +CREATE TABLE IF NOT EXISTS `cockatrice_servers` ( + `id` mediumint(8) unsigned NOT NULL, + `ssl_cert` text NOT NULL, + `hostname` varchar(255) NOT NULL, + `address` varchar(255) NOT NULL, + `game_port` mediumint(8) unsigned NOT NULL, + `control_port` mediumint(9) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; --- --- Tabellenstruktur für Tabelle `users` --- - -CREATE TABLE IF NOT EXISTS `cockatrice_users` ( - `id` int(7) unsigned zerofill NOT NULL auto_increment, - `admin` tinyint(1) NOT NULL, - `name` varchar(35) NOT NULL, - `realname` varchar(255) NOT NULL, - `gender` char(1) NOT NULL, - `password_sha512` char(120) NOT NULL, - `email` varchar(255) NOT NULL, - `country` char(2) NOT NULL, - `avatar_bmp` blob NOT NULL, - `registrationDate` datetime NOT NULL, - `active` tinyint(1) NOT NULL, - `token` binary(16) NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `name` (`name`), - KEY `token` (`token`), - KEY `email` (`email`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -CREATE TABLE `cockatrice_uptime` ( +CREATE TABLE IF NOT EXISTS `cockatrice_uptime` ( `id_server` tinyint(3) NOT NULL, `timest` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `uptime` int(11) NOT NULL, `users_count` int(11) NOT NULL, + `mods_count` int(11) NOT NULL DEFAULT 0, + `mods_list` TEXT, `games_count` int(11) NOT NULL, `rx_bytes` int(11) NOT NULL, `tx_bytes` int(11) NOT NULL, PRIMARY KEY (`timest`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; -CREATE TABLE `cockatrice_servermessages` ( - `id_server` tinyint(3) not null default 0, +CREATE TABLE IF NOT EXISTS `cockatrice_servermessages` ( + `id_server` tinyint(3) not null default 1, `timest` datetime NOT NULL default '0000-00-00 00:00:00', `message` text, PRIMARY KEY (`timest`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; -CREATE TABLE `cockatrice_ignorelist` ( - `id_user1` int(7) unsigned NOT NULL, - `id_user2` int(7) unsigned NOT NULL, - UNIQUE KEY `key` (`id_user1`, `id_user2`), - KEY `id_user1` (`id_user1`), - KEY `id_user2` (`id_user2`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +CREATE TABLE IF NOT EXISTS `cockatrice_sessions` ( + `id` int(9) NOT NULL AUTO_INCREMENT, + `user_name` varchar(35) NOT NULL, + `id_server` tinyint(3) NOT NULL, + `ip_address` varchar(45) NOT NULL, + `start_time` datetime NOT NULL, + `end_time` datetime DEFAULT NULL, + `clientid` varchar(15) NOT NULL, + `connection_type` ENUM('tcp', 'websocket'), + PRIMARY KEY (`id`), + KEY `username` (`user_name`), + INDEX `idx_start_time` (`start_time`), + INDEX `idx_clientid` (`clientid`), + INDEX `idx_ip_address` (`ip_address`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; -CREATE TABLE `cockatrice_buddylist` ( - `id_user1` int(7) unsigned NOT NULL, - `id_user2` int(7) unsigned NOT NULL, - UNIQUE KEY `key` (`id_user1`, `id_user2`), - KEY `id_user1` (`id_user1`), - KEY `id_user2` (`id_user2`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -CREATE TABLE `cockatrice_bans` ( - `user_name` varchar(255) NOT NULL, - `ip_address` varchar(255) NOT NULL, +-- server moderation +CREATE TABLE IF NOT EXISTS `cockatrice_bans` ( + `user_name` varchar(35) NOT NULL, + `ip_address` varchar(45) NOT NULL, `id_admin` int(7) unsigned zerofill NOT NULL, `time_from` datetime NOT NULL, `minutes` int(6) NOT NULL, `reason` text NOT NULL, `visible_reason` text NOT NULL, + `clientid` varchar(15) NOT NULL, PRIMARY KEY (`user_name`,`time_from`), KEY `time_from` (`time_from`,`ip_address`), - KEY `ip_address` (`ip_address`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; + KEY `ip_address` (`ip_address`), + INDEX `idx_user_name` (`user_name`), + FOREIGN KEY(`id_admin`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; -CREATE TABLE `cockatrice_sessions` ( - `id` int(9) NOT NULL AUTO_INCREMENT, - `user_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL, - `id_server` tinyint(3) NOT NULL, - `ip_address` char(15) COLLATE utf8_unicode_ci NOT NULL, - `start_time` datetime NOT NULL, - `end_time` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `username` (`user_name`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +CREATE TABLE IF NOT EXISTS `cockatrice_warnings` ( + `user_id` int(7) unsigned NOT NULL, + `user_name` varchar(35) NOT NULL, + `mod_name` varchar(35) NOT NULL, + `reason` text NOT NULL, + `time_of` datetime NOT NULL, + `clientid` varchar(15) NOT NULL, + PRIMARY KEY (`user_id`,`time_of`), + INDEX `idx_time_of` (`time_of`), + INDEX `idx_user_name` (`user_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; -CREATE TABLE `cockatrice_servers` ( - `id` mediumint(8) unsigned NOT NULL, - `ssl_cert` text COLLATE utf8_unicode_ci NOT NULL, - `hostname` varchar(255) COLLATE utf8_unicode_ci NOT NULL, - `address` varchar(255) COLLATE utf8_unicode_ci NOT NULL, - `game_port` mediumint(8) unsigned NOT NULL, - `control_port` mediumint(9) NOT NULL, +CREATE TABLE IF NOT EXISTS `cockatrice_log` ( + `log_time` datetime NOT NULL, + `sender_id` int(7) unsigned NULL, + `sender_name` varchar(35) NOT NULL, + `sender_ip` varchar(45) NOT NULL, + `log_message` text NOT NULL, + `target_type` ENUM('room', 'game', 'chat'), + `target_id` int(7) NULL, + `target_name` varchar(50) NOT NULL, + KEY `sender_name` (`sender_name`), + KEY `sender_ip` (`sender_ip`), + KEY `target_id` (`target_id`), + KEY `target_name` (`target_name`), + INDEX `idx_log_time` (`log_time`), + FOREIGN KEY(`sender_id`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE + -- No FK on target_id, it can be zero +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `cockatrice_activation_emails` ( + `name` varchar(35) NOT NULL, + FOREIGN KEY(`name`) REFERENCES `cockatrice_users`(`name`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `cockatrice_user_analytics` ( + `id` int(7) unsigned zerofill NOT NULL, + `client_ver` varchar(35) NOT NULL, + `last_login` datetime NOT NULL, + `notes` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + INDEX `idx_last_login` (`last_login`), + FOREIGN KEY(`id`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `cockatrice_donations` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `username` varchar(35) DEFAULT NULL, + `email` varchar(255) DEFAULT NULL, + `payment_pre_fee` double DEFAULT NULL, + `payment_post_fee` double DEFAULT NULL, + `term_length` int(11) DEFAULT NULL, + `date` varchar(255) DEFAULT NULL, + `pp_type` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; -CREATE TABLE `cockatrice_replays` ( - `id` int(7) NOT NULL AUTO_INCREMENT, - `id_game` int(7) NOT NULL, - `duration` int(7) NOT NULL, - `replay` mediumblob NOT NULL, - PRIMARY KEY (`id`), - KEY `id_game` (`id_game`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -CREATE TABLE `cockatrice_replays_access` ( - `id_game` int(7) NOT NULL, - `id_player` int(7) NOT NULL, - `replay_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL, - `do_not_hide` tinyint(1) NOT NULL, - KEY `id_player` (`id_player`), - KEY `id_game` (`id_game`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +CREATE TABLE IF NOT EXISTS `cockatrice_forgot_password` ( + `id` int(7) unsigned zerofill NOT NULL auto_increment, + `name` varchar(35) NOT NULL, + `requestDate` datetime NOT NULL default '0000-00-00 00:00:00', + `emailed` tinyint(1) NOT NULL default 0, + PRIMARY KEY (`id`), + KEY `user_name` (`name`), + INDEX `idx_emailed` (`emailed`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; +CREATE TABLE IF NOT EXISTS `cockatrice_audit` ( + `id` int(7) unsigned zerofill NOT NULL auto_increment, + `id_server` tinyint(3) NOT NULL, + `name` varchar(35) NOT NULL, + `ip_address` varchar(45) NOT NULL, + `clientid` varchar(15) NOT NULL, + `incidentDate` datetime NOT NULL default '0000-00-00 00:00:00', + `action` varchar(35) NOT NULL, + `results` ENUM('fail', 'success') NOT NULL DEFAULT 'fail', + `details` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `user_name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; diff --git a/servatrice/src/email_parser.cpp b/servatrice/src/email_parser.cpp new file mode 100644 index 000000000..d8c30d852 --- /dev/null +++ b/servatrice/src/email_parser.cpp @@ -0,0 +1,61 @@ +#include "email_parser.h" + +#include +#include + +QPair EmailParser::parseEmailAddress(const QString &dirtyEmailAddress) +{ + // https://www.regular-expressions.info/email.html + static const QRegularExpression emailRegex(R"(^([A-Z0-9._%+-]+)@([A-Z0-9.-]+\.[A-Z]{2,})$)", + QRegularExpression::CaseInsensitiveOption); + const auto match = emailRegex.match(dirtyEmailAddress); + + if (dirtyEmailAddress.isEmpty() || !match.hasMatch()) { + return {}; + } + + QString capturedEmailUser = match.captured(1); + QString capturedEmailAddressDomain = match.captured(2); + + // Replace googlemail.com with gmail.com, as is standard nowadays + // https://www.gmass.co/blog/domains-gmail-com-googlemail-com-and-google-com/ + if (capturedEmailAddressDomain.toLower() == "googlemail.com") { + capturedEmailAddressDomain = "gmail.com"; + } + + // Trim out dots and pluses from Google/Gmail domains + if (capturedEmailAddressDomain.toLower() == "gmail.com") { + // Remove all content after the first plus sign (as unnecessary with gmail) + // https://gmail.googleblog.com/2008/03/2-hidden-ways-to-get-more-from-your.html + const auto firstPlusSign = capturedEmailUser.indexOf("+"); + if (firstPlusSign != -1) { + capturedEmailUser = capturedEmailUser.left(firstPlusSign); + } + + // Remove all periods (as unnecessary with gmail) + // https://gmail.googleblog.com/2008/03/2-hidden-ways-to-get-more-from-your.html + capturedEmailUser.replace(".", ""); + } + // Trim out minuses from Yahoo domains + else if (capturedEmailAddressDomain.toLower() == "yahoo.com") { + const auto firstMinusSign = capturedEmailUser.indexOf("-"); + if (firstMinusSign != -1) { + capturedEmailUser = capturedEmailUser.left(firstMinusSign); + } + } + + return {capturedEmailUser, capturedEmailAddressDomain}; +} + +QString EmailParser::getParsedEmailAddress(const QString &dirtyEmailAddress) +{ + const auto parsedEmailAddress = EmailParser::parseEmailAddress(dirtyEmailAddress); + return EmailParser::getParsedEmailAddress(parsedEmailAddress); +} + +QString EmailParser::getParsedEmailAddress(const QPair &emailAddressIntermediate) +{ + const auto emailUser = emailAddressIntermediate.first; + const auto emailDomain = emailAddressIntermediate.second; + return emailUser + "@" + emailDomain; +} \ No newline at end of file diff --git a/servatrice/src/email_parser.h b/servatrice/src/email_parser.h new file mode 100644 index 000000000..4cb6ab059 --- /dev/null +++ b/servatrice/src/email_parser.h @@ -0,0 +1,15 @@ +#ifndef COCKATRICE_EMAILPARSER_H +#define COCKATRICE_EMAILPARSER_H + +#include +#include + +class EmailParser +{ +public: + static QPair parseEmailAddress(const QString &dirtyEmailAddress); + static QString getParsedEmailAddress(const QString &dirtyEmailAddress); + static QString getParsedEmailAddress(const QPair &emailAddressIntermediate); +}; + +#endif // COCKATRICE_EMAILPARSER_H diff --git a/servatrice/src/isl_interface.cpp b/servatrice/src/isl_interface.cpp index 2a9ea41b2..692a0fdba 100644 --- a/servatrice/src/isl_interface.cpp +++ b/servatrice/src/isl_interface.cpp @@ -1,415 +1,475 @@ #include "isl_interface.h" -#include -#include "server_logger.h" -#include "main.h" -#include "server_protocolhandler.h" -#include "server_room.h" -#include "get_pb_extension.h" -#include "pb/isl_message.pb.h" -#include "pb/event_game_joined.pb.h" -#include "pb/event_server_complete_list.pb.h" -#include "pb/event_user_message.pb.h" -#include "pb/event_user_joined.pb.h" -#include "pb/event_user_left.pb.h" -#include "pb/event_join_room.pb.h" -#include "pb/event_leave_room.pb.h" -#include "pb/event_room_say.pb.h" -#include "pb/event_list_games.pb.h" +#include "main.h" +#include "server_logger.h" + +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +inline Q_LOGGING_CATEGORY(IslInterfaceLog, "isl_interface"); void IslInterface::sharedCtor(const QSslCertificate &cert, const QSslKey &privateKey) { - socket = new QSslSocket(this); - socket->setLocalCertificate(cert); - socket->setPrivateKey(privateKey); - - connect(socket, SIGNAL(readyRead()), this, SLOT(readClient()), Qt::QueuedConnection); - connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(catchSocketError(QAbstractSocket::SocketError))); - connect(this, SIGNAL(outputBufferChanged()), this, SLOT(flushOutputBuffer()), Qt::QueuedConnection); + socket = new QSslSocket(this); + socket->setLocalCertificate(cert); + socket->setPrivateKey(privateKey); + + connect(socket, SIGNAL(readyRead()), this, SLOT(readClient()), Qt::QueuedConnection); + connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, + SLOT(catchSocketError(QAbstractSocket::SocketError))); + connect(this, SIGNAL(outputBufferChanged()), this, SLOT(flushOutputBuffer()), Qt::QueuedConnection); } -IslInterface::IslInterface(int _socketDescriptor, const QSslCertificate &cert, const QSslKey &privateKey, Servatrice *_server) - : QObject(), socketDescriptor(_socketDescriptor), server(_server), messageInProgress(false) +IslInterface::IslInterface(int _socketDescriptor, + const QSslCertificate &cert, + const QSslKey &privateKey, + Servatrice *_server) + : QObject(), socketDescriptor(_socketDescriptor), server(_server), messageInProgress(false) { - sharedCtor(cert, privateKey); + sharedCtor(cert, privateKey); } -IslInterface::IslInterface(int _serverId, const QString &_peerHostName, const QString &_peerAddress, int _peerPort, const QSslCertificate &_peerCert, const QSslCertificate &cert, const QSslKey &privateKey, Servatrice *_server) - : QObject(), serverId(_serverId), peerHostName(_peerHostName), peerAddress(_peerAddress), peerPort(_peerPort), peerCert(_peerCert), server(_server), messageInProgress(false) +IslInterface::IslInterface(int _serverId, + const QString &_peerHostName, + const QString &_peerAddress, + int _peerPort, + const QSslCertificate &_peerCert, + const QSslCertificate &cert, + const QSslKey &privateKey, + Servatrice *_server) + : QObject(), serverId(_serverId), peerHostName(_peerHostName), peerAddress(_peerAddress), peerPort(_peerPort), + peerCert(_peerCert), server(_server), messageInProgress(false) { - sharedCtor(cert, privateKey); + sharedCtor(cert, privateKey); } IslInterface::~IslInterface() { - logger->logMessage("[ISL] session ended", this); - - flushOutputBuffer(); - - // As these signals are connected with Qt::QueuedConnection implicitly, - // we don't need to worry about them modifying the lists while we're iterating. - - server->roomsLock.lockForRead(); - QMapIterator roomIterator(server->getRooms()); - while (roomIterator.hasNext()) { - Server_Room *room = roomIterator.next().value(); - room->usersLock.lockForRead(); - QMapIterator roomUsers(room->getExternalUsers()); - while (roomUsers.hasNext()) { - roomUsers.next(); - if (roomUsers.value().getUserInfo()->server_id() == serverId) - emit externalRoomUserLeft(room->getId(), roomUsers.key()); - } - room->usersLock.unlock(); - } - server->roomsLock.unlock(); - - server->clientsLock.lockForRead(); - QMapIterator extUsers(server->getExternalUsers()); - while (extUsers.hasNext()) { - extUsers.next(); - if (extUsers.value()->getUserInfo()->server_id() == serverId) - emit externalUserLeft(extUsers.key()); - } - server->clientsLock.unlock(); + logger->logMessage("[ISL] session ended", this); + + flushOutputBuffer(); + + // As these signals are connected with Qt::QueuedConnection implicitly, + // we don't need to worry about them modifying the lists while we're iterating. + + server->roomsLock.lockForRead(); + QMapIterator roomIterator(server->getRooms()); + while (roomIterator.hasNext()) { + Server_Room *room = roomIterator.next().value(); + room->usersLock.lockForRead(); + QMapIterator roomUsers(room->getExternalUsers()); + while (roomUsers.hasNext()) { + roomUsers.next(); + if (roomUsers.value().getUserInfo()->server_id() == serverId) + emit externalRoomUserLeft(room->getId(), roomUsers.key()); + } + room->usersLock.unlock(); + } + server->roomsLock.unlock(); + + server->clientsLock.lockForRead(); + QMapIterator extUsers(server->getExternalUsers()); + while (extUsers.hasNext()) { + extUsers.next(); + if (extUsers.value()->getUserInfo()->server_id() == serverId) + emit externalUserLeft(extUsers.key()); + } + server->clientsLock.unlock(); } void IslInterface::initServer() { - socket->setSocketDescriptor(socketDescriptor); - - logger->logMessage(QString("[ISL] incoming connection: %1").arg(socket->peerAddress().toString())); - - QList serverList = server->getServerList(); - int listIndex = -1; - for (int i = 0; i < serverList.size(); ++i) - if (serverList[i].address == socket->peerAddress()) { - listIndex = i; - break; - } - if (listIndex == -1) { - logger->logMessage(QString("[ISL] address %1 unknown, terminating connection").arg(socket->peerAddress().toString())); - deleteLater(); - return; - } - - socket->startServerEncryption(); - if (!socket->waitForEncrypted(5000)) { - QList sslErrors(socket->sslErrors()); - if (sslErrors.isEmpty()) - qDebug() << "[ISL] SSL handshake timeout, terminating connection"; - else - qDebug() << "[ISL] SSL errors:" << sslErrors; - deleteLater(); - return; - } - - if (serverList[listIndex].cert == socket->peerCertificate()) - logger->logMessage(QString("[ISL] Peer authenticated as " + serverList[listIndex].hostname)); - else { - logger->logMessage(QString("[ISL] Authentication failed, terminating connection")); - deleteLater(); - return; - } - serverId = serverList[listIndex].id; - - Event_ServerCompleteList event; - event.set_server_id(server->getServerId()); - - server->clientsLock.lockForRead(); - QMapIterator userIterator(server->getUsers()); - while (userIterator.hasNext()) - event.add_user_list()->CopyFrom(userIterator.next().value()->copyUserInfo(true, true)); - server->clientsLock.unlock(); - - server->roomsLock.lockForRead(); - QMapIterator roomIterator(server->getRooms()); - while (roomIterator.hasNext()) { - Server_Room *room = roomIterator.next().value(); - room->usersLock.lockForRead(); - room->gamesLock.lockForRead(); - room->getInfo(*event.add_room_list(), true, true, false); - } - - IslMessage message; - message.set_message_type(IslMessage::SESSION_EVENT); - SessionEvent *sessionEvent = message.mutable_session_event(); - sessionEvent->GetReflection()->MutableMessage(sessionEvent, event.GetDescriptor()->FindExtensionByName("ext"))->CopyFrom(event); - - server->islLock.lockForWrite(); - if (server->islConnectionExists(serverId)) { - qDebug() << "[ISL] Duplicate connection to #" << serverId << "terminating connection"; - deleteLater(); - } else { - transmitMessage(message); - server->addIslInterface(serverId, this); - } - server->islLock.unlock(); - - roomIterator.toFront(); - while (roomIterator.hasNext()) { - roomIterator.next(); - roomIterator.value()->gamesLock.unlock(); - roomIterator.value()->usersLock.unlock(); - } - server->roomsLock.unlock(); + socket->setSocketDescriptor(socketDescriptor); + + logger->logMessage(QString("[ISL] incoming connection: %1").arg(socket->peerAddress().toString())); + + QList serverList = server->getServerList(); + int listIndex = -1; + for (int i = 0; i < serverList.size(); ++i) + if (serverList[i].address == socket->peerAddress()) { + listIndex = i; + break; + } + if (listIndex == -1) { + logger->logMessage( + QString("[ISL] address %1 unknown, terminating connection").arg(socket->peerAddress().toString())); + deleteLater(); + return; + } + + socket->startServerEncryption(); + if (!socket->waitForEncrypted(5000)) { + QList sslErrors(socket->sslHandshakeErrors()); + if (sslErrors.isEmpty()) { + qCDebug(IslInterfaceLog) << "SSL handshake timeout, terminating connection"; + } else { + qCWarning(IslInterfaceLog) << "SSL errors:" << sslErrors; + } + deleteLater(); + return; + } + + if (serverList[listIndex].cert == socket->peerCertificate()) + logger->logMessage(QString("[ISL] Peer authenticated as " + serverList[listIndex].hostname)); + else { + logger->logMessage(QString("[ISL] Authentication failed, terminating connection")); + deleteLater(); + return; + } + serverId = serverList[listIndex].id; + + Event_ServerCompleteList event; + event.set_server_id(server->getServerID()); + + server->clientsLock.lockForRead(); + QMapIterator userIterator(server->getUsers()); + while (userIterator.hasNext()) + event.add_user_list()->CopyFrom(userIterator.next().value()->copyUserInfo(true, true)); + server->clientsLock.unlock(); + + server->roomsLock.lockForRead(); + QMapIterator roomIterator(server->getRooms()); + while (roomIterator.hasNext()) { + Server_Room *room = roomIterator.next().value(); + room->usersLock.lockForRead(); + room->gamesLock.lockForRead(); + room->getInfo(*event.add_room_list(), true, true, false); + } + + IslMessage message; + message.set_message_type(IslMessage::SESSION_EVENT); + SessionEvent *sessionEvent = message.mutable_session_event(); + sessionEvent->GetReflection() + ->MutableMessage(sessionEvent, event.GetDescriptor()->FindExtensionByName("ext")) + ->CopyFrom(event); + + server->islLock.lockForWrite(); + if (server->islConnectionExists(serverId)) { + qCDebug(IslInterfaceLog) << "Duplicate connection to #" << serverId << "terminating connection"; + deleteLater(); + } else { + transmitMessage(message); + server->addIslInterface(serverId, this); + } + server->islLock.unlock(); + + roomIterator.toFront(); + while (roomIterator.hasNext()) { + roomIterator.next(); + roomIterator.value()->gamesLock.unlock(); + roomIterator.value()->usersLock.unlock(); + } + server->roomsLock.unlock(); } void IslInterface::initClient() { - QList expectedErrors; - expectedErrors.append(QSslError(QSslError::SelfSignedCertificate, peerCert)); - socket->ignoreSslErrors(expectedErrors); - - qDebug() << "[ISL] Connecting to #" << serverId << ":" << peerAddress << ":" << peerPort; + QList expectedErrors; + expectedErrors.append(QSslError(QSslError::SelfSignedCertificate, peerCert)); + socket->ignoreSslErrors(expectedErrors); - socket->connectToHostEncrypted(peerAddress, peerPort, peerHostName); - if (!socket->waitForConnected(5000)) { - qDebug() << "[ISL] Socket error:" << socket->errorString(); - deleteLater(); - return; - } - if (!socket->waitForEncrypted(5000)) { - QList sslErrors(socket->sslErrors()); - if (sslErrors.isEmpty()) - qDebug() << "[ISL] SSL handshake timeout, terminating connection"; - else - qDebug() << "[ISL] SSL errors:" << sslErrors; - deleteLater(); - return; - } - - server->islLock.lockForWrite(); - if (server->islConnectionExists(serverId)) { - qDebug() << "[ISL] Duplicate connection to #" << serverId << "terminating connection"; - deleteLater(); - return; - } - - server->addIslInterface(serverId, this); - server->islLock.unlock(); + qCDebug(IslInterfaceLog) << "Connecting to #" << serverId << ":" << peerAddress << ":" << peerPort; + + socket->connectToHostEncrypted(peerAddress, peerPort, peerHostName); + if (!socket->waitForConnected(5000)) { + qCDebug(IslInterfaceLog) << "Socket error:" << socket->errorString(); + deleteLater(); + return; + } + if (!socket->waitForEncrypted(5000)) { + QList sslErrors(socket->sslHandshakeErrors()); + if (sslErrors.isEmpty()) { + qCDebug(IslInterfaceLog) << "SSL handshake timeout, terminating connection"; + } else { + qCWarning(IslInterfaceLog) << "SSL errors:" << sslErrors; + } + deleteLater(); + return; + } + + server->islLock.lockForWrite(); + if (server->islConnectionExists(serverId)) { + qCDebug(IslInterfaceLog) << "Duplicate connection to #" << serverId << "terminating connection"; + deleteLater(); + return; + } + + server->addIslInterface(serverId, this); + server->islLock.unlock(); } void IslInterface::flushOutputBuffer() { - QMutexLocker locker(&outputBufferMutex); - if (outputBuffer.isEmpty()) - return; - server->incTxBytes(outputBuffer.size()); - socket->write(outputBuffer); - socket->flush(); - outputBuffer.clear(); + QMutexLocker locker(&outputBufferMutex); + if (outputBuffer.isEmpty()) + return; + server->incTxBytes(outputBuffer.size()); + socket->write(outputBuffer); + socket->flush(); + outputBuffer.clear(); } void IslInterface::readClient() { - QByteArray data = socket->readAll(); - server->incRxBytes(data.size()); - inputBuffer.append(data); - - do { - if (!messageInProgress) { - if (inputBuffer.size() >= 4) { - messageLength = (((quint32) (unsigned char) inputBuffer[0]) << 24) - + (((quint32) (unsigned char) inputBuffer[1]) << 16) - + (((quint32) (unsigned char) inputBuffer[2]) << 8) - + ((quint32) (unsigned char) inputBuffer[3]); - inputBuffer.remove(0, 4); - messageInProgress = true; - } else - return; - } - if (inputBuffer.size() < messageLength) - return; - - IslMessage newMessage; - newMessage.ParseFromArray(inputBuffer.data(), messageLength); - inputBuffer.remove(0, messageLength); - messageInProgress = false; - - processMessage(newMessage); - } while (!inputBuffer.isEmpty()); + QByteArray data = socket->readAll(); + server->incRxBytes(data.size()); + inputBuffer.append(data); + + do { + if (!messageInProgress) { + if (inputBuffer.size() >= 4) { + messageLength = (((quint32)(unsigned char)inputBuffer[0]) << 24) + + (((quint32)(unsigned char)inputBuffer[1]) << 16) + + (((quint32)(unsigned char)inputBuffer[2]) << 8) + + ((quint32)(unsigned char)inputBuffer[3]); + inputBuffer.remove(0, 4); + messageInProgress = true; + } else + return; + } + if (inputBuffer.size() < messageLength) + return; + + IslMessage newMessage; + bool ok = newMessage.ParseFromArray(inputBuffer.data(), messageLength); + inputBuffer.remove(0, messageLength); + messageInProgress = false; + + if (ok) { + processMessage(newMessage); + } else { + qCWarning(IslInterfaceLog) << "parsing error!"; + } + } while (!inputBuffer.isEmpty()); } void IslInterface::catchSocketError(QAbstractSocket::SocketError socketError) { - qDebug() << "[ISL] Socket error:" << socketError; - - server->islLock.lockForWrite(); - server->removeIslInterface(serverId); - server->islLock.unlock(); - - deleteLater(); + qCWarning(IslInterfaceLog) << "Socket error:" << socketError; + + server->islLock.lockForWrite(); + server->removeIslInterface(serverId); + server->islLock.unlock(); + + deleteLater(); } void IslInterface::transmitMessage(const IslMessage &item) { - QByteArray buf; - unsigned int size = item.ByteSize(); - buf.resize(size + 4); - item.SerializeToArray(buf.data() + 4, size); - buf.data()[3] = (unsigned char) size; - buf.data()[2] = (unsigned char) (size >> 8); - buf.data()[1] = (unsigned char) (size >> 16); - buf.data()[0] = (unsigned char) (size >> 24); - - outputBufferMutex.lock(); - outputBuffer.append(buf); - outputBufferMutex.unlock(); - emit outputBufferChanged(); + QByteArray buf; +#if GOOGLE_PROTOBUF_VERSION > 3001000 + unsigned int size = static_cast(item.ByteSizeLong()); +#else + unsigned int size = static_cast(item.ByteSize()); +#endif + buf.resize(size + 4); + if (!item.SerializeToArray(buf.data() + 4, size)) { + qCWarning(IslInterfaceLog) << "transmit error!"; + return; + } + buf.data()[3] = (unsigned char)size; + buf.data()[2] = (unsigned char)(size >> 8); + buf.data()[1] = (unsigned char)(size >> 16); + buf.data()[0] = (unsigned char)(size >> 24); + + outputBufferMutex.lock(); + outputBuffer.append(buf); + outputBufferMutex.unlock(); + emit outputBufferChanged(); } void IslInterface::sessionEvent_ServerCompleteList(const Event_ServerCompleteList &event) { - for (int i = 0; i < event.user_list_size(); ++i) { - ServerInfo_User temp(event.user_list(i)); - temp.set_server_id(serverId); - emit externalUserJoined(temp); - } - for (int i = 0; i < event.room_list_size(); ++i) { - const ServerInfo_Room &room = event.room_list(i); - for (int j = 0; j < room.user_list_size(); ++j) { - ServerInfo_User userInfo(room.user_list(j)); - userInfo.set_server_id(serverId); - emit externalRoomUserJoined(room.room_id(), userInfo); - } - for (int j = 0; j < room.game_list_size(); ++j) { - ServerInfo_Game gameInfo(room.game_list(j)); - gameInfo.set_server_id(serverId); - emit externalRoomGameListChanged(room.room_id(), gameInfo); - } - } + for (int i = 0; i < event.user_list_size(); ++i) { + ServerInfo_User temp(event.user_list(i)); + temp.set_server_id(serverId); + emit externalUserJoined(temp); + } + for (int i = 0; i < event.room_list_size(); ++i) { + const ServerInfo_Room &room = event.room_list(i); + for (int j = 0; j < room.user_list_size(); ++j) { + ServerInfo_User userInfo(room.user_list(j)); + userInfo.set_server_id(serverId); + emit externalRoomUserJoined(room.room_id(), userInfo); + } + for (int j = 0; j < room.game_list_size(); ++j) { + ServerInfo_Game gameInfo(room.game_list(j)); + gameInfo.set_server_id(serverId); + emit externalRoomGameListChanged(room.room_id(), gameInfo); + } + } } void IslInterface::sessionEvent_UserJoined(const Event_UserJoined &event) { - ServerInfo_User userInfo(event.user_info()); - userInfo.set_server_id(serverId); - emit externalUserJoined(userInfo); + ServerInfo_User userInfo(event.user_info()); + userInfo.set_server_id(serverId); + emit externalUserJoined(userInfo); } void IslInterface::sessionEvent_UserLeft(const Event_UserLeft &event) { - emit externalUserLeft(QString::fromStdString(event.name())); + emit externalUserLeft(QString::fromStdString(event.name())); } void IslInterface::roomEvent_UserJoined(int roomId, const Event_JoinRoom &event) { - ServerInfo_User userInfo(event.user_info()); - userInfo.set_server_id(serverId); - emit externalRoomUserJoined(roomId, userInfo); + ServerInfo_User userInfo(event.user_info()); + userInfo.set_server_id(serverId); + emit externalRoomUserJoined(roomId, userInfo); } void IslInterface::roomEvent_UserLeft(int roomId, const Event_LeaveRoom &event) { - emit externalRoomUserLeft(roomId, QString::fromStdString(event.name())); + emit externalRoomUserLeft(roomId, QString::fromStdString(event.name())); } void IslInterface::roomEvent_Say(int roomId, const Event_RoomSay &event) { - emit externalRoomSay(roomId, QString::fromStdString(event.name()), QString::fromStdString(event.message())); + emit externalRoomSay(roomId, QString::fromStdString(event.name()), QString::fromStdString(event.message())); } void IslInterface::roomEvent_ListGames(int roomId, const Event_ListGames &event) { - for (int i = 0; i < event.game_list_size(); ++i) { - ServerInfo_Game gameInfo(event.game_list(i)); - gameInfo.set_server_id(serverId); - emit externalRoomGameListChanged(roomId, gameInfo); - } + for (int i = 0; i < event.game_list_size(); ++i) { + ServerInfo_Game gameInfo(event.game_list(i)); + gameInfo.set_server_id(serverId); + emit externalRoomGameListChanged(roomId, gameInfo); + } +} + +void IslInterface::roomEvent_RemoveMessages(int roomId, const Event_RemoveMessages &event) +{ + emit externalRoomRemoveMessages(roomId, QString::fromStdString(event.name()), event.amount()); } void IslInterface::roomCommand_JoinGame(const Command_JoinGame &cmd, int cmdId, int roomId, qint64 sessionId) { - emit joinGameCommandReceived(cmd, cmdId, roomId, serverId, sessionId); + emit joinGameCommandReceived(cmd, cmdId, roomId, serverId, sessionId); } void IslInterface::processSessionEvent(const SessionEvent &event, qint64 sessionId) { - switch (getPbExtension(event)) { - case SessionEvent::SERVER_COMPLETE_LIST: sessionEvent_ServerCompleteList(event.GetExtension(Event_ServerCompleteList::ext)); break; - case SessionEvent::USER_JOINED: sessionEvent_UserJoined(event.GetExtension(Event_UserJoined::ext)); break; - case SessionEvent::USER_LEFT: sessionEvent_UserLeft(event.GetExtension(Event_UserLeft::ext)); break; - case SessionEvent::GAME_JOINED: { - QReadLocker clientsLocker(&server->clientsLock); - Server_AbstractUserInterface *client = server->getUsersBySessionId().value(sessionId); - if (!client) { - qDebug() << "IslInterface::processSessionEvent: session id" << sessionId << "not found"; - break; - } - const Event_GameJoined &gameJoined = event.GetExtension(Event_GameJoined::ext); - client->playerAddedToGame(gameJoined.game_info().game_id(), gameJoined.game_info().room_id(), gameJoined.player_id()); - client->sendProtocolItem(event); - break; - } - case SessionEvent::USER_MESSAGE: - case SessionEvent::REPLAY_ADDED: { - QReadLocker clientsLocker(&server->clientsLock); - Server_AbstractUserInterface *client = server->getUsersBySessionId().value(sessionId); - if (!client) { - qDebug() << "IslInterface::processSessionEvent: session id" << sessionId << "not found"; - break; - } - - client->sendProtocolItem(event); - break; - } - default: ; - } + switch (getPbExtension(event)) { + case SessionEvent::SERVER_COMPLETE_LIST: + sessionEvent_ServerCompleteList(event.GetExtension(Event_ServerCompleteList::ext)); + break; + case SessionEvent::USER_JOINED: + sessionEvent_UserJoined(event.GetExtension(Event_UserJoined::ext)); + break; + case SessionEvent::USER_LEFT: + sessionEvent_UserLeft(event.GetExtension(Event_UserLeft::ext)); + break; + case SessionEvent::GAME_JOINED: { + QReadLocker clientsLocker(&server->clientsLock); + Server_AbstractUserInterface *client = server->getUsersBySessionId().value(sessionId); + if (!client) { + qCDebug(IslInterfaceLog) << "IslInterface::processSessionEvent: session id" << sessionId << "not found"; + break; + } + const Event_GameJoined &gameJoined = event.GetExtension(Event_GameJoined::ext); + client->playerAddedToGame(gameJoined.game_info().game_id(), gameJoined.game_info().room_id(), + gameJoined.player_id()); + client->sendProtocolItem(event); + break; + } + case SessionEvent::USER_MESSAGE: + case SessionEvent::REPLAY_ADDED: { + QReadLocker clientsLocker(&server->clientsLock); + Server_AbstractUserInterface *client = server->getUsersBySessionId().value(sessionId); + if (!client) { + qCWarning(IslInterfaceLog) + << "IslInterface::processSessionEvent: session id" << sessionId << "not found"; + break; + } + + client->sendProtocolItem(event); + break; + } + default:; + } } void IslInterface::processRoomEvent(const RoomEvent &event) { - switch (getPbExtension(event)) { - case RoomEvent::JOIN_ROOM: roomEvent_UserJoined(event.room_id(), event.GetExtension(Event_JoinRoom::ext)); break; - case RoomEvent::LEAVE_ROOM: roomEvent_UserLeft(event.room_id(), event.GetExtension(Event_LeaveRoom::ext)); break; - case RoomEvent::ROOM_SAY: roomEvent_Say(event.room_id(), event.GetExtension(Event_RoomSay::ext)); break; - case RoomEvent::LIST_GAMES: roomEvent_ListGames(event.room_id(), event.GetExtension(Event_ListGames::ext)); break; - default: ; - } + switch (getPbExtension(event)) { + case RoomEvent::JOIN_ROOM: + roomEvent_UserJoined(event.room_id(), event.GetExtension(Event_JoinRoom::ext)); + break; + case RoomEvent::LEAVE_ROOM: + roomEvent_UserLeft(event.room_id(), event.GetExtension(Event_LeaveRoom::ext)); + break; + case RoomEvent::ROOM_SAY: + roomEvent_Say(event.room_id(), event.GetExtension(Event_RoomSay::ext)); + break; + case RoomEvent::LIST_GAMES: + roomEvent_ListGames(event.room_id(), event.GetExtension(Event_ListGames::ext)); + break; + case RoomEvent::REMOVE_MESSAGES: + roomEvent_RemoveMessages(event.room_id(), event.GetExtension(Event_RemoveMessages::ext)); + break; + default:; + } } void IslInterface::processRoomCommand(const CommandContainer &cont, qint64 sessionId) { - for (int i = 0; i < cont.room_command_size(); ++i) { - const RoomCommand &roomCommand = cont.room_command(i); - switch (static_cast(getPbExtension(roomCommand))) { - case RoomCommand::JOIN_GAME: roomCommand_JoinGame(roomCommand.GetExtension(Command_JoinGame::ext), cont.cmd_id(), cont.room_id(), sessionId); - default: ; - } - } + for (int i = 0; i < cont.room_command_size(); ++i) { + const RoomCommand &roomCommand = cont.room_command(i); + switch (static_cast(getPbExtension(roomCommand))) { + case RoomCommand::JOIN_GAME: + roomCommand_JoinGame(roomCommand.GetExtension(Command_JoinGame::ext), cont.cmd_id(), cont.room_id(), + sessionId); + default:; + } + } } void IslInterface::processMessage(const IslMessage &item) { - qDebug() << QString::fromStdString(item.DebugString()); - - switch (item.message_type()) { - case IslMessage::ROOM_COMMAND_CONTAINER: { - processRoomCommand(item.room_command(), item.session_id()); - break; - } - case IslMessage::GAME_COMMAND_CONTAINER: { - emit gameCommandContainerReceived(item.game_command(), item.player_id(), serverId, item.session_id()); - break; - } - case IslMessage::SESSION_EVENT: { - processSessionEvent(item.session_event(), item.session_id()); - break; - } - case IslMessage::RESPONSE: { - emit responseReceived(item.response(), item.session_id()); - break; - } - case IslMessage::GAME_EVENT_CONTAINER: { - emit gameEventContainerReceived(item.game_event_container(), item.session_id()); - break; - } - case IslMessage::ROOM_EVENT: { - processRoomEvent(item.room_event()); break; - break; - } - default: ; - } + qCDebug(IslInterfaceLog) << getSafeDebugString(item); + + switch (item.message_type()) { + case IslMessage::ROOM_COMMAND_CONTAINER: { + processRoomCommand(item.room_command(), item.session_id()); + break; + } + case IslMessage::GAME_COMMAND_CONTAINER: { + emit gameCommandContainerReceived(item.game_command(), item.player_id(), serverId, item.session_id()); + break; + } + case IslMessage::SESSION_EVENT: { + processSessionEvent(item.session_event(), item.session_id()); + break; + } + case IslMessage::RESPONSE: { + emit responseReceived(item.response(), item.session_id()); + break; + } + case IslMessage::GAME_EVENT_CONTAINER: { + emit gameEventContainerReceived(item.game_event_container(), item.session_id()); + break; + } + case IslMessage::ROOM_EVENT: { + processRoomEvent(item.room_event()); + break; + } + default:; + } } diff --git a/servatrice/src/isl_interface.h b/servatrice/src/isl_interface.h index cd1787ff1..27e5b8293 100644 --- a/servatrice/src/isl_interface.h +++ b/servatrice/src/isl_interface.h @@ -2,11 +2,12 @@ #define ISL_INTERFACE_H #include "servatrice.h" + #include #include -#include "pb/serverinfo_user.pb.h" -#include "pb/serverinfo_room.pb.h" -#include "pb/serverinfo_game.pb.h" +#include +#include +#include class Servatrice; class QSslSocket; @@ -21,68 +22,81 @@ class Event_JoinRoom; class Event_LeaveRoom; class Event_RoomSay; class Event_ListGames; +class Event_RemoveMessages; class Command_JoinGame; -class IslInterface : public QObject { - Q_OBJECT +class IslInterface : public QObject +{ + Q_OBJECT private slots: - void readClient(); - void catchSocketError(QAbstractSocket::SocketError socketError); - void flushOutputBuffer(); + void readClient(); + void catchSocketError(QAbstractSocket::SocketError socketError); + void flushOutputBuffer(); signals: - void outputBufferChanged(); - - void externalUserJoined(ServerInfo_User userInfo); - void externalUserLeft(QString userName); - void externalRoomUserJoined(int roomId, ServerInfo_User userInfo); - void externalRoomUserLeft(int roomId, QString userName); - void externalRoomSay(int roomId, QString userName, QString message); - void externalRoomGameListChanged(int roomId, ServerInfo_Game gameInfo); - void joinGameCommandReceived(const Command_JoinGame &cmd, int cmdId, int roomId, int serverId, qint64 sessionId); - void gameCommandContainerReceived(const CommandContainer &cont, int playerId, int serverId, qint64 sessionId); - void responseReceived(const Response &resp, qint64 sessionId); - void gameEventContainerReceived(const GameEventContainer &cont, qint64 sessionId); + void outputBufferChanged(); + + void externalUserJoined(ServerInfo_User userInfo); + void externalUserLeft(QString userName); + void externalRoomUserJoined(int roomId, ServerInfo_User userInfo); + void externalRoomUserLeft(int roomId, QString userName); + void externalRoomSay(int roomId, QString userName, QString message); + void externalRoomGameListChanged(int roomId, ServerInfo_Game gameInfo); + void externalRoomRemoveMessages(int roomId, QString userName, int amount); + void joinGameCommandReceived(const Command_JoinGame &cmd, int cmdId, int roomId, int serverId, qint64 sessionId); + void gameCommandContainerReceived(const CommandContainer &cont, int playerId, int serverId, qint64 sessionId); + void responseReceived(const Response &resp, qint64 sessionId); + void gameEventContainerReceived(const GameEventContainer &cont, qint64 sessionId); + private: - int serverId; - int socketDescriptor; - QString peerHostName, peerAddress; - int peerPort; - QSslCertificate peerCert; - - QMutex outputBufferMutex; - Servatrice *server; - QSslSocket *socket; - - QByteArray inputBuffer, outputBuffer; - bool messageInProgress; - int messageLength; - - void sessionEvent_ServerCompleteList(const Event_ServerCompleteList &event); - void sessionEvent_UserJoined(const Event_UserJoined &event); - void sessionEvent_UserLeft(const Event_UserLeft &event); - - void roomEvent_UserJoined(int roomId, const Event_JoinRoom &event); - void roomEvent_UserLeft(int roomId, const Event_LeaveRoom &event); - void roomEvent_Say(int roomId, const Event_RoomSay &event); - void roomEvent_ListGames(int roomId, const Event_ListGames &event); - - void roomCommand_JoinGame(const Command_JoinGame &cmd, int cmdId, int roomId, qint64 sessionId); - - void processSessionEvent(const SessionEvent &event, qint64 sessionId); - void processRoomEvent(const RoomEvent &event); - void processRoomCommand(const CommandContainer &cont, qint64 sessionId); - - void processMessage(const IslMessage &item); - void sharedCtor(const QSslCertificate &cert, const QSslKey &privateKey); + int serverId; + int socketDescriptor; + QString peerHostName, peerAddress; + int peerPort; + QSslCertificate peerCert; + + QMutex outputBufferMutex; + Servatrice *server; + QSslSocket *socket; + + QByteArray inputBuffer, outputBuffer; + bool messageInProgress; + int messageLength; + + void sessionEvent_ServerCompleteList(const Event_ServerCompleteList &event); + void sessionEvent_UserJoined(const Event_UserJoined &event); + void sessionEvent_UserLeft(const Event_UserLeft &event); + + void roomEvent_UserJoined(int roomId, const Event_JoinRoom &event); + void roomEvent_UserLeft(int roomId, const Event_LeaveRoom &event); + void roomEvent_Say(int roomId, const Event_RoomSay &event); + void roomEvent_ListGames(int roomId, const Event_ListGames &event); + void roomEvent_RemoveMessages(int roomId, const Event_RemoveMessages &event); + + void roomCommand_JoinGame(const Command_JoinGame &cmd, int cmdId, int roomId, qint64 sessionId); + + void processSessionEvent(const SessionEvent &event, qint64 sessionId); + void processRoomEvent(const RoomEvent &event); + void processRoomCommand(const CommandContainer &cont, qint64 sessionId); + + void processMessage(const IslMessage &item); + void sharedCtor(const QSslCertificate &cert, const QSslKey &privateKey); public slots: - void initServer(); - void initClient(); + void initServer(); + void initClient(); + public: - IslInterface(int socketDescriptor, const QSslCertificate &cert, const QSslKey &privateKey, Servatrice *_server); - IslInterface(int _serverId, const QString &peerHostName, const QString &peerAddress, int peerPort, const QSslCertificate &peerCert, const QSslCertificate &cert, const QSslKey &privateKey, Servatrice *_server); - ~IslInterface(); - - void transmitMessage(const IslMessage &item); + IslInterface(int socketDescriptor, const QSslCertificate &cert, const QSslKey &privateKey, Servatrice *_server); + IslInterface(int _serverId, + const QString &peerHostName, + const QString &peerAddress, + int peerPort, + const QSslCertificate &peerCert, + const QSslCertificate &cert, + const QSslKey &privateKey, + Servatrice *_server); + ~IslInterface(); + + void transmitMessage(const IslMessage &item); }; #endif diff --git a/servatrice/src/main.cpp b/servatrice/src/main.cpp index 0f7f0112b..b1294a04c 100644 --- a/servatrice/src/main.cpp +++ b/servatrice/src/main.cpp @@ -18,175 +18,196 @@ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * ***************************************************************************/ -#include -#include -#include -#include -#include -#include -#include "passwordhasher.h" #include "servatrice.h" #include "server_logger.h" -#include "rng_sfmt.h" +#include "settingscache.h" +#include "signalhandler.h" +#include "smtpclient.h" #include "version_string.h" -#ifdef Q_OS_UNIX -#include -#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include RNG_Abstract *rng; ServerLogger *logger; QThread *loggerThread; +SettingsCache *settingsCache; +SignalHandler *signalhandler; +SmtpClient *smtpClient; + +/* Prototypes */ + +void testRNG(); +void testHash(); +void myMessageOutput(QtMsgType type, const QMessageLogContext &, const QString &msg); +void myMessageOutput2(QtMsgType type, const QMessageLogContext &, const QString &msg); + +/* Implementations */ void testRNG() { - const int n = 500000; - std::cerr << "Testing random number generator (n = " << n << " * bins)..." << std::endl; - - const int min = 1; - const int minMax = 2; - const int maxMax = 10; - - QVector > numbers(maxMax - minMax + 1); - QVector chisq(maxMax - minMax + 1); - for (int max = minMax; max <= maxMax; ++max) { - numbers[max - minMax] = rng->makeNumbersVector(n * (max - min + 1), min, max); - chisq[max - minMax] = rng->testRandom(numbers[max - minMax]); - } - for (int i = 0; i <= maxMax - min; ++i) { - std::cerr << (min + i); - for (int j = 0; j < numbers.size(); ++j) { - if (i < numbers[j].size()) - std::cerr << "\t" << numbers[j][i]; - else - std::cerr << "\t"; - } - std::cerr << std::endl; - } - std::cerr << std::endl << "Chi^2 ="; - for (int j = 0; j < chisq.size(); ++j) - std::cerr << "\t" << QString::number(chisq[j], 'f', 3).toStdString(); - std::cerr << std::endl << "k ="; - for (int j = 0; j < chisq.size(); ++j) - std::cerr << "\t" << (j - min + minMax); - std::cerr << std::endl << std::endl; + const int n = 500000; + std::cerr << "Testing random number generator (n = " << n << " * bins)..." << std::endl; + + const int min = 1; + const int minMax = 2; + const int maxMax = 10; + + QVector> numbers(maxMax - minMax + 1); + QVector chisq(maxMax - minMax + 1); + for (int max = minMax; max <= maxMax; ++max) { + numbers[max - minMax] = rng->makeNumbersVector(n * (max - min + 1), min, max); + chisq[max - minMax] = rng->testRandom(numbers[max - minMax]); + } + for (int i = 0; i <= maxMax - min; ++i) { + std::cerr << (min + i); + for (auto &number : numbers) { + if (i < number.size()) + std::cerr << "\t" << number[i]; + else + std::cerr << "\t"; + } + std::cerr << std::endl; + } + std::cerr << std::endl << "Chi^2 ="; + for (double j : chisq) + std::cerr << "\t" << QString::number(j, 'f', 3).toStdString(); + std::cerr << std::endl << "k ="; + for (int j = 0; j < chisq.size(); ++j) + std::cerr << "\t" << (j - min + minMax); + std::cerr << std::endl << std::endl; } void testHash() { - const int n = 5000; - std::cerr << "Benchmarking password hash function (n =" << n << ")..." << std::endl; - QDateTime startTime = QDateTime::currentDateTime(); - for (int i = 0; i < n; ++i) - PasswordHasher::computeHash("aaaaaa", "aaaaaaaaaaaaaaaa"); - QDateTime endTime = QDateTime::currentDateTime(); - std::cerr << startTime.secsTo(endTime) << "secs" << std::endl; + const int n = 5000; + std::cerr << "Benchmarking password hash function (n =" << n << ")..." << std::endl; + QDateTime startTime = QDateTime::currentDateTime(); + for (int i = 0; i < n; ++i) + PasswordHasher::computeHash("aaaaaa", "aaaaaaaaaaaaaaaa"); + QDateTime endTime = QDateTime::currentDateTime(); + std::cerr << startTime.secsTo(endTime) << "secs" << std::endl; } -void myMessageOutput(QtMsgType /*type*/, const char *msg) +void myMessageOutput(QtMsgType /*type*/, const QMessageLogContext &, const QString &msg) { - logger->logMessage(msg); + logger->logMessage(msg); } -void myMessageOutput2(QtMsgType /*type*/, const char *msg) +void myMessageOutput2(QtMsgType /*type*/, const QMessageLogContext &, const QString &msg) { - logger->logMessage(msg); - std::cerr << msg << std::endl; + logger->logMessage(msg); + std::cerr << msg.toStdString() << std::endl; } -#ifdef Q_OS_UNIX -void sigSegvHandler(int sig) -{ - if (sig == SIGSEGV) - logger->logMessage("CRASH: SIGSEGV"); - else if (sig == SIGABRT) - logger->logMessage("CRASH: SIGABRT"); - - logger->deleteLater(); - loggerThread->wait(); - delete loggerThread; - - raise(sig); -} -#endif - int main(int argc, char *argv[]) { - QCoreApplication app(argc, argv); - app.setOrganizationName("Cockatrice"); - app.setApplicationName("Servatrice"); - - QStringList args = app.arguments(); - bool testRandom = args.contains("--test-random"); - bool testHashFunction = args.contains("--test-hash"); - bool logToConsole = args.contains("--log-to-console"); - - qRegisterMetaType >("QList"); - - QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8")); - - QSettings *settings = new QSettings("servatrice.ini", QSettings::IniFormat); - - loggerThread = new QThread; - loggerThread->setObjectName("logger"); - logger = new ServerLogger(logToConsole); - logger->moveToThread(loggerThread); - - loggerThread->start(); - QMetaObject::invokeMethod(logger, "startLog", Qt::BlockingQueuedConnection, Q_ARG(QString, settings->value("server/logfile").toString())); - - if (logToConsole) - qInstallMsgHandler(myMessageOutput); - else - qInstallMsgHandler(myMessageOutput2); -#ifdef Q_OS_UNIX - struct sigaction hup; - hup.sa_handler = ServerLogger::hupSignalHandler; - sigemptyset(&hup.sa_mask); - hup.sa_flags = 0; - hup.sa_flags |= SA_RESTART; - sigaction(SIGHUP, &hup, 0); - - struct sigaction segv; - segv.sa_handler = sigSegvHandler; - segv.sa_flags = SA_RESETHAND; - sigemptyset(&segv.sa_mask); - sigaction(SIGSEGV, &segv, 0); - sigaction(SIGABRT, &segv, 0); - - signal(SIGPIPE, SIG_IGN); -#endif - rng = new RNG_SFMT; - - std::cerr << "Servatrice " << VERSION_STRING << " starting." << std::endl; - std::cerr << "-------------------------" << std::endl; - - PasswordHasher::initialize(); - - if (testRandom) - testRNG(); - if (testHashFunction) - testHash(); - - Servatrice *server = new Servatrice(settings); - QObject::connect(server, SIGNAL(destroyed()), &app, SLOT(quit()), Qt::QueuedConnection); - int retval = 0; - if (server->initServer()) { - std::cerr << "-------------------------" << std::endl; - std::cerr << "Server initialized." << std::endl; - - qInstallMsgHandler(myMessageOutput); - retval = app.exec(); - - std::cerr << "Server quit." << std::endl; - std::cerr << "-------------------------" << std::endl; - } - - delete rng; - delete settings; - - logger->deleteLater(); - loggerThread->wait(); - delete loggerThread; + QCoreApplication app(argc, argv); + QCoreApplication::setOrganizationName("Cockatrice"); + QCoreApplication::setApplicationName("Servatrice"); + QCoreApplication::setApplicationVersion(VERSION_STRING); - return retval; + QCommandLineParser parser; + parser.addHelpOption(); + parser.addVersionOption(); + + QCommandLineOption testRandomOpt("test-random", "Test PRNG (chi^2)"); + parser.addOption(testRandomOpt); + + QCommandLineOption testHashFunctionOpt("test-hash", "Test password hash function"); + parser.addOption(testHashFunctionOpt); + + QCommandLineOption logToConsoleOpt("log-to-console", "Write server logs to console"); + parser.addOption(logToConsoleOpt); + + QCommandLineOption configPathOpt("config", "Read server configuration from ", "file", ""); + parser.addOption(configPathOpt); + + parser.process(app); + + bool testRandom = parser.isSet(testRandomOpt); + bool testHashFunction = parser.isSet(testHashFunctionOpt); + bool logToConsole = parser.isSet(logToConsoleOpt); + QString configPath = parser.value(configPathOpt); + + qRegisterMetaType>("QList"); + + if (configPath.isEmpty()) { + configPath = SettingsCache::guessConfigurationPath(); + } else if (!QFile::exists(configPath)) { + qCritical() << "Could not find configuration file at" << configPath; + return 1; + } + qWarning() << "Using configuration file: " << configPath; + settingsCache = new SettingsCache(configPath); + + loggerThread = new QThread; + loggerThread->setObjectName("logger"); + logger = new ServerLogger(logToConsole); + logger->moveToThread(loggerThread); + + loggerThread->start(); + QMetaObject::invokeMethod(logger, "startLog", Qt::BlockingQueuedConnection, + Q_ARG(QString, settingsCache->value("server/logfile", QString("server.log")).toString())); + + if (logToConsole) + qInstallMessageHandler(myMessageOutput); + else + qInstallMessageHandler(myMessageOutput2); + + signalhandler = new SignalHandler(); + + rng = new RNG_SFMT; + + std::cerr << "Servatrice " << VERSION_STRING << " starting." << std::endl; + std::cerr << "-------------------------" << std::endl; + + if (testRandom) { + testRNG(); + } + if (testHashFunction) { + testHash(); + } + if (testRandom || testHashFunction) { + return 0; + } + + smtpClient = new SmtpClient(); + + auto *server = new Servatrice(); + QObject::connect(server, SIGNAL(destroyed()), &app, SLOT(quit()), Qt::QueuedConnection); + int retval = 0; + if (server->initServer()) { + std::cerr << "-------------------------" << std::endl; + std::cerr << "Server initialized." << std::endl; + + qInstallMessageHandler(myMessageOutput); + + retval = QCoreApplication::exec(); + + std::cerr << "Server quit." << std::endl; + std::cerr << "-------------------------" << std::endl; + } + + delete smtpClient; + delete rng; + delete signalhandler; + delete settingsCache; + + logger->deleteLater(); + loggerThread->wait(); + delete loggerThread; + + // Delete all global objects allocated by libprotobuf. + google::protobuf::ShutdownProtobufLibrary(); + + QCoreApplication::quit(); + return retval; } diff --git a/servatrice/src/main.h b/servatrice/src/main.h index 037e9bba1..6ce4bb263 100644 --- a/servatrice/src/main.h +++ b/servatrice/src/main.h @@ -2,6 +2,13 @@ #define MAIN_H class ServerLogger; +class QThread; +class SettingsCache; +class SmtpClient; + extern ServerLogger *logger; +extern QThread *loggerThread; +extern SettingsCache *settingsCache; +extern SmtpClient *smtpClient; #endif diff --git a/servatrice/src/passwordhasher.cpp b/servatrice/src/passwordhasher.cpp deleted file mode 100644 index 1cc8528c9..000000000 --- a/servatrice/src/passwordhasher.cpp +++ /dev/null @@ -1,29 +0,0 @@ -#include "passwordhasher.h" -#include -#include -#include - -void PasswordHasher::initialize() -{ - // These calls are required by libgcrypt before we use any of its functions. - gcry_check_version(0); - gcry_control(GCRYCTL_DISABLE_SECMEM, 0); - gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); -} - -QString PasswordHasher::computeHash(const QString &password, const QString &salt) -{ - const int algo = GCRY_MD_SHA512; - const int rounds = 1000; - - QByteArray passwordBuffer = (salt + password).toAscii(); - int hashLen = gcry_md_get_algo_dlen(algo); - char hash[hashLen], tmp[hashLen]; - gcry_md_hash_buffer(algo, hash, passwordBuffer.data(), passwordBuffer.size()); - for (int i = 1; i < rounds; ++i) { - memcpy(tmp, hash, hashLen); - gcry_md_hash_buffer(algo, hash, tmp, hashLen); - } - return salt + QString(QByteArray(hash, hashLen).toBase64()); -} - diff --git a/servatrice/src/passwordhasher.h b/servatrice/src/passwordhasher.h deleted file mode 100644 index 0cb6744c3..000000000 --- a/servatrice/src/passwordhasher.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef PASSWORDHASHER_H -#define PASSWORDHASHER_H - -#include - -class PasswordHasher { -public: - static void initialize(); - static QString computeHash(const QString &password, const QString &salt); -}; - -#endif diff --git a/servatrice/src/servatrice.cpp b/servatrice/src/servatrice.cpp index ee5133390..410bf4ed9 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -17,496 +17,1071 @@ * Free Software Foundation, Inc., * * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * ***************************************************************************/ -#include -#include -#include -#include +#include "servatrice.h" + +#include "email_parser.h" +#include "isl_interface.h" +#include "main.h" +#include "servatrice_connection_pool.h" +#include "servatrice_database_interface.h" +#include "server_logger.h" +#include "serversocketinterface.h" +#include "settingscache.h" +#include "smtpclient.h" + #include #include +#include +#include +#include +#include +#include +#include +#include #include -#include "servatrice.h" -#include "servatrice_database_interface.h" -#include "servatrice_connection_pool.h" -#include "server_room.h" -#include "serversocketinterface.h" -#include "isl_interface.h" -#include "server_logger.h" -#include "main.h" -#include "decklist.h" -#include "pb/event_server_message.pb.h" -#include "pb/event_server_shutdown.pb.h" -#include "pb/event_connection_closed.pb.h" +#include +#include +#include +#include +#include +#include -Servatrice_GameServer::Servatrice_GameServer(Servatrice *_server, int _numberPools, const QSqlDatabase &_sqlDatabase, QObject *parent) - : QTcpServer(parent), - server(_server) +Servatrice_GameServer::Servatrice_GameServer(Servatrice *_server, + int _numberPools, + const QSqlDatabase &_sqlDatabase, + QObject *parent) + : QTcpServer(parent), server(_server) { - if (_numberPools == 0) { - server->setThreaded(false); - Servatrice_DatabaseInterface *newDatabaseInterface = new Servatrice_DatabaseInterface(0, server); - Servatrice_ConnectionPool *newPool = new Servatrice_ConnectionPool(newDatabaseInterface); - - server->addDatabaseInterface(thread(), newDatabaseInterface); - newDatabaseInterface->initDatabase(_sqlDatabase); - - connectionPools.append(newPool); - } else - for (int i = 0; i < _numberPools; ++i) { - Servatrice_DatabaseInterface *newDatabaseInterface = new Servatrice_DatabaseInterface(i, server); - Servatrice_ConnectionPool *newPool = new Servatrice_ConnectionPool(newDatabaseInterface); - - QThread *newThread = new QThread; - newThread->setObjectName("pool_" + QString::number(i)); - newPool->moveToThread(newThread); - newDatabaseInterface->moveToThread(newThread); - server->addDatabaseInterface(newThread, newDatabaseInterface); - - newThread->start(); - QMetaObject::invokeMethod(newDatabaseInterface, "initDatabase", Qt::BlockingQueuedConnection, Q_ARG(QSqlDatabase, _sqlDatabase)); - - connectionPools.append(newPool); - } + for (int i = 0; i < _numberPools; ++i) { + auto newDatabaseInterface = new Servatrice_DatabaseInterface(i, server); + auto newPool = new Servatrice_ConnectionPool(newDatabaseInterface); + + auto newThread = new QThread; + newThread->setObjectName("pool_" + QString::number(i)); + newPool->moveToThread(newThread); + newDatabaseInterface->moveToThread(newThread); + server->addDatabaseInterface(newThread, newDatabaseInterface); + + newThread->start(); + QMetaObject::invokeMethod(newDatabaseInterface, "initDatabase", Qt::BlockingQueuedConnection, + Q_ARG(QSqlDatabase, _sqlDatabase)); + + connectionPools.append(newPool); + } } Servatrice_GameServer::~Servatrice_GameServer() { - for (int i = 0; i < connectionPools.size(); ++i) { - logger->logMessage(QString("Closing pool %1...").arg(i)); - QThread *poolThread = connectionPools[i]->thread(); - connectionPools[i]->deleteLater(); // pool destructor calls thread()->quit() - poolThread->wait(); - } + for (int i = 0; i < connectionPools.size(); ++i) { + logger->logMessage(QString("Closing pool %1...").arg(i)); + QThread *poolThread = connectionPools[i]->thread(); + connectionPools[i]->deleteLater(); // pool destructor calls thread()->quit() + poolThread->wait(); + poolThread->deleteLater(); + } } -void Servatrice_GameServer::incomingConnection(int socketDescriptor) +void Servatrice_GameServer::incomingConnection(qintptr socketDescriptor) { - // Determine connection pool with smallest client count - int minClientCount = -1; - int poolIndex = -1; - QStringList debugStr; - for (int i = 0; i < connectionPools.size(); ++i) { - const int clientCount = connectionPools[i]->getClientCount(); - if ((poolIndex == -1) || (clientCount < minClientCount)) { - minClientCount = clientCount; - poolIndex = i; - } - debugStr.append(QString::number(clientCount)); - } - qDebug() << "Pool utilisation:" << debugStr; - Servatrice_ConnectionPool *pool = connectionPools[poolIndex]; - - ServerSocketInterface *ssi = new ServerSocketInterface(server, pool->getDatabaseInterface()); - ssi->moveToThread(pool->thread()); - pool->addClient(); - connect(ssi, SIGNAL(destroyed()), pool, SLOT(removeClient())); - - QMetaObject::invokeMethod(ssi, "initConnection", Qt::QueuedConnection, Q_ARG(int, socketDescriptor)); + Servatrice_ConnectionPool *pool = findLeastUsedConnectionPool(); + + auto ssi = new TcpServerSocketInterface(server, pool->getDatabaseInterface()); + connect(ssi, SIGNAL(incTxBytes(qint64)), this, SLOT(incTxBytes(qint64))); + ssi->moveToThread(pool->thread()); + pool->addClient(); + connect(ssi, SIGNAL(destroyed()), pool, SLOT(removeClient())); + + QMetaObject::invokeMethod(ssi, "initConnection", Qt::QueuedConnection, Q_ARG(int, socketDescriptor)); } -void Servatrice_IslServer::incomingConnection(int socketDescriptor) +Servatrice_ConnectionPool *Servatrice_GameServer::findLeastUsedConnectionPool() { - QThread *thread = new QThread; - connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); - - IslInterface *interface = new IslInterface(socketDescriptor, cert, privateKey, server); - interface->moveToThread(thread); - connect(interface, SIGNAL(destroyed()), thread, SLOT(quit())); - - thread->start(); - QMetaObject::invokeMethod(interface, "initServer", Qt::QueuedConnection); + int minClientCount = -1; + int poolIndex = -1; + QStringList debugStr; + for (int i = 0; i < connectionPools.size(); ++i) { + const int clientCount = connectionPools[i]->getClientCount(); + if ((poolIndex == -1) || (clientCount < minClientCount)) { + minClientCount = clientCount; + poolIndex = i; + } + debugStr.append(QString::number(clientCount)); + } + qDebug().noquote() << "Pool utilisation:" << debugStr.join(", "); + return connectionPools[poolIndex]; } -Servatrice::Servatrice(QSettings *_settings, QObject *parent) - : Server(true, parent), settings(_settings), uptime(0), shutdownTimer(0) +#define WEBSOCKET_POOL_NUMBER 999 + +Servatrice_WebsocketGameServer::Servatrice_WebsocketGameServer(Servatrice *_server, + int _numberPools, + const QSqlDatabase &_sqlDatabase, + QObject *parent) + : QWebSocketServer("Servatrice", QWebSocketServer::NonSecureMode, parent), server(_server) { - qRegisterMetaType("QSqlDatabase"); + for (int i = 0; i < _numberPools; ++i) { + int poolNumber = WEBSOCKET_POOL_NUMBER + i; + auto newDatabaseInterface = new Servatrice_DatabaseInterface(poolNumber, server); + auto newPool = new Servatrice_ConnectionPool(newDatabaseInterface); + + auto newThread = new QThread; + newThread->setObjectName("pool_" + QString::number(poolNumber)); + newPool->moveToThread(newThread); + newDatabaseInterface->moveToThread(newThread); + server->addDatabaseInterface(newThread, newDatabaseInterface); + + newThread->start(); + QMetaObject::invokeMethod(newDatabaseInterface, "initDatabase", Qt::BlockingQueuedConnection, + Q_ARG(QSqlDatabase, _sqlDatabase)); + + connectionPools.append(newPool); + + connect(this, SIGNAL(newConnection()), this, SLOT(onNewConnection())); + } +} + +Servatrice_WebsocketGameServer::~Servatrice_WebsocketGameServer() +{ + for (int i = 0; i < connectionPools.size(); ++i) { + logger->logMessage(QString("Closing websocket pool %1...").arg(i)); + QThread *poolThread = connectionPools[i]->thread(); + connectionPools[i]->deleteLater(); // pool destructor calls thread()->quit() + poolThread->wait(); + poolThread->deleteLater(); + } +} + +void Servatrice_WebsocketGameServer::onNewConnection() +{ + Servatrice_ConnectionPool *pool = findLeastUsedConnectionPool(); + + auto ssi = new WebsocketServerSocketInterface(server, pool->getDatabaseInterface()); + connect(ssi, SIGNAL(incTxBytes(quint64)), this, SLOT(incTxBytes(quint64))); + /* + * Due to a Qt limitation, websockets can't be moved to another thread. + * This will hopefully change in Qt6 if QtWebSocket will be integrated in QtNetwork + */ + // ssi->moveToThread(pool->thread()); + pool->addClient(); + connect(ssi, SIGNAL(destroyed()), pool, SLOT(removeClient())); + + QMetaObject::invokeMethod(ssi, "initConnection", Qt::QueuedConnection, Q_ARG(void *, nextPendingConnection())); +} + +Servatrice_ConnectionPool *Servatrice_WebsocketGameServer::findLeastUsedConnectionPool() +{ + int minClientCount = -1; + int poolIndex = -1; + QStringList debugStr; + for (int i = 0; i < connectionPools.size(); ++i) { + const int clientCount = connectionPools[i]->getClientCount(); + if ((poolIndex == -1) || (clientCount < minClientCount)) { + minClientCount = clientCount; + poolIndex = i; + } + debugStr.append(QString::number(clientCount)); + } + qDebug().noquote() << "Pool utilisation:" << debugStr.join(", "); + return connectionPools[poolIndex]; +} + +void Servatrice_IslServer::incomingConnection(qintptr socketDescriptor) +{ + auto thread = new QThread; + connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); + + auto interface = new IslInterface(static_cast(socketDescriptor), cert, privateKey, server); + interface->moveToThread(thread); + connect(interface, SIGNAL(destroyed()), thread, SLOT(quit())); + + thread->start(); + QMetaObject::invokeMethod(interface, "initServer", Qt::QueuedConnection); +} + +Servatrice::Servatrice(QObject *parent) + : Server(parent), authenticationMethod(AuthenticationNone), uptime(0), txBytes(0), rxBytes(0), + shutdownTimer(nullptr) +{ + qRegisterMetaType("QSqlDatabase"); } Servatrice::~Servatrice() { - gameServer->close(); - prepareDestroy(); + gameServer->close(); + + // we are destroying the clients outside their thread! + for (auto *client : clients) { + client->prepareDestroy(); + } + + if (shutdownTimer) { + shutdownTimer->deleteLater(); + } + + servatriceDatabaseInterface->deleteLater(); + prepareDestroy(); } bool Servatrice::initServer() { - serverName = settings->value("server/name").toString(); - serverId = settings->value("server/id", 0).toInt(); - - const QString authenticationMethodStr = settings->value("authentication/method").toString(); - if (authenticationMethodStr == "sql") - authenticationMethod = AuthenticationSql; - else - authenticationMethod = AuthenticationNone; - - QString dbTypeStr = settings->value("database/type").toString(); - if (dbTypeStr == "mysql") - databaseType = DatabaseMySql; - else - databaseType = DatabaseNone; - - servatriceDatabaseInterface = new Servatrice_DatabaseInterface(-1, this); - setDatabaseInterface(servatriceDatabaseInterface); - - if (databaseType != DatabaseNone) { - settings->beginGroup("database"); - dbPrefix = settings->value("prefix").toString(); - servatriceDatabaseInterface->initDatabase("QMYSQL", - settings->value("hostname").toString(), - settings->value("database").toString(), - settings->value("user").toString(), - settings->value("password").toString()); - settings->endGroup(); - - updateServerList(); - - qDebug() << "Clearing previous sessions..."; - servatriceDatabaseInterface->clearSessionTables(); - } - - const QString roomMethod = settings->value("rooms/method").toString(); - if (roomMethod == "sql") { - QSqlQuery query(servatriceDatabaseInterface->getDatabase()); - query.prepare("select id, name, descr, auto_join, join_message from " + dbPrefix + "_rooms order by id asc"); - servatriceDatabaseInterface->execSqlQuery(query); - while (query.next()) { - QSqlQuery query2(servatriceDatabaseInterface->getDatabase()); - query2.prepare("select name from " + dbPrefix + "_rooms_gametypes where id_room = :id_room"); - query2.bindValue(":id_room", query.value(0).toInt()); - servatriceDatabaseInterface->execSqlQuery(query2); - QStringList gameTypes; - while (query2.next()) - gameTypes.append(query2.value(0).toString()); - - addRoom(new Server_Room(query.value(0).toInt(), - query.value(1).toString(), - query.value(2).toString(), - query.value(3).toInt(), - query.value(4).toString(), - gameTypes, - this - )); - } - } else { - int size = settings->beginReadArray("rooms/roomlist"); - for (int i = 0; i < size; ++i) { - settings->setArrayIndex(i); - - QStringList gameTypes; - int size2 = settings->beginReadArray("game_types"); - for (int j = 0; j < size2; ++j) { - settings->setArrayIndex(j); - gameTypes.append(settings->value("name").toString()); - } - settings->endArray(); - - Server_Room *newRoom = new Server_Room( - i, - settings->value("name").toString(), - settings->value("description").toString(), - settings->value("autojoin").toBool(), - settings->value("joinmessage").toString(), - gameTypes, - this - ); - addRoom(newRoom); - } - settings->endArray(); - } - - updateLoginMessage(); - - maxGameInactivityTime = settings->value("game/max_game_inactivity_time").toInt(); - maxPlayerInactivityTime = settings->value("game/max_player_inactivity_time").toInt(); - - maxUsersPerAddress = settings->value("security/max_users_per_address").toInt(); - messageCountingInterval = settings->value("security/message_counting_interval").toInt(); - maxMessageCountPerInterval = settings->value("security/max_message_count_per_interval").toInt(); - maxMessageSizePerInterval = settings->value("security/max_message_size_per_interval").toInt(); - maxGamesPerUser = settings->value("security/max_games_per_user").toInt(); - try { if (settings->value("servernetwork/active", 0).toInt()) { - qDebug() << "Connecting to ISL network."; - const QString certFileName = settings->value("servernetwork/ssl_cert").toString(); - const QString keyFileName = settings->value("servernetwork/ssl_key").toString(); - qDebug() << "Loading certificate..."; - QFile certFile(certFileName); - if (!certFile.open(QIODevice::ReadOnly)) - throw QString("Error opening certificate file: %1").arg(certFileName); - QSslCertificate cert(&certFile); - if (!cert.isValid()) - throw(QString("Invalid certificate.")); - qDebug() << "Loading private key..."; - QFile keyFile(keyFileName); - if (!keyFile.open(QIODevice::ReadOnly)) - throw QString("Error opening private key file: %1").arg(keyFileName); - QSslKey key(&keyFile, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey); - if (key.isNull()) - throw QString("Invalid private key."); - - QMutableListIterator serverIterator(serverList); - while (serverIterator.hasNext()) { - const ServerProperties &prop = serverIterator.next(); - if (prop.cert == cert) { - serverIterator.remove(); - continue; - } - - QThread *thread = new QThread; - thread->setObjectName("isl_" + QString::number(prop.id)); - connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); - - IslInterface *interface = new IslInterface(prop.id, prop.hostname, prop.address.toString(), prop.controlPort, prop.cert, cert, key, this); - interface->moveToThread(thread); - connect(interface, SIGNAL(destroyed()), thread, SLOT(quit())); - - thread->start(); - QMetaObject::invokeMethod(interface, "initClient", Qt::BlockingQueuedConnection); - } - - const int networkPort = settings->value("servernetwork/port", 14747).toInt(); - qDebug() << "Starting ISL server on port" << networkPort; - - islServer = new Servatrice_IslServer(this, cert, key, this); - if (islServer->listen(QHostAddress::Any, networkPort)) - qDebug() << "ISL server listening."; - else - throw QString("islServer->listen()"); - } } catch (QString error) { - qDebug() << "ERROR --" << error; - return false; - } - - pingClock = new QTimer(this); - connect(pingClock, SIGNAL(timeout()), this, SIGNAL(pingClockTimeout())); - pingClock->start(1000); - - int statusUpdateTime = settings->value("server/statusupdate").toInt(); - statusUpdateClock = new QTimer(this); - connect(statusUpdateClock, SIGNAL(timeout()), this, SLOT(statusUpdate())); - if (statusUpdateTime != 0) { - qDebug() << "Starting status update clock, interval " << statusUpdateTime << " ms"; - statusUpdateClock->start(statusUpdateTime); - } - - const int numberPools = settings->value("server/number_pools", 1).toInt(); - gameServer = new Servatrice_GameServer(this, numberPools, servatriceDatabaseInterface->getDatabase(), this); - gameServer->setMaxPendingConnections(1000); - const int gamePort = settings->value("server/port", 4747).toInt(); - qDebug() << "Starting server on port" << gamePort; - if (gameServer->listen(QHostAddress::Any, gamePort)) - qDebug() << "Server listening."; - else { - qDebug() << "gameServer->listen(): Error."; - return false; - } - return true; + serverId = getServerID(); + if (getAuthenticationMethodString() == "sql") { + qDebug() << "Authenticating method: sql"; + authenticationMethod = AuthenticationSql; + } else if (getAuthenticationMethodString() == "password") { + qDebug() << "Authenticating method: password"; + authenticationMethod = AuthenticationPassword; + } else { + if (getRegOnlyServerEnabled()) { + qDebug() << "Registration only server enabled but no authentication method defined: Error."; + return false; + } + qDebug() << "Authenticating method: none"; + authenticationMethod = AuthenticationNone; + } + + qDebug() << "Store Replays:" << getStoreReplaysEnabled(); + qDebug() << "Client ID Required:" << getClientIDRequiredEnabled(); + qDebug() << "Maximum user limit enabled:" << getMaxUserLimitEnabled(); + + if (getMaxUserLimitEnabled()) { + qDebug() << "Maximum total user limit:" << getMaxUserTotal(); + qDebug() << "Maximum tcp user limit:" << getMaxTcpUserLimit(); + qDebug() << "Maximum websocket user limit:" << getMaxWebSocketUserLimit(); + } + + qDebug() << "Accept registered users only:" << getRegOnlyServerEnabled(); + qDebug() << "Registration enabled:" << getRegistrationEnabled(); + if (getRegistrationEnabled()) { + QStringList emailBlackListFilters = getEmailBlackList().split(",", Qt::SkipEmptyParts); + QStringList emailWhiteListFilters = getEmailWhiteList().split(",", Qt::SkipEmptyParts); + qDebug() << "Email blacklist:" << emailBlackListFilters; + qDebug() << "Email whitelist:" << emailWhiteListFilters; + qDebug() << "Require email address to register:" << getRequireEmailForRegistrationEnabled(); + qDebug() << "Require email activation via token:" << getRequireEmailActivationEnabled(); + if (getMaxAccountsPerEmail()) { + qDebug() << "Maximum number of accounts per email:" << getMaxAccountsPerEmail(); + } else { + qDebug() << "Maximum number of accounts per email: unlimited"; + } + qDebug() << "Enable Internal SMTP Client:" << getEnableInternalSMTPClient(); + if (!getEnableInternalSMTPClient()) { + qDebug() << "WARNING: Registrations are enabled but internal SMTP client is disabled. Users activation " + "emails will not be automatically mailed to users!"; + } + } + + qDebug() << "Reset password enabled:" << getEnableForgotPassword(); + if (getEnableForgotPassword()) { + qDebug() << "Reset password token life (in minutes):" << getForgotPasswordTokenLife(); + qDebug() << "Reset password challenge on:" << getEnableForgotPasswordChallenge(); + } + + qDebug() << "Auditing enabled:" << getEnableAudit(); + if (getEnableAudit()) { + qDebug() << "Audit registration attempts enabled:" << getEnableRegistrationAudit(); + qDebug() << "Audit reset password attepts enabled:" << getEnableForgotPasswordAudit(); + } + + if (getDBTypeString() == "mysql") { + databaseType = DatabaseMySql; + } else { + databaseType = DatabaseNone; + } + servatriceDatabaseInterface = new Servatrice_DatabaseInterface(-1, this); + setDatabaseInterface(servatriceDatabaseInterface); + + if (databaseType != DatabaseNone) { + dbPrefix = getDBPrefixString(); + bool dbOpened = servatriceDatabaseInterface->initDatabase( + "QMYSQL", getDBHostNameString(), getDBDatabaseNameString(), getDBUserNameString(), getDBPasswordString()); + if (!dbOpened) { + qDebug() << "Failed to open database"; + return false; + } + updateServerList(); + qDebug() << "Clearing previous sessions..."; + servatriceDatabaseInterface->clearSessionTables(); + } + + if (getRoomsMethodString() == "sql") { + QSqlQuery *query = servatriceDatabaseInterface->prepareQuery( + "select id, name, descr, permissionlevel, privlevel, auto_join, join_message, chat_history_size from " + "{prefix}_rooms where id_server = :id_server order by id asc"); + query->bindValue(":id_server", serverId); + servatriceDatabaseInterface->execSqlQuery(query); + while (query->next()) { + QSqlQuery *query2 = servatriceDatabaseInterface->prepareQuery( + "select name from {prefix}_rooms_gametypes where id_room = :id_room AND id_server = :id_server"); + query2->bindValue(":id_server", serverId); + query2->bindValue(":id_room", query->value(0).toInt()); + servatriceDatabaseInterface->execSqlQuery(query2); + QStringList gameTypes; + while (query2->next()) + gameTypes.append(query2->value(0).toString()); + addRoom(new Server_Room(query->value(0).toInt(), query->value(7).toInt(), query->value(1).toString(), + query->value(2).toString(), query->value(3).toString().toLower(), + query->value(4).toString().toLower(), static_cast(query->value(5).toInt()), + query->value(6).toString(), gameTypes, this)); + } + } else { + int size = settingsCache->beginReadArray("rooms/roomlist"); + for (int i = 0; i < size; ++i) { + settingsCache->setArrayIndex(i); + QStringList gameTypes; + int size2 = settingsCache->beginReadArray("game_types"); + for (int j = 0; j < size2; ++j) { + settingsCache->setArrayIndex(j); + gameTypes.append(settingsCache->value("name").toString()); + } + settingsCache->endArray(); + Server_Room *newRoom = new Server_Room( + i, settingsCache->value("chathistorysize").toInt(), settingsCache->value("name").toString(), + settingsCache->value("description").toString(), + settingsCache->value("permissionlevel").toString().toLower(), + settingsCache->value("privilegelevel").toString().toLower(), settingsCache->value("autojoin").toBool(), + settingsCache->value("joinmessage").toString(), gameTypes, this); + addRoom(newRoom); + } + + if (size == 0) { + // no room defined in config, add a dummy one + Server_Room *newRoom = new Server_Room(0, 100, "General room", "Play anything here.", "none", "none", true, + "", QStringList("Standard"), this); + addRoom(newRoom); + } + + settingsCache->endArray(); + } + + updateLoginMessage(); + + try { + if (getISLNetworkEnabled()) { + qDebug() << "Connecting to ISL network."; + qDebug() << "Loading certificate..."; + QFile certFile(getISLNetworkSSLCertFile()); + if (!certFile.open(QIODevice::ReadOnly)) + throw QString("Error opening certificate file: %1").arg(getISLNetworkSSLCertFile()); + QSslCertificate cert(&certFile); + + const QDateTime currentTime = QDateTime::currentDateTime(); + if (currentTime < cert.effectiveDate() || currentTime > cert.expiryDate() || cert.isBlacklisted()) + throw QString("Invalid certificate."); + + qDebug() << "Loading private key..."; + QFile keyFile(getISLNetworkSSLKeyFile()); + if (!keyFile.open(QIODevice::ReadOnly)) + throw QString("Error opening private key file: %1").arg(getISLNetworkSSLKeyFile()); + QSslKey key(&keyFile, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey); + if (key.isNull()) + throw QString("Invalid private key."); + + QMutableListIterator serverIterator(serverList); + while (serverIterator.hasNext()) { + const ServerProperties &prop = serverIterator.next(); + if (prop.cert == cert) { + serverIterator.remove(); + continue; + } + + auto *thread = new QThread; + thread->setObjectName("isl_" + QString::number(prop.id)); + connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); + + IslInterface *interface = new IslInterface(prop.id, prop.hostname, prop.address.toString(), + prop.controlPort, prop.cert, cert, key, this); + interface->moveToThread(thread); + connect(interface, SIGNAL(destroyed()), thread, SLOT(quit())); + + thread->start(); + QMetaObject::invokeMethod(interface, "initClient", Qt::BlockingQueuedConnection); + } + + qDebug() << "Starting ISL server on port" << getISLNetworkPort(); + islServer = new Servatrice_IslServer(this, cert, key, this); + if (islServer->listen(QHostAddress::Any, static_cast(getISLNetworkPort()))) + qDebug() << "ISL server listening."; + else + throw QString("islServer->listen()"); + } + } catch (QString &error) { + qDebug() << "ERROR --" << error; + return false; + } + + pingClock = new QTimer(this); + connect(pingClock, SIGNAL(timeout()), this, SIGNAL(pingClockTimeout())); + pingClock->start(getClientKeepAlive() * 1000); + + statusUpdateClock = new QTimer(this); + connect(statusUpdateClock, SIGNAL(timeout()), this, SLOT(statusUpdate())); + if (getServerStatusUpdateTime() != 0) { + qDebug() << "Starting status update clock, interval" << getServerStatusUpdateTime() << "ms"; + statusUpdateClock->start(getServerStatusUpdateTime()); + } + + // SOCKET SERVER + if (getNumberOfTCPPools() > 0) { + gameServer = + new Servatrice_GameServer(this, getNumberOfTCPPools(), servatriceDatabaseInterface->getDatabase(), this); + gameServer->setMaxPendingConnections(1000); + QHostAddress tcpHost = getServerTCPHost(); + qDebug() << "Starting server on host" << tcpHost.toString() << "port" << getServerTCPPort(); + if (gameServer->listen(tcpHost, static_cast(getServerTCPPort()))) + qDebug() << "Server listening."; + else { + qDebug() << "gameServer->listen(): Error:" << gameServer->errorString(); + return false; + } + } + + // WEBSOCKET SERVER + if (getNumberOfWebSocketPools() > 0) { + websocketGameServer = new Servatrice_WebsocketGameServer(this, getNumberOfWebSocketPools(), + servatriceDatabaseInterface->getDatabase(), this); + websocketGameServer->setMaxPendingConnections(1000); + QHostAddress webSocketHost = getServerWebSocketHost(); + qDebug() << "Starting websocket server on host" << webSocketHost.toString() << "port" + << getServerWebSocketPort(); + if (websocketGameServer->listen(webSocketHost, static_cast(getServerWebSocketPort()))) + qDebug() << "Websocket server listening."; + else { + qDebug() << "websocketGameServer->listen(): Error:" << websocketGameServer->errorString(); + return false; + } + } + + if (getIdleClientTimeout() > 0) { + qDebug() << "Idle client timeout value:" << getIdleClientTimeout(); + if (getIdleClientTimeout() < 300) + qDebug() << "WARNING: It is not recommended to set the IdleClientTimeout value very low. Doing so will " + "cause clients to very quickly be disconnected. Many players when connected may be searching " + "for card details outside the client in the middle of matches or possibly drafting outside the " + "client and short time out values will remove these players."; + } + + setRequiredFeatures(getRequiredFeatures()); + return true; } void Servatrice::addDatabaseInterface(QThread *thread, Servatrice_DatabaseInterface *databaseInterface) { - databaseInterfaces.insert(thread, databaseInterface); + databaseInterfaces.insert(thread, databaseInterface); } void Servatrice::updateServerList() { - qDebug() << "Updating server list..."; - - serverListMutex.lock(); - serverList.clear(); - - QSqlQuery query(servatriceDatabaseInterface->getDatabase()); - query.prepare("select id, ssl_cert, hostname, address, game_port, control_port from " + dbPrefix + "_servers order by id asc"); - servatriceDatabaseInterface->execSqlQuery(query); - while (query.next()) { - ServerProperties prop(query.value(0).toInt(), QSslCertificate(query.value(1).toString().toAscii()), query.value(2).toString(), QHostAddress(query.value(3).toString()), query.value(4).toInt(), query.value(5).toInt()); - serverList.append(prop); - qDebug() << QString("#%1 CERT=%2 NAME=%3 IP=%4:%5 CPORT=%6").arg(prop.id).arg(QString(prop.cert.digest().toHex())).arg(prop.hostname).arg(prop.address.toString()).arg(prop.gamePort).arg(prop.controlPort); - } - - serverListMutex.unlock(); + qDebug() << "Updating server list..."; + + serverListMutex.lock(); + serverList.clear(); + + QSqlQuery *query = servatriceDatabaseInterface->prepareQuery( + "select id, ssl_cert, hostname, address, game_port, control_port from {prefix}_servers order by id asc"); + servatriceDatabaseInterface->execSqlQuery(query); + while (query->next()) { + ServerProperties prop(query->value(0).toInt(), QSslCertificate(query->value(1).toString().toUtf8()), + query->value(2).toString(), QHostAddress(query->value(3).toString()), + query->value(4).toInt(), query->value(5).toInt()); + serverList.append(prop); + qDebug() << QString("#%1 CERT=%2 NAME=%3 IP=%4:%5 CPORT=%6") + .arg(prop.id) + .arg(QString(prop.cert.digest().toHex())) + .arg(prop.hostname) + .arg(prop.address.toString()) + .arg(prop.gamePort) + .arg(prop.controlPort); + } + + serverListMutex.unlock(); } QList Servatrice::getServerList() const { - serverListMutex.lock(); - QList result = serverList; - serverListMutex.unlock(); - - return result; + serverListMutex.lock(); + QList result = serverList; + serverListMutex.unlock(); + + return result; } int Servatrice::getUsersWithAddress(const QHostAddress &address) const { - int result = 0; - QReadLocker locker(&clientsLock); - for (int i = 0; i < clients.size(); ++i) - if (static_cast(clients[i])->getPeerAddress() == address) - ++result; - return result; + int result = 0; + QReadLocker locker(&clientsLock); + for (auto client : clients) + if (static_cast(client)->getPeerAddress() == address) + ++result; + + return result; } -QList Servatrice::getUsersWithAddressAsList(const QHostAddress &address) const +QList Servatrice::getUsersWithAddressAsList(const QHostAddress &address) const { - QList result; - QReadLocker locker(&clientsLock); - for (int i = 0; i < clients.size(); ++i) - if (static_cast(clients[i])->getPeerAddress() == address) - result.append(static_cast(clients[i])); - return result; + QList result; + QReadLocker locker(&clientsLock); + for (auto client : clients) + if (static_cast(client)->getPeerAddress() == address) + result.append(static_cast(client)); + return result; } void Servatrice::updateLoginMessage() { - if (!servatriceDatabaseInterface->checkSql()) - return; - - QSqlQuery query(servatriceDatabaseInterface->getDatabase()); - query.prepare("select message from " + dbPrefix + "_servermessages where id_server = :id_server order by timest desc limit 1"); - query.bindValue(":id_server", serverId); - if (servatriceDatabaseInterface->execSqlQuery(query)) - if (query.next()) { - const QString newLoginMessage = query.value(0).toString(); - - loginMessageMutex.lock(); - loginMessage = newLoginMessage; - loginMessageMutex.unlock(); - - Event_ServerMessage event; - event.set_message(newLoginMessage.toStdString()); - SessionEvent *se = Server_ProtocolHandler::prepareSessionEvent(event); - QMapIterator usersIterator(users); - while (usersIterator.hasNext()) - usersIterator.next().value()->sendProtocolItem(*se); - delete se; - } + if (!servatriceDatabaseInterface->checkSql()) + return; + + QSqlQuery *query = servatriceDatabaseInterface->prepareQuery( + "select message from {prefix}_servermessages where id_server = :id_server order by timest desc limit 1"); + query->bindValue(":id_server", serverId); + if (servatriceDatabaseInterface->execSqlQuery(query)) + if (query->next()) { + const QString newLoginMessage = query->value(0).toString(); + + loginMessageMutex.lock(); + loginMessage = newLoginMessage; + loginMessageMutex.unlock(); + + Event_ServerMessage event; + event.set_message(newLoginMessage.toStdString()); + SessionEvent *se = Server_ProtocolHandler::prepareSessionEvent(event); + QMapIterator usersIterator(users); + while (usersIterator.hasNext()) + usersIterator.next().value()->sendProtocolItem(*se); + delete se; + } +} + +void Servatrice::setRequiredFeatures(const QString &featureList) +{ + FeatureSet features; + serverRequiredFeatureList.clear(); + features.initalizeFeatureList(serverRequiredFeatureList); + QStringList listReqFeatures = featureList.split(",", Qt::SkipEmptyParts); + if (!listReqFeatures.isEmpty()) + for (const QString &reqFeature : listReqFeatures) { + features.enableRequiredFeature(serverRequiredFeatureList, reqFeature); + } + + qDebug() << "Set required client features to:" << serverRequiredFeatureList; } void Servatrice::statusUpdate() { - if (!servatriceDatabaseInterface->checkSql()) - return; - - const int uc = getUsersCount(); // for correct mutex locking order - const int gc = getGamesCount(); - - uptime += statusUpdateClock->interval() / 1000; - - txBytesMutex.lock(); - quint64 tx = txBytes; - txBytes = 0; - txBytesMutex.unlock(); - rxBytesMutex.lock(); - quint64 rx = rxBytes; - rxBytes = 0; - rxBytesMutex.unlock(); - - QSqlQuery query(servatriceDatabaseInterface->getDatabase()); - query.prepare("insert into " + dbPrefix + "_uptime (id_server, timest, uptime, users_count, games_count, tx_bytes, rx_bytes) values(:id, NOW(), :uptime, :users_count, :games_count, :tx, :rx)"); - query.bindValue(":id", serverId); - query.bindValue(":uptime", uptime); - query.bindValue(":users_count", uc); - query.bindValue(":games_count", gc); - query.bindValue(":tx", tx); - query.bindValue(":rx", rx); - servatriceDatabaseInterface->execSqlQuery(query); + if (!servatriceDatabaseInterface->checkSql()) + return; + + const int uc = getUsersCount(); // for correct mutex locking order + + const QStringList mods_info = getOnlineModeratorList(); + const int mc = mods_info.size(); + const QString ml = mods_info.join(", "); + + const int gc = getGamesCount(); + + uptime += statusUpdateClock->interval() / 1000; + + txBytesMutex.lock(); + quint64 tx = txBytes; + txBytes = 0; + txBytesMutex.unlock(); + rxBytesMutex.lock(); + quint64 rx = rxBytes; + rxBytes = 0; + rxBytesMutex.unlock(); + + QSqlQuery *query = servatriceDatabaseInterface->prepareQuery( + "insert into {prefix}_uptime (id_server, timest, uptime, users_count, mods_count, mods_list, games_count, " + "tx_bytes, rx_bytes) values(:id, NOW(), :uptime, :users_count, :mods_count, :mods_list, :games_count, :tx, " + ":rx)"); + query->bindValue(":id", serverId); + query->bindValue(":uptime", uptime); + query->bindValue(":users_count", uc); + query->bindValue(":mods_count", mc); + query->bindValue(":mods_list", ml); + query->bindValue(":games_count", gc); + query->bindValue(":tx", tx); + query->bindValue(":rx", rx); + servatriceDatabaseInterface->execSqlQuery(query); + + if (getRegistrationEnabled() && getEnableInternalSMTPClient()) { + if (getRequireEmailActivationEnabled()) { + auto servDbSelQuery = servatriceDatabaseInterface->prepareQuery("select a.name, b.email, b.token from " + "{prefix}_activation_emails a left join " + "{prefix}_users b on a.name = b.name"); + if (!servatriceDatabaseInterface->execSqlQuery(servDbSelQuery)) + return; + + auto *queryDelete = + servatriceDatabaseInterface->prepareQuery("delete from {prefix}_activation_emails where name = :name"); + + while (servDbSelQuery->next()) { + const QString userName = servDbSelQuery->value(0).toString(); + const auto emailAddress = EmailParser::getParsedEmailAddress(servDbSelQuery->value(1).toString()); + const QString token = servDbSelQuery->value(2).toString(); + + if (smtpClient->enqueueActivationTokenMail(userName, emailAddress, token)) { + queryDelete->bindValue(":name", userName); + servatriceDatabaseInterface->execSqlQuery(queryDelete); + } + } + } + + if (getEnableForgotPassword()) { + auto *forgotPwQuery = servatriceDatabaseInterface->prepareQuery( + "select a.name, b.email, b.token from {prefix}_forgot_password a left join {prefix}_users b on a.name " + "= b.name where a.emailed = 0"); + if (!servatriceDatabaseInterface->execSqlQuery(forgotPwQuery)) + return; + + QSqlQuery *queryDelete = servatriceDatabaseInterface->prepareQuery( + "update {prefix}_forgot_password set emailed = 1 where name = :name"); + + while (forgotPwQuery->next()) { + const QString userName = forgotPwQuery->value(0).toString(); + const auto emailAddress = EmailParser::getParsedEmailAddress(forgotPwQuery->value(1).toString()); + const QString token = forgotPwQuery->value(2).toString(); + + if (smtpClient->enqueueForgotPasswordTokenMail(userName, emailAddress, token)) { + queryDelete->bindValue(":name", userName); + servatriceDatabaseInterface->execSqlQuery(queryDelete); + } + } + } + + smtpClient->sendAllEmails(); + } } void Servatrice::scheduleShutdown(const QString &reason, int minutes) { - shutdownReason = reason; - shutdownMinutes = minutes + 1; - if (minutes > 0) { - shutdownTimer = new QTimer; - connect(shutdownTimer, SIGNAL(timeout()), this, SLOT(shutdownTimeout())); - shutdownTimer->start(60000); - } - shutdownTimeout(); + shutdownReason = reason; + shutdownMinutes = minutes; + nextShutdownMessageMinutes = shutdownMinutes; + if (minutes > 0) { + shutdownTimer = new QTimer; + connect(shutdownTimer, SIGNAL(timeout()), this, SLOT(shutdownTimeout())); + shutdownTimer->start(60000); + } + shutdownTimeout(); } void Servatrice::incTxBytes(quint64 num) { - txBytesMutex.lock(); - txBytes += num; - txBytesMutex.unlock(); + txBytesMutex.lock(); + txBytes += num; + txBytesMutex.unlock(); } void Servatrice::incRxBytes(quint64 num) { - rxBytesMutex.lock(); - rxBytes += num; - rxBytesMutex.unlock(); + rxBytesMutex.lock(); + rxBytes += num; + rxBytesMutex.unlock(); } void Servatrice::shutdownTimeout() { - --shutdownMinutes; - - SessionEvent *se; - if (shutdownMinutes) { - Event_ServerShutdown event; - event.set_reason(shutdownReason.toStdString()); - event.set_minutes(shutdownMinutes); - se = Server_ProtocolHandler::prepareSessionEvent(event); - } else { - Event_ConnectionClosed event; - event.set_reason(Event_ConnectionClosed::SERVER_SHUTDOWN); - se = Server_ProtocolHandler::prepareSessionEvent(event); - } - - clientsLock.lockForRead(); - for (int i = 0; i < clients.size(); ++i) - clients[i]->sendProtocolItem(*se); - clientsLock.unlock(); - delete se; - - if (!shutdownMinutes) - deleteLater(); + // Show every time counter cut in half & every minute for last 5 minutes + if (shutdownMinutes <= 5 || shutdownMinutes == nextShutdownMessageMinutes) { + if (shutdownMinutes == nextShutdownMessageMinutes) + nextShutdownMessageMinutes = shutdownMinutes / 2; + + SessionEvent *se; + if (shutdownMinutes) { + Event_ServerShutdown event; + event.set_reason(shutdownReason.toStdString()); + event.set_minutes(static_cast(shutdownMinutes)); + se = Server_ProtocolHandler::prepareSessionEvent(event); + } else { + Event_ConnectionClosed event; + event.set_reason(Event_ConnectionClosed::SERVER_SHUTDOWN); + se = Server_ProtocolHandler::prepareSessionEvent(event); + } + + clientsLock.lockForRead(); + for (auto &client : clients) + client->sendProtocolItem(*se); + clientsLock.unlock(); + delete se; + + if (!shutdownMinutes) { + deleteLater(); + } + } + shutdownMinutes--; } -bool Servatrice::islConnectionExists(int serverId) const +bool Servatrice::islConnectionExists(int _serverId) const { - // Only call with islLock locked at least for reading - - return islInterfaces.contains(serverId); + // Only call with islLock locked at least for reading + return islInterfaces.contains(_serverId); } -void Servatrice::addIslInterface(int serverId, IslInterface *interface) +void Servatrice::addIslInterface(int _serverId, IslInterface *interface) { - // Only call with islLock locked for writing - - islInterfaces.insert(serverId, interface); - connect(interface, SIGNAL(externalUserJoined(ServerInfo_User)), this, SLOT(externalUserJoined(ServerInfo_User))); - connect(interface, SIGNAL(externalUserLeft(QString)), this, SLOT(externalUserLeft(QString))); - connect(interface, SIGNAL(externalRoomUserJoined(int, ServerInfo_User)), this, SLOT(externalRoomUserJoined(int, ServerInfo_User))); - connect(interface, SIGNAL(externalRoomUserLeft(int, QString)), this, SLOT(externalRoomUserLeft(int, QString))); - connect(interface, SIGNAL(externalRoomSay(int, QString, QString)), this, SLOT(externalRoomSay(int, QString, QString))); - connect(interface, SIGNAL(externalRoomGameListChanged(int, ServerInfo_Game)), this, SLOT(externalRoomGameListChanged(int, ServerInfo_Game))); - connect(interface, SIGNAL(joinGameCommandReceived(Command_JoinGame, int, int, int, qint64)), this, SLOT(externalJoinGameCommandReceived(Command_JoinGame, int, int, int, qint64))); - connect(interface, SIGNAL(gameCommandContainerReceived(CommandContainer, int, int, qint64)), this, SLOT(externalGameCommandContainerReceived(CommandContainer, int, int, qint64))); - connect(interface, SIGNAL(responseReceived(Response, qint64)), this, SLOT(externalResponseReceived(Response, qint64))); - connect(interface, SIGNAL(gameEventContainerReceived(GameEventContainer, qint64)), this, SLOT(externalGameEventContainerReceived(GameEventContainer, qint64))); + // Only call with islLock locked for writing + islInterfaces.insert(_serverId, interface); + connect(interface, SIGNAL(externalUserJoined(ServerInfo_User)), this, SLOT(externalUserJoined(ServerInfo_User))); + connect(interface, SIGNAL(externalUserLeft(QString)), this, SLOT(externalUserLeft(QString))); + connect(interface, SIGNAL(externalRoomUserJoined(int, ServerInfo_User)), this, + SLOT(externalRoomUserJoined(int, ServerInfo_User))); + connect(interface, SIGNAL(externalRoomUserLeft(int, QString)), this, SLOT(externalRoomUserLeft(int, QString))); + connect(interface, SIGNAL(externalRoomSay(int, QString, QString)), this, + SLOT(externalRoomSay(int, QString, QString))); + connect(interface, SIGNAL(externalRoomRemoveMessages(int, QString, int)), this, + SLOT(externalRoomRemoveMessages(int, QString, int))); + connect(interface, SIGNAL(externalRoomGameListChanged(int, ServerInfo_Game)), this, + SLOT(externalRoomGameListChanged(int, ServerInfo_Game))); + connect(interface, SIGNAL(joinGameCommandReceived(Command_JoinGame, int, int, int, qint64)), this, + SLOT(externalJoinGameCommandReceived(Command_JoinGame, int, int, int, qint64))); + connect(interface, SIGNAL(gameCommandContainerReceived(CommandContainer, int, int, qint64)), this, + SLOT(externalGameCommandContainerReceived(CommandContainer, int, int, qint64))); + connect(interface, SIGNAL(responseReceived(Response, qint64)), this, + SLOT(externalResponseReceived(Response, qint64))); + connect(interface, SIGNAL(gameEventContainerReceived(GameEventContainer, qint64)), this, + SLOT(externalGameEventContainerReceived(GameEventContainer, qint64))); } -void Servatrice::removeIslInterface(int serverId) +void Servatrice::removeIslInterface(int _serverId) { - // Only call with islLock locked for writing - - // XXX we probably need to delete everything that belonged to it... - islInterfaces.remove(serverId); + // Only call with islLock locked for writing + // XXX we probably need to delete everything that belonged to it... <-- THIS SHOULD BE FIXED FOR ISL FUNCTIONALITY + // TO WORK COMPLETLY! + islInterfaces.remove(_serverId); } -void Servatrice::doSendIslMessage(const IslMessage &msg, int serverId) +void Servatrice::doSendIslMessage(const IslMessage &msg, int _serverId) { - QReadLocker locker(&islLock); - - if (serverId == -1) { - QMapIterator islIterator(islInterfaces); - while (islIterator.hasNext()) - islIterator.next().value()->transmitMessage(msg); - } else { - IslInterface *interface = islInterfaces.value(serverId); - if (interface) - interface->transmitMessage(msg); - } + QReadLocker locker(&islLock); + + if (_serverId == -1) { + QMapIterator islIterator(islInterfaces); + while (islIterator.hasNext()) + islIterator.next().value()->transmitMessage(msg); + } else { + IslInterface *interface = islInterfaces.value(_serverId); + if (interface) + interface->transmitMessage(msg); + } +} + +// start helper functions + +int Servatrice::getMaxUserTotal() const +{ + return settingsCache->value("security/max_users_total", 500).toInt(); +} + +bool Servatrice::getMaxUserLimitEnabled() const +{ + return settingsCache->value("security/enable_max_user_limit", false).toBool(); +} + +QString Servatrice::getServerName() const +{ + return settingsCache->value("server/name", "My Cockatrice server").toString(); +} + +int Servatrice::getServerID() const +{ + return settingsCache->value("server/id", 0).toInt(); +} + +bool Servatrice::getClientIDRequiredEnabled() const +{ + return settingsCache->value("server/requireclientid", 0).toBool(); +} + +bool Servatrice::getRegOnlyServerEnabled() const +{ + return settingsCache->value("authentication/regonly", 0).toBool(); +} + +QString Servatrice::getAuthenticationMethodString() const +{ + if (QProcessEnvironment::systemEnvironment().contains("DATABASE_URL")) { + return {"sql"}; + } + return settingsCache->value("authentication/method").toString(); +} + +bool Servatrice::getStoreReplaysEnabled() const +{ + return settingsCache->value("game/store_replays", true).toBool(); +} + +int Servatrice::getMaxTcpUserLimit() const +{ + return settingsCache->value("security/max_users_tcp", 500).toInt(); +} + +int Servatrice::getMaxWebSocketUserLimit() const +{ + return settingsCache->value("security/max_users_websocket", 500).toInt(); +} + +bool Servatrice::getRegistrationEnabled() const +{ + return settingsCache->value("registration/enabled", false).toBool(); +} + +bool Servatrice::getRequireEmailForRegistrationEnabled() const +{ + return settingsCache->value("registration/requireemail", true).toBool(); +} + +bool Servatrice::getRequireEmailActivationEnabled() const +{ + return settingsCache->value("registration/requireemailactivation", true).toBool(); +} + +QString Servatrice::getRequiredFeatures() const +{ + return settingsCache->value("server/requiredfeatures", "").toString(); +} + +QString Servatrice::getDBTypeString() const +{ + if (QProcessEnvironment::systemEnvironment().contains("DATABASE_URL")) { + return {"mysql"}; + } + return settingsCache->value("database/type").toString(); +} + +QString Servatrice::getDBPrefixString() const +{ + if (QProcessEnvironment::systemEnvironment().contains("DATABASE_URL")) { + return {"cockatrice"}; + } + return settingsCache->value("database/prefix").toString(); +} + +QString Servatrice::getDBHostNameString() const +{ + if (QProcessEnvironment::systemEnvironment().contains("DATABASE_URL")) { + return QUrl(QProcessEnvironment::systemEnvironment().value("DATABASE_URL")).host(); + } + return settingsCache->value("database/hostname").toString(); +} + +QString Servatrice::getDBDatabaseNameString() const +{ + if (QProcessEnvironment::systemEnvironment().contains("DATABASE_URL")) { + QString path = QUrl(QProcessEnvironment::systemEnvironment().value("DATABASE_URL")).path(); + return path.right(path.length() - 1); + } + return settingsCache->value("database/database").toString(); +} + +QString Servatrice::getDBUserNameString() const +{ + if (QProcessEnvironment::systemEnvironment().contains("DATABASE_URL")) { + return QUrl(QProcessEnvironment::systemEnvironment().value("DATABASE_URL")).userName(); + } + return settingsCache->value("database/user").toString(); +} + +QString Servatrice::getDBPasswordString() const +{ + if (QProcessEnvironment::systemEnvironment().contains("DATABASE_URL")) { + return QUrl(QProcessEnvironment::systemEnvironment().value("DATABASE_URL")).password(); + } + return settingsCache->value("database/password").toString(); +} + +QString Servatrice::getRoomsMethodString() const +{ + if (QProcessEnvironment::systemEnvironment().contains("DATABASE_URL")) { + return {"sql"}; + } + return settingsCache->value("rooms/method").toString(); +} + +int Servatrice::getMaxGameInactivityTime() const +{ + return settingsCache->value("game/max_game_inactivity_time", 120).toInt(); +} + +int Servatrice::getMaxPlayerInactivityTime() const +{ + return settingsCache->value("server/max_player_inactivity_time", 15).toInt(); +} + +int Servatrice::getClientKeepAlive() const +{ + return settingsCache->value("server/clientkeepalive", 1).toInt(); +} + +int Servatrice::getMaxUsersPerAddress() const +{ + return settingsCache->value("security/max_users_per_address", 4).toInt(); +} + +int Servatrice::getMessageCountingInterval() const +{ + return settingsCache->value("security/message_counting_interval", 10).toInt(); +} + +int Servatrice::getMaxMessageCountPerInterval() const +{ + return settingsCache->value("security/max_message_count_per_interval", 15).toInt(); +} + +int Servatrice::getMaxMessageSizePerInterval() const +{ + return settingsCache->value("security/max_message_size_per_interval", 1000).toInt(); +} + +int Servatrice::getMaxGamesPerUser() const +{ + return settingsCache->value("security/max_games_per_user", 5).toInt(); +} + +int Servatrice::getCommandCountingInterval() const +{ + return settingsCache->value("security/command_counting_interval", 10).toInt(); +} + +int Servatrice::getMaxCommandCountPerInterval() const +{ + return settingsCache->value("security/max_command_count_per_interval", 20).toInt(); +} + +int Servatrice::getServerStatusUpdateTime() const +{ + return settingsCache->value("server/statusupdate", 15000).toInt(); +} + +int Servatrice::getNumberOfTCPPools() const +{ + return settingsCache->value("server/number_pools", 1).toInt(); +} + +bool Servatrice::permitCreateGameAsJudge() const +{ + return settingsCache->value("game/allow_create_as_judge", false).toBool(); +} + +QHostAddress Servatrice::getServerTCPHost() const +{ + QString host = settingsCache->value("server/host", "any").toString(); + if (host == "any") + return QHostAddress::Any; + else + return QHostAddress(host); +} + +int Servatrice::getServerTCPPort() const +{ + return settingsCache->value("server/port", 4747).toInt(); +} + +int Servatrice::getNumberOfWebSocketPools() const +{ + return settingsCache->value("server/websocket_number_pools", 1).toInt(); +} + +QHostAddress Servatrice::getServerWebSocketHost() const +{ + QString host = settingsCache->value("server/websocket_host", "any").toString(); + if (host == "any") + return QHostAddress::Any; + else + return QHostAddress(host); +} + +int Servatrice::getServerWebSocketPort() const +{ + if (QProcessEnvironment::systemEnvironment().contains("PORT")) { + return QProcessEnvironment::systemEnvironment().value("PORT").toInt(); + } + return settingsCache->value("server/websocket_port", 4748).toInt(); +} + +bool Servatrice::getISLNetworkEnabled() const +{ + return settingsCache->value("servernetwork/active", false).toBool(); +} + +QString Servatrice::getISLNetworkSSLCertFile() const +{ + return settingsCache->value("servernetwork/ssl_cert").toString(); +} + +QString Servatrice::getISLNetworkSSLKeyFile() const +{ + return settingsCache->value("servernetwork/ssl_key").toString(); +} + +int Servatrice::getISLNetworkPort() const +{ + return settingsCache->value("servernetwork/port", 14747).toInt(); +} + +int Servatrice::getIdleClientTimeout() const +{ + return settingsCache->value("server/idleclienttimeout", 3600).toInt(); +} + +bool Servatrice::getEnableLogQuery() const +{ + return settingsCache->value("logging/enablelogquery", false).toBool(); +} + +int Servatrice::getMaxAccountsPerEmail() const +{ + return settingsCache->value("registration/maxaccountsperemail", 0).toInt(); +} + +bool Servatrice::getEnableInternalSMTPClient() const +{ + return settingsCache->value("smtp/enableinternalsmtpclient", true).toBool(); +} + +bool Servatrice::getEnableForgotPassword() const +{ + return settingsCache->value("forgotpassword/enable", false).toBool(); +} + +int Servatrice::getForgotPasswordTokenLife() const +{ + return settingsCache->value("forgotpassword/tokenlife", 60).toInt(); +} + +bool Servatrice::getEnableForgotPasswordChallenge() const +{ + return settingsCache->value("forgotpassword/enablechallenge", false).toBool(); +} + +QString Servatrice::getEmailBlackList() const +{ + return settingsCache->value("registration/emailproviderblacklist").toString(); +} + +QString Servatrice::getEmailWhiteList() const +{ + return settingsCache->value("registration/emailproviderwhitelist").toString(); +} + +bool Servatrice::getEnableAudit() const +{ + return settingsCache->value("audit/enable_audit", true).toBool(); +} + +bool Servatrice::getEnableRegistrationAudit() const +{ + return settingsCache->value("audit/enable_registration_audit", true).toBool(); +} + +bool Servatrice::getEnableForgotPasswordAudit() const +{ + return settingsCache->value("audit/enable_forgotpassword_audit", true).toBool(); +} + +int Servatrice::getMinPasswordLength() const +{ + return settingsCache->value("users/minpasswordlength", 6).toInt(); } diff --git a/servatrice/src/servatrice.h b/servatrice/src/servatrice.h index fbfac0350..62fb382cb 100644 --- a/servatrice/src/servatrice.h +++ b/servatrice/src/servatrice.h @@ -20,19 +20,20 @@ #ifndef SERVATRICE_H #define SERVATRICE_H -#include -#include -#include -#include #include +#include +#include #include #include -#include -#include "server.h" +#include +#include +#include +#include +#include +#include Q_DECLARE_METATYPE(QSqlDatabase) -class QSettings; class QSqlQuery; class QTimer; @@ -40,118 +41,246 @@ class GameReplay; class Servatrice; class Servatrice_ConnectionPool; class Servatrice_DatabaseInterface; -class ServerSocketInterface; +class AbstractServerSocketInterface; class IslInterface; +class FeatureSet; -class Servatrice_GameServer : public QTcpServer { - Q_OBJECT +class Servatrice_GameServer : public QTcpServer +{ + Q_OBJECT private: - Servatrice *server; - QList connectionPools; + Servatrice *server; + QList connectionPools; + public: - Servatrice_GameServer(Servatrice *_server, int _numberPools, const QSqlDatabase &_sqlDatabase, QObject *parent = 0); - ~Servatrice_GameServer(); + Servatrice_GameServer(Servatrice *_server, + int _numberPools, + const QSqlDatabase &_sqlDatabase, + QObject *parent = nullptr); + ~Servatrice_GameServer() override; + protected: - void incomingConnection(int socketDescriptor); + void incomingConnection(qintptr socketDescriptor) override; + Servatrice_ConnectionPool *findLeastUsedConnectionPool(); }; -class Servatrice_IslServer : public QTcpServer { - Q_OBJECT +class Servatrice_WebsocketGameServer : public QWebSocketServer +{ + Q_OBJECT private: - Servatrice *server; - QSslCertificate cert; - QSslKey privateKey; + Servatrice *server; + QList connectionPools; + public: - Servatrice_IslServer(Servatrice *_server, const QSslCertificate &_cert, const QSslKey &_privateKey, QObject *parent = 0) - : QTcpServer(parent), server(_server), cert(_cert), privateKey(_privateKey) { } + Servatrice_WebsocketGameServer(Servatrice *_server, + int _numberPools, + const QSqlDatabase &_sqlDatabase, + QObject *parent = nullptr); + ~Servatrice_WebsocketGameServer() override; + protected: - void incomingConnection(int socketDescriptor); + Servatrice_ConnectionPool *findLeastUsedConnectionPool(); +protected slots: + void onNewConnection(); }; -class ServerProperties { +class Servatrice_IslServer : public QTcpServer +{ + Q_OBJECT +private: + Servatrice *server; + QSslCertificate cert; + QSslKey privateKey; + public: - int id; - QSslCertificate cert; - QString hostname; - QHostAddress address; - int gamePort; - int controlPort; - - ServerProperties(int _id, const QSslCertificate &_cert, const QString &_hostname, const QHostAddress &_address, int _gamePort, int _controlPort) - : id(_id), cert(_cert), hostname(_hostname), address(_address), gamePort(_gamePort), controlPort(_controlPort) { } + Servatrice_IslServer(Servatrice *_server, + const QSslCertificate &_cert, + QSslKey _privateKey, + QObject *parent = nullptr) + : QTcpServer(parent), server(_server), cert(_cert), privateKey(std::move(_privateKey)) + { + } + +protected: + void incomingConnection(qintptr socketDescriptor) override; +}; + +class ServerProperties +{ +public: + int id; + QSslCertificate cert; + QString hostname; + QHostAddress address; + int gamePort; + int controlPort; + + ServerProperties(int _id, + const QSslCertificate &_cert, + QString _hostname, + const QHostAddress &_address, + int _gamePort, + int _controlPort) + : id(_id), cert(_cert), hostname(std::move(_hostname)), address(_address), gamePort(_gamePort), + controlPort(_controlPort) + { + } }; class Servatrice : public Server { - Q_OBJECT + Q_OBJECT public: - enum AuthenticationMethod { AuthenticationNone, AuthenticationSql }; + enum AuthenticationMethod + { + AuthenticationNone, + AuthenticationSql, + AuthenticationPassword + }; private slots: - void statusUpdate(); - void shutdownTimeout(); -protected: - void doSendIslMessage(const IslMessage &msg, int serverId); -private: - enum DatabaseType { DatabaseNone, DatabaseMySql }; - AuthenticationMethod authenticationMethod; - DatabaseType databaseType; - QTimer *pingClock, *statusUpdateClock; - Servatrice_GameServer *gameServer; - Servatrice_IslServer *islServer; - QString serverName; - mutable QMutex loginMessageMutex; - QString loginMessage; - QString dbPrefix; - QSettings *settings; - Servatrice_DatabaseInterface *servatriceDatabaseInterface; - int serverId; - int uptime; - QMutex txBytesMutex, rxBytesMutex; - quint64 txBytes, rxBytes; - int maxGameInactivityTime, maxPlayerInactivityTime; - int maxUsersPerAddress, messageCountingInterval, maxMessageCountPerInterval, maxMessageSizePerInterval, maxGamesPerUser; - - QString shutdownReason; - int shutdownMinutes; - QTimer *shutdownTimer; - - mutable QMutex serverListMutex; - QList serverList; - void updateServerList(); - - QMap islInterfaces; -public slots: - void scheduleShutdown(const QString &reason, int minutes); - void updateLoginMessage(); -public: - Servatrice(QSettings *_settings, QObject *parent = 0); - ~Servatrice(); - bool initServer(); - QString getServerName() const { return serverName; } - QString getLoginMessage() const { QMutexLocker locker(&loginMessageMutex); return loginMessage; } - bool getGameShouldPing() const { return true; } - int getMaxGameInactivityTime() const { return maxGameInactivityTime; } - int getMaxPlayerInactivityTime() const { return maxPlayerInactivityTime; } - int getMaxUsersPerAddress() const { return maxUsersPerAddress; } - int getMessageCountingInterval() const { return messageCountingInterval; } - int getMaxMessageCountPerInterval() const { return maxMessageCountPerInterval; } - int getMaxMessageSizePerInterval() const { return maxMessageSizePerInterval; } - int getMaxGamesPerUser() const { return maxGamesPerUser; } - AuthenticationMethod getAuthenticationMethod() const { return authenticationMethod; } - QString getDbPrefix() const { return dbPrefix; } - int getServerId() const { return serverId; } - int getUsersWithAddress(const QHostAddress &address) const; - QList getUsersWithAddressAsList(const QHostAddress &address) const; - void incTxBytes(quint64 num); - void incRxBytes(quint64 num); - void addDatabaseInterface(QThread *thread, Servatrice_DatabaseInterface *databaseInterface); - - bool islConnectionExists(int serverId) const; - void addIslInterface(int serverId, IslInterface *interface); - void removeIslInterface(int serverId); - QReadWriteLock islLock; + void statusUpdate(); + void shutdownTimeout(); - QList getServerList() const; +protected: + void doSendIslMessage(const IslMessage &msg, int _serverId) override; + +private: + enum DatabaseType + { + DatabaseNone, + DatabaseMySql + }; + AuthenticationMethod authenticationMethod; + DatabaseType databaseType; + QTimer *pingClock, *statusUpdateClock; + Servatrice_GameServer *gameServer; + Servatrice_WebsocketGameServer *websocketGameServer; + Servatrice_IslServer *islServer; + mutable QMutex loginMessageMutex; + QString loginMessage; + QString dbPrefix; + QMap serverRequiredFeatureList; + QString officialWarnings; + Servatrice_DatabaseInterface *servatriceDatabaseInterface; + int serverId; + int uptime; + QMutex txBytesMutex, rxBytesMutex; + quint64 txBytes, rxBytes; + + QString shutdownReason; + int shutdownMinutes; + int nextShutdownMessageMinutes; + QTimer *shutdownTimer; + + mutable QMutex serverListMutex; + QList serverList; + void updateServerList(); + + QMap islInterfaces; + + QString getDBPrefixString() const; + QString getDBHostNameString() const; + QString getDBDatabaseNameString() const; + QString getDBUserNameString() const; + QString getDBPasswordString() const; + QString getRoomsMethodString() const; + QString getISLNetworkSSLCertFile() const; + QString getISLNetworkSSLKeyFile() const; + int getServerStatusUpdateTime() const; + int getNumberOfTCPPools() const; + int getServerTCPPort() const; + int getNumberOfWebSocketPools() const; + int getServerWebSocketPort() const; + int getISLNetworkPort() const; + bool getISLNetworkEnabled() const; + bool getEnableInternalSMTPClient() const; + QHostAddress getServerTCPHost() const; + QHostAddress getServerWebSocketHost() const; + +public slots: + void scheduleShutdown(const QString &reason, int minutes); + void updateLoginMessage(); + void setRequiredFeatures(const QString &featureList); + +public: + explicit Servatrice(QObject *parent = nullptr); + ~Servatrice() override; + bool initServer(); + QMap getServerRequiredFeatureList() const override + { + return serverRequiredFeatureList; + } + QString getServerName() const; + QString getLoginMessage() const override + { + QMutexLocker locker(&loginMessageMutex); + return loginMessage; + } + QString getRequiredFeatures() const override; + QString getAuthenticationMethodString() const; + QString getDBTypeString() const; + QString getDbPrefix() const + { + return dbPrefix; + } + QString getEmailBlackList() const; + QString getEmailWhiteList() const; + AuthenticationMethod getAuthenticationMethod() const + { + return authenticationMethod; + } + bool permitUnregisteredUsers() const override + { + return authenticationMethod != AuthenticationNone; + } + bool getGameShouldPing() const override + { + return true; + } + bool getClientIDRequiredEnabled() const override; + bool getRegOnlyServerEnabled() const override; + bool getMaxUserLimitEnabled() const override; + bool getStoreReplaysEnabled() const override; + bool getRegistrationEnabled() const; + bool getRequireEmailForRegistrationEnabled() const; + bool getRequireEmailActivationEnabled() const; + bool getEnableLogQuery() const override; + bool getEnableForgotPassword() const; + bool getEnableForgotPasswordChallenge() const; + bool getEnableAudit() const; + bool getEnableRegistrationAudit() const; + bool getEnableForgotPasswordAudit() const; + int getMinPasswordLength() const; + int getIdleClientTimeout() const override; + int getServerID() const override; + int getMaxGameInactivityTime() const override; + int getMaxPlayerInactivityTime() const override; + int getClientKeepAlive() const override; + int getMaxUsersPerAddress() const; + int getMessageCountingInterval() const override; + int getMaxMessageCountPerInterval() const override; + int getMaxMessageSizePerInterval() const override; + int getMaxGamesPerUser() const override; + int getCommandCountingInterval() const override; + int getMaxCommandCountPerInterval() const override; + int getMaxUserTotal() const override; + bool permitCreateGameAsJudge() const override; + int getMaxTcpUserLimit() const; + int getMaxWebSocketUserLimit() const; + int getUsersWithAddress(const QHostAddress &address) const; + int getMaxAccountsPerEmail() const; + int getForgotPasswordTokenLife() const; + QList getUsersWithAddressAsList(const QHostAddress &address) const; + void incTxBytes(quint64 num); + void incRxBytes(quint64 num); + void addDatabaseInterface(QThread *thread, Servatrice_DatabaseInterface *databaseInterface); + + bool islConnectionExists(int _serverId) const; + void addIslInterface(int _serverId, IslInterface *interface); + void removeIslInterface(int _serverId); + QReadWriteLock islLock; + + QList getServerList() const; }; #endif diff --git a/servatrice/src/servatrice_connection_pool.cpp b/servatrice/src/servatrice_connection_pool.cpp index a2f849be4..3edb4da27 100644 --- a/servatrice/src/servatrice_connection_pool.cpp +++ b/servatrice/src/servatrice_connection_pool.cpp @@ -1,15 +1,16 @@ #include "servatrice_connection_pool.h" + #include "servatrice_database_interface.h" + #include Servatrice_ConnectionPool::Servatrice_ConnectionPool(Servatrice_DatabaseInterface *_databaseInterface) - : databaseInterface(_databaseInterface), - clientCount(0) + : databaseInterface(_databaseInterface), threaded(false), clientCount(0) { } Servatrice_ConnectionPool::~Servatrice_ConnectionPool() { - delete databaseInterface; - thread()->quit(); + delete databaseInterface; + thread()->quit(); } diff --git a/servatrice/src/servatrice_connection_pool.h b/servatrice/src/servatrice_connection_pool.h index f1bbc26f7..7479122cf 100644 --- a/servatrice/src/servatrice_connection_pool.h +++ b/servatrice/src/servatrice_connection_pool.h @@ -1,29 +1,46 @@ #ifndef SERVATRICE_CONNECTION_POOL_H #define SERVATRICE_CONNECTION_POOL_H -#include #include #include +#include class Servatrice_DatabaseInterface; -class Servatrice_ConnectionPool : public QObject { - Q_OBJECT +class Servatrice_ConnectionPool : public QObject +{ + Q_OBJECT private: - Servatrice_DatabaseInterface *databaseInterface; - bool threaded; - mutable QMutex clientCountMutex; - int clientCount; + Servatrice_DatabaseInterface *databaseInterface; + bool threaded; + mutable QMutex clientCountMutex; + int clientCount; + public: - Servatrice_ConnectionPool(Servatrice_DatabaseInterface *_databaseInterface); - ~Servatrice_ConnectionPool(); - - Servatrice_DatabaseInterface *getDatabaseInterface() const { return databaseInterface; } - - int getClientCount() const { QMutexLocker locker(&clientCountMutex); return clientCount; } - void addClient() { QMutexLocker locker(&clientCountMutex); ++clientCount; } + explicit Servatrice_ConnectionPool(Servatrice_DatabaseInterface *_databaseInterface); + ~Servatrice_ConnectionPool() override; + + Servatrice_DatabaseInterface *getDatabaseInterface() const + { + return databaseInterface; + } + + int getClientCount() const + { + QMutexLocker locker(&clientCountMutex); + return clientCount; + } + void addClient() + { + QMutexLocker locker(&clientCountMutex); + ++clientCount; + } public slots: - void removeClient() { QMutexLocker locker(&clientCountMutex); --clientCount; } + void removeClient() + { + QMutexLocker locker(&clientCountMutex); + --clientCount; + } }; #endif diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index 58856f4fc..bce3542e8 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -1,527 +1,1462 @@ -#include "servatrice.h" #include "servatrice_database_interface.h" -#include "passwordhasher.h" + +#include "servatrice.h" #include "serversocketinterface.h" -#include "decklist.h" -#include "pb/game_replay.pb.h" +#include "settingscache.h" + +#include +#include #include +#include #include #include +#include +#include +#include + +inline Q_LOGGING_CATEGORY(DatabaseInterfaceLog, "database_interface"); Servatrice_DatabaseInterface::Servatrice_DatabaseInterface(int _instanceId, Servatrice *_server) - : instanceId(_instanceId), - sqlDatabase(QSqlDatabase()), - server(_server) + : instanceId(_instanceId), sqlDatabase(QSqlDatabase()), server(_server) { } Servatrice_DatabaseInterface::~Servatrice_DatabaseInterface() { - sqlDatabase.close(); + // reset all prepared statements + qDeleteAll(preparedStatements); + preparedStatements.clear(); + + sqlDatabase.close(); } void Servatrice_DatabaseInterface::initDatabase(const QSqlDatabase &_sqlDatabase) { - if (_sqlDatabase.isValid()) { - sqlDatabase = QSqlDatabase::cloneDatabase(_sqlDatabase, "pool_" + QString::number(instanceId)); - openDatabase(); - } + if (_sqlDatabase.isValid()) { + sqlDatabase = QSqlDatabase::cloneDatabase(_sqlDatabase, "pool_" + QString::number(instanceId)); + openDatabase(); + } } -void Servatrice_DatabaseInterface::initDatabase(const QString &type, const QString &hostName, const QString &databaseName, const QString &userName, const QString &password) +bool Servatrice_DatabaseInterface::initDatabase(const QString &type, + const QString &hostName, + const QString &databaseName, + const QString &userName, + const QString &password) { - sqlDatabase = QSqlDatabase::addDatabase(type, "main"); - sqlDatabase.setHostName(hostName); - sqlDatabase.setDatabaseName(databaseName); - sqlDatabase.setUserName(userName); - sqlDatabase.setPassword(password); - - openDatabase(); + sqlDatabase = QSqlDatabase::addDatabase(type, "main"); + sqlDatabase.setHostName(hostName); + sqlDatabase.setDatabaseName(databaseName); + sqlDatabase.setUserName(userName); + sqlDatabase.setPassword(password); + + return openDatabase(); } bool Servatrice_DatabaseInterface::openDatabase() { - if (sqlDatabase.isOpen()) - sqlDatabase.close(); - - const QString poolStr = instanceId == -1 ? QString("main") : QString("pool %1").arg(instanceId); - qDebug() << QString("[%1] Opening database...").arg(poolStr); - if (!sqlDatabase.open()) { - qCritical() << QString("[%1] Error opening database: %2").arg(poolStr).arg(sqlDatabase.lastError().text()); - return false; - } - - return true; + if (sqlDatabase.isOpen()) + sqlDatabase.close(); + + const QString poolStr = instanceId == -1 ? QString("main") : QString("pool %1").arg(instanceId); + qCDebug(DatabaseInterfaceLog).noquote() << poolStr << "Opening database..."; + if (!sqlDatabase.open()) { + qCCritical(DatabaseInterfaceLog) << poolStr << "Error opening database:" << sqlDatabase.lastError().text(); + return false; + } + + QSqlQuery *versionQuery = prepareQuery("select version from {prefix}_schema_version limit 1"); + if (!execSqlQuery(versionQuery)) { + qCCritical(DatabaseInterfaceLog) << poolStr << "Error opening database: unable to load database schema version" + << "(hint: ensure the cockatrice_schema_version exists)"; + return false; + } + + if (versionQuery->next()) { + const int dbversion = versionQuery->value(0).toInt(); + const int expectedversion = DATABASE_SCHEMA_VERSION; + if (dbversion < expectedversion) { + qCCritical(DatabaseInterfaceLog) << poolStr + << "Error opening database: the database schema version is too old, you " + "need to run the migrations to update it from version" + << dbversion << "to version" << expectedversion; + return false; + } else if (dbversion > expectedversion) { + qCCritical(DatabaseInterfaceLog) << poolStr << "Error opening database: the database schema version" + << dbversion << "is too new, you need to update servatrice" + << "(this servatrice actually uses version" << expectedversion << ")"; + return false; + } + } else { + qCCritical(DatabaseInterfaceLog) << poolStr + << "Error opening database: unable to load database schema version (hint: " + "ensure the cockatrice_schema_version contains a single record)"; + return false; + } + + // reset all prepared statements + qDeleteAll(preparedStatements); + preparedStatements.clear(); + return true; } bool Servatrice_DatabaseInterface::checkSql() { - if (!sqlDatabase.isValid()) - return false; - - if (!sqlDatabase.exec("select 1").isActive()) - return openDatabase(); - return true; + if (!sqlDatabase.isValid()) { + return false; + } + + auto query = QSqlQuery(sqlDatabase); + if (query.exec("select 1") && !query.isActive()) { + return openDatabase(); + } + + if (query.lastError().isValid()) { + const auto &poolStr = instanceId == -1 ? QString("main") : QString("pool %1").arg(instanceId); + qCCritical(DatabaseInterfaceLog) << poolStr << "Error executing query:" << query.lastError().text(); + + sqlDatabase.close(); + return openDatabase(); + } + + return true; } -bool Servatrice_DatabaseInterface::execSqlQuery(QSqlQuery &query) +QSqlQuery *Servatrice_DatabaseInterface::prepareQuery(const QString &queryText) { - if (query.exec()) - return true; - const QString poolStr = instanceId == -1 ? QString("main") : QString("pool %1").arg(instanceId); - qCritical() << QString("[%1] Error executing query: %2").arg(poolStr).arg(query.lastError().text()); - return false; + if (preparedStatements.contains(queryText)) { + return preparedStatements.value(queryText); + } + + QString prefixedQueryText = queryText; + prefixedQueryText.replace("{prefix}", server->getDbPrefix()); + auto *query = new QSqlQuery(sqlDatabase); + query->prepare(prefixedQueryText); + + preparedStatements.insert(queryText, query); + return query; } -bool Servatrice_DatabaseInterface::usernameIsValid(const QString &user) +bool Servatrice_DatabaseInterface::execSqlQuery(QSqlQuery *query) { - QString result; - result.reserve(user.size()); - foreach (const QChar& c, user) { - switch (c.category()) { - // TODO: Figure out exactly which categories are OK and not - case QChar::Other_Control: break; - default: result += c; - } - } - result = result.trimmed(); - return (result.size() > 0); + if (query->exec()) + return true; + const QString poolStr = instanceId == -1 ? QString("main") : QString("pool %1").arg(instanceId); + qCCritical(DatabaseInterfaceLog) << poolStr << "Error executing query:" << query->lastError().text(); + sqlDatabase.close(); + openDatabase(); + return false; } -AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_ProtocolHandler *handler, const QString &user, const QString &password, QString &reasonStr, int &banSecondsLeft) +bool Servatrice_DatabaseInterface::usernameIsValid(const QString &user, QString &error) { - switch (server->getAuthenticationMethod()) { - case Servatrice::AuthenticationNone: return UnknownUser; - case Servatrice::AuthenticationSql: { - if (!checkSql()) - return UnknownUser; + int minNameLength = settingsCache->value("users/minnamelength", 6).toInt(); + if (minNameLength < 1) + minNameLength = 1; + int maxNameLength = settingsCache->value("users/maxnamelength", 12).toInt(); + bool allowLowercase = settingsCache->value("users/allowlowercase", true).toBool(); + bool allowUppercase = settingsCache->value("users/allowuppercase", true).toBool(); + bool allowNumerics = settingsCache->value("users/allownumerics", true).toBool(); + bool allowPunctuationPrefix = settingsCache->value("users/allowpunctuationprefix", false).toBool(); + QString allowedPunctuation = settingsCache->value("users/allowedpunctuation", "_").toString(); + QString disallowedWordsStr = settingsCache->value("users/disallowedwords", "").toString(); + QStringList disallowedWords = disallowedWordsStr.split(",", Qt::SkipEmptyParts); + disallowedWords.removeDuplicates(); + QVariant displayDisallowedWords = settingsCache->value("users/displaydisallowedwords"); + QString disallowedRegExpStr; + if (displayDisallowedWords.isValid()) { + disallowedWordsStr = displayDisallowedWords.toString().trimmed(); + if (!disallowedWordsStr.isEmpty()) { + disallowedWordsStr.prepend("\n"); + } + } else { + disallowedRegExpStr = settingsCache->value("users/disallowedregexp", "").toString(); + } - if (!usernameIsValid(user)) - return UsernameInvalid; - - QSqlQuery ipBanQuery(sqlDatabase); - ipBanQuery.prepare("select time_to_sec(timediff(now(), date_add(b.time_from, interval b.minutes minute))), b.minutes <=> 0, b.visible_reason from " + server->getDbPrefix() + "_bans b where b.time_from = (select max(c.time_from) from " + server->getDbPrefix() + "_bans c where c.ip_address = :address) and b.ip_address = :address2"); - ipBanQuery.bindValue(":address", static_cast(handler)->getPeerAddress().toString()); - ipBanQuery.bindValue(":address2", static_cast(handler)->getPeerAddress().toString()); - if (!execSqlQuery(ipBanQuery)) { - qDebug("Login denied: SQL error"); - return NotLoggedIn; - } - - if (ipBanQuery.next()) { - const int secondsLeft = -ipBanQuery.value(0).toInt(); - const bool permanentBan = ipBanQuery.value(1).toInt(); - if ((secondsLeft > 0) || permanentBan) { - reasonStr = ipBanQuery.value(2).toString(); - banSecondsLeft = permanentBan ? 0 : secondsLeft; - qDebug("Login denied: banned by address"); - return UserIsBanned; - } - } - - QSqlQuery nameBanQuery(sqlDatabase); - nameBanQuery.prepare("select time_to_sec(timediff(now(), date_add(b.time_from, interval b.minutes minute))), b.minutes <=> 0, b.visible_reason from " + server->getDbPrefix() + "_bans b where b.time_from = (select max(c.time_from) from " + server->getDbPrefix() + "_bans c where c.user_name = :name2) and b.user_name = :name1"); - nameBanQuery.bindValue(":name1", user); - nameBanQuery.bindValue(":name2", user); - if (!execSqlQuery(nameBanQuery)) { - qDebug("Login denied: SQL error"); - return NotLoggedIn; - } - - if (nameBanQuery.next()) { - const int secondsLeft = -nameBanQuery.value(0).toInt(); - const bool permanentBan = nameBanQuery.value(1).toInt(); - if ((secondsLeft > 0) || permanentBan) { - reasonStr = nameBanQuery.value(2).toString(); - banSecondsLeft = permanentBan ? 0 : secondsLeft; - qDebug("Login denied: banned by name"); - return UserIsBanned; - } - } - - QSqlQuery passwordQuery(sqlDatabase); - passwordQuery.prepare("select password_sha512 from " + server->getDbPrefix() + "_users where name = :name and active = 1"); - passwordQuery.bindValue(":name", user); - if (!execSqlQuery(passwordQuery)) { - qDebug("Login denied: SQL error"); - return NotLoggedIn; - } - - if (passwordQuery.next()) { - const QString correctPassword = passwordQuery.value(0).toString(); - if (correctPassword == PasswordHasher::computeHash(password, correctPassword.left(16))) { - qDebug("Login accepted: password right"); - return PasswordRight; - } else { - qDebug("Login denied: password wrong"); - return NotLoggedIn; - } - } else { - qDebug("Login accepted: unknown user"); - return UnknownUser; - } - } - } - return UnknownUser; + error = QString("%1|%2|%3|%4|%5|%6|%7|%8|%9") + .arg(minNameLength) + .arg(maxNameLength) + .arg(allowLowercase) + .arg(allowUppercase) + .arg(allowNumerics) + .arg(allowPunctuationPrefix) + .arg(allowedPunctuation) + .arg(disallowedWordsStr) + .arg(disallowedRegExpStr); + + if (user.length() < minNameLength || user.length() > maxNameLength) + return false; + + if (!allowPunctuationPrefix && allowedPunctuation.contains(user.at(0))) + return false; + + for (const QString &word : disallowedWords) { + if (user.contains(word, Qt::CaseInsensitive)) + return false; + } + + for (const QRegularExpression ®Exp : settingsCache->disallowedRegExp) { + if (regExp.match(user).hasMatch()) + return false; + } + + QString regEx("\\A["); + if (allowLowercase) + regEx.append("a-z"); + if (allowUppercase) + regEx.append("A-Z"); + if (allowNumerics) + regEx.append("0-9"); + regEx.append(QRegularExpression::escape(allowedPunctuation)); + regEx.append("]+\\z"); + + QRegularExpression re = QRegularExpression(regEx); + return re.match(user).hasMatch(); +} + +bool Servatrice_DatabaseInterface::registerUser(const QString &userName, + const QString &realName, + const QString &password, + bool passwordNeedsHash, + const QString &emailAddress, + const QString &country, + bool active) +{ + if (!checkSql()) + return false; + + QString passwordSha512; + if (passwordNeedsHash) { + passwordSha512 = PasswordHasher::computeHash(password, PasswordHasher::generateRandomSalt()); + } else { + passwordSha512 = password; + } + QString token = active ? QString() : PasswordHasher::generateActivationToken(); + + QSqlQuery *query = + prepareQuery("insert into {prefix}_users " + "(name, realname, password_sha512, email, country, registrationDate, active, token, " + "admin, avatar_bmp, clientid, privlevel, privlevelStartDate, privlevelEndDate) " + "values " + "(:userName, :realName, :password_sha512, :email, :country, UTC_TIMESTAMP(), :active, " + ":token, 0, '', '', 'NONE', UTC_TIMESTAMP(), UTC_TIMESTAMP())"); + query->bindValue(":userName", userName); + query->bindValue(":realName", realName); + query->bindValue(":password_sha512", passwordSha512); + query->bindValue(":email", emailAddress); + query->bindValue(":country", country); + query->bindValue(":active", active ? 1 : 0); + query->bindValue(":token", token); + + if (!execSqlQuery(query)) { + qCWarning(DatabaseInterfaceLog) << "Failed to insert user: " << query->lastError() + << " sql: " << query->lastQuery(); + return false; + } + + return true; +} + +bool Servatrice_DatabaseInterface::activateUser(const QString &userName, const QString &token) +{ + if (!checkSql()) + return false; + + QSqlQuery *activateQuery = + prepareQuery("select name from {prefix}_users where active=0 and name=:username and token=:token"); + + activateQuery->bindValue(":username", userName); + activateQuery->bindValue(":token", token); + if (!execSqlQuery(activateQuery)) { + qCWarning(DatabaseInterfaceLog) << "Account activation failed: SQL error." << activateQuery->lastError() + << " sql: " << activateQuery->lastQuery(); + return false; + } + + if (activateQuery->next()) { + const QString name = activateQuery->value(0).toString(); + // redundant check + if (name == userName) { + + QSqlQuery *query = prepareQuery("update {prefix}_users set active=1 where name = :userName"); + query->bindValue(":userName", userName); + + if (!execSqlQuery(query)) { + qCWarning(DatabaseInterfaceLog) + << "Failed to activate user: " << query->lastError() << " sql: " << query->lastQuery(); + return false; + } + + return true; + } + } + return false; +} + +AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_ProtocolHandler *handler, + const QString &user, + const QString &password, + const QString &clientId, + QString &reasonStr, + int &banSecondsLeft, + bool passwordNeedsHash) +{ + switch (server->getAuthenticationMethod()) { + case Servatrice::AuthenticationNone: + return UnknownUser; + case Servatrice::AuthenticationPassword: { + QString configPassword = settingsCache->value("authentication/password").toString(); + if (configPassword == password) + return PasswordRight; + + return NotLoggedIn; + } + case Servatrice::AuthenticationSql: { + if (!checkSql()) + return UnknownUser; + + if (!usernameIsValid(user, reasonStr)) + return UsernameInvalid; + + if (checkUserIsBanned(handler->getAddress(), user, clientId, reasonStr, banSecondsLeft)) + return UserIsBanned; + + QSqlQuery *passwordQuery = + prepareQuery("select password_sha512, active from {prefix}_users where name = :name"); + passwordQuery->bindValue(":name", user); + if (!execSqlQuery(passwordQuery)) { + qCWarning(DatabaseInterfaceLog) << "Login denied: SQL error"; + return NotLoggedIn; + } + + if (passwordQuery->next()) { + const QString correctPasswordSha512 = passwordQuery->value(0).toString(); + const bool userIsActive = passwordQuery->value(1).toBool(); + if (!userIsActive) { + qCWarning(DatabaseInterfaceLog) << "Login denied: user not active"; + return UserIsInactive; + } + QString hashedPassword; + if (passwordNeedsHash) { + hashedPassword = PasswordHasher::computeHash(password, correctPasswordSha512.left(16)); + } else { + hashedPassword = password; + } + if (correctPasswordSha512 == hashedPassword) { + qCDebug(DatabaseInterfaceLog) << "Login accepted: password right"; + return PasswordRight; + } else { + qCDebug(DatabaseInterfaceLog) << "Login denied: password wrong"; + return NotLoggedIn; + } + } else { + qCDebug(DatabaseInterfaceLog) << "Login accepted: unknown user"; + return UnknownUser; + } + } + } + return UnknownUser; +} + +bool Servatrice_DatabaseInterface::checkUserIsBanned(const QString &ipAddress, + const QString &userName, + const QString &clientId, + QString &banReason, + int &banSecondsRemaining) +{ + if (server->getAuthenticationMethod() != Servatrice::AuthenticationSql) + return false; + + if (!checkSql()) { + qCWarning(DatabaseInterfaceLog) << "Failed to check if user is banned. Database invalid."; + return false; + } + + return checkUserIsIpBanned(ipAddress, banReason, banSecondsRemaining) || + checkUserIsNameBanned(userName, banReason, banSecondsRemaining) || + checkUserIsIdBanned(clientId, banReason, banSecondsRemaining); +} + +bool Servatrice_DatabaseInterface::checkUserIsIdBanned(const QString &clientId, + QString &banReason, + int &banSecondsRemaining) +{ + if (clientId.isEmpty()) + return false; + + QSqlQuery *idBanQuery = + prepareQuery("select" + " timestampdiff(second, now(), date_add(b.time_from, interval b.minutes minute))," + " b.minutes <=> 0," + " b.visible_reason" + " from {prefix}_bans b" + " where" + " b.time_from = (select max(c.time_from)" + " from {prefix}_bans c" + " where c.clientid = :id)" + " and b.clientid = :id2"); + + idBanQuery->bindValue(":id", clientId); + idBanQuery->bindValue(":id2", clientId); + if (!execSqlQuery(idBanQuery)) { + qCWarning(DatabaseInterfaceLog) << "Id ban check failed: SQL error." << idBanQuery->lastError(); + return false; + } + + if (idBanQuery->next()) { + const int secondsLeft = idBanQuery->value(0).toInt(); + const bool permanentBan = idBanQuery->value(1).toInt(); + if ((secondsLeft > 0) || permanentBan) { + banReason = idBanQuery->value(2).toString(); + banSecondsRemaining = permanentBan ? 0 : secondsLeft; + qCDebug(DatabaseInterfaceLog) << "User is banned by client id" << clientId; + return true; + } + } + return false; +} + +bool Servatrice_DatabaseInterface::checkUserIsNameBanned(const QString &userName, + QString &banReason, + int &banSecondsRemaining) +{ + QSqlQuery *nameBanQuery = + prepareQuery("select timestampdiff(second, now(), date_add(b.time_from, interval b.minutes minute)), b.minutes " + "<=> 0, b.visible_reason from {prefix}_bans b where b.time_from = (select max(c.time_from) from " + "{prefix}_bans c where c.user_name = :name2) and b.user_name = :name1"); + nameBanQuery->bindValue(":name1", userName); + nameBanQuery->bindValue(":name2", userName); + if (!execSqlQuery(nameBanQuery)) { + qCWarning(DatabaseInterfaceLog) << "Name ban check failed: SQL error" << nameBanQuery->lastError(); + return false; + } + + if (nameBanQuery->next()) { + const int secondsLeft = nameBanQuery->value(0).toInt(); + const bool permanentBan = nameBanQuery->value(1).toInt(); + if ((secondsLeft > 0) || permanentBan) { + banReason = nameBanQuery->value(2).toString(); + banSecondsRemaining = permanentBan ? 0 : secondsLeft; + qCDebug(DatabaseInterfaceLog) << "Username" << userName << "is banned by name"; + return true; + } + } + return false; +} + +bool Servatrice_DatabaseInterface::checkUserIsIpBanned(const QString &ipAddress, + QString &banReason, + int &banSecondsRemaining) +{ + QSqlQuery *ipBanQuery = + prepareQuery("select" + " timestampdiff(second, now(), date_add(b.time_from, interval b.minutes minute))," + " b.minutes <=> 0," + " b.visible_reason" + " from {prefix}_bans b" + " where" + " b.time_from = (select max(c.time_from)" + " from {prefix}_bans c" + " where c.ip_address = :address)" + " and b.ip_address = :address2"); + + ipBanQuery->bindValue(":address", ipAddress); + ipBanQuery->bindValue(":address2", ipAddress); + if (!execSqlQuery(ipBanQuery)) { + qCWarning(DatabaseInterfaceLog) << "IP ban check failed: SQL error." << ipBanQuery->lastError(); + return false; + } + + if (ipBanQuery->next()) { + const int secondsLeft = ipBanQuery->value(0).toInt(); + const bool permanentBan = ipBanQuery->value(1).toInt(); + if ((secondsLeft > 0) || permanentBan) { + banReason = ipBanQuery->value(2).toString(); + banSecondsRemaining = permanentBan ? 0 : secondsLeft; + qCDebug(DatabaseInterfaceLog) << "User is banned by address" << ipAddress; + return true; + } + } + return false; +} + +bool Servatrice_DatabaseInterface::activeUserExists(const QString &user) +{ + if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) { + checkSql(); + + QSqlQuery *query = prepareQuery("select 1 from {prefix}_users where name = :name and active = 1"); + query->bindValue(":name", user); + if (!execSqlQuery(query)) + return false; + return query->next(); + } + return false; } bool Servatrice_DatabaseInterface::userExists(const QString &user) { - if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) { - checkSql(); - - QSqlQuery query(sqlDatabase); - query.prepare("select 1 from " + server->getDbPrefix() + "_users where name = :name and active = 1"); - query.bindValue(":name", user); - if (!execSqlQuery(query)) - return false; - return query.next(); - } - return false; + if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) { + checkSql(); + + QSqlQuery *query = prepareQuery("select 1 from {prefix}_users where name = :name"); + query->bindValue(":name", user); + if (!execSqlQuery(query)) + return false; + return query->next(); + } + return false; +} + +QString Servatrice_DatabaseInterface::getUserSalt(const QString &user) +{ + if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) { + checkSql(); + + QSqlQuery *query = + prepareQuery("SELECT SUBSTRING(password_sha512, 1, 16) FROM {prefix}_users WHERE name = :name"); + + query->bindValue(":name", user); + if (!execSqlQuery(query)) { + return {}; + } + + if (!query->next()) { + return {}; + } + + return query->value(0).toString(); + } + return {}; } int Servatrice_DatabaseInterface::getUserIdInDB(const QString &name) { - if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) { - QSqlQuery query(sqlDatabase); - query.prepare("select id from " + server->getDbPrefix() + "_users where name = :name and active = 1"); - query.bindValue(":name", name); - if (!execSqlQuery(query)) - return -1; - if (!query.next()) - return -1; - return query.value(0).toInt(); - } - return -1; + if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) { + QSqlQuery *query = prepareQuery("select id from {prefix}_users where name = :name and active = 1"); + query->bindValue(":name", name); + if (!execSqlQuery(query)) + return -1; + if (!query->next()) + return -1; + return query->value(0).toInt(); + } + return -1; } bool Servatrice_DatabaseInterface::isInBuddyList(const QString &whoseList, const QString &who) { - if (server->getAuthenticationMethod() == Servatrice::AuthenticationNone) - return false; - - if (!checkSql()) - return false; - - int id1 = getUserIdInDB(whoseList); - int id2 = getUserIdInDB(who); - - QSqlQuery query(sqlDatabase); - query.prepare("select 1 from " + server->getDbPrefix() + "_buddylist where id_user1 = :id_user1 and id_user2 = :id_user2"); - query.bindValue(":id_user1", id1); - query.bindValue(":id_user2", id2); - if (!execSqlQuery(query)) - return false; - return query.next(); + if (server->getAuthenticationMethod() == Servatrice::AuthenticationNone) + return false; + + if (!checkSql()) + return false; + + int id1 = getUserIdInDB(whoseList); + int id2 = getUserIdInDB(who); + + QSqlQuery *query = + prepareQuery("select 1 from {prefix}_buddylist where id_user1 = :id_user1 and id_user2 = :id_user2"); + query->bindValue(":id_user1", id1); + query->bindValue(":id_user2", id2); + if (!execSqlQuery(query)) + return false; + return query->next(); } bool Servatrice_DatabaseInterface::isInIgnoreList(const QString &whoseList, const QString &who) { - if (server->getAuthenticationMethod() == Servatrice::AuthenticationNone) - return false; - - if (!checkSql()) - return false; - - int id1 = getUserIdInDB(whoseList); - int id2 = getUserIdInDB(who); - - QSqlQuery query(sqlDatabase); - query.prepare("select 1 from " + server->getDbPrefix() + "_ignorelist where id_user1 = :id_user1 and id_user2 = :id_user2"); - query.bindValue(":id_user1", id1); - query.bindValue(":id_user2", id2); - if (!execSqlQuery(query)) - return false; - return query.next(); + if (server->getAuthenticationMethod() == Servatrice::AuthenticationNone) + return false; + + if (!checkSql()) + return false; + + int id1 = getUserIdInDB(whoseList); + int id2 = getUserIdInDB(who); + + QSqlQuery *query = + prepareQuery("select 1 from {prefix}_ignorelist where id_user1 = :id_user1 and id_user2 = :id_user2"); + query->bindValue(":id_user1", id1); + query->bindValue(":id_user2", id2); + if (!execSqlQuery(query)) + return false; + return query->next(); } -ServerInfo_User Servatrice_DatabaseInterface::evalUserQueryResult(const QSqlQuery &query, bool complete, bool withId) +ServerInfo_User Servatrice_DatabaseInterface::evalUserQueryResult(const QSqlQuery *query, bool complete, bool withId) { - ServerInfo_User result; - - if (withId) - result.set_id(query.value(0).toInt()); - result.set_name(query.value(1).toString().toStdString()); - - const QString country = query.value(5).toString(); - if (!country.isEmpty()) - result.set_country(country.toStdString()); - - if (complete) { - const QByteArray avatarBmp = query.value(6).toByteArray(); - if (avatarBmp.size()) - result.set_avatar_bmp(avatarBmp.data(), avatarBmp.size()); - } - - const QString genderStr = query.value(4).toString(); - if (genderStr == "m") - result.set_gender(ServerInfo_User::Male); - else if (genderStr == "f") - result.set_gender(ServerInfo_User::Female); - - const int is_admin = query.value(2).toInt(); - int userLevel = ServerInfo_User::IsUser | ServerInfo_User::IsRegistered; - if (is_admin == 1) - userLevel |= ServerInfo_User::IsAdmin | ServerInfo_User::IsModerator; - else if (is_admin == 2) - userLevel |= ServerInfo_User::IsModerator; - result.set_user_level(userLevel); - - const QString realName = query.value(3).toString(); - if (!realName.isEmpty()) - result.set_real_name(realName.toStdString()); - - return result; + ServerInfo_User result; + + if (withId) + result.set_id(query->value(0).toInt()); + result.set_name(query->value(1).toString().toStdString()); + + const int is_admin = query->value(2).toInt(); + int userLevel = ServerInfo_User::IsUser | ServerInfo_User::IsRegistered; + if (is_admin & 1) + userLevel |= ServerInfo_User::IsAdmin | ServerInfo_User::IsModerator; + else if (is_admin & 2) + userLevel |= ServerInfo_User::IsModerator; + + if (is_admin & 4) + userLevel |= ServerInfo_User::IsJudge; + + result.set_user_level(userLevel); + + const QString country = query->value(3).toString(); + if (!country.isEmpty()) + result.set_country(country.toStdString()); + + const QString privlevel = query->value(4).toString(); + if (!privlevel.isEmpty()) + result.set_privlevel(privlevel.toStdString()); + + const auto &pawn_left_override = query->value(5).toString(); + const auto &pawn_right_override = query->value(6).toString(); + if (!pawn_left_override.isEmpty()) { + result.mutable_pawn_colors()->set_left_side(pawn_left_override.toStdString()); + } + if (!pawn_right_override.isEmpty()) { + result.mutable_pawn_colors()->set_right_side(pawn_right_override.toStdString()); + } + + if (complete) { + const QString realName = query->value(7).toString(); + if (!realName.isEmpty()) + result.set_real_name(realName.toStdString()); + + const QByteArray avatarBmp = query->value(8).toByteArray(); + if (avatarBmp.size()) + result.set_avatar_bmp(avatarBmp.data(), avatarBmp.size()); + + const QDateTime regDate = query->value(9).toDateTime(); + if (!regDate.toString(Qt::ISODate).isEmpty()) { + // the registration date is in utc + qint64 accountAgeInSeconds = regDate.secsTo(QDateTime::currentDateTimeUtc()); + result.set_accountage_secs(accountAgeInSeconds); + } + + const QString email = query->value(10).toString(); + if (!email.isEmpty()) + result.set_email(email.toStdString()); + + const QString clientid = query->value(11).toString(); + if (!clientid.isEmpty()) + result.set_clientid(clientid.toStdString()); + } + return result; } ServerInfo_User Servatrice_DatabaseInterface::getUserData(const QString &name, bool withId) { - ServerInfo_User result; - result.set_name(name.toStdString()); - result.set_user_level(ServerInfo_User::IsUser); - - if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) { - if (!checkSql()) - return result; - - QSqlQuery query(sqlDatabase); - query.prepare("select id, name, admin, realname, gender, country, avatar_bmp from " + server->getDbPrefix() + "_users where name = :name and active = 1"); - query.bindValue(":name", name); - if (!execSqlQuery(query)) - return result; - - if (query.next()) - return evalUserQueryResult(query, true, withId); - else - return result; - } else - return result; + ServerInfo_User result; + result.set_name(name.toStdString()); + result.set_user_level(ServerInfo_User::IsUser); + + if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) { + if (!checkSql()) + return result; + + QSqlQuery *query = prepareQuery("select id, name, admin, country, privlevel, leftPawnColorOverride, " + "rightPawnColorOverride, realname, avatar_bmp, registrationDate, " + "email, clientid from {prefix}_users where " + "name = :name and active = 1"); + query->bindValue(":name", name); + if (!execSqlQuery(query)) + return result; + + if (query->next()) + return evalUserQueryResult(query, true, withId); + else + return result; + } else + return result; } void Servatrice_DatabaseInterface::clearSessionTables() { - lockSessionTables(); - QSqlQuery query(sqlDatabase); - query.prepare("update " + server->getDbPrefix() + "_sessions set end_time=now() where end_time is null and id_server = :id_server"); - query.bindValue(":id_server", server->getServerId()); - query.exec(); - unlockSessionTables(); + lockSessionTables(); + QSqlQuery *query = + prepareQuery("update {prefix}_sessions set end_time=now() where end_time is null and id_server = :id_server"); + query->bindValue(":id_server", server->getServerID()); + execSqlQuery(query); + unlockSessionTables(); } void Servatrice_DatabaseInterface::lockSessionTables() { - QSqlQuery("lock tables " + server->getDbPrefix() + "_sessions write, " + server->getDbPrefix() + "_users read", sqlDatabase).exec(); + QSqlQuery *query = prepareQuery("lock tables {prefix}_sessions write, {prefix}_users read"); + execSqlQuery(query); } void Servatrice_DatabaseInterface::unlockSessionTables() { - QSqlQuery("unlock tables", sqlDatabase).exec(); + QSqlQuery *query = prepareQuery("unlock tables"); + execSqlQuery(query); } bool Servatrice_DatabaseInterface::userSessionExists(const QString &userName) { - // Call only after lockSessionTables(). - - QSqlQuery query(sqlDatabase); - query.prepare("select 1 from " + server->getDbPrefix() + "_sessions where user_name = :user_name and end_time is null"); - query.bindValue(":user_name", userName); - query.exec(); - return query.next(); + // Call only after lockSessionTables(). + + QSqlQuery *query = prepareQuery( + "select 1 from {prefix}_sessions where user_name = :user_name and id_server = :id_server and end_time is null"); + query->bindValue(":id_server", server->getServerID()); + query->bindValue(":user_name", userName); + if (!execSqlQuery(query)) { + return false; + }; + return query->next(); } -qint64 Servatrice_DatabaseInterface::startSession(const QString &userName, const QString &address) +qint64 Servatrice_DatabaseInterface::startSession(const QString &userName, + const QString &address, + const QString &clientId, + const QString &connectionType) { - if (server->getAuthenticationMethod() == Servatrice::AuthenticationNone) - return -1; - - if (!checkSql()) - return -1; - - QSqlQuery query(sqlDatabase); - query.prepare("insert into " + server->getDbPrefix() + "_sessions (user_name, id_server, ip_address, start_time) values(:user_name, :id_server, :ip_address, NOW())"); - query.bindValue(":user_name", userName); - query.bindValue(":id_server", server->getServerId()); - query.bindValue(":ip_address", address); - if (execSqlQuery(query)) - return query.lastInsertId().toInt(); - return -1; + if (server->getAuthenticationMethod() == Servatrice::AuthenticationNone) + return -1; + + if (!checkSql()) + return -1; + + QSqlQuery *query = prepareQuery("insert into {prefix}_sessions (user_name, id_server, ip_address, start_time, " + "clientid, connection_type) values(:user_name, :id_server, :ip_address, NOW(), " + ":client_id, :connection_type)"); + query->bindValue(":user_name", userName); + query->bindValue(":id_server", server->getServerID()); + query->bindValue(":ip_address", address); + query->bindValue(":client_id", clientId); + query->bindValue(":connection_type", connectionType); + if (execSqlQuery(query)) + return query->lastInsertId().toInt(); + return -1; } void Servatrice_DatabaseInterface::endSession(qint64 sessionId) { - if (server->getAuthenticationMethod() == Servatrice::AuthenticationNone) - return; - - if (!checkSql()) - return; - - QSqlQuery query(sqlDatabase); - query.exec("lock tables " + server->getDbPrefix() + "_sessions write"); - query.prepare("update " + server->getDbPrefix() + "_sessions set end_time=NOW() where id = :id_session"); - query.bindValue(":id_session", sessionId); - execSqlQuery(query); - query.exec("unlock tables"); + if (server->getAuthenticationMethod() == Servatrice::AuthenticationNone) + return; + + if (!checkSql()) + return; + + auto *query = prepareQuery("update {prefix}_sessions set end_time=NOW() where id = :id_session"); + query->bindValue(":id_session", sessionId); + execSqlQuery(query); } QMap Servatrice_DatabaseInterface::getBuddyList(const QString &name) { - QMap result; - - if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) { - checkSql(); + QMap result; - QSqlQuery query(sqlDatabase); - query.prepare("select a.id, a.name, a.admin, a.realname, a.gender, a.country from " + server->getDbPrefix() + "_users a left join " + server->getDbPrefix() + "_buddylist b on a.id = b.id_user2 left join " + server->getDbPrefix() + "_users c on b.id_user1 = c.id where c.name = :name"); - query.bindValue(":name", name); - if (!execSqlQuery(query)) - return result; - - while (query.next()) { - const ServerInfo_User &temp = evalUserQueryResult(query, false); - result.insert(QString::fromStdString(temp.name()), temp); - } - } - return result; + if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) { + checkSql(); + + QSqlQuery *query = prepareQuery("select a.id, a.name, a.admin, a.country, a.privlevel, " + "a.leftPawnColorOverride, a.rightPawnColorOverride from {prefix}_users a " + "left join {prefix}_buddylist b on a.id = b.id_user2 left join {prefix}_users " + "c on b.id_user1 = c.id where c.name = :name"); + query->bindValue(":name", name); + if (!execSqlQuery(query)) + return result; + + while (query->next()) { + const ServerInfo_User &temp = evalUserQueryResult(query, false); + result.insert(QString::fromStdString(temp.name()), temp); + } + } + return result; } QMap Servatrice_DatabaseInterface::getIgnoreList(const QString &name) { - QMap result; - - if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) { - checkSql(); + QMap result; - QSqlQuery query(sqlDatabase); - query.prepare("select a.id, a.name, a.admin, a.realname, a.gender, a.country from " + server->getDbPrefix() + "_users a left join " + server->getDbPrefix() + "_ignorelist b on a.id = b.id_user2 left join " + server->getDbPrefix() + "_users c on b.id_user1 = c.id where c.name = :name"); - query.bindValue(":name", name); - if (!execSqlQuery(query)) - return result; - - while (query.next()) { - ServerInfo_User temp = evalUserQueryResult(query, false); - result.insert(QString::fromStdString(temp.name()), temp); - } - } - return result; + if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) { + checkSql(); + + QSqlQuery *query = prepareQuery("select a.id, a.name, a.admin, a.country, a.privlevel, " + "a.leftPawnColorOverride, a.rightPawnColorOverride from {prefix}_users a " + "left join {prefix}_ignorelist b on a.id = b.id_user2 left join {prefix}_users " + "c on b.id_user1 = c.id where c.name = :name"); + query->bindValue(":name", name); + if (!execSqlQuery(query)) + return result; + + while (query->next()) { + ServerInfo_User temp = evalUserQueryResult(query, false); + result.insert(QString::fromStdString(temp.name()), temp); + } + } + return result; } int Servatrice_DatabaseInterface::getNextGameId() { - if (!sqlDatabase.isValid()) - return server->getNextLocalGameId(); - - if (!checkSql()) - return -1; - - QSqlQuery query(sqlDatabase); - query.prepare("insert into " + server->getDbPrefix() + "_games (time_started) values (now())"); - execSqlQuery(query); - - return query.lastInsertId().toInt(); + if (!sqlDatabase.isValid()) + return server->getNextLocalGameId(); + + if (!checkSql()) + return -1; + + QSqlQuery *query = prepareQuery("insert into {prefix}_games (time_started) values (now())"); + + if (!execSqlQuery(query)) { + return -1; + } + + return query->lastInsertId().toInt(); } int Servatrice_DatabaseInterface::getNextReplayId() { - if (!checkSql()) - return -1; - - QSqlQuery query(sqlDatabase); - query.prepare("insert into " + server->getDbPrefix() + "_replays () values ()"); - execSqlQuery(query); - - return query.lastInsertId().toInt(); + if (!checkSql()) + return -1; + + QSqlQuery *query = prepareQuery("insert into {prefix}_replays (id_game) values (NULL)"); + + if (!execSqlQuery(query)) { + return -1; + } + + return query->lastInsertId().toInt(); } -void Servatrice_DatabaseInterface::storeGameInformation(const QString &roomName, const QStringList &roomGameTypes, const ServerInfo_Game &gameInfo, const QSet &allPlayersEver, const QSet &allSpectatorsEver, const QList &replayList) +void Servatrice_DatabaseInterface::storeGameInformation(const QString &roomName, + const QStringList &roomGameTypes, + const ServerInfo_Game &gameInfo, + const QSet &allPlayersEver, + const QSet &allSpectatorsEver, + const QList &replayList) { - if (!checkSql()) - return; - - QVariantList gameIds1, playerNames, gameIds2, userIds, replayNames; - QSetIterator playerIterator(allPlayersEver); - while (playerIterator.hasNext()) { - gameIds1.append(gameInfo.game_id()); - const QString &playerName = playerIterator.next(); - playerNames.append(playerName); - } - QSet allUsersInGame = allPlayersEver + allSpectatorsEver; - QSetIterator allUsersIterator(allUsersInGame); - while (allUsersIterator.hasNext()) { - int id = getUserIdInDB(allUsersIterator.next()); - if (id == -1) - continue; - gameIds2.append(gameInfo.game_id()); - userIds.append(id); - replayNames.append(QString::fromStdString(gameInfo.description())); - } - - QVariantList replayIds, replayGameIds, replayDurations, replayBlobs; - for (int i = 0; i < replayList.size(); ++i) { - QByteArray blob; - const unsigned int size = replayList[i]->ByteSize(); - blob.resize(size); - replayList[i]->SerializeToArray(blob.data(), size); - - replayIds.append(QVariant((qulonglong) replayList[i]->replay_id())); - replayGameIds.append(gameInfo.game_id()); - replayDurations.append(replayList[i]->duration_seconds()); - replayBlobs.append(blob); - } - - { - QSqlQuery query(sqlDatabase); - query.prepare("update " + server->getDbPrefix() + "_games set room_name=:room_name, descr=:descr, creator_name=:creator_name, password=:password, game_types=:game_types, player_count=:player_count, time_finished=now() where id=:id_game"); - query.bindValue(":room_name", roomName); - query.bindValue(":id_game", gameInfo.game_id()); - query.bindValue(":descr", QString::fromStdString(gameInfo.description())); - query.bindValue(":creator_name", QString::fromStdString(gameInfo.creator_info().name())); - query.bindValue(":password", gameInfo.with_password() ? 1 : 0); - query.bindValue(":game_types", roomGameTypes.isEmpty() ? QString("") : roomGameTypes.join(", ")); - query.bindValue(":player_count", gameInfo.max_players()); - if (!execSqlQuery(query)) - return; - } - { - QSqlQuery query(sqlDatabase); - query.prepare("insert into " + server->getDbPrefix() + "_games_players (id_game, player_name) values (:id_game, :player_name)"); - query.bindValue(":id_game", gameIds1); - query.bindValue(":player_name", playerNames); - query.execBatch(); - } - { - QSqlQuery query(sqlDatabase); - query.prepare("update " + server->getDbPrefix() + "_replays set id_game=:id_game, duration=:duration, replay=:replay where id=:id_replay"); - query.bindValue(":id_replay", replayIds); - query.bindValue(":id_game", replayGameIds); - query.bindValue(":duration", replayDurations); - query.bindValue(":replay", replayBlobs); - query.execBatch(); - } - { - QSqlQuery query(sqlDatabase); - query.prepare("insert into " + server->getDbPrefix() + "_replays_access (id_game, id_player, replay_name) values (:id_game, :id_player, :replay_name)"); - query.bindValue(":id_game", gameIds2); - query.bindValue(":id_player", userIds); - query.bindValue(":replay_name", replayNames); - query.execBatch(); - } + if (!checkSql()) + return; + + if (!settingsCache->value("game/store_replays", 1).toBool()) + return; + + QVariantList gameIds1, playerNames, gameIds2, userIds, replayNames; + QSetIterator playerIterator(allPlayersEver); + while (playerIterator.hasNext()) { + gameIds1.append(gameInfo.game_id()); + const QString &playerName = playerIterator.next(); + playerNames.append(playerName); + } + QSet allUsersInGame = allPlayersEver + allSpectatorsEver; + QSetIterator allUsersIterator(allUsersInGame); + while (allUsersIterator.hasNext()) { + int id = getUserIdInDB(allUsersIterator.next()); + if (id == -1) + continue; + gameIds2.append(gameInfo.game_id()); + userIds.append(id); + replayNames.append(QString::fromStdString(gameInfo.description())); + } + + QVariantList replayIds, replayGameIds, replayDurations, replayBlobs; + for (int i = 0; i < replayList.size(); ++i) { + QByteArray blob; +#if GOOGLE_PROTOBUF_VERSION > 3001000 + const unsigned int size = static_cast(replayList[i]->ByteSizeLong()); +#else + const unsigned int size = static_cast(replayList[i]->ByteSize()); +#endif + blob.resize(size); + qulonglong replayId = replayList[i]->replay_id(); + if (replayList[i]->SerializeToArray(blob.data(), size)) { + + replayIds.append(QVariant(replayId)); + replayGameIds.append(gameInfo.game_id()); + replayDurations.append(replayList[i]->duration_seconds()); + replayBlobs.append(blob); + } else { + qCWarning(DatabaseInterfaceLog) + << "failed to serialise replay, id:" << replayId << "game:" << gameInfo.game_id(); + } + } + + { + QSqlQuery *query = prepareQuery("update {prefix}_games set room_name=:room_name, descr=:descr, " + "creator_name=:creator_name, password=:password, game_types=:game_types, " + "player_count=:player_count, time_finished=now() where id=:id_game"); + query->bindValue(":room_name", roomName); + query->bindValue(":id_game", gameInfo.game_id()); + query->bindValue(":descr", QString::fromStdString(gameInfo.description())); + query->bindValue(":creator_name", QString::fromStdString(gameInfo.creator_info().name())); + query->bindValue(":password", gameInfo.with_password() ? 1 : 0); + query->bindValue(":game_types", roomGameTypes.isEmpty() ? QString("") : roomGameTypes.join(", ")); + query->bindValue(":player_count", gameInfo.max_players()); + if (!execSqlQuery(query)) + return; + } + { + QSqlQuery *query = + prepareQuery("insert into {prefix}_games_players (id_game, player_name) values (:id_game, :player_name)"); + query->bindValue(":id_game", gameIds1); + query->bindValue(":player_name", playerNames); + query->execBatch(); + } + { + QSqlQuery *query = prepareQuery( + "update {prefix}_replays set id_game=:id_game, duration=:duration, replay=:replay where id=:id_replay"); + query->bindValue(":id_replay", replayIds); + query->bindValue(":id_game", replayGameIds); + query->bindValue(":duration", replayDurations); + query->bindValue(":replay", replayBlobs); + query->execBatch(); + } + { + QSqlQuery *query = prepareQuery("insert into {prefix}_replays_access (id_game, id_player, replay_name) values " + "(:id_game, :id_player, :replay_name)"); + query->bindValue(":id_game", gameIds2); + query->bindValue(":id_player", userIds); + query->bindValue(":replay_name", replayNames); + query->execBatch(); + } } DeckList *Servatrice_DatabaseInterface::getDeckFromDatabase(int deckId, int userId) { - checkSql(); - - QSqlQuery query(sqlDatabase); - - query.prepare("select content from " + server->getDbPrefix() + "_decklist_files where id = :id and id_user = :id_user"); - query.bindValue(":id", deckId); - query.bindValue(":id_user", userId); - execSqlQuery(query); - if (!query.next()) - throw Response::RespNameNotFound; - - DeckList *deck = new DeckList; - deck->loadFromString_Native(query.value(0).toString()); - - return deck; + checkSql(); + + QSqlQuery *query = + prepareQuery("select content from {prefix}_decklist_files where id = :id and id_user = :id_user"); + query->bindValue(":id", deckId); + query->bindValue(":id_user", userId); + execSqlQuery(query); + if (!query->next()) + throw Response::RespNameNotFound; + + DeckList *deck = new DeckList; + deck->loadFromString_Native(query->value(0).toString()); + + return deck; +} + +void Servatrice_DatabaseInterface::logMessage(const int senderId, + const QString &senderName, + const QString &senderIp, + const QString &logMessage, + LogMessage_TargetType targetType, + const int targetId, + const QString &targetName) +{ + QString targetTypeString; + switch (targetType) { + case MessageTargetRoom: + if (!settingsCache->value("logging/log_user_msg_room", 0).toBool()) + return; + targetTypeString = "room"; + break; + case MessageTargetGame: + if (!settingsCache->value("logging/log_user_msg_game", 0).toBool()) + return; + targetTypeString = "game"; + break; + case MessageTargetChat: + if (!settingsCache->value("logging/log_user_msg_chat", 0).toBool()) + return; + targetTypeString = "chat"; + break; + case MessageTargetIslRoom: + if (!settingsCache->value("logging/log_user_msg_isl", 0).toBool()) + return; + targetTypeString = "room"; + break; + default: + return; + } + + QSqlQuery *query = prepareQuery("insert into {prefix}_log (log_time, sender_id, sender_name, sender_ip, " + "log_message, target_type, target_id, target_name) values (now(), :sender_id, " + ":sender_name, :sender_ip, :log_message, :target_type, :target_id, :target_name)"); + query->bindValue(":sender_id", senderId < 1 ? QVariant() : senderId); + query->bindValue(":sender_name", senderName); + query->bindValue(":sender_ip", senderIp); + query->bindValue(":log_message", logMessage); + query->bindValue(":target_type", targetTypeString); + query->bindValue(":target_id", (targetType == MessageTargetChat && targetId < 1) ? QVariant() : targetId); + query->bindValue(":target_name", targetName); + execSqlQuery(query); +} + +bool Servatrice_DatabaseInterface::changeUserPassword(const QString &user, + const QString &password, + bool passwordNeedsHash) +{ + QString passwordSha512 = password; + if (passwordNeedsHash) { + passwordSha512 = PasswordHasher::computeHash(password, PasswordHasher::generateRandomSalt()); + } + + QSqlQuery *passwordQuery = prepareQuery("update {prefix}_users set password_sha512=:password, " + "passwordLastChangedDate = NOW() where name = :name"); + passwordQuery->bindValue(":password", passwordSha512); + passwordQuery->bindValue(":name", user); + if (execSqlQuery(passwordQuery)) + return true; + + return false; +} + +bool Servatrice_DatabaseInterface::changeUserPassword(const QString &user, + const QString &oldPassword, + bool oldPasswordNeedsHash, + const QString &newPassword, + bool newPasswordNeedsHash) +{ + if (server->getAuthenticationMethod() != Servatrice::AuthenticationSql) + return false; + + if (!checkSql()) + return false; + + QString error; + if (!usernameIsValid(user, error)) + return false; + + QSqlQuery *passwordQuery = prepareQuery("select password_sha512 from {prefix}_users where name = :name"); + passwordQuery->bindValue(":name", user); + + if (!execSqlQuery(passwordQuery)) { + qCWarning(DatabaseInterfaceLog) << "Change password denied: SQL error"; + return false; + } + + if (!passwordQuery->next()) + return false; + + const QString correctPasswordSha512 = passwordQuery->value(0).toString(); + QString oldPasswordSha512 = oldPassword; + if (oldPasswordNeedsHash) { + QString salt = correctPasswordSha512.left(16); + oldPasswordSha512 = PasswordHasher::computeHash(oldPassword, salt); + } + if (correctPasswordSha512 != oldPasswordSha512) + return false; + + return changeUserPassword(user, newPassword, newPasswordNeedsHash); +} + +int Servatrice_DatabaseInterface::getActiveUserCount(QString connectionType) +{ + int userCount = 0; + + if (!checkSql()) + return userCount; + + QString text = "select count(*) from {prefix}_sessions where id_server = :serverid AND end_time is NULL"; + if (!connectionType.isEmpty()) + text += " AND connection_type = :connection_type"; + QSqlQuery *query = prepareQuery(text); + + query->bindValue(":serverid", server->getServerID()); + if (!connectionType.isEmpty()) + query->bindValue(":connection_type", connectionType); + + if (!execSqlQuery(query)) + return userCount; + + if (query->next()) + userCount = query->value(0).toInt(); + + return userCount; +} + +void Servatrice_DatabaseInterface::updateUsersClientID(const QString &userName, const QString &userClientID) +{ + + if (!checkSql()) + return; + + QSqlQuery *query = prepareQuery("update {prefix}_users set clientid = :clientid where name = :username"); + query->bindValue(":clientid", userClientID); + query->bindValue(":username", userName); + execSqlQuery(query); +} + +void Servatrice_DatabaseInterface::updateUsersLastLoginData(const QString &userName, const QString &clientVersion) +{ + + if (!checkSql()) + return; + + int usersID = 0; + + QSqlQuery *query = prepareQuery("select id from {prefix}_users where name = :user_name"); + query->bindValue(":user_name", userName); + if (!execSqlQuery(query)) { + qCWarning(DatabaseInterfaceLog) << "Failed to locate user id when updating users last login data: SQL Error"; + return; + } + + if (query->next()) { + usersID = query->value(0).toInt(); + } + + if (usersID) { + int userCount = 0; + query = prepareQuery("select count(id) from {prefix}_user_analytics where id = :user_id"); + query->bindValue(":user_id", usersID); + if (!execSqlQuery(query)) + return; + + if (query->next()) { + userCount = query->value(0).toInt(); + } + + if (!userCount) { + query = prepareQuery( + "insert into {prefix}_user_analytics (id,client_ver,last_login) values (:user_id,:client_ver,NOW())"); + query->bindValue(":user_id", usersID); + query->bindValue(":client_ver", clientVersion); + execSqlQuery(query); + } else { + query = prepareQuery( + "update {prefix}_user_analytics set last_login = NOW(), client_ver = :client_ver where id = :user_id"); + query->bindValue(":client_ver", clientVersion); + query->bindValue(":user_id", usersID); + execSqlQuery(query); + } + } +} + +QList Servatrice_DatabaseInterface::getUserBanHistory(const QString userName) +{ + QList results; + ServerInfo_Ban banDetails; + + if (!checkSql()) + return results; + + QSqlQuery *query = + prepareQuery("SELECT A.id_admin, A.time_from, A.minutes, A.reason, A.visible_reason, B.name AS name_admin FROM " + "{prefix}_bans A LEFT JOIN {prefix}_users B ON A.id_admin=B.id WHERE A.user_name = :user_name"); + query->bindValue(":user_name", userName); + + if (!execSqlQuery(query)) { + qCWarning(DatabaseInterfaceLog) << "Failed to collect ban history information: SQL Error"; + return results; + } + + while (query->next()) { + banDetails.set_admin_id(QString(query->value(0).toString()).toStdString()); + banDetails.set_admin_name(QString(query->value(5).toString()).toStdString()); + banDetails.set_ban_time(QString(query->value(1).toString()).toStdString()); + banDetails.set_ban_length(QString(query->value(2).toString()).toStdString()); + banDetails.set_ban_reason(QString(query->value(3).toString()).toStdString()); + banDetails.set_visible_reason(QString(query->value(4).toString()).toStdString()); + results << banDetails; + } + + return results; +} + +bool Servatrice_DatabaseInterface::addWarning(const QString userName, + const QString adminName, + const QString warningReason, + const QString clientID) +{ + if (!checkSql()) + return false; + + int userID = getUserIdInDB(userName); + QSqlQuery *query = + prepareQuery("insert into {prefix}_warnings (user_id,user_name,mod_name,reason,time_of,clientid) values " + "(:user_id,:user_name,:mod_name,:warn_reason,NOW(),:client_id)"); + query->bindValue(":user_id", userID); + query->bindValue(":user_name", userName); + query->bindValue(":mod_name", adminName); + query->bindValue(":warn_reason", warningReason); + query->bindValue(":client_id", clientID); + if (!execSqlQuery(query)) { + qCWarning(DatabaseInterfaceLog) << "Failed to collect create warning history information: SQL Error"; + return false; + } + + return true; +} + +QList Servatrice_DatabaseInterface::getUserWarnHistory(const QString userName) +{ + QList results; + ServerInfo_Warning warnDetails; + + if (!checkSql()) + return results; + + int userID = getUserIdInDB(userName); + QSqlQuery *query = + prepareQuery("SELECT user_name, mod_name, reason, time_of FROM {prefix}_warnings WHERE user_id = :user_id"); + query->bindValue(":user_id", userID); + + if (!execSqlQuery(query)) { + qCWarning(DatabaseInterfaceLog) << "Failed to collect warning history information: SQL Error"; + return results; + } + + while (query->next()) { + warnDetails.set_user_name(QString(query->value(0).toString()).toStdString()); + warnDetails.set_admin_name(QString(query->value(1).toString()).toStdString()); + warnDetails.set_reason(QString(query->value(2).toString()).toStdString()); + warnDetails.set_time_of(QString(query->value(3).toString()).toStdString()); + results << warnDetails; + } + + return results; +} + +QList Servatrice_DatabaseInterface::getMessageLogHistory(const QString &user, + const QString &ipaddress, + const QString &gamename, + const QString &gameid, + const QString &message, + bool &chat, + bool &game, + bool &room, + int &range, + int &maxresults) +{ + + QList results; + ServerInfo_ChatMessage chatMessage; + + if (!checkSql()) + return results; + + if (user.isEmpty() && ipaddress.isEmpty() && gameid.isEmpty() && gamename.isEmpty()) { + // To ensure quick results and minimal lag, require an indexed field + return results; + } + + // BUILD QUERY STRING BASED ON PASSED IN VALUES + QString queryString = "SELECT * FROM {prefix}_log WHERE `sender_ip` IS NOT NULL"; + if (!user.isEmpty()) + queryString.append(" AND (`sender_name` = :user_name OR `target_name` = :user_name)"); + + if (!ipaddress.isEmpty()) + queryString.append(" AND `sender_ip` = :ip_to_find"); + + if (!gameid.isEmpty()) + queryString.append(" AND (`target_id` = :game_id AND `target_type` = 'game')"); + + if (!gamename.isEmpty()) + queryString.append(" AND (`target_name` = :game_name AND `target_type` = 'game')"); + + if (!message.isEmpty()) + queryString.append(" AND `log_message` LIKE :log_message"); + + if (chat || game || room) { + queryString.append(" AND ("); + + if (chat) + queryString.append("`target_type` = 'chat'"); + + if (game) { + if (chat) + queryString.append(" OR `target_type` = 'game'"); + else + queryString.append("`target_type` = 'game'"); + } + + if (room) { + if (game || chat) + queryString.append(" OR `target_type` = 'room'"); + else + queryString.append("`target_type` = 'room'"); + } + queryString.append(")"); + } + + if (range) + queryString.append(" AND log_time >= DATE_SUB(now(), INTERVAL :range_time HOUR)"); + + if (maxresults) + queryString.append(" LIMIT :limit_size"); + + QSqlQuery *query = prepareQuery(queryString); + if (!user.isEmpty()) { + query->bindValue(":user_name", user); + } + if (!ipaddress.isEmpty()) { + query->bindValue(":ip_to_find", ipaddress); + } + if (!gameid.isEmpty()) { + query->bindValue(":game_id", gameid); + } + if (!gamename.isEmpty()) { + query->bindValue(":game_name", gamename); + } + if (!message.isEmpty()) { + query->bindValue(":log_message", message); + } + if (range) { + query->bindValue(":range_time", range); + } + if (maxresults) { + query->bindValue(":limit_size", maxresults); + } + + if (!execSqlQuery(query)) { + qCWarning(DatabaseInterfaceLog) << "Failed to collect log history information: SQL Error"; + return results; + } + + while (query->next()) { + chatMessage.set_time(QString(query->value(0).toString()).toStdString()); + chatMessage.set_sender_id(QString(query->value(1).toString()).toStdString()); + chatMessage.set_sender_name(QString(query->value(2).toString()).toStdString()); + chatMessage.set_sender_ip(QString(query->value(3).toString()).toStdString()); + chatMessage.set_message(QString(query->value(4).toString()).toStdString()); + chatMessage.set_target_type(QString(query->value(5).toString()).toStdString()); + chatMessage.set_target_id(QString(query->value(6).toString()).toStdString()); + chatMessage.set_target_name(QString(query->value(7).toString()).toStdString()); + results << chatMessage; + } + + return results; +} + +int Servatrice_DatabaseInterface::checkNumberOfUserAccounts(const QString &email) +{ + if (!checkSql()) + return 0; + + QSqlQuery *query = prepareQuery("SELECT count(email) FROM {prefix}_users WHERE email = :user_email"); + query->bindValue(":user_email", email); + + if (!execSqlQuery(query)) { + qCWarning(DatabaseInterfaceLog) + << "Failed to identify the number of users accounts for users email address: SQL Error"; + return 0; + } + + if (query->next()) + return query->value(0).toInt(); + + return 0; +} + +bool Servatrice_DatabaseInterface::addForgotPassword(const QString &user) +{ + if (!checkSql()) + return false; + + if (!updateUserToken(PasswordHasher::generateActivationToken(), user)) + return false; + + QSqlQuery *query = prepareQuery("insert into {prefix}_forgot_password (name,requestDate) values (:username,NOW())"); + query->bindValue(":username", user); + if (execSqlQuery(query)) + return true; + + return false; +} + +bool Servatrice_DatabaseInterface::removeForgotPassword(const QString &user) +{ + if (!checkSql()) + return false; + + QSqlQuery *query = prepareQuery("delete from {prefix}_forgot_password where name = :username"); + query->bindValue(":username", user); + if (execSqlQuery(query)) + return true; + + return false; +} + +bool Servatrice_DatabaseInterface::doesForgotPasswordExist(const QString &user) +{ + if (!checkSql()) + return false; + + QSqlQuery *query = prepareQuery("select count(name) from {prefix}_forgot_password where name = :user_name AND " + "requestDate > (now() - interval :minutes minute)"); + query->bindValue(":user_name", user); + query->bindValue(":minutes", QString::number(server->getForgotPasswordTokenLife())); + + if (!execSqlQuery(query)) + return false; + + if (query->next()) + if (query->value("count(name)").toInt() > 0) + return true; + + return false; +} + +bool Servatrice_DatabaseInterface::updateUserToken(const QString &token, const QString &user) +{ + if (!checkSql()) + return false; + + if (token.isEmpty() || user.isEmpty()) + return false; + + QSqlQuery *query = prepareQuery("update {prefix}_users set token = :token where name = :user_name"); + query->bindValue(":user_name", user); + query->bindValue(":token", token); + + if (execSqlQuery(query)) + return true; + + return false; +} + +bool Servatrice_DatabaseInterface::validateTableColumnStringData(const QString &table, + const QString &column, + const QString &_user, + const QString &_datatocheck) +{ + if (!checkSql()) + return false; + + if (table.isEmpty() || column.isEmpty() || _user.isEmpty() || _datatocheck.isEmpty()) + return false; + + QString formatedQuery = QString("select %1 from %2 where name = :user_name").arg(column).arg(table); + QSqlQuery *query = prepareQuery(formatedQuery); + query->bindValue(":user_name", _user); + + if (!execSqlQuery(query)) + return false; + + if (query->next()) + if (query->value(column).toString().toLower() == _datatocheck.toLower()) + return true; + + return false; +} + +void Servatrice_DatabaseInterface::addAuditRecord(const QString &user, + const QString &ipaddress, + const QString &clientid, + const QString &action, + const QString &details, + const bool &results = false) +{ + if (!checkSql()) + return; + + if (!server->getEnableAudit()) + return; + + if (user.isEmpty() || ipaddress.isEmpty() || clientid.isEmpty() || action.isEmpty()) + return; + + QSqlQuery *query = prepareQuery("insert into {prefix}_audit " + "(id_server,name,ip_address,clientid,incidentDate,action,results,details) values " + "(:idserver,:username,:ipaddress,:clientid,NOW(),:action,:results,:details)"); + query->bindValue(":idserver", server->getServerID()); + query->bindValue(":username", user); + query->bindValue(":ipaddress", ipaddress); + query->bindValue(":clientid", clientid); + query->bindValue(":action", action); + query->bindValue(":results", results ? "success" : "fail"); + + query->bindValue(":details", details); + execSqlQuery(query); } diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index e1d6d813d..68080404c 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -1,56 +1,152 @@ #ifndef SERVATRICE_DATABASE_INTERFACE_H #define SERVATRICE_DATABASE_INTERFACE_H +#include +#include #include #include +#include +#include +#include +#include -#include "server.h" -#include "server_database_interface.h" +#define DATABASE_SCHEMA_VERSION 34 class Servatrice; -class Servatrice_DatabaseInterface : public Server_DatabaseInterface { - Q_OBJECT +class Servatrice_DatabaseInterface : public Server_DatabaseInterface +{ + Q_OBJECT private: - int instanceId; - QSqlDatabase sqlDatabase; - Servatrice *server; - ServerInfo_User evalUserQueryResult(const QSqlQuery &query, bool complete, bool withId = false); - bool usernameIsValid(const QString &user); -protected: - AuthenticationResult checkUserPassword(Server_ProtocolHandler *handler, const QString &user, const QString &password, QString &reasonStr, int &secondsLeft); -public slots: - void initDatabase(const QSqlDatabase &_sqlDatabase); -public: - Servatrice_DatabaseInterface(int _instanceId, Servatrice *_server); - ~Servatrice_DatabaseInterface(); - void initDatabase(const QString &type, const QString &hostName, const QString &databaseName, const QString &userName, const QString &password); - bool openDatabase(); - bool checkSql(); - bool execSqlQuery(QSqlQuery &query); - const QSqlDatabase &getDatabase() { return sqlDatabase; } + int instanceId; + QSqlDatabase sqlDatabase; + QHash preparedStatements; + Servatrice *server; + ServerInfo_User evalUserQueryResult(const QSqlQuery *query, bool complete, bool withId = false); + /** Must be called after checkSql and server is known to be in auth mode. */ + bool checkUserIsIdBanned(const QString &clientId, QString &banReason, int &banSecondsRemaining); + /** Must be called after checkSql and server is known to be in auth mode. */ + bool checkUserIsIpBanned(const QString &ipAddress, QString &banReason, int &banSecondsRemaining); + /** Must be called after checkSql and server is known to be in auth mode. */ + bool checkUserIsNameBanned(QString const &userName, QString &banReason, int &banSecondsRemaining); - bool userExists(const QString &user); - int getUserIdInDB(const QString &name); - QMap getBuddyList(const QString &name); - QMap getIgnoreList(const QString &name); - bool isInBuddyList(const QString &whoseList, const QString &who); - bool isInIgnoreList(const QString &whoseList, const QString &who); - ServerInfo_User getUserData(const QString &name, bool withId = false); - void storeGameInformation(const QString &roomName, const QStringList &roomGameTypes, const ServerInfo_Game &gameInfo, const QSet &allPlayersEver, const QSet &allSpectatorsEver, const QList &replayList); - DeckList *getDeckFromDatabase(int deckId, int userId); - - int getNextGameId(); - int getNextReplayId(); - - qint64 startSession(const QString &userName, const QString &address); - void endSession(qint64 sessionId); - - void clearSessionTables(); - void lockSessionTables(); - void unlockSessionTables(); - bool userSessionExists(const QString &userName); - +protected: + AuthenticationResult checkUserPassword(Server_ProtocolHandler *handler, + const QString &user, + const QString &password, + const QString &clientId, + QString &reasonStr, + int &banSecondsLeft, + bool passwordNeedsHash) override; + +public slots: + void initDatabase(const QSqlDatabase &_sqlDatabase); + +public: + explicit Servatrice_DatabaseInterface(int _instanceId, Servatrice *_server); + ~Servatrice_DatabaseInterface() override; + bool initDatabase(const QString &type, + const QString &hostName, + const QString &databaseName, + const QString &userName, + const QString &password); + bool openDatabase(); + bool checkSql(); + QSqlQuery *prepareQuery(const QString &queryText); + bool execSqlQuery(QSqlQuery *query); + const QSqlDatabase &getDatabase() + { + return sqlDatabase; + } + + bool activeUserExists(const QString &user) override; + bool userExists(const QString &user) override; + QString getUserSalt(const QString &user) override; + int getUserIdInDB(const QString &name); + QMap getBuddyList(const QString &name) override; + QMap getIgnoreList(const QString &name) override; + bool isInBuddyList(const QString &whoseList, const QString &who) override; + bool isInIgnoreList(const QString &whoseList, const QString &who) override; + ServerInfo_User getUserData(const QString &name, bool withId = false) override; + void storeGameInformation(const QString &roomName, + const QStringList &roomGameTypes, + const ServerInfo_Game &gameInfo, + const QSet &allPlayersEver, + const QSet &allSpectatorsEver, + const QList &replayList) override; + DeckList *getDeckFromDatabase(int deckId, int userId) override; + + int getNextGameId() override; + int getNextReplayId() override; + int getActiveUserCount(QString connectionType = QString()) override; + + qint64 startSession(const QString &userName, + const QString &address, + const QString &clientId, + const QString &connectionType) override; + void endSession(qint64 sessionId) override; + void clearSessionTables() override; + void lockSessionTables() override; + void unlockSessionTables() override; + bool userSessionExists(const QString &userName) override; + bool usernameIsValid(const QString &user, QString &error) override; + bool checkUserIsBanned(const QString &ipAddress, + const QString &userName, + const QString &clientId, + QString &banReason, + int &banSecondsRemaining) override; + int checkNumberOfUserAccounts(const QString &email) override; + bool registerUser(const QString &userName, + const QString &realName, + const QString &password, + bool passwordNeedsHash, + const QString &emailAddress, + const QString &country, + bool active = false) override; + bool activateUser(const QString &userName, const QString &token) override; + void updateUsersClientID(const QString &userName, const QString &userClientID) override; + void updateUsersLastLoginData(const QString &userName, const QString &clientVersion) override; + void logMessage(const int senderId, + const QString &senderName, + const QString &senderIp, + const QString &logMessage, + LogMessage_TargetType targetType, + const int targetId, + const QString &targetName) override; + bool changeUserPassword(const QString &user, const QString &password, bool passwordNeedsHash) override; + bool changeUserPassword(const QString &user, + const QString &oldPassword, + bool oldPasswordNeedsHash, + const QString &newPassword, + bool newPasswordNeedsHash) override; + QList getUserBanHistory(const QString userName); + bool + addWarning(const QString userName, const QString adminName, const QString warningReason, const QString clientID); + QList getUserWarnHistory(const QString userName); + QList getMessageLogHistory(const QString &user, + const QString &ipaddress, + const QString &gamename, + const QString &gameid, + const QString &message, + bool &chat, + bool &game, + bool &room, + int &range, + int &maxresults); + bool addForgotPassword(const QString &user); + bool removeForgotPassword(const QString &user) override; + bool doesForgotPasswordExist(const QString &user); + bool updateUserToken(const QString &token, const QString &user); + bool validateTableColumnStringData(const QString &table, + const QString &column, + const QString &_user, + const QString &_datatocheck); + void addAuditRecord(const QString &user, + const QString &ipaddress, + const QString &clientid, + const QString &action, + const QString &details, + const bool &results); }; #endif diff --git a/servatrice/src/server_logger.cpp b/servatrice/src/server_logger.cpp index 3eac4f369..de0befacb 100644 --- a/servatrice/src/server_logger.cpp +++ b/servatrice/src/server_logger.cpp @@ -1,111 +1,124 @@ #include "server_logger.h" -#include -#include -#include + +#include "settingscache.h" + #include +#include +#include +#include +#include #include -#ifdef Q_OS_UNIX -# include -# include -# include -#endif ServerLogger::ServerLogger(bool _logToConsole, QObject *parent) - : QObject(parent), logToConsole(_logToConsole), flushRunning(false) + : QObject(parent), logToConsole(_logToConsole), flushRunning(false) { } ServerLogger::~ServerLogger() { - flushBuffer(); - // This does not work with the destroyed() signal as this destructor is called after the main event loop is done. - thread()->quit(); + flushBuffer(); + // This does not work with the destroyed() signal as this destructor is called after the main event loop is done. + thread()->quit(); } void ServerLogger::startLog(const QString &logFileName) { - if (!logFileName.isEmpty()) { - logFile = new QFile("server.log", this); - logFile->open(QIODevice::Append); -#ifdef Q_OS_UNIX - ::socketpair(AF_UNIX, SOCK_STREAM, 0, sigHupFD); + if (!logFileName.isEmpty()) { + QFileInfo fi(logFileName); + QDir fileDir(fi.path()); + if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) { + std::cerr << "ERROR: logfile folder doesn't exist and i can't create it." << std::endl; + logFile = 0; + return; + } - snHup = new QSocketNotifier(sigHupFD[1], QSocketNotifier::Read, this); - connect(snHup, SIGNAL(activated(int)), this, SLOT(handleSigHup())); -#endif - } else - logFile = 0; - - connect(this, SIGNAL(sigFlushBuffer()), this, SLOT(flushBuffer()), Qt::QueuedConnection); + logFile = new QFile(logFileName, this); + if (!logFile->open(QIODevice::Append)) { + std::cerr << "ERROR: can't open() logfile." << std::endl; + delete logFile; + logFile = 0; + return; + } + } else + logFile = 0; + + connect(this, SIGNAL(sigFlushBuffer()), this, SLOT(flushBuffer()), Qt::QueuedConnection); } -void ServerLogger::logMessage(QString message, void *caller) +void ServerLogger::logMessage(const QString &message, void *caller) { - if (!logFile) - return; - - bufferMutex.lock(); - QString callerString; - if (caller) - callerString = QString::number((qulonglong) caller, 16) + " "; - buffer.append(QDateTime::currentDateTime().toString() + " " + callerString + message); - bufferMutex.unlock(); - - emit sigFlushBuffer(); + if (!logFile) + return; + + QString callerString; + if (caller) + callerString = QString::number((qulonglong)caller, 16) + " "; + + // filter out all log entries based on values in configuration file + bool shouldWeWriteLog = settingsCache->value("server/writelog", 1).toBool(); + QString logFilters = settingsCache->value("server/logfilters").toString(); + QStringList listlogFilters = logFilters.split(",", Qt::SkipEmptyParts); + bool shouldWeSkipLine = false; + + if (!shouldWeWriteLog) + return; + + if (!logFilters.trimmed().isEmpty()) { + shouldWeSkipLine = true; + for (const QString &logFilter : listlogFilters) { + if (message.contains(logFilter, Qt::CaseInsensitive)) { + shouldWeSkipLine = false; + break; + } + } + } + + if (shouldWeSkipLine) + return; + + bufferMutex.lock(); + buffer.append(QDateTime::currentDateTime().toString() + " " + callerString + message); + bufferMutex.unlock(); + emit sigFlushBuffer(); } void ServerLogger::flushBuffer() { - if (flushRunning) - return; - - flushRunning = true; - QTextStream stream(logFile); - forever { - bufferMutex.lock(); - if (buffer.isEmpty()) { - bufferMutex.unlock(); - flushRunning = false; - return; - } - QString message = buffer.takeFirst(); - bufferMutex.unlock(); - - stream << message << "\n"; - stream.flush(); - - if (logToConsole) - std::cout << message.toStdString() << std::endl; - } + if (flushRunning) + return; + + flushRunning = true; + QTextStream stream(logFile); + forever + { + bufferMutex.lock(); + if (buffer.isEmpty()) { + bufferMutex.unlock(); + flushRunning = false; + return; + } + QString message = buffer.takeFirst(); + bufferMutex.unlock(); + + stream << message << "\n"; + stream.flush(); + + if (logToConsole) + std::cout << message.toStdString() << std::endl; + } } -void ServerLogger::hupSignalHandler(int /*unused*/) +void ServerLogger::rotateLogs() { -#ifdef Q_OS_UNIX - if (!logFile) - return; - - char a = 1; - ::write(sigHupFD[0], &a, sizeof(a)); -#endif -} + if (!logFile) + return; -void ServerLogger::handleSigHup() -{ -#ifdef Q_OS_UNIX - if (!logFile) - return; - - snHup->setEnabled(false); - char tmp; - ::read(sigHupFD[1], &tmp, sizeof(tmp)); - - logFile->close(); - logFile->open(QIODevice::Append); - - snHup->setEnabled(true); -#endif + flushBuffer(); + + logFile->close(); + if (!logFile->open(QIODevice::Append)) { + std::cerr << "ERROR: Failed to open log file for writing!" << std::endl; + } } QFile *ServerLogger::logFile; -int ServerLogger::sigHupFD[2]; diff --git a/servatrice/src/server_logger.h b/servatrice/src/server_logger.h index 67b293cf9..0ad092b66 100644 --- a/servatrice/src/server_logger.h +++ b/servatrice/src/server_logger.h @@ -1,38 +1,36 @@ #ifndef SERVER_LOGGER_H #define SERVER_LOGGER_H -#include -#include #include -#include +#include #include +#include +#include -class QSocketNotifier; class QFile; class Server_ProtocolHandler; -class ServerLogger : public QObject { - Q_OBJECT +class ServerLogger : public QObject +{ + Q_OBJECT public: - ServerLogger(bool _logToConsole, QObject *parent = 0); - ~ServerLogger(); - static void hupSignalHandler(int unused); + ServerLogger(bool _logToConsole, QObject *parent = 0); + ~ServerLogger(); public slots: - void startLog(const QString &logFileName); - void logMessage(QString message, void *caller = 0); + void startLog(const QString &logFileName); + void logMessage(const QString &message, void *caller = 0); + void rotateLogs(); private slots: - void handleSigHup(); - void flushBuffer(); + void flushBuffer(); signals: - void sigFlushBuffer(); + void sigFlushBuffer(); + private: - bool logToConsole; - static int sigHupFD[2]; - QSocketNotifier *snHup; - static QFile *logFile; - bool flushRunning; - QStringList buffer; - QMutex bufferMutex; + bool logToConsole; + static QFile *logFile; + bool flushRunning; + QStringList buffer; + QMutex bufferMutex; }; #endif diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index f91029d70..41e61ddec 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -18,742 +18,2230 @@ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * ***************************************************************************/ -#include -#include -#include -#include #include "serversocketinterface.h" + +#include "email_parser.h" +#include "main.h" #include "servatrice.h" #include "servatrice_database_interface.h" -#include "decklist.h" -#include "server_player.h" -#include "main.h" #include "server_logger.h" -#include "server_response_containers.h" -#include "pb/commands.pb.h" -#include "pb/command_deck_list.pb.h" -#include "pb/command_deck_upload.pb.h" -#include "pb/command_deck_download.pb.h" -#include "pb/command_deck_new_dir.pb.h" -#include "pb/command_deck_del_dir.pb.h" -#include "pb/command_deck_del.pb.h" -#include "pb/command_replay_list.pb.h" -#include "pb/command_replay_download.pb.h" -#include "pb/command_replay_modify_match.pb.h" -#include "pb/command_replay_delete_match.pb.h" -#include "pb/event_connection_closed.pb.h" -#include "pb/event_server_message.pb.h" -#include "pb/event_server_identification.pb.h" -#include "pb/event_add_to_list.pb.h" -#include "pb/event_remove_from_list.pb.h" -#include "pb/response_deck_list.pb.h" -#include "pb/response_deck_download.pb.h" -#include "pb/response_deck_upload.pb.h" -#include "pb/response_replay_list.pb.h" -#include "pb/response_replay_download.pb.h" -#include "pb/serverinfo_replay.pb.h" -#include "pb/serverinfo_user.pb.h" -#include "pb/serverinfo_deckstorage.pb.h" - +#include "settingscache.h" #include "version_string.h" -#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +inline Q_LOGGING_CATEGORY(AbstractServerSocketInterfaceLog, "abstract_server_socket_interface"); +inline Q_LOGGING_CATEGORY(TcpServerSocketInterfaceLog, "tcp_server_socket_interface"); +inline Q_LOGGING_CATEGORY(WebsocketServerSocketInterfaceLog, "websocket_server_socket_interface"); static const int protocolVersion = 14; -ServerSocketInterface::ServerSocketInterface(Servatrice *_server, Servatrice_DatabaseInterface *_databaseInterface, QObject *parent) - : Server_ProtocolHandler(_server, _databaseInterface, parent), - servatrice(_server), - sqlInterface(reinterpret_cast(databaseInterface)), - messageInProgress(false), - handshakeStarted(false) +AbstractServerSocketInterface::AbstractServerSocketInterface(Servatrice *_server, + Servatrice_DatabaseInterface *_databaseInterface, + QObject *parent) + : Server_ProtocolHandler(_server, _databaseInterface, parent), servatrice(_server), + sqlInterface(reinterpret_cast(databaseInterface)) { - socket = new QTcpSocket(this); - socket->setSocketOption(QAbstractSocket::LowDelayOption, 1); - connect(socket, SIGNAL(readyRead()), this, SLOT(readClient())); - connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(catchSocketError(QAbstractSocket::SocketError))); - - // Never call flushOutputQueue directly from outputQueueChanged. In case of a socket error, - // it could lead to this object being destroyed while another function is still on the call stack. -> mutex deadlocks etc. - connect(this, SIGNAL(outputQueueChanged()), this, SLOT(flushOutputQueue()), Qt::QueuedConnection); + // Never call flushOutputQueue directly from outputQueueChanged. In case of a socket error, + // it could lead to this object being destroyed while another function is still on the call stack. -> mutex + // deadlocks etc. + connect(this, SIGNAL(outputQueueChanged()), this, SLOT(flushOutputQueue()), Qt::QueuedConnection); } -ServerSocketInterface::~ServerSocketInterface() +bool AbstractServerSocketInterface::initSession() { - logger->logMessage("ServerSocketInterface destructor", this); - - flushOutputQueue(); + Event_ServerIdentification identEvent; + identEvent.set_server_name(servatrice->getServerName().toStdString()); + identEvent.set_server_version(VERSION_STRING); + identEvent.set_protocol_version(protocolVersion); + if (servatrice->getAuthenticationMethod() == Servatrice::AuthenticationSql) { + identEvent.set_server_options(Event_ServerIdentification::SupportsPasswordHash); + } + SessionEvent *identSe = prepareSessionEvent(identEvent); + sendProtocolItem(*identSe); + delete identSe; + + // allow unlimited number of connections from the trusted sources + QString trustedSources = settingsCache->value("security/trusted_sources", "127.0.0.1,::1").toString(); + if (trustedSources.contains(getAddress(), Qt::CaseInsensitive)) + return true; + + int maxUsers = servatrice->getMaxUsersPerAddress(); + if ((maxUsers > 0) && (servatrice->getUsersWithAddress(getPeerAddress()) > maxUsers)) { + Event_ConnectionClosed event; + event.set_reason(Event_ConnectionClosed::TOO_MANY_CONNECTIONS); + SessionEvent *se = prepareSessionEvent(event); + sendProtocolItem(*se); + delete se; + return false; + } + + return true; } -void ServerSocketInterface::initConnection(int socketDescriptor) +void AbstractServerSocketInterface::catchSocketError(QAbstractSocket::SocketError socketError) { - // Add this object to the server's list of connections before it can receive socket events. - // Otherwise, in case a of a socket error, it could be removed from the list before it is added. - server->addClient(this); - - socket->setSocketDescriptor(socketDescriptor); - logger->logMessage(QString("Incoming connection: %1").arg(socket->peerAddress().toString()), this); - initSessionDeprecated(); + qCWarning(AbstractServerSocketInterfaceLog) << "Socket error:" << socketError; + + prepareDestroy(); } -void ServerSocketInterface::initSessionDeprecated() +void AbstractServerSocketInterface::catchSocketDisconnected() { - // dirty hack to make v13 client display the correct error message - - QByteArray buf; - buf.append(""); - socket->write(buf); - socket->flush(); + prepareDestroy(); } -bool ServerSocketInterface::initSession() +void AbstractServerSocketInterface::transmitProtocolItem(const ServerMessage &item) { - Event_ServerIdentification identEvent; - identEvent.set_server_name(servatrice->getServerName().toStdString()); - identEvent.set_server_version(VERSION_STRING); - identEvent.set_protocol_version(protocolVersion); - SessionEvent *identSe = prepareSessionEvent(identEvent); - sendProtocolItem(*identSe); - delete identSe; - - int maxUsers = servatrice->getMaxUsersPerAddress(); - if ((maxUsers > 0) && (servatrice->getUsersWithAddress(socket->peerAddress()) >= maxUsers)) { - Event_ConnectionClosed event; - event.set_reason(Event_ConnectionClosed::TOO_MANY_CONNECTIONS); - SessionEvent *se = prepareSessionEvent(event); - sendProtocolItem(*se); - delete se; - - return false; - } - - return true; + outputQueueMutex.lock(); + outputQueue.append(item); + outputQueueMutex.unlock(); + + emit outputQueueChanged(); } -void ServerSocketInterface::readClient() +void AbstractServerSocketInterface::logDebugMessage(const QString &message) { - QByteArray data = socket->readAll(); - servatrice->incRxBytes(data.size()); - inputBuffer.append(data); - - do { - if (!messageInProgress) { - if (inputBuffer.size() >= 4) { - messageLength = (((quint32) (unsigned char) inputBuffer[0]) << 24) - + (((quint32) (unsigned char) inputBuffer[1]) << 16) - + (((quint32) (unsigned char) inputBuffer[2]) << 8) - + ((quint32) (unsigned char) inputBuffer[3]); - inputBuffer.remove(0, 4); - messageInProgress = true; - } else - return; - } - if (inputBuffer.size() < messageLength) - return; - - CommandContainer newCommandContainer; - newCommandContainer.ParseFromArray(inputBuffer.data(), messageLength); - inputBuffer.remove(0, messageLength); - messageInProgress = false; - - // dirty hack to make v13 client display the correct error message - if (handshakeStarted) - processCommandContainer(newCommandContainer); - else if (!newCommandContainer.has_cmd_id()) { - handshakeStarted = true; - if (!initSession()) - prepareDestroy(); - } - // end of hack - } while (!inputBuffer.isEmpty()); + logger->logMessage(message, this); } -void ServerSocketInterface::catchSocketError(QAbstractSocket::SocketError socketError) +Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionCommand(int cmdType, + const SessionCommand &cmd, + ResponseContainer &rc) { - qDebug() << "Socket error:" << socketError; - - prepareDestroy(); + switch ((SessionCommand::SessionCommandType)cmdType) { + case SessionCommand::ADD_TO_LIST: + return cmdAddToList(cmd.GetExtension(Command_AddToList::ext), rc); + case SessionCommand::REMOVE_FROM_LIST: + return cmdRemoveFromList(cmd.GetExtension(Command_RemoveFromList::ext), rc); + case SessionCommand::DECK_LIST: + return cmdDeckList(cmd.GetExtension(Command_DeckList::ext), rc); + case SessionCommand::DECK_NEW_DIR: + return cmdDeckNewDir(cmd.GetExtension(Command_DeckNewDir::ext), rc); + case SessionCommand::DECK_DEL_DIR: + return cmdDeckDelDir(cmd.GetExtension(Command_DeckDelDir::ext), rc); + case SessionCommand::DECK_DEL: + return cmdDeckDel(cmd.GetExtension(Command_DeckDel::ext), rc); + case SessionCommand::DECK_UPLOAD: + return cmdDeckUpload(cmd.GetExtension(Command_DeckUpload::ext), rc); + case SessionCommand::DECK_DOWNLOAD: + return cmdDeckDownload(cmd.GetExtension(Command_DeckDownload::ext), rc); + case SessionCommand::REPLAY_LIST: + return cmdReplayList(cmd.GetExtension(Command_ReplayList::ext), rc); + case SessionCommand::REPLAY_DOWNLOAD: + return cmdReplayDownload(cmd.GetExtension(Command_ReplayDownload::ext), rc); + case SessionCommand::REPLAY_MODIFY_MATCH: + return cmdReplayModifyMatch(cmd.GetExtension(Command_ReplayModifyMatch::ext), rc); + case SessionCommand::REPLAY_DELETE_MATCH: + return cmdReplayDeleteMatch(cmd.GetExtension(Command_ReplayDeleteMatch::ext), rc); + case SessionCommand::REPLAY_GET_CODE: + return cmdReplayGetCode(cmd.GetExtension(Command_ReplayGetCode::ext), rc); + case SessionCommand::REPLAY_SUBMIT_CODE: + return cmdReplaySubmitCode(cmd.GetExtension(Command_ReplaySubmitCode::ext), rc); + case SessionCommand::REGISTER: + return cmdRegisterAccount(cmd.GetExtension(Command_Register::ext), rc); + break; + case SessionCommand::ACTIVATE: + return cmdActivateAccount(cmd.GetExtension(Command_Activate::ext), rc); + break; + case SessionCommand::FORGOT_PASSWORD_REQUEST: + return cmdForgotPasswordRequest(cmd.GetExtension(Command_ForgotPasswordRequest::ext), rc); + break; + case SessionCommand::FORGOT_PASSWORD_RESET: + return cmdForgotPasswordReset(cmd.GetExtension(Command_ForgotPasswordReset::ext), rc); + break; + case SessionCommand::FORGOT_PASSWORD_CHALLENGE: + return cmdForgotPasswordChallenge(cmd.GetExtension(Command_ForgotPasswordChallenge::ext), rc); + break; + case SessionCommand::ACCOUNT_EDIT: + return cmdAccountEdit(cmd.GetExtension(Command_AccountEdit::ext), rc); + case SessionCommand::ACCOUNT_IMAGE: + return cmdAccountImage(cmd.GetExtension(Command_AccountImage::ext), rc); + case SessionCommand::ACCOUNT_PASSWORD: + return cmdAccountPassword(cmd.GetExtension(Command_AccountPassword::ext), rc); + case SessionCommand::REQUEST_PASSWORD_SALT: + return cmdRequestPasswordSalt(cmd.GetExtension(Command_RequestPasswordSalt::ext), rc); + break; + default: + return Response::RespFunctionNotAllowed; + } } -void ServerSocketInterface::transmitProtocolItem(const ServerMessage &item) +Response::ResponseCode AbstractServerSocketInterface::processExtendedModeratorCommand(int cmdType, + const ModeratorCommand &cmd, + ResponseContainer &rc) { - outputQueueMutex.lock(); - outputQueue.append(item); - outputQueueMutex.unlock(); - - emit outputQueueChanged(); + switch ((ModeratorCommand::ModeratorCommandType)cmdType) { + case ModeratorCommand::BAN_FROM_SERVER: + return cmdBanFromServer(cmd.GetExtension(Command_BanFromServer::ext), rc); + case ModeratorCommand::BAN_HISTORY: + return cmdGetBanHistory(cmd.GetExtension(Command_GetBanHistory::ext), rc); + case ModeratorCommand::WARN_USER: + return cmdWarnUser(cmd.GetExtension(Command_WarnUser::ext), rc); + case ModeratorCommand::WARN_HISTORY: + return cmdGetWarnHistory(cmd.GetExtension(Command_GetWarnHistory::ext), rc); + case ModeratorCommand::WARN_LIST: + return cmdGetWarnList(cmd.GetExtension(Command_GetWarnList::ext), rc); + case ModeratorCommand::VIEWLOG_HISTORY: + return cmdGetLogHistory(cmd.GetExtension(Command_ViewLogHistory::ext), rc); + case ModeratorCommand::GRANT_REPLAY_ACCESS: + return cmdGrantReplayAccess(cmd.GetExtension(Command_GrantReplayAccess::ext), rc); + case ModeratorCommand::FORCE_ACTIVATE_USER: + return cmdForceActivateUser(cmd.GetExtension(Command_ForceActivateUser::ext), rc); + case ModeratorCommand::GET_ADMIN_NOTES: + return cmdGetAdminNotes(cmd.GetExtension(Command_GetAdminNotes::ext), rc); + case ModeratorCommand::UPDATE_ADMIN_NOTES: + return cmdUpdateAdminNotes(cmd.GetExtension(Command_UpdateAdminNotes::ext), rc); + default: + return Response::RespFunctionNotAllowed; + } } -void ServerSocketInterface::flushOutputQueue() +Response::ResponseCode +AbstractServerSocketInterface::processExtendedAdminCommand(int cmdType, const AdminCommand &cmd, ResponseContainer &rc) { - QMutexLocker locker(&outputQueueMutex); - if (outputQueue.isEmpty()) - return; - - int totalBytes = 0; - while (!outputQueue.isEmpty()) { - ServerMessage item = outputQueue.takeFirst(); - locker.unlock(); - - QByteArray buf; - unsigned int size = item.ByteSize(); - buf.resize(size + 4); - item.SerializeToArray(buf.data() + 4, size); - buf.data()[3] = (unsigned char) size; - buf.data()[2] = (unsigned char) (size >> 8); - buf.data()[1] = (unsigned char) (size >> 16); - buf.data()[0] = (unsigned char) (size >> 24); - // In case socket->write() calls catchSocketError(), the mutex must not be locked during this call. - socket->write(buf); - - totalBytes += size + 4; - locker.relock(); - } - locker.unlock(); - servatrice->incTxBytes(totalBytes); - // see above wrt mutex - socket->flush(); + switch ((AdminCommand::AdminCommandType)cmdType) { + case AdminCommand::SHUTDOWN_SERVER: + return cmdShutdownServer(cmd.GetExtension(Command_ShutdownServer::ext), rc); + case AdminCommand::UPDATE_SERVER_MESSAGE: + return cmdUpdateServerMessage(cmd.GetExtension(Command_UpdateServerMessage::ext), rc); + case AdminCommand::RELOAD_CONFIG: + return cmdReloadConfig(cmd.GetExtension(Command_ReloadConfig::ext), rc); + case AdminCommand::ADJUST_MOD: + return cmdAdjustMod(cmd.GetExtension(Command_AdjustMod::ext), rc); + default: + return Response::RespFunctionNotAllowed; + } } -void ServerSocketInterface::logDebugMessage(const QString &message) +Response::ResponseCode AbstractServerSocketInterface::cmdAddToList(const Command_AddToList &cmd, ResponseContainer &rc) { - logger->logMessage(message, this); + if (authState != PasswordRight) + return Response::RespFunctionNotAllowed; + + QString list = nameFromStdString(cmd.list()); + QString user = nameFromStdString(cmd.user_name()); + + if ((list != "buddy") && (list != "ignore")) + return Response::RespContextError; + + if (list == "buddy") + if (databaseInterface->isInBuddyList(QString::fromStdString(userInfo->name()), user)) + return Response::RespContextError; + if (list == "ignore") + if (databaseInterface->isInIgnoreList(QString::fromStdString(userInfo->name()), user)) + return Response::RespContextError; + + int id1 = userInfo->id(); + int id2 = sqlInterface->getUserIdInDB(user); + if (id2 < 0) + return Response::RespNameNotFound; + if (id1 == id2) + return Response::RespContextError; + + QSqlQuery *query = + sqlInterface->prepareQuery("insert into {prefix}_" + list + "list (id_user1, id_user2) values(:id1, :id2)"); + query->bindValue(":id1", id1); + query->bindValue(":id2", id2); + if (!sqlInterface->execSqlQuery(query)) + return Response::RespInternalError; + + Event_AddToList event; + event.set_list_name(cmd.list()); + event.mutable_user_info()->CopyFrom(databaseInterface->getUserData(user)); + rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); + + return Response::RespOk; } -Response::ResponseCode ServerSocketInterface::processExtendedSessionCommand(int cmdType, const SessionCommand &cmd, ResponseContainer &rc) +Response::ResponseCode AbstractServerSocketInterface::cmdRemoveFromList(const Command_RemoveFromList &cmd, + ResponseContainer &rc) { - switch ((SessionCommand::SessionCommandType) cmdType) { - case SessionCommand::ADD_TO_LIST: return cmdAddToList(cmd.GetExtension(Command_AddToList::ext), rc); - case SessionCommand::REMOVE_FROM_LIST: return cmdRemoveFromList(cmd.GetExtension(Command_RemoveFromList::ext), rc); - case SessionCommand::DECK_LIST: return cmdDeckList(cmd.GetExtension(Command_DeckList::ext), rc); - case SessionCommand::DECK_NEW_DIR: return cmdDeckNewDir(cmd.GetExtension(Command_DeckNewDir::ext), rc); - case SessionCommand::DECK_DEL_DIR: return cmdDeckDelDir(cmd.GetExtension(Command_DeckDelDir::ext), rc); - case SessionCommand::DECK_DEL: return cmdDeckDel(cmd.GetExtension(Command_DeckDel::ext), rc); - case SessionCommand::DECK_UPLOAD: return cmdDeckUpload(cmd.GetExtension(Command_DeckUpload::ext), rc); - case SessionCommand::DECK_DOWNLOAD: return cmdDeckDownload(cmd.GetExtension(Command_DeckDownload::ext), rc); - case SessionCommand::REPLAY_LIST: return cmdReplayList(cmd.GetExtension(Command_ReplayList::ext), rc); - case SessionCommand::REPLAY_DOWNLOAD: return cmdReplayDownload(cmd.GetExtension(Command_ReplayDownload::ext), rc); - case SessionCommand::REPLAY_MODIFY_MATCH: return cmdReplayModifyMatch(cmd.GetExtension(Command_ReplayModifyMatch::ext), rc); - case SessionCommand::REPLAY_DELETE_MATCH: return cmdReplayDeleteMatch(cmd.GetExtension(Command_ReplayDeleteMatch::ext), rc); - default: return Response::RespFunctionNotAllowed; - } + if (authState != PasswordRight) + return Response::RespFunctionNotAllowed; + + QString list = nameFromStdString(cmd.list()); + QString user = nameFromStdString(cmd.user_name()); + + if ((list != "buddy") && (list != "ignore")) + return Response::RespContextError; + + if (list == "buddy") + if (!databaseInterface->isInBuddyList(QString::fromStdString(userInfo->name()), user)) + return Response::RespContextError; + if (list == "ignore") + if (!databaseInterface->isInIgnoreList(QString::fromStdString(userInfo->name()), user)) + return Response::RespContextError; + + int id1 = userInfo->id(); + int id2 = sqlInterface->getUserIdInDB(user); + if (id2 < 0) + return Response::RespNameNotFound; + + QSqlQuery *query = + sqlInterface->prepareQuery("delete from {prefix}_" + list + "list where id_user1 = :id1 and id_user2 = :id2"); + query->bindValue(":id1", id1); + query->bindValue(":id2", id2); + if (!sqlInterface->execSqlQuery(query)) + return Response::RespInternalError; + + Event_RemoveFromList event; + event.set_list_name(cmd.list()); + event.set_user_name(cmd.user_name()); + rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); + + return Response::RespOk; } -Response::ResponseCode ServerSocketInterface::processExtendedModeratorCommand(int cmdType, const ModeratorCommand &cmd, ResponseContainer &rc) +int AbstractServerSocketInterface::getDeckPathId(int basePathId, QStringList path) { - switch ((ModeratorCommand::ModeratorCommandType) cmdType) { - case ModeratorCommand::BAN_FROM_SERVER: return cmdBanFromServer(cmd.GetExtension(Command_BanFromServer::ext), rc); - default: return Response::RespFunctionNotAllowed; - } + if (path.isEmpty()) + return 0; + if (path[0].isEmpty()) + return 0; + + QSqlQuery *query = sqlInterface->prepareQuery("select id from {prefix}_decklist_folders where id_parent = " + ":id_parent and name = :name and id_user = :id_user"); + query->bindValue(":id_parent", basePathId); + query->bindValue(":name", path.takeFirst()); + query->bindValue(":id_user", userInfo->id()); + if (!sqlInterface->execSqlQuery(query)) + return -1; + if (!query->next()) + return -1; + int id = query->value(0).toInt(); + if (path.isEmpty()) + return id; + else + return getDeckPathId(id, path); } -Response::ResponseCode ServerSocketInterface::processExtendedAdminCommand(int cmdType, const AdminCommand &cmd, ResponseContainer &rc) +int AbstractServerSocketInterface::getDeckPathId(const QString &path) { - switch ((AdminCommand::AdminCommandType) cmdType) { - case AdminCommand::SHUTDOWN_SERVER: return cmdShutdownServer(cmd.GetExtension(Command_ShutdownServer::ext), rc); - case AdminCommand::UPDATE_SERVER_MESSAGE: return cmdUpdateServerMessage(cmd.GetExtension(Command_UpdateServerMessage::ext), rc); - default: return Response::RespFunctionNotAllowed; - } + return getDeckPathId(0, path.split("/")); } -Response::ResponseCode ServerSocketInterface::cmdAddToList(const Command_AddToList &cmd, ResponseContainer &rc) +bool AbstractServerSocketInterface::deckListHelper(int folderId, ServerInfo_DeckStorage_Folder *folder) { - if (authState != PasswordRight) - return Response::RespFunctionNotAllowed; - - QString list = QString::fromStdString(cmd.list()); - QString user = QString::fromStdString(cmd.user_name()); - - if ((list != "buddy") && (list != "ignore")) - return Response::RespContextError; - - if (list == "buddy") - if (databaseInterface->isInBuddyList(QString::fromStdString(userInfo->name()), user)) - return Response::RespContextError; - if (list == "ignore") - if (databaseInterface->isInIgnoreList(QString::fromStdString(userInfo->name()), user)) - return Response::RespContextError; - - int id1 = userInfo->id(); - int id2 = sqlInterface->getUserIdInDB(user); - if (id2 < 0) - return Response::RespNameNotFound; - if (id1 == id2) - return Response::RespContextError; - - QSqlQuery query(sqlInterface->getDatabase()); - query.prepare("insert into " + servatrice->getDbPrefix() + "_" + list + "list (id_user1, id_user2) values(:id1, :id2)"); - query.bindValue(":id1", id1); - query.bindValue(":id2", id2); - if (!sqlInterface->execSqlQuery(query)) - return Response::RespInternalError; - - Event_AddToList event; - event.set_list_name(cmd.list()); - event.mutable_user_info()->CopyFrom(databaseInterface->getUserData(user)); - rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); - - return Response::RespOk; -} + QSqlQuery *query = sqlInterface->prepareQuery( + "select id, name from {prefix}_decklist_folders where id_parent = :id_parent and id_user = :id_user"); + query->bindValue(":id_parent", folderId); + query->bindValue(":id_user", userInfo->id()); + if (!sqlInterface->execSqlQuery(query)) + return false; -Response::ResponseCode ServerSocketInterface::cmdRemoveFromList(const Command_RemoveFromList &cmd, ResponseContainer &rc) -{ - if (authState != PasswordRight) - return Response::RespFunctionNotAllowed; - - QString list = QString::fromStdString(cmd.list()); - QString user = QString::fromStdString(cmd.user_name()); - - if ((list != "buddy") && (list != "ignore")) - return Response::RespContextError; - - if (list == "buddy") - if (!databaseInterface->isInBuddyList(QString::fromStdString(userInfo->name()), user)) - return Response::RespContextError; - if (list == "ignore") - if (!databaseInterface->isInIgnoreList(QString::fromStdString(userInfo->name()), user)) - return Response::RespContextError; - - int id1 = userInfo->id(); - int id2 = sqlInterface->getUserIdInDB(user); - if (id2 < 0) - return Response::RespNameNotFound; - - QSqlQuery query(sqlInterface->getDatabase()); - query.prepare("delete from " + servatrice->getDbPrefix() + "_" + list + "list where id_user1 = :id1 and id_user2 = :id2"); - query.bindValue(":id1", id1); - query.bindValue(":id2", id2); - if (!sqlInterface->execSqlQuery(query)) - return Response::RespInternalError; - - Event_RemoveFromList event; - event.set_list_name(cmd.list()); - event.set_user_name(cmd.user_name()); - rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); - - return Response::RespOk; -} + QMap results; + while (query->next()) + results[query->value(0).toInt()] = query->value(1).toString(); -int ServerSocketInterface::getDeckPathId(int basePathId, QStringList path) -{ - if (path.isEmpty()) - return 0; - if (path[0].isEmpty()) - return 0; - - QSqlQuery query(sqlInterface->getDatabase()); - query.prepare("select id from " + servatrice->getDbPrefix() + "_decklist_folders where id_parent = :id_parent and name = :name and id_user = :id_user"); - query.bindValue(":id_parent", basePathId); - query.bindValue(":name", path.takeFirst()); - query.bindValue(":id_user", userInfo->id()); - if (!sqlInterface->execSqlQuery(query)) - return -1; - if (!query.next()) - return -1; - int id = query.value(0).toInt(); - if (path.isEmpty()) - return id; - else - return getDeckPathId(id, path); -} + for (int key : results.keys()) { + ServerInfo_DeckStorage_TreeItem *newItem = folder->add_items(); + newItem->set_id(key); + newItem->set_name(results.value(key).toStdString()); -int ServerSocketInterface::getDeckPathId(const QString &path) -{ - return getDeckPathId(0, path.split("/")); -} + if (!deckListHelper(newItem->id(), newItem->mutable_folder())) + return false; + } -bool ServerSocketInterface::deckListHelper(int folderId, ServerInfo_DeckStorage_Folder *folder) -{ - QSqlQuery query(sqlInterface->getDatabase()); - query.prepare("select id, name from " + servatrice->getDbPrefix() + "_decklist_folders where id_parent = :id_parent and id_user = :id_user"); - query.bindValue(":id_parent", folderId); - query.bindValue(":id_user", userInfo->id()); - if (!sqlInterface->execSqlQuery(query)) - return false; - - while (query.next()) { - ServerInfo_DeckStorage_TreeItem *newItem = folder->add_items(); - newItem->set_id(query.value(0).toInt()); - newItem->set_name(query.value(1).toString().toStdString()); - - if (!deckListHelper(newItem->id(), newItem->mutable_folder())) - return false; - } - - query.prepare("select id, name, upload_time from " + servatrice->getDbPrefix() + "_decklist_files where id_folder = :id_folder and id_user = :id_user"); - query.bindValue(":id_folder", folderId); - query.bindValue(":id_user", userInfo->id()); - if (!sqlInterface->execSqlQuery(query)) - return false; - - while (query.next()) { - ServerInfo_DeckStorage_TreeItem *newItem = folder->add_items(); - newItem->set_id(query.value(0).toInt()); - newItem->set_name(query.value(1).toString().toStdString()); - - ServerInfo_DeckStorage_File *newFile = newItem->mutable_file(); - newFile->set_creation_time(query.value(2).toDateTime().toTime_t()); - } - - return true; + query = sqlInterface->prepareQuery("select id, name, upload_time from {prefix}_decklist_files where id_folder = " + ":id_folder and id_user = :id_user"); + query->bindValue(":id_folder", folderId); + query->bindValue(":id_user", userInfo->id()); + if (!sqlInterface->execSqlQuery(query)) + return false; + + while (query->next()) { + ServerInfo_DeckStorage_TreeItem *newItem = folder->add_items(); + newItem->set_id(query->value(0).toInt()); + newItem->set_name(query->value(1).toString().toStdString()); + + ServerInfo_DeckStorage_File *newFile = newItem->mutable_file(); + newFile->set_creation_time(query->value(2).toDateTime().toSecsSinceEpoch()); + } + + return true; } // CHECK AUTHENTICATION! // Also check for every function that data belonging to other users cannot be accessed. -Response::ResponseCode ServerSocketInterface::cmdDeckList(const Command_DeckList & /*cmd*/, ResponseContainer &rc) +Response::ResponseCode AbstractServerSocketInterface::cmdDeckList(const Command_DeckList & /*cmd*/, + ResponseContainer &rc) { - if (authState != PasswordRight) - return Response::RespFunctionNotAllowed; - - sqlInterface->checkSql(); - - Response_DeckList *re = new Response_DeckList; - ServerInfo_DeckStorage_Folder *root = re->mutable_root(); - - if (!deckListHelper(0, root)) - return Response::RespContextError; - - rc.setResponseExtension(re); - return Response::RespOk; + if (authState != PasswordRight) + return Response::RespFunctionNotAllowed; + + sqlInterface->checkSql(); + + Response_DeckList *re = new Response_DeckList; + ServerInfo_DeckStorage_Folder *root = re->mutable_root(); + + if (!deckListHelper(0, root)) + return Response::RespContextError; + + rc.setResponseExtension(re); + return Response::RespOk; } -Response::ResponseCode ServerSocketInterface::cmdDeckNewDir(const Command_DeckNewDir &cmd, ResponseContainer & /*rc*/) +Response::ResponseCode AbstractServerSocketInterface::cmdDeckNewDir(const Command_DeckNewDir &cmd, + ResponseContainer & /*rc*/) { - if (authState != PasswordRight) - return Response::RespFunctionNotAllowed; - - sqlInterface->checkSql(); - - int folderId = getDeckPathId(QString::fromStdString(cmd.path())); - if (folderId == -1) - return Response::RespNameNotFound; - - QSqlQuery query(sqlInterface->getDatabase()); - query.prepare("insert into " + servatrice->getDbPrefix() + "_decklist_folders (id_parent, id_user, name) values(:id_parent, :id_user, :name)"); - query.bindValue(":id_parent", folderId); - query.bindValue(":id_user", userInfo->id()); - query.bindValue(":name", QString::fromStdString(cmd.dir_name())); - if (!sqlInterface->execSqlQuery(query)) - return Response::RespContextError; - return Response::RespOk; + if (authState != PasswordRight) + return Response::RespFunctionNotAllowed; + + sqlInterface->checkSql(); + + QString path = nameFromStdString(cmd.path()); + int folderId = getDeckPathId(path); + if (folderId == -1) + return Response::RespNameNotFound; + + QString name = nameFromStdString(cmd.dir_name()); + if (path.length() + name.length() + 1 > MAX_NAME_LENGTH) + return Response::RespContextError; // do not allow creation of paths that would be too long to delete + + QSqlQuery *query = sqlInterface->prepareQuery( + "insert into {prefix}_decklist_folders (id_parent, id_user, name) values(:id_parent, :id_user, :name)"); + query->bindValue(":id_parent", folderId); + query->bindValue(":id_user", userInfo->id()); + query->bindValue(":name", name); + if (!sqlInterface->execSqlQuery(query)) + return Response::RespContextError; + return Response::RespOk; } -void ServerSocketInterface::deckDelDirHelper(int basePathId) +void AbstractServerSocketInterface::deckDelDirHelper(int basePathId) { - sqlInterface->checkSql(); - QSqlQuery query(sqlInterface->getDatabase()); - - query.prepare("select id from " + servatrice->getDbPrefix() + "_decklist_folders where id_parent = :id_parent"); - query.bindValue(":id_parent", basePathId); - sqlInterface->execSqlQuery(query); - while (query.next()) - deckDelDirHelper(query.value(0).toInt()); - - query.prepare("delete from " + servatrice->getDbPrefix() + "_decklist_files where id_folder = :id_folder"); - query.bindValue(":id_folder", basePathId); - sqlInterface->execSqlQuery(query); - - query.prepare("delete from " + servatrice->getDbPrefix() + "_decklist_folders where id = :id"); - query.bindValue(":id", basePathId); - sqlInterface->execSqlQuery(query); + sqlInterface->checkSql(); + QSqlQuery *query = + sqlInterface->prepareQuery("select id from {prefix}_decklist_folders where id_parent = :id_parent"); + query->bindValue(":id_parent", basePathId); + sqlInterface->execSqlQuery(query); + while (query->next()) + deckDelDirHelper(query->value(0).toInt()); + + query = sqlInterface->prepareQuery("delete from {prefix}_decklist_files where id_folder = :id_folder"); + query->bindValue(":id_folder", basePathId); + sqlInterface->execSqlQuery(query); + + query = sqlInterface->prepareQuery("delete from {prefix}_decklist_folders where id = :id"); + query->bindValue(":id", basePathId); + sqlInterface->execSqlQuery(query); } -Response::ResponseCode ServerSocketInterface::cmdDeckDelDir(const Command_DeckDelDir &cmd, ResponseContainer & /*rc*/) +void AbstractServerSocketInterface::sendServerMessage(const QString userName, const QString message) { - if (authState != PasswordRight) - return Response::RespFunctionNotAllowed; - - sqlInterface->checkSql(); - - int basePathId = getDeckPathId(QString::fromStdString(cmd.path())); - if ((basePathId == -1) || (basePathId == 0)) - return Response::RespNameNotFound; - deckDelDirHelper(basePathId); - return Response::RespOk; + AbstractServerSocketInterface *user = + static_cast(server->getUsers().value(userName)); + if (!user) + return; + + Event_UserMessage event; + event.set_sender_name("Servatrice"); + event.set_receiver_name(userName.toStdString()); + event.set_message(message.toStdString()); + SessionEvent *se = user->prepareSessionEvent(event); + user->sendProtocolItem(*se); + delete se; } -Response::ResponseCode ServerSocketInterface::cmdDeckDel(const Command_DeckDel &cmd, ResponseContainer & /*rc*/) +Response::ResponseCode AbstractServerSocketInterface::cmdDeckDelDir(const Command_DeckDelDir &cmd, + ResponseContainer & /*rc*/) { - if (authState != PasswordRight) - return Response::RespFunctionNotAllowed; - - sqlInterface->checkSql(); - QSqlQuery query(sqlInterface->getDatabase()); - - query.prepare("select id from " + servatrice->getDbPrefix() + "_decklist_files where id = :id and id_user = :id_user"); - query.bindValue(":id", cmd.deck_id()); - query.bindValue(":id_user", userInfo->id()); - sqlInterface->execSqlQuery(query); - if (!query.next()) - return Response::RespNameNotFound; - - query.prepare("delete from " + servatrice->getDbPrefix() + "_decklist_files where id = :id"); - query.bindValue(":id", cmd.deck_id()); - sqlInterface->execSqlQuery(query); - - return Response::RespOk; + if (authState != PasswordRight) + return Response::RespFunctionNotAllowed; + + sqlInterface->checkSql(); + + int basePathId = getDeckPathId(nameFromStdString(cmd.path())); + if ((basePathId == -1) || (basePathId == 0)) + return Response::RespNameNotFound; + deckDelDirHelper(basePathId); + return Response::RespOk; } -Response::ResponseCode ServerSocketInterface::cmdDeckUpload(const Command_DeckUpload &cmd, ResponseContainer &rc) +Response::ResponseCode AbstractServerSocketInterface::cmdDeckDel(const Command_DeckDel &cmd, ResponseContainer & /*rc*/) { - if (authState != PasswordRight) - return Response::RespFunctionNotAllowed; - - if (!cmd.has_deck_list()) - return Response::RespInvalidData; - - sqlInterface->checkSql(); - - QString deckStr = QString::fromStdString(cmd.deck_list()); - DeckList deck(deckStr); - - QString deckName = deck.getName(); - if (deckName.isEmpty()) - deckName = "Unnamed deck"; - - if (cmd.has_path()) { - int folderId = getDeckPathId(QString::fromStdString(cmd.path())); - if (folderId == -1) - return Response::RespNameNotFound; - - QSqlQuery query(sqlInterface->getDatabase()); - query.prepare("insert into " + servatrice->getDbPrefix() + "_decklist_files (id_folder, id_user, name, upload_time, content) values(:id_folder, :id_user, :name, NOW(), :content)"); - query.bindValue(":id_folder", folderId); - query.bindValue(":id_user", userInfo->id()); - query.bindValue(":name", deckName); - query.bindValue(":content", deckStr); - sqlInterface->execSqlQuery(query); - - Response_DeckUpload *re = new Response_DeckUpload; - ServerInfo_DeckStorage_TreeItem *fileInfo = re->mutable_new_file(); - fileInfo->set_id(query.lastInsertId().toInt()); - fileInfo->set_name(deckName.toStdString()); - fileInfo->mutable_file()->set_creation_time(QDateTime::currentDateTime().toTime_t()); - rc.setResponseExtension(re); - } else if (cmd.has_deck_id()) { - QSqlQuery query(sqlInterface->getDatabase()); - query.prepare("update " + servatrice->getDbPrefix() + "_decklist_files set name=:name, upload_time=NOW(), content=:content where id = :id_deck and id_user = :id_user"); - query.bindValue(":id_deck", cmd.deck_id()); - query.bindValue(":id_user", userInfo->id()); - query.bindValue(":name", deckName); - query.bindValue(":content", deckStr); - sqlInterface->execSqlQuery(query); - - if (query.numRowsAffected() == 0) - return Response::RespNameNotFound; - - Response_DeckUpload *re = new Response_DeckUpload; - ServerInfo_DeckStorage_TreeItem *fileInfo = re->mutable_new_file(); - fileInfo->set_id(cmd.deck_id()); - fileInfo->set_name(deckName.toStdString()); - fileInfo->mutable_file()->set_creation_time(QDateTime::currentDateTime().toTime_t()); - rc.setResponseExtension(re); - } else - return Response::RespInvalidData; - - return Response::RespOk; + if (authState != PasswordRight) + return Response::RespFunctionNotAllowed; + + sqlInterface->checkSql(); + QSqlQuery *query = + sqlInterface->prepareQuery("select id from {prefix}_decklist_files where id = :id and id_user = :id_user"); + query->bindValue(":id", cmd.deck_id()); + query->bindValue(":id_user", userInfo->id()); + sqlInterface->execSqlQuery(query); + if (!query->next()) + return Response::RespNameNotFound; + + query = sqlInterface->prepareQuery("delete from {prefix}_decklist_files where id = :id"); + query->bindValue(":id", cmd.deck_id()); + sqlInterface->execSqlQuery(query); + + return Response::RespOk; } -Response::ResponseCode ServerSocketInterface::cmdDeckDownload(const Command_DeckDownload &cmd, ResponseContainer &rc) +Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Command_DeckUpload &cmd, + ResponseContainer &rc) { - if (authState != PasswordRight) - return Response::RespFunctionNotAllowed; - - DeckList *deck; - try { - deck = sqlInterface->getDeckFromDatabase(cmd.deck_id(), userInfo->id()); - } catch(Response::ResponseCode r) { - return r; - } - - Response_DeckDownload *re = new Response_DeckDownload; - re->set_deck(deck->writeToString_Native().toStdString()); - rc.setResponseExtension(re); - delete deck; - - return Response::RespOk; + if (authState != PasswordRight) + return Response::RespFunctionNotAllowed; + + if (!cmd.has_deck_list()) + return Response::RespInvalidData; + + sqlInterface->checkSql(); + + QString deckStr = fileFromStdString(cmd.deck_list()); + DeckList deck; + if (!deck.loadFromString_Native(deckStr)) + return Response::RespContextError; + + QString deckName = deck.getName(); + if (deckName.isEmpty()) + deckName = "Unnamed deck"; + + if (cmd.has_path()) { + int folderId = getDeckPathId(nameFromStdString(cmd.path())); + if (folderId == -1) + return Response::RespNameNotFound; + + QSqlQuery *query = + sqlInterface->prepareQuery("insert into {prefix}_decklist_files (id_folder, id_user, name, upload_time, " + "content) values(:id_folder, :id_user, :name, NOW(), :content)"); + query->bindValue(":id_folder", folderId); + query->bindValue(":id_user", userInfo->id()); + query->bindValue(":name", deckName); + query->bindValue(":content", deckStr); + sqlInterface->execSqlQuery(query); + + Response_DeckUpload *re = new Response_DeckUpload; + ServerInfo_DeckStorage_TreeItem *fileInfo = re->mutable_new_file(); + fileInfo->set_id(query->lastInsertId().toInt()); + fileInfo->set_name(deckName.toStdString()); + fileInfo->mutable_file()->set_creation_time(QDateTime::currentDateTime().toSecsSinceEpoch()); + rc.setResponseExtension(re); + } else if (cmd.has_deck_id()) { + QSqlQuery *query = + sqlInterface->prepareQuery("update {prefix}_decklist_files set name=:name, upload_time=NOW(), " + "content=:content where id = :id_deck and id_user = :id_user"); + query->bindValue(":id_deck", cmd.deck_id()); + query->bindValue(":id_user", userInfo->id()); + query->bindValue(":name", deckName); + query->bindValue(":content", deckStr); + sqlInterface->execSqlQuery(query); + + if (query->numRowsAffected() == 0) + return Response::RespNameNotFound; + + Response_DeckUpload *re = new Response_DeckUpload; + ServerInfo_DeckStorage_TreeItem *fileInfo = re->mutable_new_file(); + fileInfo->set_id(cmd.deck_id()); + fileInfo->set_name(deckName.toStdString()); + fileInfo->mutable_file()->set_creation_time(QDateTime::currentDateTime().toSecsSinceEpoch()); + rc.setResponseExtension(re); + } else + return Response::RespInvalidData; + + return Response::RespOk; } -Response::ResponseCode ServerSocketInterface::cmdReplayList(const Command_ReplayList & /*cmd*/, ResponseContainer &rc) +Response::ResponseCode AbstractServerSocketInterface::cmdDeckDownload(const Command_DeckDownload &cmd, + ResponseContainer &rc) { - if (authState != PasswordRight) - return Response::RespFunctionNotAllowed; - - Response_ReplayList *re = new Response_ReplayList; - - QSqlQuery query1(sqlInterface->getDatabase()); - query1.prepare("select a.id_game, a.replay_name, b.room_name, b.time_started, b.time_finished, b.descr, a.do_not_hide from cockatrice_replays_access a left join cockatrice_games b on b.id = a.id_game where a.id_player = :id_player and (a.do_not_hide = 1 or date_add(b.time_started, interval 7 day) > now())"); - query1.bindValue(":id_player", userInfo->id()); - sqlInterface->execSqlQuery(query1); - while (query1.next()) { - ServerInfo_ReplayMatch *matchInfo = re->add_match_list(); - - const int gameId = query1.value(0).toInt(); - matchInfo->set_game_id(gameId); - matchInfo->set_room_name(query1.value(2).toString().toStdString()); - const int timeStarted = query1.value(3).toDateTime().toTime_t(); - const int timeFinished = query1.value(4).toDateTime().toTime_t(); - matchInfo->set_time_started(timeStarted); - matchInfo->set_length(timeFinished - timeStarted); - matchInfo->set_game_name(query1.value(5).toString().toStdString()); - const QString replayName = query1.value(1).toString(); - matchInfo->set_do_not_hide(query1.value(6).toBool()); - - { - QSqlQuery query2(sqlInterface->getDatabase()); - query2.prepare("select player_name from cockatrice_games_players where id_game = :id_game"); - query2.bindValue(":id_game", gameId); - sqlInterface->execSqlQuery(query2); - while (query2.next()) - matchInfo->add_player_names(query2.value(0).toString().toStdString()); - } - { - QSqlQuery query3(sqlInterface->getDatabase()); - query3.prepare("select id, duration from " + servatrice->getDbPrefix() + "_replays where id_game = :id_game"); - query3.bindValue(":id_game", gameId); - sqlInterface->execSqlQuery(query3); - while (query3.next()) { - ServerInfo_Replay *replayInfo = matchInfo->add_replay_list(); - replayInfo->set_replay_id(query3.value(0).toInt()); - replayInfo->set_replay_name(replayName.toStdString()); - replayInfo->set_duration(query3.value(1).toInt()); - } - } - } - - rc.setResponseExtension(re); - return Response::RespOk; + if (authState != PasswordRight) + return Response::RespFunctionNotAllowed; + + DeckList *deck; + try { + deck = sqlInterface->getDeckFromDatabase(cmd.deck_id(), userInfo->id()); + } catch (Response::ResponseCode &r) { + return r; + } + + Response_DeckDownload *re = new Response_DeckDownload; + re->set_deck(deck->writeToString_Native().toStdString()); + rc.setResponseExtension(re); + delete deck; + + return Response::RespOk; } -Response::ResponseCode ServerSocketInterface::cmdReplayDownload(const Command_ReplayDownload &cmd, ResponseContainer &rc) +Response::ResponseCode AbstractServerSocketInterface::cmdReplayList(const Command_ReplayList & /*cmd*/, + ResponseContainer &rc) { - if (authState != PasswordRight) - return Response::RespFunctionNotAllowed; - - { - QSqlQuery query(sqlInterface->getDatabase()); - query.prepare("select 1 from " + servatrice->getDbPrefix() + "_replays_access a left join " + servatrice->getDbPrefix() + "_replays b on a.id_game = b.id_game where b.id = :id_replay and a.id_player = :id_player"); - query.bindValue(":id_replay", cmd.replay_id()); - query.bindValue(":id_player", userInfo->id()); - if (!sqlInterface->execSqlQuery(query)) - return Response::RespInternalError; - if (!query.next()) - return Response::RespAccessDenied; - } - - QSqlQuery query(sqlInterface->getDatabase()); - query.prepare("select replay from " + servatrice->getDbPrefix() + "_replays where id = :id_replay"); - query.bindValue(":id_replay", cmd.replay_id()); - if (!sqlInterface->execSqlQuery(query)) - return Response::RespInternalError; - if (!query.next()) - return Response::RespNameNotFound; - - QByteArray data = query.value(0).toByteArray(); - - Response_ReplayDownload *re = new Response_ReplayDownload; - re->set_replay_data(data.data(), data.size()); - rc.setResponseExtension(re); - - return Response::RespOk; + if (authState != PasswordRight) + return Response::RespFunctionNotAllowed; + + Response_ReplayList *re = new Response_ReplayList; + + QSqlQuery *query1 = sqlInterface->prepareQuery( + "select a.id_game, a.replay_name, b.room_name, b.time_started, b.time_finished, b.descr, a.do_not_hide from " + "{prefix}_replays_access a left join {prefix}_games b on b.id = a.id_game where a.id_player = :id_player and " + "(a.do_not_hide = 1 or date_add(b.time_started, interval 7 day) > now())"); + query1->bindValue(":id_player", userInfo->id()); + sqlInterface->execSqlQuery(query1); + while (query1->next()) { + ServerInfo_ReplayMatch *matchInfo = re->add_match_list(); + + const int gameId = query1->value(0).toInt(); + matchInfo->set_game_id(gameId); + matchInfo->set_room_name(query1->value(2).toString().toStdString()); + const int timeStarted = query1->value(3).toDateTime().toSecsSinceEpoch(); + const int timeFinished = query1->value(4).toDateTime().toSecsSinceEpoch(); + matchInfo->set_time_started(timeStarted); + matchInfo->set_length(timeFinished - timeStarted); + matchInfo->set_game_name(query1->value(5).toString().toStdString()); + const QString replayName = query1->value(1).toString(); + matchInfo->set_do_not_hide(query1->value(6).toBool()); + + { + QSqlQuery *query2 = + sqlInterface->prepareQuery("select player_name from {prefix}_games_players where id_game = :id_game"); + query2->bindValue(":id_game", gameId); + sqlInterface->execSqlQuery(query2); + while (query2->next()) + matchInfo->add_player_names(query2->value(0).toString().toStdString()); + } + { + QSqlQuery *query3 = + sqlInterface->prepareQuery("select id, duration from {prefix}_replays where id_game = :id_game"); + query3->bindValue(":id_game", gameId); + sqlInterface->execSqlQuery(query3); + while (query3->next()) { + ServerInfo_Replay *replayInfo = matchInfo->add_replay_list(); + replayInfo->set_replay_id(query3->value(0).toInt()); + replayInfo->set_replay_name(replayName.toStdString()); + replayInfo->set_duration(query3->value(1).toInt()); + } + } + } + + rc.setResponseExtension(re); + return Response::RespOk; } -Response::ResponseCode ServerSocketInterface::cmdReplayModifyMatch(const Command_ReplayModifyMatch &cmd, ResponseContainer & /*rc*/) +Response::ResponseCode AbstractServerSocketInterface::cmdReplayDownload(const Command_ReplayDownload &cmd, + ResponseContainer &rc) { - if (authState != PasswordRight) - return Response::RespFunctionNotAllowed; - - if (!sqlInterface->checkSql()) - return Response::RespInternalError; - - QSqlQuery query(sqlInterface->getDatabase()); - query.prepare("update " + servatrice->getDbPrefix() + "_replays_access set do_not_hide=:do_not_hide where id_player = :id_player and id_game = :id_game"); - query.bindValue(":id_player", userInfo->id()); - query.bindValue(":id_game", cmd.game_id()); - query.bindValue(":do_not_hide", cmd.do_not_hide()); - - if (!sqlInterface->execSqlQuery(query)) - return Response::RespInternalError; - return query.numRowsAffected() > 0 ? Response::RespOk : Response::RespNameNotFound; + if (authState != PasswordRight) + return Response::RespFunctionNotAllowed; + + { + QSqlQuery *query = + sqlInterface->prepareQuery("select 1 from {prefix}_replays_access a left join {prefix}_replays b on " + "a.id_game = b.id_game where b.id = :id_replay and a.id_player = :id_player"); + query->bindValue(":id_replay", cmd.replay_id()); + query->bindValue(":id_player", userInfo->id()); + if (!sqlInterface->execSqlQuery(query)) + return Response::RespInternalError; + if (!query->next()) + return Response::RespAccessDenied; + } + + QSqlQuery *query = sqlInterface->prepareQuery("select replay from {prefix}_replays where id = :id_replay"); + query->bindValue(":id_replay", cmd.replay_id()); + if (!sqlInterface->execSqlQuery(query)) + return Response::RespInternalError; + if (!query->next()) + return Response::RespNameNotFound; + + QByteArray data = query->value(0).toByteArray(); + + Response_ReplayDownload *re = new Response_ReplayDownload; + re->set_replay_data(data.data(), data.size()); + rc.setResponseExtension(re); + + return Response::RespOk; } -Response::ResponseCode ServerSocketInterface::cmdReplayDeleteMatch(const Command_ReplayDeleteMatch &cmd, ResponseContainer & /*rc*/) +Response::ResponseCode AbstractServerSocketInterface::cmdReplayModifyMatch(const Command_ReplayModifyMatch &cmd, + ResponseContainer & /*rc*/) { - if (authState != PasswordRight) - return Response::RespFunctionNotAllowed; - - if (!sqlInterface->checkSql()) - return Response::RespInternalError; - - QSqlQuery query(sqlInterface->getDatabase()); - query.prepare("delete from " + servatrice->getDbPrefix() + "_replays_access where id_player = :id_player and id_game = :id_game"); - query.bindValue(":id_player", userInfo->id()); - query.bindValue(":id_game", cmd.game_id()); - - if (!sqlInterface->execSqlQuery(query)) - return Response::RespInternalError; - return query.numRowsAffected() > 0 ? Response::RespOk : Response::RespNameNotFound; + if (authState != PasswordRight) + return Response::RespFunctionNotAllowed; + + if (!sqlInterface->checkSql()) + return Response::RespInternalError; + + QSqlQuery *query = sqlInterface->prepareQuery("update {prefix}_replays_access set do_not_hide=:do_not_hide where " + "id_player = :id_player and id_game = :id_game"); + query->bindValue(":id_player", userInfo->id()); + query->bindValue(":id_game", cmd.game_id()); + query->bindValue(":do_not_hide", cmd.do_not_hide()); + + if (!sqlInterface->execSqlQuery(query)) + return Response::RespInternalError; + return query->numRowsAffected() > 0 ? Response::RespOk : Response::RespNameNotFound; } +Response::ResponseCode AbstractServerSocketInterface::cmdReplayDeleteMatch(const Command_ReplayDeleteMatch &cmd, + ResponseContainer & /*rc*/) +{ + if (authState != PasswordRight) + return Response::RespFunctionNotAllowed; + + if (!sqlInterface->checkSql()) + return Response::RespInternalError; + + QSqlQuery *query = sqlInterface->prepareQuery( + "delete from {prefix}_replays_access where id_player = :id_player and id_game = :id_game"); + query->bindValue(":id_player", userInfo->id()); + query->bindValue(":id_game", cmd.game_id()); + + if (!sqlInterface->execSqlQuery(query)) + return Response::RespInternalError; + return query->numRowsAffected() > 0 ? Response::RespOk : Response::RespNameNotFound; +} + +/** + * Generates a hash for the given replay folder, used for auth when replay sharing. + * This is a separate function in case we change the hash implementation in the future. + * + * Currently, we append together the first 128 bytes of the first 3 replays in the game. + * Then we md5 hash it, base64 encode it, and truncate the result to 10 characters. + * + * @param gameId The replay match to hash + * @return The hash as a QString. Returns an empty string if failed + */ +QString AbstractServerSocketInterface::createHashForReplay(int gameId) +{ + QSqlQuery *query = + sqlInterface->prepareQuery("select replay from {prefix}_replays where id_game = :id_game limit 3"); + query->bindValue(":id_game", gameId); + + if (!sqlInterface->execSqlQuery(query)) + return ""; + + QByteArray replaysBytes; + while (query->next()) { + QByteArray replay = query->value(0).toByteArray(); + replay.truncate(128); + replaysBytes.append(replay); + } + + auto hash = + QCryptographicHash::hash(replaysBytes, QCryptographicHash::Md5).toBase64(QByteArray::OmitTrailingEquals); + hash.truncate(10); + return hash; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdReplayGetCode(const Command_ReplayGetCode &cmd, + ResponseContainer &rc) +{ + if (authState != PasswordRight) + return Response::RespFunctionNotAllowed; + + // Check that user has access to replay match + { + QSqlQuery *query = sqlInterface->prepareQuery( + "select 1 from {prefix}_replays_access where id_game = :id_game and id_player = :id_player"); + query->bindValue(":id_game", cmd.game_id()); + query->bindValue(":id_player", userInfo->id()); + if (!sqlInterface->execSqlQuery(query)) + return Response::RespInternalError; + if (!query->next()) + return Response::RespAccessDenied; + } + + QString hash = createHashForReplay(cmd.game_id()); + if (hash.isEmpty()) { + return Response::RespInternalError; + } + + // code is of the form - + QString code = QString(QString::number(cmd.game_id()) + "-" + hash); + + Response_ReplayGetCode *re = new Response_ReplayGetCode; + re->set_replay_code(code.toStdString()); + rc.setResponseExtension(re); + + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdReplaySubmitCode(const Command_ReplaySubmitCode &cmd, + ResponseContainer & /*rc*/) +{ + // code is of the form - + QString code = QString::fromStdString(cmd.replay_code()); + QStringList split = code.split("-"); + if (split.size() != 2) { + // always return the same error response if code is incorrect, to not leak info to user + return Response::RespNameNotFound; + } + QString gameId = split[0]; + QString hash = split[1]; + + // Determine if the replay actually exists (and grab the replay name while at it) + auto *replayExistsQuery = + sqlInterface->prepareQuery("select replay_name from {prefix}_replays_access where id_game = :id_game limit 1"); + replayExistsQuery->bindValue(":id_game", gameId); + if (!sqlInterface->execSqlQuery(replayExistsQuery)) { + return Response::RespInternalError; + } + if (!replayExistsQuery->next()) { + return Response::RespNameNotFound; + } + + const auto &replayName = replayExistsQuery->value(0).toString(); + + // Check if hash is correct + if (hash != createHashForReplay(gameId.toInt())) { + return Response::RespNameNotFound; + } + + // Determine if user already has access to replay + auto *alreadyAccessQuery = sqlInterface->prepareQuery( + "select 1 from {prefix}_replays_access where id_game = :id_game and id_player = :id_player"); + alreadyAccessQuery->bindValue(":id_game", gameId); + alreadyAccessQuery->bindValue(":id_player", userInfo->id()); + if (!sqlInterface->execSqlQuery(alreadyAccessQuery)) { + return Response::RespInternalError; + } + if (alreadyAccessQuery->next()) { + return Response::RespOk; + } + + // Grant the User access to the replay + auto *grantReplayAccessQuery = + sqlInterface->prepareQuery("insert into {prefix}_replays_access (id_game, id_player, replay_name, do_not_hide) " + "values(:idgame, :idplayer, :replayname, 0)"); + grantReplayAccessQuery->bindValue(":idgame", gameId); + grantReplayAccessQuery->bindValue(":idplayer", userInfo->id()); + grantReplayAccessQuery->bindValue(":replayname", replayName); + + if (!sqlInterface->execSqlQuery(grantReplayAccessQuery)) { + return Response::RespInternalError; + } + + // update user's view + Event_ReplayAdded event; + SessionEvent *se = prepareSessionEvent(event); + sendProtocolItem(*se); + delete se; + + return Response::RespOk; +} // MODERATOR FUNCTIONS. // May be called by admins and moderators. Permission is checked by the calling function. - -Response::ResponseCode ServerSocketInterface::cmdBanFromServer(const Command_BanFromServer &cmd, ResponseContainer & /*rc*/) +Response::ResponseCode AbstractServerSocketInterface::cmdGetLogHistory(const Command_ViewLogHistory &cmd, + ResponseContainer &rc) { - if (!sqlInterface->checkSql()) - return Response::RespInternalError; - - QString userName = QString::fromStdString(cmd.user_name()); - QString address = QString::fromStdString(cmd.address()); - int minutes = cmd.minutes(); - - QSqlQuery query(sqlInterface->getDatabase()); - query.prepare("insert into " + servatrice->getDbPrefix() + "_bans (user_name, ip_address, id_admin, time_from, minutes, reason, visible_reason) values(:user_name, :ip_address, :id_admin, NOW(), :minutes, :reason, :visible_reason)"); - query.bindValue(":user_name", userName); - query.bindValue(":ip_address", address); - query.bindValue(":id_admin", userInfo->id()); - query.bindValue(":minutes", minutes); - query.bindValue(":reason", QString::fromStdString(cmd.reason())); - query.bindValue(":visible_reason", QString::fromStdString(cmd.visible_reason())); - sqlInterface->execSqlQuery(query); - - servatrice->clientsLock.lockForRead(); - QList userList = servatrice->getUsersWithAddressAsList(QHostAddress(address)); - ServerSocketInterface *user = static_cast(server->getUsers().value(userName)); - if (user && !userList.contains(user)) - userList.append(user); - if (!userList.isEmpty()) { - Event_ConnectionClosed event; - event.set_reason(Event_ConnectionClosed::BANNED); - if (cmd.has_visible_reason()) - event.set_reason_str(cmd.visible_reason()); - if (minutes) - event.set_end_time(QDateTime::currentDateTime().addSecs(60 * minutes).toTime_t()); - for (int i = 0; i < userList.size(); ++i) { - SessionEvent *se = userList[i]->prepareSessionEvent(event); - userList[i]->sendProtocolItem(*se); - delete se; - QMetaObject::invokeMethod(userList[i], "prepareDestroy", Qt::QueuedConnection); - } - } - servatrice->clientsLock.unlock(); - - return Response::RespOk; + + QList messageList; + QString userName = nameFromStdString(cmd.user_name()); + QString ipAddress = nameFromStdString(cmd.ip_address()); + QString gameName = nameFromStdString(cmd.game_name()); + QString gameID = nameFromStdString(cmd.game_id()); + QString message = textFromStdString(cmd.message()); + bool chatType = false; + bool gameType = false; + bool roomType = false; + + for (int i = 0; i != cmd.log_location_size(); ++i) { + if (nameFromStdString(cmd.log_location(i)).simplified() == "room") + roomType = true; + if (nameFromStdString(cmd.log_location(i)).simplified() == "game") + gameType = true; + if (nameFromStdString(cmd.log_location(i)).simplified() == "chat") + chatType = true; + } + + int dateRange = cmd.date_range(); + int maximumResults = cmd.maximum_results(); + + Response_ViewLogHistory *re = new Response_ViewLogHistory; + + if (servatrice->getEnableLogQuery()) { + QListIterator messageIterator(sqlInterface->getMessageLogHistory( + userName, ipAddress, gameName, gameID, message, chatType, gameType, roomType, dateRange, maximumResults)); + while (messageIterator.hasNext()) + re->add_log_message()->CopyFrom(messageIterator.next()); + } else { + ServerInfo_ChatMessage chatMessage; + + // create dummy chat message for room tab in the event the query is for room messages (and possibly not others) + chatMessage.set_time(QString(tr("Log query disabled, please contact server owner for details.")).toStdString()); + chatMessage.set_sender_id(QString("").toStdString()); + chatMessage.set_sender_name(QString("").toStdString()); + chatMessage.set_sender_ip(QString("").toStdString()); + chatMessage.set_message(QString("").toStdString()); + chatMessage.set_target_type(QString("room").toStdString()); + chatMessage.set_target_id(QString("").toStdString()); + chatMessage.set_target_name(QString("").toStdString()); + messageList << chatMessage; + + // create dummy chat message for room tab in the event the query is for game messages (and possibly not others) + chatMessage.set_time(QString(tr("Log query disabled, please contact server owner for details.")).toStdString()); + chatMessage.set_sender_id(QString("").toStdString()); + chatMessage.set_sender_name(QString("").toStdString()); + chatMessage.set_sender_ip(QString("").toStdString()); + chatMessage.set_message(QString("").toStdString()); + chatMessage.set_target_type(QString("game").toStdString()); + chatMessage.set_target_id(QString("").toStdString()); + chatMessage.set_target_name(QString("").toStdString()); + messageList << chatMessage; + + // create dummy chat message for room tab in the event the query is for chat messages (and possibly not others) + chatMessage.set_time(QString(tr("Log query disabled, please contact server owner for details.")).toStdString()); + chatMessage.set_sender_id(QString("").toStdString()); + chatMessage.set_sender_name(QString("").toStdString()); + chatMessage.set_sender_ip(QString("").toStdString()); + chatMessage.set_message(QString("").toStdString()); + chatMessage.set_target_type(QString("chat").toStdString()); + chatMessage.set_target_id(QString("").toStdString()); + chatMessage.set_target_name(QString("").toStdString()); + messageList << chatMessage; + + QListIterator messageIterator(messageList); + while (messageIterator.hasNext()) + re->add_log_message()->CopyFrom(messageIterator.next()); + } + + rc.setResponseExtension(re); + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdGetBanHistory(const Command_GetBanHistory &cmd, + ResponseContainer &rc) +{ + QList banList; + QString userName = nameFromStdString(cmd.user_name()); + + Response_BanHistory *re = new Response_BanHistory; + QListIterator banIterator(sqlInterface->getUserBanHistory(userName)); + while (banIterator.hasNext()) + re->add_ban_list()->CopyFrom(banIterator.next()); + rc.setResponseExtension(re); + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdGetWarnList(const Command_GetWarnList &cmd, + ResponseContainer &rc) +{ + Response_WarnList *re = new Response_WarnList; + + QString officialWarnings = settingsCache->value("server/officialwarnings").toString(); + QStringList warningsList = officialWarnings.split(",", Qt::SkipEmptyParts); + for (const QString &warning : warningsList) { + re->add_warning(warning.toStdString()); + } + re->set_user_name(nameFromStdString(cmd.user_name()).toStdString()); + re->set_user_clientid(nameFromStdString(cmd.user_clientid()).toStdString()); + rc.setResponseExtension(re); + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdGetWarnHistory(const Command_GetWarnHistory &cmd, + ResponseContainer &rc) +{ + QList warnList; + QString userName = nameFromStdString(cmd.user_name()); + + Response_WarnHistory *re = new Response_WarnHistory; + QListIterator warnIterator(sqlInterface->getUserWarnHistory(userName)); + while (warnIterator.hasNext()) + re->add_warn_list()->CopyFrom(warnIterator.next()); + rc.setResponseExtension(re); + return Response::RespOk; +} + +void AbstractServerSocketInterface::removeSaidMessages(const QString &userName, int amount) +{ + for (auto *room : rooms.values()) { + room->removeSaidMessages(userName, amount); + } +} + +Response::ResponseCode AbstractServerSocketInterface::cmdWarnUser(const Command_WarnUser &cmd, + ResponseContainer & /*rc*/) +{ + if (!sqlInterface->checkSql()) { // sql database is required, without database there are no moderators anyway + return Response::RespInternalError; + } + + QString userName = nameFromStdString(cmd.user_name()).simplified(); + QString warningReason = textFromStdString(cmd.reason()).simplified(); + QString clientId = nameFromStdString(cmd.clientid()).simplified(); + QString sendingModerator = QString::fromStdString(userInfo->name()).simplified(); + int amountRemove = cmd.remove_messages(); + if (amountRemove != 0) { + removeSaidMessages(userName, amountRemove); + } + + if (sqlInterface->addWarning(userName, sendingModerator, warningReason, clientId)) { + servatrice->clientsLock.lockForRead(); + AbstractServerSocketInterface *user = + static_cast(server->getUsers().value(userName)); + QList moderatorList = server->getOnlineModeratorList(); + servatrice->clientsLock.unlock(); + + if (user != nullptr) { + Event_NotifyUser event; + event.set_type(Event_NotifyUser::WARNING); + event.set_warning_reason(warningReason.toStdString()); + SessionEvent *se = user->prepareSessionEvent(event); + user->sendProtocolItem(*se); + delete se; + } + + for (QString &moderator : moderatorList) { + QString notificationMessage = sendingModerator + " has sent a warning with the following information"; + notificationMessage.append("\n Username: " + userName); + notificationMessage.append("\n Reason: " + warningReason); + sendServerMessage(moderator.simplified(), notificationMessage); + } + + return Response::RespOk; + } else { + return Response::RespInternalError; + } +} + +Response::ResponseCode AbstractServerSocketInterface::cmdBanFromServer(const Command_BanFromServer &cmd, + ResponseContainer & /*rc*/) +{ + if (!sqlInterface->checkSql()) + return Response::RespInternalError; + + QString userName = nameFromStdString(cmd.user_name()).simplified(); + QString address = nameFromStdString(cmd.address()).simplified(); + QString clientId = nameFromStdString(cmd.clientid()).simplified(); + QString visibleReason = textFromStdString(cmd.visible_reason()); + + if (userName.isEmpty() && address.isEmpty() && clientId.isEmpty()) + return Response::RespOk; + + int amountRemove = cmd.remove_messages(); + if (amountRemove != 0) { + removeSaidMessages(userName, amountRemove); + } + QString trustedSources = settingsCache->value("server/trusted_sources", "127.0.0.1,::1").toString(); + int minutes = cmd.minutes(); + if (trustedSources.contains(address, Qt::CaseInsensitive)) + address = ""; + + QSqlQuery *query = sqlInterface->prepareQuery( + "insert into {prefix}_bans (user_name, ip_address, id_admin, time_from, minutes, reason, visible_reason, " + "clientid) values(:user_name, :ip_address, :id_admin, NOW(), :minutes, :reason, :visible_reason, :client_id)"); + query->bindValue(":user_name", userName); + query->bindValue(":ip_address", address); + query->bindValue(":id_admin", userInfo->id()); + query->bindValue(":minutes", minutes); + query->bindValue(":reason", textFromStdString(cmd.reason())); + query->bindValue(":visible_reason", visibleReason); + query->bindValue(":client_id", nameFromStdString(cmd.clientid())); + sqlInterface->execSqlQuery(query); + + servatrice->clientsLock.lockForRead(); + QList moderatorList = server->getOnlineModeratorList(); + QList userList = servatrice->getUsersWithAddressAsList(QHostAddress(address)); + + if (!userName.isEmpty()) { + AbstractServerSocketInterface *user = + static_cast(server->getUsers().value(userName)); + if (user && !userList.contains(user)) + userList.append(user); + } + + if (userName.isEmpty() && address.isEmpty() && (!clientId.isEmpty())) { + QSqlQuery *clientIdQuery = + sqlInterface->prepareQuery("select name from {prefix}_users where clientid = :client_id"); + clientIdQuery->bindValue(":client_id", nameFromStdString(cmd.clientid())); + sqlInterface->execSqlQuery(clientIdQuery); + if (!sqlInterface->execSqlQuery(clientIdQuery)) { + qCWarning(AbstractServerSocketInterfaceLog) << "ClientID username ban lookup failed: SQL Error"; + } else { + while (clientIdQuery->next()) { + userName = clientIdQuery->value(0).toString(); + AbstractServerSocketInterface *user = + static_cast(server->getUsers().value(userName)); + if (user && !userList.contains(user)) + userList.append(user); + } + } + } + servatrice->clientsLock.unlock(); + + if (!userList.isEmpty()) { + Event_ConnectionClosed event; + event.set_reason(Event_ConnectionClosed::BANNED); + if (cmd.has_visible_reason()) + event.set_reason_str(visibleReason.toStdString()); + if (minutes) + event.set_end_time(QDateTime::currentDateTime().addSecs(60 * minutes).toSecsSinceEpoch()); + for (int i = 0; i < userList.size(); ++i) { + SessionEvent *se = userList[i]->prepareSessionEvent(event); + userList[i]->sendProtocolItem(*se); + delete se; + QMetaObject::invokeMethod(userList[i], "prepareDestroy", Qt::QueuedConnection); + } + } + + for (QString &moderator : moderatorList) { + QString notificationMessage = + QString::fromStdString(userInfo->name()).simplified() + " has placed a ban with the following information"; + if (!userName.isEmpty()) + notificationMessage.append("\n Username: " + userName); + if (!address.isEmpty()) + notificationMessage.append("\n IP Address: " + address); + if (!clientId.isEmpty()) + notificationMessage.append("\n Client ID: " + clientId); + + notificationMessage.append("\n Length: " + QString::number(minutes) + " minute(s)"); + notificationMessage.append("\n Internal Reason: " + textFromStdString(cmd.reason())); + notificationMessage.append("\n Visible Reason: " + textFromStdString(cmd.visible_reason())); + sendServerMessage(moderator.simplified(), notificationMessage); + } + + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdRegisterAccount(const Command_Register &cmd, + ResponseContainer &rc) +{ + QString userName = nameFromStdString(cmd.user_name()); + QString clientId = nameFromStdString(cmd.clientid()); + qCDebug(AbstractServerSocketInterfaceLog) << "Got register command for user:" << userName; + + bool registrationEnabled = settingsCache->value("registration/enabled", false).toBool(); + if (!registrationEnabled) { + if (servatrice->getEnableRegistrationAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "REGISTER_ACCOUNT", "Server functionality disabled", false); + + return Response::RespRegistrationDisabled; + } + + const QString emailBlackList = servatrice->getEmailBlackList(); + const QString emailWhiteList = servatrice->getEmailWhiteList(); + const auto parsedEmailParts = EmailParser::parseEmailAddress(nameFromStdString(cmd.email())); + const auto emailUser = parsedEmailParts.first; + const auto emailDomain = parsedEmailParts.second; + const QStringList emailBlackListFilters = emailBlackList.split(",", Qt::SkipEmptyParts); + const QStringList emailWhiteListFilters = emailWhiteList.split(",", Qt::SkipEmptyParts); + + bool requireEmailForRegistration = settingsCache->value("registration/requireemail", true).toBool(); + if (requireEmailForRegistration && emailUser.isEmpty()) { + return Response::RespEmailRequiredToRegister; + } + + // If a whitelist exists, ensure the email address domain IS in the whitelist + if (!emailWhiteListFilters.isEmpty() && !emailWhiteListFilters.contains(emailDomain, Qt::CaseInsensitive)) { + if (servatrice->getEnableRegistrationAudit()) { + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "REGISTER_ACCOUNT", "Email used is not whitelisted", false); + } + auto *re = new Response_Register; + re->set_denied_reason_str( + "The email address provider used during registration has not been approved for use on this server."); + rc.setResponseExtension(re); + return Response::RespEmailBlackListed; + } + + // If a blacklist exists, ensure the email address domain is NOT in the blacklist + if (!emailBlackListFilters.isEmpty() && emailBlackListFilters.contains(emailDomain, Qt::CaseInsensitive)) { + if (servatrice->getEnableRegistrationAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "REGISTER_ACCOUNT", "Email used is blacklisted", false); + + return Response::RespEmailBlackListed; + } + + //! \todo Move this method outside of the db interface + QString errorString; + if (!sqlInterface->usernameIsValid(userName, errorString)) { + if (servatrice->getEnableRegistrationAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "REGISTER_ACCOUNT", "Username is invalid", false); + + Response_Register *re = new Response_Register; + re->set_denied_reason_str(errorString.toStdString()); + rc.setResponseExtension(re); + return Response::RespUsernameInvalid; + } + + if (userName.toLower().simplified() == "servatrice") { + if (servatrice->getEnableRegistrationAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "REGISTER_ACCOUNT", "Username is invalid", false); + + return Response::RespUsernameInvalid; + } + + if (sqlInterface->userExists(userName)) { + if (servatrice->getEnableRegistrationAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "REGISTER_ACCOUNT", "Username already exists", false); + + return Response::RespUserAlreadyExists; + } + + const auto parsedEmailAddress = EmailParser::getParsedEmailAddress(parsedEmailParts); + if (servatrice->getMaxAccountsPerEmail() > 0 && + sqlInterface->checkNumberOfUserAccounts(parsedEmailAddress) >= servatrice->getMaxAccountsPerEmail()) { + if (servatrice->getEnableRegistrationAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "REGISTER_ACCOUNT", "Too many usernames registered with this email address", + false); + + return Response::RespTooManyRequests; + } + + QString banReason; + int banSecondsRemaining; + if (sqlInterface->checkUserIsBanned(this->getAddress(), userName, clientId, banReason, banSecondsRemaining)) { + if (servatrice->getEnableRegistrationAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "REGISTER_ACCOUNT", "User is banned", false); + + Response_Register *re = new Response_Register; + re->set_denied_reason_str(banReason.toStdString()); + if (banSecondsRemaining != 0) + re->set_denied_end_time(QDateTime::currentDateTime().addSecs(banSecondsRemaining).toSecsSinceEpoch()); + rc.setResponseExtension(re); + return Response::RespUserIsBanned; + } + + if (tooManyRegistrationAttempts(this->getAddress())) { + if (servatrice->getEnableRegistrationAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "REGISTER_ACCOUNT", "Too many registration attempts from this ip address", + false); + + return Response::RespTooManyRequests; + } + + QString realName = nameFromStdString(cmd.real_name()); + QString country = nameFromStdString(cmd.country()); + QString password; + bool passwordNeedsHash = false; + if (cmd.has_password()) { + if (cmd.password().length() > MAX_NAME_LENGTH) + return Response::RespRegistrationFailed; + password = QString::fromStdString(cmd.password()); + passwordNeedsHash = true; + if (!isPasswordLongEnough(password.length())) { + if (servatrice->getEnableRegistrationAudit()) { + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "REGISTER_ACCOUNT", "Password is too short", false); + } + return Response::RespPasswordTooShort; + } + } else if (cmd.hashed_password().length() > MAX_NAME_LENGTH) { + return Response::RespRegistrationFailed; + } else { + password = QString::fromStdString(cmd.hashed_password()); + } + + bool requireEmailActivation = settingsCache->value("registration/requireemailactivation", true).toBool(); + bool regSucceeded = sqlInterface->registerUser(userName, realName, password, passwordNeedsHash, parsedEmailAddress, + country, !requireEmailActivation); + + if (regSucceeded) { + qCDebug(AbstractServerSocketInterfaceLog) << "Accepted register command for user:" << userName; + if (requireEmailActivation) { + QSqlQuery *query = + sqlInterface->prepareQuery("insert into {prefix}_activation_emails (name) values(:name)"); + query->bindValue(":name", userName); + if (!sqlInterface->execSqlQuery(query)) + return Response::RespRegistrationFailed; + + if (servatrice->getEnableRegistrationAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "REGISTER_ACCOUNT", "", true); + + return Response::RespRegistrationAcceptedNeedsActivation; + } else { + + if (servatrice->getEnableRegistrationAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "REGISTER_ACCOUNT", "", true); + + return Response::RespRegistrationAccepted; + } + } else { + if (servatrice->getEnableRegistrationAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "REGISTER_ACCOUNT", "Unknown reason for failure", false); + + return Response::RespRegistrationFailed; + } +} + +bool AbstractServerSocketInterface::tooManyRegistrationAttempts(const QString &ipAddress) +{ + //! \todo implement + Q_UNUSED(ipAddress); + return false; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdActivateAccount(const Command_Activate &cmd, + ResponseContainer & /*rc*/) +{ + QString userName = nameFromStdString(cmd.user_name()); + QString token = nameFromStdString(cmd.token()); + QString clientId = nameFromStdString(cmd.clientid()); + + if (clientId.isEmpty()) + clientId = "UNKNOWN"; + + if (sqlInterface->activateUser(userName, token)) { + qCDebug(AbstractServerSocketInterfaceLog) << "Accepted activation for user" << userName; + + if (servatrice->getEnableRegistrationAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "ACTIVATE_ACCOUNT", "", true); + + return Response::RespActivationAccepted; + } else { + qCDebug(AbstractServerSocketInterfaceLog) << "Failed activation for user" << userName; + + if (servatrice->getEnableRegistrationAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "ACTIVATE_ACCOUNT", "Failed to activate account, incorrect activation token", + false); + + return Response::RespActivationFailed; + } +} + +Response::ResponseCode AbstractServerSocketInterface::cmdAccountEdit(const Command_AccountEdit &cmd, + ResponseContainer & /* rc */) +{ + if (authState != PasswordRight) + return Response::RespFunctionNotAllowed; + + QString realName = nameFromStdString(cmd.real_name()); + const auto parsedEmailAddress = EmailParser::getParsedEmailAddress(nameFromStdString(cmd.email())); + QString country = nameFromStdString(cmd.country()); + + bool checkedPassword = false; + QString userName = QString::fromStdString(userInfo->name()); + if (cmd.has_password_check()) { + if (cmd.password_check().length() > MAX_NAME_LENGTH) + return Response::RespWrongPassword; + QString password = QString::fromStdString(cmd.password_check()); + QString clientId = QString::fromStdString(userInfo->clientid()); + QString reasonStr{}; + int secondsLeft{}; + AuthenticationResult checkStatus = + databaseInterface->checkUserPassword(this, userName, password, clientId, reasonStr, secondsLeft, true); + if (checkStatus == PasswordRight) { + checkedPassword = true; + } else { + // the user already logged in with this info, the only change is their password + return Response::RespWrongPassword; + } + } + + QStringList queryList({}); + if (cmd.has_real_name()) { + queryList << "realname=:realName"; + } + if (cmd.has_email()) { + // a real password is required in order to change the email address + if (usingRealPassword || checkedPassword) { + queryList << "email=:email"; + } else { + return Response::RespFunctionNotAllowed; + } + } + if (cmd.has_country()) { + queryList << "country=:country"; + } + + if (queryList.isEmpty()) + return Response::RespOk; + + QString queryText = QString("update {prefix}_users set %1 where name=:userName").arg(queryList.join(", ")); + QSqlQuery *query = sqlInterface->prepareQuery(queryText); + if (cmd.has_real_name()) { + auto _realName = nameFromStdString(cmd.real_name()); + query->bindValue(":realName", _realName); + } + if (cmd.has_email()) { + const auto _parsedEmailAddress = EmailParser::getParsedEmailAddress(nameFromStdString(cmd.email())); + query->bindValue(":email", _parsedEmailAddress); + } + if (cmd.has_country()) { + auto _country = nameFromStdString(cmd.country()); + query->bindValue(":country", _country); + } + query->bindValue(":userName", userName); + + if (!sqlInterface->execSqlQuery(query)) + return Response::RespInternalError; + + if (cmd.has_real_name()) { + userInfo->set_real_name(realName.toStdString()); + } + if (cmd.has_email()) { + userInfo->set_email(parsedEmailAddress.toStdString()); + } + if (cmd.has_country()) { + userInfo->set_country(country.toStdString()); + } + + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdAccountImage(const Command_AccountImage &cmd, + ResponseContainer & /* rc */) +{ + if (authState != PasswordRight) + return Response::RespFunctionNotAllowed; + + size_t length = qMin(cmd.image().length(), (size_t)MAX_FILE_LENGTH); + QByteArray image(cmd.image().c_str(), length); + int id = userInfo->id(); + + QSqlQuery *query = sqlInterface->prepareQuery("update {prefix}_users set avatar_bmp=:image where id=:id"); + query->bindValue(":image", image); + query->bindValue(":id", id); + if (!sqlInterface->execSqlQuery(query)) + return Response::RespInternalError; + + userInfo->set_avatar_bmp(cmd.image().c_str(), length); + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdAccountPassword(const Command_AccountPassword &cmd, + ResponseContainer & /* rc */) +{ + if (authState != PasswordRight) + return Response::RespFunctionNotAllowed; + + if (cmd.old_password().length() > MAX_NAME_LENGTH) + return Response::RespWrongPassword; + QString oldPassword = QString::fromStdString(cmd.old_password()); + QString newPassword; + bool newPasswordNeedsHash = false; + if (cmd.has_new_password()) { + if (cmd.new_password().length() > MAX_NAME_LENGTH) + return Response::RespContextError; + newPassword = QString::fromStdString(cmd.new_password()); + newPasswordNeedsHash = true; + if (!isPasswordLongEnough(newPassword.length())) + return Response::RespPasswordTooShort; + } else if (cmd.hashed_new_password().length() > MAX_NAME_LENGTH) { + return Response::RespContextError; + } else { + newPassword = QString::fromStdString(cmd.hashed_new_password()); + } + + QString userName = QString::fromStdString(userInfo->name()); + if (!databaseInterface->changeUserPassword(userName, oldPassword, true, newPassword, newPasswordNeedsHash)) + return Response::RespWrongPassword; + + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdForgotPasswordRequest(const Command_ForgotPasswordRequest &cmd, + ResponseContainer &rc) +{ + const QString userName = nameFromStdString(cmd.user_name()); + const QString clientId = nameFromStdString(cmd.clientid()); + + qCDebug(AbstractServerSocketInterfaceLog) << "Received reset password request from user:" << userName; + + if (!servatrice->getEnableForgotPassword()) { + if (servatrice->getEnableForgotPasswordAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "PASSWORD_RESET_REQUEST", "Server functionality disabled", false); + + return Response::RespFunctionNotAllowed; + } + + if (servatrice->getEnableForgotPasswordChallenge()) { + Response_ForgotPasswordRequest *re = new Response_ForgotPasswordRequest; + re->set_challenge_email(true); + rc.setResponseExtension(re); + return Response::RespOk; + } + + if (!sqlInterface->userExists(userName)) { + if (servatrice->getEnableForgotPasswordAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "PASSWORD_RESET_REQUEST", "User does not exist", false); + + return Response::RespFunctionNotAllowed; + } + + return continuePasswordRequest(userName, clientId, rc); +} + +Response::ResponseCode AbstractServerSocketInterface::continuePasswordRequest(const QString &userName, + const QString &clientId, + ResponseContainer &rc, + bool challenged) +{ + if (sqlInterface->doesForgotPasswordExist(userName)) { + + if (servatrice->getEnableForgotPasswordAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "PASSWORD_RESET_REQUEST", "Request already exists", true); + + Response_ForgotPasswordRequest *re = new Response_ForgotPasswordRequest; + re->set_challenge_email(false); + rc.setResponseExtension(re); + return Response::RespOk; + } + + QString banReason; + int banTimeRemaining; + if (sqlInterface->checkUserIsBanned(this->getAddress(), userName, clientId, banReason, banTimeRemaining)) { + if (servatrice->getEnableForgotPasswordAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "PASSWORD_RESET_REQUEST", "User is banned", false); + + return Response::RespFunctionNotAllowed; + } + + if (!sqlInterface->addForgotPassword(userName)) { + if (servatrice->getEnableForgotPasswordAudit()) { + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "PASSWORD_RESET_REQUEST", "Failed to create password reset", false); + } + return Response::RespFunctionNotAllowed; + } + + if (servatrice->getEnableForgotPasswordAudit()) { + QString details = + challenged ? "Request does not exist, challenge passed" : "Request does not exist, challenge not requested"; + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "PASSWORD_RESET_REQUEST", details, true); + } + + Response_ForgotPasswordRequest *re = new Response_ForgotPasswordRequest; + re->set_challenge_email(false); + rc.setResponseExtension(re); + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdForgotPasswordReset(const Command_ForgotPasswordReset &cmd, + ResponseContainer &rc) +{ + Q_UNUSED(rc); + QString userName = nameFromStdString(cmd.user_name()); + QString clientId = nameFromStdString(cmd.clientid()); + qCDebug(AbstractServerSocketInterfaceLog) << "Received reset password reset from user:" << userName; + + if (!sqlInterface->doesForgotPasswordExist(userName)) { + if (servatrice->getEnableForgotPasswordAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "PASSWORD_RESET", "Request does not exist for user", false); + + return Response::RespFunctionNotAllowed; + } + + if (!sqlInterface->validateTableColumnStringData("{prefix}_users", "token", userName, + nameFromStdString(cmd.token()))) { + if (servatrice->getEnableForgotPasswordAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), userName.simplified(), + "PASSWORD_RESET", "Failed token validation", false); + + return Response::RespFunctionNotAllowed; + } + + QString password; + bool passwordNeedsHash = false; + if (cmd.has_new_password()) { + if (cmd.new_password().length() > MAX_NAME_LENGTH) + return Response::RespContextError; + password = QString::fromStdString(cmd.new_password()); + passwordNeedsHash = true; + } else if (cmd.hashed_new_password().length() > MAX_NAME_LENGTH) { + return Response::RespContextError; + } else { + password = QString::fromStdString(cmd.hashed_new_password()); + } + + if (sqlInterface->changeUserPassword(nameFromStdString(cmd.user_name()), password, passwordNeedsHash)) { + if (servatrice->getEnableForgotPasswordAudit()) + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "PASSWORD_RESET", "", true); + + sqlInterface->removeForgotPassword(nameFromStdString(cmd.user_name())); + return Response::RespOk; + } + + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode +AbstractServerSocketInterface::cmdForgotPasswordChallenge(const Command_ForgotPasswordChallenge &cmd, + ResponseContainer &rc) +{ + const QString userName = nameFromStdString(cmd.user_name()); + const QString clientId = nameFromStdString(cmd.clientid()); + + qCDebug(AbstractServerSocketInterfaceLog) << "Received reset password challenge from user:" << userName; + + if (!servatrice->getEnableForgotPasswordChallenge()) { + if (servatrice->getEnableForgotPasswordAudit()) { + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "PASSWORD_RESET_CHALLENGE", "Feature not enabled", false); + } + return Response::RespFunctionNotAllowed; + } + + if (!sqlInterface->userExists(userName)) { + if (servatrice->getEnableForgotPasswordAudit()) { + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "PASSWORD_RESET_CHALLENGE", "User does not exist", false); + } + return Response::RespFunctionNotAllowed; + } + + const auto parsedEmailAddress = EmailParser::getParsedEmailAddress(nameFromStdString(cmd.email())); + if (!sqlInterface->validateTableColumnStringData("{prefix}_users", "email", userName, parsedEmailAddress)) { + if (servatrice->getEnableForgotPasswordAudit()) { + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "PASSWORD_RESET_CHALLENGE", "Failed to answer email challenge question", + false); + } + return Response::RespFunctionNotAllowed; + } + + if (servatrice->getEnableForgotPasswordAudit()) { + sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), + "PASSWORD_RESET_CHALLENGE", "", true); + } + return continuePasswordRequest(userName, clientId, rc, true); +} + +Response::ResponseCode AbstractServerSocketInterface::cmdRequestPasswordSalt(const Command_RequestPasswordSalt &cmd, + ResponseContainer &rc) +{ + const QString userName = nameFromStdString(cmd.user_name()); + QString passwordSalt = sqlInterface->getUserSalt(userName); + if (passwordSalt.isEmpty()) { + if (server->getRegOnlyServerEnabled()) { + return Response::RespRegistrationRequired; + } else { + // user does not exist but is allowed to log in unregistered without password + return Response::RespOk; + } + } + auto *re = new Response_PasswordSalt; + re->set_password_salt(passwordSalt.toStdString()); + rc.setResponseExtension(re); + return Response::RespOk; } // ADMIN FUNCTIONS. // Permission is checked by the calling function. -Response::ResponseCode ServerSocketInterface::cmdUpdateServerMessage(const Command_UpdateServerMessage & /*cmd*/, ResponseContainer & /*rc*/) +Response::ResponseCode +AbstractServerSocketInterface::cmdUpdateServerMessage(const Command_UpdateServerMessage & /*cmd*/, + ResponseContainer & /*rc*/) { - QMetaObject::invokeMethod(server, "updateLoginMessage"); - return Response::RespOk; + QMetaObject::invokeMethod(server, "updateLoginMessage"); + return Response::RespOk; } -Response::ResponseCode ServerSocketInterface::cmdShutdownServer(const Command_ShutdownServer &cmd, ResponseContainer & /*rc*/) +Response::ResponseCode AbstractServerSocketInterface::cmdShutdownServer(const Command_ShutdownServer &cmd, + ResponseContainer & /*rc*/) { - QMetaObject::invokeMethod(server, "scheduleShutdown", Q_ARG(QString, QString::fromStdString(cmd.reason())), Q_ARG(int, cmd.minutes())); - return Response::RespOk; + QMetaObject::invokeMethod(server, "scheduleShutdown", Q_ARG(QString, textFromStdString(cmd.reason())), + Q_ARG(int, cmd.minutes())); + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdReloadConfig(const Command_ReloadConfig & /* cmd */, + ResponseContainer & /*rc*/) +{ + logDebugMessage("Received admin command: reloading configuration"); + settingsCache->sync(); + QMetaObject::invokeMethod(server, "setRequiredFeatures", Q_ARG(QString, server->getRequiredFeatures())); + return Response::RespOk; +} + +bool AbstractServerSocketInterface::addAdminFlagToUser(const QString &userName, int flag) +{ + QSqlQuery *query = + sqlInterface->prepareQuery("update {prefix}_users set admin = (admin | :adminlevel) where name = :username"); + query->bindValue(":adminlevel", flag); + query->bindValue(":username", userName); + if (!sqlInterface->execSqlQuery(query)) { + logger->logMessage(QString("Failed to promote user %1: %2").arg(userName).arg(query->lastError().text())); + return false; + } + + AbstractServerSocketInterface *user = + static_cast(server->getUsers().value(userName)); + if (user) { + Event_NotifyUser event; + event.set_type(Event_NotifyUser::PROMOTED); + SessionEvent *se = user->prepareSessionEvent(event); + user->sendProtocolItem(*se); + delete se; + } + + return true; +} + +bool AbstractServerSocketInterface::removeAdminFlagFromUser(const QString &userName, int flag) +{ + QSqlQuery *query = + sqlInterface->prepareQuery("update {prefix}_users set admin = (admin & ~ :adminlevel) where name = :username"); + query->bindValue(":adminlevel", flag); + query->bindValue(":username", userName); + if (!sqlInterface->execSqlQuery(query)) { + logger->logMessage(QString("Failed to demote user %1: %2").arg(userName).arg(query->lastError().text())); + return false; + } + + AbstractServerSocketInterface *user = + static_cast(server->getUsers().value(userName)); + if (user) { + Event_ConnectionClosed event; + event.set_reason(Event_ConnectionClosed::DEMOTED); + event.set_reason_str("Your moderator and/or judge status has been revoked."); + event.set_end_time(QDateTime::currentDateTime().toSecsSinceEpoch()); + + SessionEvent *se = user->prepareSessionEvent(event); + user->sendProtocolItem(*se); + delete se; + } + + QMetaObject::invokeMethod(user, "prepareDestroy", Qt::QueuedConnection); + return true; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdAdjustMod(const Command_AdjustMod &cmd, + ResponseContainer & /*rc*/) +{ + + QString userName = nameFromStdString(cmd.user_name()); + + if (cmd.has_should_be_mod()) { + if (cmd.should_be_mod()) { + if (!addAdminFlagToUser(userName, 2)) { + return Response::RespInternalError; + } + } else { + if (!removeAdminFlagFromUser(userName, 2)) { + return Response::RespInternalError; + } + } + } + + if (cmd.has_should_be_judge()) { + if (cmd.should_be_judge()) { + if (!addAdminFlagToUser(userName, 4)) { + return Response::RespInternalError; + } + } else { + if (!removeAdminFlagFromUser(userName, 4)) { + return Response::RespInternalError; + } + } + } + + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdGrantReplayAccess(const Command_GrantReplayAccess &cmd, + ResponseContainer & /*rc*/) +{ + // Determine if the replay actually exists already + auto *replayExistsQuery = + sqlInterface->prepareQuery("select count(*) from {prefix}_replays_access where id_game = :idgame"); + replayExistsQuery->bindValue(":idgame", cmd.replay_id()); + if (!sqlInterface->execSqlQuery(replayExistsQuery)) { + return Response::RespInternalError; + } + if (!replayExistsQuery->next()) { + return Response::RespInternalError; + } + + const auto &replayExists = replayExistsQuery->value(0).toInt() > 0; + if (!replayExists) { + return Response::RespContextError; + } + + // Determine the Moderator's User ID (As it's not apart of client, only username is) + auto *getModeratorUserIdQuery = sqlInterface->prepareQuery("select id from {prefix}_users WHERE name = :name"); + getModeratorUserIdQuery->bindValue(":name", QString::fromStdString(cmd.moderator_name())); + if (!sqlInterface->execSqlQuery(getModeratorUserIdQuery)) { + return Response::RespInternalError; + } + if (!getModeratorUserIdQuery->next()) { + return Response::RespInternalError; + } + + const auto &moderator_id = getModeratorUserIdQuery->value(0).toString(); + + // Grant the Moderator access to the replay + auto *grantReplayAccessQuery = + sqlInterface->prepareQuery("insert into {prefix}_replays_access (id_game, id_player, replay_name, do_not_hide) " + "values(:idgame, :idplayer, :replayname, 0)"); + grantReplayAccessQuery->bindValue(":idgame", cmd.replay_id()); + grantReplayAccessQuery->bindValue(":idplayer", moderator_id); + grantReplayAccessQuery->bindValue(":replayname", "Moderator Access Replay Grant"); + + if (!sqlInterface->execSqlQuery(grantReplayAccessQuery)) { + return Response::RespInternalError; + } + + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdForceActivateUser(const Command_ForceActivateUser &cmd, + ResponseContainer &rc) +{ + // Determine if user exists + auto *getUserTokenQuery = sqlInterface->prepareQuery("select token from {prefix}_users WHERE name = :name"); + getUserTokenQuery->bindValue(":name", QString::fromStdString(cmd.username_to_activate())); + if (!sqlInterface->execSqlQuery(getUserTokenQuery)) { + // Internal server error + return Response::RespInternalError; + } + if (!getUserTokenQuery->next()) { + // User doesn't exist + return Response::RespNameNotFound; + } + const auto &token = getUserTokenQuery->value(0).toString(); + + // Add audit log that Moderator activated account on behalf of user + const auto &msg = QString("Attempt Force Activation by %1").arg(QString::fromStdString(cmd.moderator_name())); + sqlInterface->addAuditRecord(QString::fromStdString(cmd.username_to_activate()), this->getAddress(), "UNKNOWN", + "ACTIVATE_ACCOUNT", msg, true); + + // Build up activation request + Command_Activate cmdActivate; + cmdActivate.set_user_name(cmd.username_to_activate()); + cmdActivate.set_token(token.toStdString()); + + // Send activation request -- Either User exists or User activated + return cmdActivateAccount(cmdActivate, rc); +} + +Response::ResponseCode AbstractServerSocketInterface::cmdGetAdminNotes(const Command_GetAdminNotes &cmd, + ResponseContainer &rc) +{ + auto *getAdminNotesQuery = sqlInterface->prepareQuery("select adminnotes from {prefix}_users WHERE name = :name"); + getAdminNotesQuery->bindValue(":name", QString::fromStdString(cmd.user_name())); + if (!sqlInterface->execSqlQuery(getAdminNotesQuery)) { + // Internal server error + return Response::RespInternalError; + } + if (!getAdminNotesQuery->next()) { + // User doesn't exist + return Response::RespNameNotFound; + } + const auto &adminNotes = getAdminNotesQuery->value(0).toString(); + + Response_GetAdminNotes *re = new Response_GetAdminNotes; + re->set_user_name(cmd.user_name()); + re->set_notes(adminNotes.toStdString()); + rc.setResponseExtension(re); + + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdUpdateAdminNotes(const Command_UpdateAdminNotes &cmd, + ResponseContainer & /*rc*/) +{ + auto *updateAdminNotesQuery = + sqlInterface->prepareQuery("update {prefix}_users set adminnotes = :adminnotes where name = :name"); + updateAdminNotesQuery->bindValue(":adminnotes", QString::fromStdString(cmd.notes())); + updateAdminNotesQuery->bindValue(":name", QString::fromStdString(cmd.user_name())); + + if (!sqlInterface->execSqlQuery(updateAdminNotesQuery)) { + return Response::RespInternalError; + } + + if (updateAdminNotesQuery->numRowsAffected() == 0) { + return Response::RespNameNotFound; + } + + return Response::RespOk; +} + +TcpServerSocketInterface::TcpServerSocketInterface(Servatrice *_server, + Servatrice_DatabaseInterface *_databaseInterface, + QObject *parent) + : AbstractServerSocketInterface(_server, _databaseInterface, parent), messageInProgress(false), + handshakeStarted(false) +{ + socket = new QTcpSocket(this); + socket->setSocketOption(QAbstractSocket::LowDelayOption, 1); + connect(socket, SIGNAL(readyRead()), this, SLOT(readClient())); + connect(socket, SIGNAL(disconnected()), this, SLOT(catchSocketDisconnected())); + connect(socket, SIGNAL(errorOccurred(QAbstractSocket::SocketError)), this, + SLOT(catchSocketError(QAbstractSocket::SocketError))); +} + +TcpServerSocketInterface::~TcpServerSocketInterface() +{ + logger->logMessage("TcpServerSocketInterface destructor", this); + + flushOutputQueue(); +} + +void TcpServerSocketInterface::initConnection(int socketDescriptor) +{ + // Add this object to the server's list of connections before it can receive socket events. + // Otherwise, in case a of a socket error, it could be removed from the list before it is added. + server->addClient(this); + + socket->setSocketDescriptor(socketDescriptor); + logger->logMessage(QString("Incoming connection: %1").arg(socket->peerAddress().toString()), this); + initSessionDeprecated(); +} + +void TcpServerSocketInterface::initSessionDeprecated() +{ + // dirty hack to make v13 client display the correct error message + + QByteArray buf; + buf.append(""); + writeToSocket(buf); + flushSocket(); +} + +void TcpServerSocketInterface::flushOutputQueue() +{ + QMutexLocker locker(&outputQueueMutex); + if (outputQueue.isEmpty()) + return; + + int totalBytes = 0; + while (!outputQueue.isEmpty()) { + ServerMessage item = outputQueue.takeFirst(); + locker.unlock(); + + QByteArray buf; +#if GOOGLE_PROTOBUF_VERSION > 3001000 + unsigned int size = static_cast(item.ByteSizeLong()); +#else + unsigned int size = static_cast(item.ByteSize()); +#endif + buf.resize(size + 4); + if (item.SerializeToArray(buf.data() + 4, size)) { + buf.data()[3] = (unsigned char)size; + buf.data()[2] = (unsigned char)(size >> 8); + buf.data()[1] = (unsigned char)(size >> 16); + buf.data()[0] = (unsigned char)(size >> 24); + // In case socket->write() calls catchSocketError(), the mutex must not be locked during this call. + writeToSocket(buf); + } else { + qCWarning(TcpServerSocketInterfaceLog) << "serialisation error!"; + } + + totalBytes += size + 4; + locker.relock(); + } + locker.unlock(); + emit incTxBytes(totalBytes); + // see above wrt mutex + flushSocket(); +} + +void TcpServerSocketInterface::readClient() +{ + QByteArray data = socket->readAll(); + servatrice->incRxBytes(data.size()); + inputBuffer.append(data); + + do { + if (!messageInProgress) { + if (inputBuffer.size() >= 4) { + messageLength = (((quint32)(unsigned char)inputBuffer[0]) << 24) + + (((quint32)(unsigned char)inputBuffer[1]) << 16) + + (((quint32)(unsigned char)inputBuffer[2]) << 8) + + ((quint32)(unsigned char)inputBuffer[3]); + inputBuffer.remove(0, 4); + messageInProgress = true; + } else + return; + } + if (inputBuffer.size() < messageLength || messageLength < 0) + return; + + CommandContainer newCommandContainer; + bool ok; + try { + ok = newCommandContainer.ParseFromArray(inputBuffer.data(), messageLength); + } catch (std::exception &e) { + qCWarning(TcpServerSocketInterfaceLog) << "Caught std::exception in" << __FILE__ << __LINE__ << +#ifdef _MSC_VER // Visual Studio + __FUNCTION__ +#else + __PRETTY_FUNCTION__ +#endif + << Qt::endl + << "Exception:" << e.what() << Qt::endl + << "Message coming from:" << getAddress() << Qt::endl + << "Message length:" << messageLength << Qt::endl + << "Message content:" << inputBuffer.toHex(); + } catch (...) { + qCWarning(TcpServerSocketInterfaceLog) << "Unhandled exception in" << __FILE__ << __LINE__ << +#ifdef _MSC_VER // Visual Studio + __FUNCTION__ +#else + __PRETTY_FUNCTION__ +#endif + << Qt::endl + << "Message coming from:" << getAddress(); + } + inputBuffer.remove(0, messageLength); + messageInProgress = false; + + if (ok) { + // dirty hack to make v13 client display the correct error message + if (handshakeStarted) + processCommandContainer(newCommandContainer); + else if (!newCommandContainer.has_cmd_id()) { + handshakeStarted = true; + if (!initTcpSession()) + prepareDestroy(); + } + // end of hack + } else { + qCWarning(TcpServerSocketInterfaceLog) << "parsing error!"; + } + + } while (!inputBuffer.isEmpty()); +} + +bool TcpServerSocketInterface::initTcpSession() +{ + if (!initSession()) + return false; + + // limit the number of websocket users based on configuration settings + bool enforceUserLimit = settingsCache->value("security/enable_max_user_limit", false).toBool(); + if (enforceUserLimit) { + int userLimit = settingsCache->value("security/max_users_tcp", 500).toInt(); + int playerCount = (server->getTCPUserCount() + 1); + if (playerCount > userLimit) { + std::cerr << "Max Tcp Users Limit Reached, please increase the max_users_tcp setting." << std::endl; + logger->logMessage(QString("Max Tcp Users Limit Reached, please increase the max_users_tcp setting."), + this); + Event_ConnectionClosed event; + event.set_reason(Event_ConnectionClosed::USER_LIMIT_REACHED); + SessionEvent *se = prepareSessionEvent(event); + sendProtocolItem(*se); + delete se; + return false; + } + } + + return true; +} + +WebsocketServerSocketInterface::WebsocketServerSocketInterface(Servatrice *_server, + Servatrice_DatabaseInterface *_databaseInterface, + QObject *parent) + : AbstractServerSocketInterface(_server, _databaseInterface, parent), socket(nullptr) +{ +} + +WebsocketServerSocketInterface::~WebsocketServerSocketInterface() +{ + logger->logMessage("WebsocketServerSocketInterface destructor", this); + + flushOutputQueue(); +} + +void WebsocketServerSocketInterface::initConnection(void *_socket) +{ + if (_socket == nullptr) { + return; + } + socket = (QWebSocket *)_socket; + socket->setParent(this); + // https://bugreports.qt.io/browse/QTBUG-70693 + socket->setMaxAllowedIncomingMessageSize(1500000); // 1.5MB + + address = socket->peerAddress(); + + QByteArray websocketIPHeader = settingsCache->value("server/web_socket_ip_header", "").toByteArray(); + if (websocketIPHeader.length() > 0 && socket->request().hasRawHeader(websocketIPHeader)) { + QString header(socket->request().rawHeader(websocketIPHeader)); + QHostAddress parsed(header); + if (!parsed.isNull()) { + address = parsed; + } + } + + connect(socket, SIGNAL(binaryMessageReceived(const QByteArray &)), this, + SLOT(binaryMessageReceived(const QByteArray &))); + connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, + SLOT(catchSocketError(QAbstractSocket::SocketError))); + connect(socket, SIGNAL(disconnected()), this, SLOT(catchSocketDisconnected())); + + // Add this object to the server's list of connections before it can receive socket events. + // Otherwise, in case of a socket error, it could be removed from the list before it is added. + server->addClient(this); + + logger->logMessage( + QString("Incoming websocket connection: %1 (%2)").arg(address.toString()).arg(socket->peerAddress().toString()), + this); + + if (!initWebsocketSession()) + prepareDestroy(); +} + +bool WebsocketServerSocketInterface::initWebsocketSession() +{ + if (!initSession()) + return false; + + // limit the number of websocket users based on configuration settings + bool enforceUserLimit = settingsCache->value("security/enable_max_user_limit", false).toBool(); + if (enforceUserLimit) { + int userLimit = settingsCache->value("security/max_users_websocket", 500).toInt(); + int playerCount = (server->getWebSocketUserCount() + 1); + if (playerCount > userLimit) { + std::cerr << "Max Websocket Users Limit Reached, please increase the max_users_websocket setting." + << std::endl; + logger->logMessage( + QString("Max Websocket Users Limit Reached, please increase the max_users_websocket setting."), this); + Event_ConnectionClosed event; + event.set_reason(Event_ConnectionClosed::USER_LIMIT_REACHED); + SessionEvent *se = prepareSessionEvent(event); + sendProtocolItem(*se); + delete se; + return false; + } + } + + return true; +} + +void WebsocketServerSocketInterface::flushOutputQueue() +{ + QMutexLocker locker(&outputQueueMutex); + if (outputQueue.isEmpty()) + return; + + qint64 totalBytes = 0; + while (!outputQueue.isEmpty()) { + ServerMessage item = outputQueue.takeFirst(); + locker.unlock(); + + QByteArray buf; +#if GOOGLE_PROTOBUF_VERSION > 3001000 + unsigned int size = static_cast(item.ByteSizeLong()); +#else + unsigned int size = static_cast(item.ByteSize()); +#endif + buf.resize(size); + if (item.SerializeToArray(buf.data(), size)) { + // In case socket->write() calls catchSocketError(), the mutex must not be locked during this call. + writeToSocket(buf); + } else { + qCWarning(TcpServerSocketInterfaceLog) << "serialisation error!"; + } + + totalBytes += size; + locker.relock(); + } + locker.unlock(); + emit incTxBytes(totalBytes); + // see above wrt mutex + flushSocket(); +} + +void WebsocketServerSocketInterface::binaryMessageReceived(const QByteArray &message) +{ + servatrice->incRxBytes(message.size()); + + CommandContainer newCommandContainer; + bool ok; + try { + ok = newCommandContainer.ParseFromArray(message.data(), message.size()); + } catch (std::exception &e) { + qCWarning(WebsocketServerSocketInterfaceLog) << "Caught std::exception in" << __FILE__ << __LINE__ << +#ifdef _MSC_VER // Visual Studio + __FUNCTION__ +#else + __PRETTY_FUNCTION__ +#endif + << Qt::endl + << "Exception:" << e.what() << Qt::endl + << "Message coming from:" << getAddress() << Qt::endl + << "Message length:" << message.size() << Qt::endl + << "Message content:" << message.toHex(); + } catch (...) { + qCWarning(WebsocketServerSocketInterfaceLog) << "Unhandled exception in" << __FILE__ << __LINE__ << +#ifdef _MSC_VER // Visual Studio + __FUNCTION__ +#else + __PRETTY_FUNCTION__ +#endif + << Qt::endl + << "Message coming from:" << getAddress(); + } + + if (ok) { + processCommandContainer(newCommandContainer); + } else { + qCWarning(WebsocketServerSocketInterfaceLog) << "parsing error!"; + } +} + +bool AbstractServerSocketInterface::isPasswordLongEnough(const int passwordLength) +{ + return passwordLength >= servatrice->getMinPasswordLength(); } diff --git a/servatrice/src/serversocketinterface.h b/servatrice/src/serversocketinterface.h index 6a2a41f8e..e10aa0dde 100644 --- a/servatrice/src/serversocketinterface.h +++ b/servatrice/src/serversocketinterface.h @@ -20,12 +20,12 @@ #ifndef SERVERSOCKETINTERFACE_H #define SERVERSOCKETINTERFACE_H -#include #include #include -#include "server_protocolhandler.h" +#include +#include +#include -class QTcpSocket; class Servatrice; class Servatrice_DatabaseInterface; class DeckList; @@ -43,69 +43,208 @@ class Command_ReplayList; class Command_ReplayDownload; class Command_ReplayModifyMatch; class Command_ReplayDeleteMatch; +class Command_ReplayGetCode; +class Command_ReplaySubmitCode; class Command_BanFromServer; class Command_UpdateServerMessage; class Command_ShutdownServer; +class Command_ReloadConfig; -class ServerSocketInterface : public Server_ProtocolHandler +class Command_AccountEdit; +class Command_AccountImage; +class Command_AccountPassword; + +class AbstractServerSocketInterface : public Server_ProtocolHandler { - Q_OBJECT -private slots: - void readClient(); - void catchSocketError(QAbstractSocket::SocketError socketError); - void flushOutputQueue(); + Q_OBJECT +protected slots: + void catchSocketError(QAbstractSocket::SocketError socketError); + void catchSocketDisconnected(); + virtual void flushOutputQueue() = 0; signals: - void outputQueueChanged(); -protected: - void logDebugMessage(const QString &message); -private: - QMutex outputQueueMutex; - Servatrice *servatrice; - Servatrice_DatabaseInterface *sqlInterface; - QTcpSocket *socket; - - QByteArray inputBuffer; - QList outputQueue; - bool messageInProgress; - bool handshakeStarted; - int messageLength; - - Response::ResponseCode cmdAddToList(const Command_AddToList &cmd, ResponseContainer &rc); - Response::ResponseCode cmdRemoveFromList(const Command_RemoveFromList &cmd, ResponseContainer &rc); - int getDeckPathId(int basePathId, QStringList path); - int getDeckPathId(const QString &path); - bool deckListHelper(int folderId, ServerInfo_DeckStorage_Folder *folder); - Response::ResponseCode cmdDeckList(const Command_DeckList &cmd, ResponseContainer &rc); - Response::ResponseCode cmdDeckNewDir(const Command_DeckNewDir &cmd, ResponseContainer &rc); - void deckDelDirHelper(int basePathId); - Response::ResponseCode cmdDeckDelDir(const Command_DeckDelDir &cmd, ResponseContainer &rc); - Response::ResponseCode cmdDeckDel(const Command_DeckDel &cmd, ResponseContainer &rc); - Response::ResponseCode cmdDeckUpload(const Command_DeckUpload &cmd, ResponseContainer &rc); - DeckList *getDeckFromDatabase(int deckId); - Response::ResponseCode cmdDeckDownload(const Command_DeckDownload &cmd, ResponseContainer &rc); - Response::ResponseCode cmdReplayList(const Command_ReplayList &cmd, ResponseContainer &rc); - Response::ResponseCode cmdReplayDownload(const Command_ReplayDownload &cmd, ResponseContainer &rc); - Response::ResponseCode cmdReplayModifyMatch(const Command_ReplayModifyMatch &cmd, ResponseContainer &rc); - Response::ResponseCode cmdReplayDeleteMatch(const Command_ReplayDeleteMatch &cmd, ResponseContainer &rc); - Response::ResponseCode cmdBanFromServer(const Command_BanFromServer &cmd, ResponseContainer &rc); - Response::ResponseCode cmdShutdownServer(const Command_ShutdownServer &cmd, ResponseContainer &rc); - Response::ResponseCode cmdUpdateServerMessage(const Command_UpdateServerMessage &cmd, ResponseContainer &rc); - - Response::ResponseCode processExtendedSessionCommand(int cmdType, const SessionCommand &cmd, ResponseContainer &rc); - Response::ResponseCode processExtendedModeratorCommand(int cmdType, const ModeratorCommand &cmd, ResponseContainer &rc); - Response::ResponseCode processExtendedAdminCommand(int cmdType, const AdminCommand &cmd, ResponseContainer &rc); -public: - ServerSocketInterface(Servatrice *_server, Servatrice_DatabaseInterface *_databaseInterface, QObject *parent = 0); - ~ServerSocketInterface(); - void initSessionDeprecated(); - bool initSession(); - QHostAddress getPeerAddress() const { return socket->peerAddress(); } - QString getAddress() const { return socket->peerAddress().toString(); } + void outputQueueChanged(); + void incTxBytes(qint64 amount); - void transmitProtocolItem(const ServerMessage &item); +protected: + void logDebugMessage(const QString &message); + bool tooManyRegistrationAttempts(const QString &ipAddress); + + virtual void writeToSocket(QByteArray &data) = 0; + virtual void flushSocket() = 0; + + Servatrice *servatrice; + QList outputQueue; + QMutex outputQueueMutex; + +private: + Servatrice_DatabaseInterface *sqlInterface; + + Response::ResponseCode cmdAddToList(const Command_AddToList &cmd, ResponseContainer &rc); + Response::ResponseCode cmdRemoveFromList(const Command_RemoveFromList &cmd, ResponseContainer &rc); + int getDeckPathId(int basePathId, QStringList path); + int getDeckPathId(const QString &path); + bool deckListHelper(int folderId, ServerInfo_DeckStorage_Folder *folder); + Response::ResponseCode cmdDeckList(const Command_DeckList &cmd, ResponseContainer &rc); + Response::ResponseCode cmdDeckNewDir(const Command_DeckNewDir &cmd, ResponseContainer &rc); + void deckDelDirHelper(int basePathId); + void sendServerMessage(const QString userName, const QString message); + Response::ResponseCode cmdDeckDelDir(const Command_DeckDelDir &cmd, ResponseContainer &rc); + Response::ResponseCode cmdDeckDel(const Command_DeckDel &cmd, ResponseContainer &rc); + Response::ResponseCode cmdDeckUpload(const Command_DeckUpload &cmd, ResponseContainer &rc); + DeckList *getDeckFromDatabase(int deckId); + Response::ResponseCode cmdDeckDownload(const Command_DeckDownload &cmd, ResponseContainer &rc); + Response::ResponseCode cmdReplayList(const Command_ReplayList &cmd, ResponseContainer &rc); + Response::ResponseCode cmdReplayDownload(const Command_ReplayDownload &cmd, ResponseContainer &rc); + Response::ResponseCode cmdReplayModifyMatch(const Command_ReplayModifyMatch &cmd, ResponseContainer &rc); + Response::ResponseCode cmdReplayDeleteMatch(const Command_ReplayDeleteMatch &cmd, ResponseContainer &rc); + QString createHashForReplay(int gameId); + Response::ResponseCode cmdReplayGetCode(const Command_ReplayGetCode &cmd, ResponseContainer &rc); + Response::ResponseCode cmdReplaySubmitCode(const Command_ReplaySubmitCode &cmd, ResponseContainer &rc); + Response::ResponseCode cmdBanFromServer(const Command_BanFromServer &cmd, ResponseContainer &rc); + Response::ResponseCode cmdWarnUser(const Command_WarnUser &cmd, ResponseContainer &rc); + Response::ResponseCode cmdGetLogHistory(const Command_ViewLogHistory &cmd, ResponseContainer &rc); + Response::ResponseCode cmdGetBanHistory(const Command_GetBanHistory &cmd, ResponseContainer &rc); + Response::ResponseCode cmdGetWarnList(const Command_GetWarnList &cmd, ResponseContainer &rc); + Response::ResponseCode cmdGetWarnHistory(const Command_GetWarnHistory &cmd, ResponseContainer &rc); + Response::ResponseCode cmdShutdownServer(const Command_ShutdownServer &cmd, ResponseContainer &rc); + Response::ResponseCode cmdUpdateServerMessage(const Command_UpdateServerMessage &cmd, ResponseContainer &rc); + Response::ResponseCode cmdRegisterAccount(const Command_Register &cmd, ResponseContainer &rc); + Response::ResponseCode cmdActivateAccount(const Command_Activate &cmd, ResponseContainer & /* rc */); + Response::ResponseCode cmdReloadConfig(const Command_ReloadConfig & /* cmd */, ResponseContainer & /*rc*/); + Response::ResponseCode cmdAdjustMod(const Command_AdjustMod &cmd, ResponseContainer & /*rc*/); + Response::ResponseCode cmdForgotPasswordRequest(const Command_ForgotPasswordRequest &cmd, ResponseContainer &rc); + Response::ResponseCode continuePasswordRequest(const QString &userName, + const QString &clientId, + ResponseContainer &rc, + bool challenged = false); + Response::ResponseCode cmdForgotPasswordReset(const Command_ForgotPasswordReset &cmd, ResponseContainer &rc); + Response::ResponseCode cmdForgotPasswordChallenge(const Command_ForgotPasswordChallenge &cmd, + ResponseContainer &rc); + Response::ResponseCode cmdRequestPasswordSalt(const Command_RequestPasswordSalt &cmd, ResponseContainer &rc); + Response::ResponseCode processExtendedSessionCommand(int cmdType, const SessionCommand &cmd, ResponseContainer &rc); + Response::ResponseCode + processExtendedModeratorCommand(int cmdType, const ModeratorCommand &cmd, ResponseContainer &rc); + Response::ResponseCode processExtendedAdminCommand(int cmdType, const AdminCommand &cmd, ResponseContainer &rc); + + Response::ResponseCode cmdAccountEdit(const Command_AccountEdit &cmd, ResponseContainer &rc); + Response::ResponseCode cmdAccountImage(const Command_AccountImage &cmd, ResponseContainer &rc); + Response::ResponseCode cmdAccountPassword(const Command_AccountPassword &cmd, ResponseContainer &rc); + Response::ResponseCode cmdGrantReplayAccess(const Command_GrantReplayAccess &cmd, ResponseContainer &rc); + Response::ResponseCode cmdForceActivateUser(const Command_ForceActivateUser &cmd, ResponseContainer &rc); + + Response::ResponseCode cmdGetAdminNotes(const Command_GetAdminNotes &cmd, ResponseContainer &rc); + Response::ResponseCode cmdUpdateAdminNotes(const Command_UpdateAdminNotes &cmd, ResponseContainer &rc); + + bool addAdminFlagToUser(const QString &user, int flag); + bool removeAdminFlagFromUser(const QString &user, int flag); + + bool isPasswordLongEnough(const int passwordLength); + void removeSaidMessages(const QString &userName, int amount); + +public: + AbstractServerSocketInterface(Servatrice *_server, + Servatrice_DatabaseInterface *_databaseInterface, + QObject *parent = 0); + ~AbstractServerSocketInterface() + { + } + bool initSession(); + + virtual QHostAddress getPeerAddress() const = 0; + virtual QString getAddress() const = 0; + + void transmitProtocolItem(const ServerMessage &item); +}; + +class TcpServerSocketInterface : public AbstractServerSocketInterface +{ + Q_OBJECT +public: + TcpServerSocketInterface(Servatrice *_server, + Servatrice_DatabaseInterface *_databaseInterface, + QObject *parent = 0); + ~TcpServerSocketInterface(); + + QHostAddress getPeerAddress() const + { + return socket->peerAddress(); + } + QString getAddress() const + { + return socket->peerAddress().toString(); + } + QString getConnectionType() const + { + return "tcp"; + } + +private: + QTcpSocket *socket; + QByteArray inputBuffer; + bool messageInProgress; + bool handshakeStarted; + int messageLength; + +protected: + void writeToSocket(QByteArray &data) + { + socket->write(data); + } + void flushSocket() + { + socket->flush(); + } + void initSessionDeprecated(); + bool initTcpSession(); +protected slots: + void readClient(); + void flushOutputQueue(); public slots: - void initConnection(int socketDescriptor); + void initConnection(int socketDescriptor); +}; + +class WebsocketServerSocketInterface : public AbstractServerSocketInterface +{ + Q_OBJECT +public: + WebsocketServerSocketInterface(Servatrice *_server, + Servatrice_DatabaseInterface *_databaseInterface, + QObject *parent = nullptr); + ~WebsocketServerSocketInterface(); + + QHostAddress getPeerAddress() const + { + return address; + } + QString getAddress() const + { + return address.toString(); + } + QString getConnectionType() const + { + return "websocket"; + } + +private: + QWebSocket *socket; + QHostAddress address; + +protected: + void writeToSocket(QByteArray &data) + { + socket->sendBinaryMessage(data); + } + void flushSocket() + { + socket->flush(); + } + bool initWebsocketSession(); +protected slots: + void binaryMessageReceived(const QByteArray &message); + void flushOutputQueue(); +public slots: + void initConnection(void *_socket); }; #endif diff --git a/servatrice/src/settingscache.cpp b/servatrice/src/settingscache.cpp new file mode 100644 index 000000000..f6dcd5fc8 --- /dev/null +++ b/servatrice/src/settingscache.cpp @@ -0,0 +1,45 @@ +#include "settingscache.h" + +#include +#include +#include +#include + +SettingsCache::SettingsCache(const QString &fileName, QSettings::Format format, QObject *parent) + : QSettings(fileName, format, parent) +{ + // first, figure out if we are running in portable mode + isPortableBuild = QFile::exists(qApp->applicationDirPath() + "/portable.dat"); + + QStringList disallowedRegExpStr = value("users/disallowedregexp", "").toString().split(",", Qt::SkipEmptyParts); + disallowedRegExpStr.removeDuplicates(); + for (const QString ®ExpStr : disallowedRegExpStr) { + disallowedRegExp.append(QRegularExpression(QString("\\A%1\\z").arg(regExpStr))); + } +} + +QString SettingsCache::guessConfigurationPath() +{ + const QString fileName = "servatrice.ini"; + if (QFile::exists(qApp->applicationDirPath() + "/portable.dat")) { + qDebug() << "Portable mode enabled"; + return fileName; + } + + QString guessFileName; + + // application directory path + guessFileName = QCoreApplication::applicationDirPath() + "/" + fileName; + if (QFile::exists(guessFileName)) + return guessFileName; + +#ifdef Q_OS_UNIX + // /etc + guessFileName = "/etc/servatrice/" + fileName; + if (QFile::exists(guessFileName)) + return guessFileName; +#endif + + guessFileName = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/" + fileName; + return guessFileName; +} diff --git a/servatrice/src/settingscache.h b/servatrice/src/settingscache.h new file mode 100644 index 000000000..6922d3508 --- /dev/null +++ b/servatrice/src/settingscache.h @@ -0,0 +1,29 @@ +#ifndef SERVATRICE_SETTINGSCACHE_H +#define SERVATRICE_SETTINGSCACHE_H + +#include +#include +#include +#include + +class SettingsCache : public QSettings +{ + Q_OBJECT +private: + bool isPortableBuild; + +public: + SettingsCache(const QString &fileName = "servatrice.ini", + QSettings::Format format = QSettings::IniFormat, + QObject *parent = 0); + static QString guessConfigurationPath(); + QList disallowedRegExp; + bool getIsPortableBuild() const + { + return isPortableBuild; + } +}; + +extern SettingsCache *settingsCache; + +#endif diff --git a/servatrice/src/signalhandler.cpp b/servatrice/src/signalhandler.cpp new file mode 100644 index 000000000..f7cb207a9 --- /dev/null +++ b/servatrice/src/signalhandler.cpp @@ -0,0 +1,104 @@ +#include "signalhandler.h" + +#include "main.h" +#include "server_logger.h" +#include "settingscache.h" + +#include + +#ifdef Q_OS_UNIX +#include +#include +#include +#include +#include +#include +#include +#endif + +#define SIGSEGV_TRACE_LINES 40 + +int SignalHandler::sigHupFD[2]; + +SignalHandler::SignalHandler(QObject *parent) : QObject(parent), snHup(nullptr) +{ +#ifdef Q_OS_UNIX + ::socketpair(AF_UNIX, SOCK_STREAM, 0, sigHupFD); + + snHup = new QSocketNotifier(sigHupFD[1], QSocketNotifier::Read, this); + connect(snHup, SIGNAL(activated(int)), this, SLOT(internalSigHupHandler())); + + struct sigaction hup; + hup.sa_handler = SignalHandler::sigHupHandler; + sigemptyset(&hup.sa_mask); + hup.sa_flags = 0; + hup.sa_flags |= SA_RESTART; + sigaction(SIGHUP, &hup, 0); + + struct sigaction segv; + segv.sa_handler = SignalHandler::sigSegvHandler; + segv.sa_flags = SA_RESETHAND; + sigemptyset(&segv.sa_mask); + sigaction(SIGSEGV, &segv, 0); + sigaction(SIGABRT, &segv, 0); + + signal(SIGPIPE, SIG_IGN); +#endif +} + +void SignalHandler::sigHupHandler(int /* sig */) +{ +#ifdef Q_OS_UNIX + char a = 1; + ssize_t writeValue = ::write(sigHupFD[0], &a, sizeof(a)); + Q_UNUSED(writeValue); +#endif +} + +void SignalHandler::internalSigHupHandler() +{ + snHup->setEnabled(false); +#ifdef Q_OS_UNIX + char tmp; + ssize_t readValue = ::read(sigHupFD[1], &tmp, sizeof(tmp)); + Q_UNUSED(readValue); + + std::cerr << "Received SIGHUP" << std::endl; +#endif + logger->logMessage("Received SIGHUP, rotating logs and reloading configuration", this); + logger->rotateLogs(); + + settingsCache->sync(); + + snHup->setEnabled(true); +} + +#ifdef Q_OS_UNIX +void SignalHandler::sigSegvHandler(int sig) +{ + void *array[SIGSEGV_TRACE_LINES]; + size_t size; + + // get void*'s for all entries on the stack + size = backtrace(array, SIGSEGV_TRACE_LINES); + + // print out all the frames to stderr + fprintf(stderr, "Error: signal %d:\n", sig); + backtrace_symbols_fd(array, size, STDERR_FILENO); + + if (sig == SIGSEGV) + logger->logMessage("CRASH: SIGSEGV"); + else if (sig == SIGABRT) + logger->logMessage("CRASH: SIGABRT"); + + logger->deleteLater(); + loggerThread->wait(); + delete loggerThread; + + raise(sig); +} +#else +void SignalHandler::sigSegvHandler(int /* sig */) +{ +} +#endif diff --git a/servatrice/src/signalhandler.h b/servatrice/src/signalhandler.h new file mode 100644 index 000000000..bf8d9e52a --- /dev/null +++ b/servatrice/src/signalhandler.h @@ -0,0 +1,26 @@ +#ifndef SIGNALHANDLER_H +#define SIGNALHANDLER_H + +#include + +class QSocketNotifier; + +class SignalHandler : public QObject +{ + Q_OBJECT +public: + SignalHandler(QObject *parent = 0); + ~SignalHandler() + { + } + static void sigHupHandler(int /* sig */); + static void sigSegvHandler(int sig); + +private: + static int sigHupFD[2]; + QSocketNotifier *snHup; +private slots: + void internalSigHupHandler(); +}; + +#endif diff --git a/servatrice/src/smtp/qxtglobal.h b/servatrice/src/smtp/qxtglobal.h new file mode 100644 index 000000000..51d374a79 --- /dev/null +++ b/servatrice/src/smtp/qxtglobal.h @@ -0,0 +1,208 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtCore module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#ifndef QXTGLOBAL_H +#define QXTGLOBAL_H + +#include + +#define QXT_VERSION 0x000602 +#define QXT_VERSION_STR "0.6.2" +#define QXT_STATIC + +//--------------------------global macros------------------------------ + +#ifndef QXT_NO_MACROS + +#endif // QXT_NO_MACROS + +//--------------------------export macros------------------------------ + +#define QXT_DLLEXPORT DO_NOT_USE_THIS_ANYMORE + +#if !defined(QXT_STATIC) +# if defined(BUILD_QXT_CORE) +# define QXT_CORE_EXPORT Q_DECL_EXPORT +# else +# define QXT_CORE_EXPORT Q_DECL_IMPORT +# endif +#else +# define QXT_CORE_EXPORT +#endif // BUILD_QXT_CORE + +#if !defined(QXT_STATIC) +# if defined(BUILD_QXT_GUI) +# define QXT_GUI_EXPORT Q_DECL_EXPORT +# else +# define QXT_GUI_EXPORT Q_DECL_IMPORT +# endif +#else +# define QXT_GUI_EXPORT +#endif // BUILD_QXT_GUI + +#if !defined(QXT_STATIC) +# if defined(BUILD_QXT_NETWORK) +# define QXT_NETWORK_EXPORT Q_DECL_EXPORT +# else +# define QXT_NETWORK_EXPORT Q_DECL_IMPORT +# endif +#else +# define QXT_NETWORK_EXPORT +#endif // BUILD_QXT_NETWORK + +#if !defined(QXT_STATIC) +# if defined(BUILD_QXT_SQL) +# define QXT_SQL_EXPORT Q_DECL_EXPORT +# else +# define QXT_SQL_EXPORT Q_DECL_IMPORT +# endif +#else +# define QXT_SQL_EXPORT +#endif // BUILD_QXT_SQL + +#if !defined(QXT_STATIC) +# if defined(BUILD_QXT_WEB) +# define QXT_WEB_EXPORT Q_DECL_EXPORT +# else +# define QXT_WEB_EXPORT Q_DECL_IMPORT +# endif +#else +# define QXT_WEB_EXPORT +#endif // BUILD_QXT_WEB + +#if !defined(QXT_STATIC) +# if defined(BUILD_QXT_BERKELEY) +# define QXT_BERKELEY_EXPORT Q_DECL_EXPORT +# else +# define QXT_BERKELEY_EXPORT Q_DECL_IMPORT +# endif +#else +# define QXT_BERKELEY_EXPORT +#endif // BUILD_QXT_BERKELEY + +#if !defined(QXT_STATIC) +# if defined(BUILD_QXT_ZEROCONF) +# define QXT_ZEROCONF_EXPORT Q_DECL_EXPORT +# else +# define QXT_ZEROCONF_EXPORT Q_DECL_IMPORT +# endif +#else +# define QXT_ZEROCONF_EXPORT +#endif // QXT_ZEROCONF_EXPORT + +#if defined BUILD_QXT_CORE || defined BUILD_QXT_GUI || defined BUILD_QXT_SQL || defined BUILD_QXT_NETWORK || defined BUILD_QXT_WEB || defined BUILD_QXT_BERKELEY || defined BUILD_QXT_ZEROCONF +# define BUILD_QXT +#endif + +QXT_CORE_EXPORT const char* qxtVersion(); + +#ifndef QT_BEGIN_NAMESPACE +#define QT_BEGIN_NAMESPACE +#endif + +#ifndef QT_END_NAMESPACE +#define QT_END_NAMESPACE +#endif + +#ifndef QT_FORWARD_DECLARE_CLASS +#define QT_FORWARD_DECLARE_CLASS(Class) class Class; +#endif + +/**************************************************************************** +** This file is derived from code bearing the following notice: +** The sole author of this file, Adam Higerd, has explicitly disclaimed all +** copyright interest and protection for the content within. This file has +** been placed in the public domain according to United States copyright +** statute and case law. In jurisdictions where this public domain dedication +** is not legally recognized, anyone who receives a copy of this file is +** permitted to use, modify, duplicate, and redistribute this file, in whole +** or in part, with no restrictions or conditions. In these jurisdictions, +** this file shall be copyright (C) 2006-2008 by Adam Higerd. +****************************************************************************/ + +#define QXT_DECLARE_PRIVATE(PUB) friend class PUB##Private; QxtPrivateInterface qxt_d; +#define QXT_DECLARE_PUBLIC(PUB) friend class PUB; +#define QXT_INIT_PRIVATE(PUB) qxt_d.setPublic(this); +#define QXT_D(PUB) PUB##Private& d = qxt_d() +#define QXT_P(PUB) PUB& p = qxt_p() + +template +class QxtPrivate +{ +public: + virtual ~QxtPrivate() + {} + inline void QXT_setPublic(PUB* pub) + { + qxt_p_ptr = pub; + } + +protected: + inline PUB& qxt_p() + { + return *qxt_p_ptr; + } + inline const PUB& qxt_p() const + { + return *qxt_p_ptr; + } + +private: + PUB* qxt_p_ptr; +}; + +template +class QxtPrivateInterface +{ + friend class QxtPrivate; +public: + QxtPrivateInterface() + { + pvt = new PVT; + } + ~QxtPrivateInterface() + { + delete pvt; + } + + inline void setPublic(PUB* pub) + { + pvt->QXT_setPublic(pub); + } + inline PVT& operator()() + { + return *static_cast(pvt); + } + inline const PVT& operator()() const + { + return *static_cast(pvt); + } +private: + QxtPrivateInterface(const QxtPrivateInterface&) { } + QxtPrivateInterface& operator=(const QxtPrivateInterface&) { } + QxtPrivate* pvt; +}; + +#endif // QXT_GLOBAL diff --git a/servatrice/src/smtp/qxthmac.cpp b/servatrice/src/smtp/qxthmac.cpp new file mode 100644 index 000000000..3b7489f72 --- /dev/null +++ b/servatrice/src/smtp/qxthmac.cpp @@ -0,0 +1,210 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtCore module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#include "qxthmac.h" +#include + +/* +\class QxtHmac + +\inmodule QxtCore + +\brief The QxtHmac class calculates keyed-Hash Message Authentication Codes + +HMAC is a well-known algorithm for generating a message authentication code (MAC) that can be used to verify the +integrity and authenticity of a message. + +This class requires Qt 4.3.0 or greater. + +To verify a message, the sender creates a MAC using a key, which is a secret known only to the sender and recipient, +and the content of the message. This MAC is then sent along with the message. The recipient then creates another MAC +using the shared key and the content of the message. If the two codes match, the message is verified. + +HMAC has been used as a password encryption scheme. The final output of the HMAC algorithm depends on the shared key +and an inner hash. This inner hash is generated from the message content and the key. To use HMAC as a password +scheme, the key should be the username; the message should be the user's password. The authenticating party (for +instance, a login server) only needs to store this inner hash generated by the innerHash() function. When requesting +authentication, the user calculates a HMAC using this key and message and sends his username and this HMAC to the +authenticator. The authenticator can then use verify() using the provided HMAC and the stored inner hash. When using +this scheme, the password is never stored or transmitted in plain text. +*/ + +#ifndef QXT_DOXYGEN_RUN +class QxtHmacPrivate : public QxtPrivate +{ +public: + QXT_DECLARE_PUBLIC(QxtHmac) + QxtHmacPrivate() : ohash(0), ihash(0) {} + ~QxtHmacPrivate() + { + // deleting NULL is safe, so no tests are needed here + delete ohash; + delete ihash; + } + QCryptographicHash* ohash; + QCryptographicHash* ihash; + QByteArray opad, ipad, result; + QCryptographicHash::Algorithm algorithm; +}; +#endif + +/*! + * Constructs a QxtHmac object using the specified algorithm. + */ +QxtHmac::QxtHmac(QCryptographicHash::Algorithm algorithm) +{ + QXT_INIT_PRIVATE(QxtHmac); + qxt_d().ohash = new QCryptographicHash(algorithm); + qxt_d().ihash = new QCryptographicHash(algorithm); + qxt_d().algorithm = algorithm; +} + +/*! + * Sets the shared secret key for the message authentication code. + * + * Any data that had been processed using addData() will be discarded. + */ +void QxtHmac::setKey(QByteArray key) +{ + // We make the assumption that all hashes use a 512-bit block size; as of Qt 4.4.0 this is true of all supported hash functions + QxtHmacPrivate* d = &qxt_d(); + d->opad = QByteArray(64, 0x5c); + d->ipad = QByteArray(64, 0x36); + if (key.size() > 64) + { + key = QCryptographicHash::hash(key, d->algorithm); + } + for (int i = key.size() - 1; i >= 0; --i) + { + d->opad[i] = d->opad[i] ^ key[i]; + d->ipad[i] = d->ipad[i] ^ key[i]; + } + reset(); +} + +/*! + * Resets the object. + * + * Any data that had been processed using addData() will be discarded. + * The key, if set, will be preserved. + */ +void QxtHmac::reset() +{ + QxtHmacPrivate* d = &qxt_d(); + d->ihash->reset(); + d->ihash->addData(d->ipad); +} + +/*! + * Returns the inner hash of the HMAC function. + * + * This hash can be stored in lieu of the shared secret on the authenticating side + * and used for verifying an HMAC code. When used in this manner, HMAC can be used + * to provide a form of secure password authentication. See the documentation above + * for details. + */ +QByteArray QxtHmac::innerHash() const +{ + return qxt_d().ihash->result(); +} + +/*! + * Returns the authentication code for the message. + */ +QByteArray QxtHmac::result() +{ + QxtHmacPrivate* d = &qxt_d(); + Q_ASSERT(d->opad.size()); + if (d->result.size()) + return d->result; + d->ohash->reset(); + d->ohash->addData(d->opad); + d->ohash->addData(innerHash()); + d->result = d->ohash->result(); + return d->result; +} + +/*! + * Verifies the authentication code against a known inner hash. + * + * \sa innerHash() + */ +bool QxtHmac::verify(const QByteArray& otherInner) +{ + result(); // populates d->result + QxtHmacPrivate* d = &qxt_d(); + d->ohash->reset(); + d->ohash->addData(d->opad); + d->ohash->addData(otherInner); + return d->result == d->ohash->result(); +} + +/*! + * Adds the provided data to the message to be authenticated. + */ +void QxtHmac::addData(const char* data, int length) +{ + Q_ASSERT(qxt_d().opad.size()); +#if (QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)) + qxt_d().ihash->addData(QByteArrayView(data, length)); +#else + qxt_d().ihash->addData(data, length); +#endif + qxt_d().result.clear(); +} + +/*! + * Adds the provided data to the message to be authenticated. + */ +void QxtHmac::addData(const QByteArray& data) +{ + addData(data.constData(), data.size()); +} + +/*! + * Returns the HMAC of the provided data using the specified key and hashing algorithm. + */ +QByteArray QxtHmac::hash(const QByteArray& key, const QByteArray& data, Algorithm algorithm) +{ + QxtHmac hmac(algorithm); + hmac.setKey(key); + hmac.addData(data); + return hmac.result(); +} + +/*! + * Verifies a HMAC against a known key and inner hash using the specified hashing algorithm. + */ +bool QxtHmac::verify(const QByteArray& key, const QByteArray& hmac, const QByteArray& inner, Algorithm algorithm) +{ + QxtHmac calc(algorithm); + calc.setKey(key); + + QxtHmacPrivate* d = &calc.qxt_d(); + d->ohash->reset(); + d->ohash->addData(d->opad); + d->ohash->addData(inner); + return hmac == d->ohash->result(); +} diff --git a/servatrice/src/smtp/qxthmac.h b/servatrice/src/smtp/qxthmac.h new file mode 100644 index 000000000..e4d133b0a --- /dev/null +++ b/servatrice/src/smtp/qxthmac.h @@ -0,0 +1,58 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtCore module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#ifndef QXTHMAC_H +#define QXTHMAC_H + +#include +#include +#include "qxtglobal.h" + +class QxtHmacPrivate; +class QXT_CORE_EXPORT QxtHmac +{ +public: + typedef QCryptographicHash::Algorithm Algorithm; + + QxtHmac(QCryptographicHash::Algorithm algorithm); + + void setKey(QByteArray key); + void reset(); + + void addData(const char* data, int length); + void addData(const QByteArray& data); + + QByteArray innerHash() const; + QByteArray result(); + bool verify(const QByteArray& otherInner); + + static QByteArray hash(const QByteArray& key, const QByteArray& data, Algorithm algorithm); + static bool verify(const QByteArray& key, const QByteArray& hmac, const QByteArray& inner, Algorithm algorithm); + +private: + QXT_DECLARE_PRIVATE(QxtHmac) +}; + +#endif diff --git a/servatrice/src/smtp/qxtmail_p.h b/servatrice/src/smtp/qxtmail_p.h new file mode 100644 index 000000000..48e8607d4 --- /dev/null +++ b/servatrice/src/smtp/qxtmail_p.h @@ -0,0 +1,35 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ +#ifndef QXTMAIL_P_H +#define QXTMAIL_P_H + +#include + +#define QXT_MUST_QP(x) (x < char(32) || x > char(126) || x == '=' || x == '?') +QByteArray qxt_fold_mime_header(const QString &key, + const QString &value, + const QByteArray &prefix = QByteArray()); + +#endif // QXTMAIL_P_H diff --git a/servatrice/src/smtp/qxtmailattachment.cpp b/servatrice/src/smtp/qxtmailattachment.cpp new file mode 100644 index 000000000..8f494c350 --- /dev/null +++ b/servatrice/src/smtp/qxtmailattachment.cpp @@ -0,0 +1,209 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +/*! + * \class QxtMailAttachment + * \inmodule QxtNetwork + * \brief The QxtMailAttachment class represents an attachement to a QxtMailMessage + */ + + + + +#include "qxtmailattachment.h" +#include "qxtmail_p.h" +#include +#include +#include +#include + +struct QxtMailAttachmentPrivate : public QSharedData +{ + QHash extraHeaders; + QString contentType; + QPointer content; + bool deleteContent; + + QxtMailAttachmentPrivate() + { + content = 0; + deleteContent = false; + contentType = "text/plain"; + } + + ~QxtMailAttachmentPrivate() + { + if (deleteContent && content) + content->deleteLater(); + deleteContent = false; + content = 0; + } +}; + +QxtMailAttachment::QxtMailAttachment() +{ + qxt_d = new QxtMailAttachmentPrivate; +} + +QxtMailAttachment::QxtMailAttachment(const QxtMailAttachment& other) : qxt_d(other.qxt_d) +{ + // trivial copy constructor +} + +QxtMailAttachment::QxtMailAttachment(const QByteArray& content, const QString& contentType) +{ + qxt_d = new QxtMailAttachmentPrivate; + setContentType(contentType); + setContent(content); +} + +QxtMailAttachment::QxtMailAttachment(QIODevice* content, const QString& contentType) +{ + qxt_d = new QxtMailAttachmentPrivate; + setContentType(contentType); + setContent(content); +} + +QxtMailAttachment& QxtMailAttachment::operator=(const QxtMailAttachment & other) +{ + qxt_d = other.qxt_d; + return *this; +} + +QxtMailAttachment::~QxtMailAttachment() +{ + // trivial destructor +} + +QIODevice* QxtMailAttachment::content() const +{ + return qxt_d->content; +} + +void QxtMailAttachment::setContent(const QByteArray& content) +{ + if (qxt_d->deleteContent && qxt_d->content) + qxt_d->content->deleteLater(); + qxt_d->content = new QBuffer; + static_cast(qxt_d->content.data())->setData(content); +} + +void QxtMailAttachment::setContent(QIODevice* content) +{ + if (qxt_d->deleteContent && qxt_d->content) + qxt_d->content->deleteLater(); + qxt_d->content = content; +} + +bool QxtMailAttachment::deleteContent() const +{ + return qxt_d->deleteContent; +} + +void QxtMailAttachment::setDeleteContent(bool enable) +{ + qxt_d->deleteContent = enable; +} + +QString QxtMailAttachment::contentType() const +{ + return qxt_d->contentType; +} + +void QxtMailAttachment::setContentType(const QString& contentType) +{ + qxt_d->contentType = contentType; +} + +QHash QxtMailAttachment::extraHeaders() const +{ + return qxt_d->extraHeaders; +} + +QByteArray QxtMailAttachment::extraHeader(const QString& key) const +{ + return qxt_d->extraHeaders[key.toLower()].toLatin1(); +} + +bool QxtMailAttachment::hasExtraHeader(const QString& key) const +{ + return qxt_d->extraHeaders.contains(key.toLower()); +} + +void QxtMailAttachment::setExtraHeader(const QString& key, const QString& value) +{ + qxt_d->extraHeaders[key.toLower()] = value; +} + +void QxtMailAttachment::setExtraHeaders(const QHash& a) +{ + QHash& headers = qxt_d->extraHeaders; + headers.clear(); + for (const QString& key: a.keys()) + { + headers[key.toLower()] = a[key]; + } +} + +void QxtMailAttachment::removeExtraHeader(const QString& key) +{ + qxt_d->extraHeaders.remove(key.toLower()); +} + +QByteArray QxtMailAttachment::mimeData() +{ + QIODevice* c = content(); + if (!c) + { + qWarning() << "QxtMailAttachment::mimeData(): Content not set or already output"; + return QByteArray(); + } + if (!c->isOpen() && !c->open(QIODevice::ReadOnly)) + { + qWarning() << "QxtMailAttachment::mimeData(): Cannot open content for reading"; + return QByteArray(); + } + + QByteArray rv = "Content-Type: " + qxt_d->contentType.toLatin1() + "\r\nContent-Transfer-Encoding: base64\r\n"; + for(const QString& r: qxt_d->extraHeaders.keys()) + { + rv += qxt_fold_mime_header(r.toLatin1(), extraHeader(r)); + } + rv += "\r\n"; + + while (!c->atEnd()) + { + rv += c->read(57).toBase64() + "\r\n"; + } + setContent((QIODevice*)0); + return rv; +} + +QxtMailAttachment QxtMailAttachment::fromFile(const QString& filename) +{ + QxtMailAttachment rv(new QFile(filename)); + rv.setDeleteContent(true); + return rv; +} diff --git a/servatrice/src/smtp/qxtmailattachment.h b/servatrice/src/smtp/qxtmailattachment.h new file mode 100644 index 000000000..62a0e9ad5 --- /dev/null +++ b/servatrice/src/smtp/qxtmailattachment.h @@ -0,0 +1,73 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ +#ifndef QXTMAILATTACHMENT_H +#define QXTMAILATTACHMENT_H + +#include "qxtglobal.h" + +#include +#include +#include +#include +#include +#include + +struct QxtMailAttachmentPrivate; +class QXT_NETWORK_EXPORT QxtMailAttachment +{ +public: + QxtMailAttachment(); + QxtMailAttachment(const QxtMailAttachment &other); + QxtMailAttachment(const QByteArray &content, const QString &contentType = QString("application/octet-stream")); + QxtMailAttachment(QIODevice *content, const QString &contentType = QString("application/octet-stream")); + QxtMailAttachment &operator=(const QxtMailAttachment &other); + ~QxtMailAttachment(); + static QxtMailAttachment fromFile(const QString &filename); + + QIODevice *content() const; + void setContent(const QByteArray &content); + void setContent(QIODevice *content); + + bool deleteContent() const; + void setDeleteContent(bool enable); + + QString contentType() const; + void setContentType(const QString &contentType); + + QHash extraHeaders() const; + QByteArray extraHeader(const QString &) const; + bool hasExtraHeader(const QString &) const; + void setExtraHeader(const QString &key, const QString &value); + void setExtraHeaders(const QHash &); + void removeExtraHeader(const QString &key); + + QByteArray mimeData(); + +private: + QSharedDataPointer qxt_d; +}; +Q_DECLARE_TYPEINFO(QxtMailAttachment, Q_MOVABLE_TYPE); + +#endif // QXTMAILATTACHMENT_H diff --git a/servatrice/src/smtp/qxtmailmessage.cpp b/servatrice/src/smtp/qxtmailmessage.cpp new file mode 100644 index 000000000..ff376c1a9 --- /dev/null +++ b/servatrice/src/smtp/qxtmailmessage.cpp @@ -0,0 +1,475 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +/*! + * \class QxtMailMessage + * \inmodule QxtNetwork + * \brief The QxtMailMessage class encapsulates an e-mail according to RFC 2822 and related specifications + */ +//! \todo {implicitshared} +#include "qxtmailmessage.h" + +#include "qxtmail_p.h" + +#include +#include +#include + +static bool isASCII(const QString &string) { + for(const QChar &chr : string){ + if(chr.unicode() > 0x7f) + return false; + } + return true; +} + +struct QxtMailMessagePrivate : public QSharedData +{ + QxtMailMessagePrivate() + { + } + QxtMailMessagePrivate(const QxtMailMessagePrivate &other) + : QSharedData(other), rcptTo(other.rcptTo), rcptCc(other.rcptCc), rcptBcc(other.rcptBcc), + subject(other.subject), body(other.body), sender(other.sender), extraHeaders(other.extraHeaders), + attachments(other.attachments) + { + } + QStringList rcptTo, rcptCc, rcptBcc; + QString subject, body, sender; + QHash extraHeaders; + QHash attachments; + mutable QByteArray boundary; +}; + +QxtMailMessage::QxtMailMessage() +{ + qxt_d = new QxtMailMessagePrivate; +} + +QxtMailMessage::QxtMailMessage(const QxtMailMessage &other) : qxt_d(other.qxt_d) +{ + // trivial copy constructor +} + +QxtMailMessage::QxtMailMessage(const QString &sender, const QString &recipient) +{ + qxt_d = new QxtMailMessagePrivate; + setSender(sender); + addRecipient(recipient); +} + +QxtMailMessage::~QxtMailMessage() +{ + // trivial destructor +} + +QxtMailMessage &QxtMailMessage::operator=(const QxtMailMessage &other) +{ + qxt_d = other.qxt_d; + return *this; +} + +QString QxtMailMessage::sender() const +{ + return qxt_d->sender; +} + +void QxtMailMessage::setSender(const QString &a) +{ + qxt_d->sender = a; +} + +QString QxtMailMessage::subject() const +{ + return qxt_d->subject; +} + +void QxtMailMessage::setSubject(const QString &a) +{ + qxt_d->subject = a; +} + +QString QxtMailMessage::body() const +{ + return qxt_d->body; +} + +void QxtMailMessage::setBody(const QString &a) +{ + qxt_d->body = a; +} + +QStringList QxtMailMessage::recipients(QxtMailMessage::RecipientType type) const +{ + if (type == Bcc) + return qxt_d->rcptBcc; + if (type == Cc) + return qxt_d->rcptCc; + return qxt_d->rcptTo; +} + +void QxtMailMessage::addRecipient(const QString &a, QxtMailMessage::RecipientType type) +{ + if (type == Bcc) + qxt_d->rcptBcc.append(a); + else if (type == Cc) + qxt_d->rcptCc.append(a); + else + qxt_d->rcptTo.append(a); +} + +void QxtMailMessage::removeRecipient(const QString &a) +{ + qxt_d->rcptTo.removeAll(a); + qxt_d->rcptCc.removeAll(a); + qxt_d->rcptBcc.removeAll(a); +} + +QHash QxtMailMessage::extraHeaders() const +{ + return qxt_d->extraHeaders; +} + +QByteArray QxtMailMessage::extraHeader(const QString &key) const +{ + return qxt_d->extraHeaders[key.toLower()].toLatin1(); +} + +bool QxtMailMessage::hasExtraHeader(const QString &key) const +{ + return qxt_d->extraHeaders.contains(key.toLower()); +} + +void QxtMailMessage::setExtraHeader(const QString &key, const QString &value) +{ + qxt_d->extraHeaders[key.toLower()] = value; +} + +void QxtMailMessage::setExtraHeaders(const QHash &a) +{ + QHash &headers = qxt_d->extraHeaders; + headers.clear(); + for (const QString &key : a.keys()) { + headers[key.toLower()] = a[key]; + } +} + +void QxtMailMessage::removeExtraHeader(const QString &key) +{ + qxt_d->extraHeaders.remove(key.toLower()); +} + +QHash QxtMailMessage::attachments() const +{ + return qxt_d->attachments; +} + +QxtMailAttachment QxtMailMessage::attachment(const QString &filename) const +{ + return qxt_d->attachments[filename]; +} + +void QxtMailMessage::addAttachment(const QString &filename, const QxtMailAttachment &attach) +{ + if (qxt_d->attachments.contains(filename)) { + qWarning() << "QxtMailMessage::addAttachment: " << filename << " already in use"; + int i = 1; + while (qxt_d->attachments.contains(filename + "." + QString::number(i))) { + i++; + } + qxt_d->attachments[filename + "." + QString::number(i)] = attach; + } else { + qxt_d->attachments[filename] = attach; + } +} + +void QxtMailMessage::removeAttachment(const QString &filename) +{ + qxt_d->attachments.remove(filename); +} + +QByteArray qxt_fold_mime_header(const QString &key, const QString &value, const QByteArray &prefix) +{ + QByteArray rv = ""; + QByteArray line = key.toLatin1() + ": "; + if (!prefix.isEmpty()) + line += prefix; + if (!value.contains("=?") && isASCII(value)) { + bool firstWord = true; + for (const QByteArray &word : value.toLatin1().split(' ')) { + if (line.size() > 78) { + rv = rv + line + "\r\n"; + line.clear(); + } + if (firstWord) + line += word; + else + line += " " + word; + firstWord = false; + } + } else { + // The text cannot be losslessly encoded as Latin-1. Therefore, we + // must use quoted-printable or base64 encoding. This is a quick + // heuristic based on the first 100 characters to see which + // encoding to use. + QByteArray utf8 = value.toUtf8(); + int ct = utf8.length(); + int nonAscii = 0; + for (int i = 0; i < ct && i < 100; i++) { + if (QXT_MUST_QP(utf8[i])) + nonAscii++; + } + if (nonAscii > 20) { + // more than 20%-ish non-ASCII characters: use base64 + QByteArray base64 = utf8.toBase64(); + ct = base64.length(); + line += "=?utf-8?b?"; + for (int i = 0; i < ct; i += 4) { + if (line.length() > 72) { + rv += line + "?\r\n"; + line = " =?utf-8?b?"; + } + line = line + base64.mid(i, 4); + } + } else { + // otherwise use Q-encoding + line += "=?utf-8?q?"; + for (int i = 0; i < ct; i++) { + if (line.length() > 73) { + rv += line + "?\r\n"; + line = " =?utf-8?q?"; + } + if (QXT_MUST_QP(utf8[i]) || utf8[i] == ' ') { + line += "=" + utf8.mid(i, 1).toHex().toUpper(); + } else { + line += utf8[i]; + } + } + } + line += "?="; // end encoded-word atom + } + return rv + line + "\r\n"; +} + +QByteArray QxtMailMessage::rfc2822() const +{ + // Use quoted-printable if requested + bool useQuotedPrintable = (extraHeader("Content-Transfer-Encoding").toLower() == "quoted-printable"); + // Use base64 if requested + bool useBase64 = (extraHeader("Content-Transfer-Encoding").toLower() == "base64"); + // Check to see if plain text is ASCII-clean; assume it isn't if QP or base64 was requested + bool bodyIsAscii = !useQuotedPrintable && !useBase64 && isASCII(body()); + + QHash attach = attachments(); + QByteArray rv; + + if (!sender().isEmpty() && !hasExtraHeader("From")) { + rv += qxt_fold_mime_header("From", sender()); + } + + if (!qxt_d->rcptTo.isEmpty()) { + rv += qxt_fold_mime_header("To", qxt_d->rcptTo.join(", ")); + } + + if (!qxt_d->rcptCc.isEmpty()) { + rv += qxt_fold_mime_header("Cc", qxt_d->rcptCc.join(", ")); + } + + if (!subject().isEmpty()) { + rv += qxt_fold_mime_header("Subject", subject()); + } + + if (!bodyIsAscii) { + if (!hasExtraHeader("MIME-Version") && !attach.count()) + rv += "MIME-Version: 1.0\r\n"; + + // If no transfer encoding has been requested, guess. + // Heuristic: If >20% of the first 100 characters aren't + // 7-bit clean, use base64, otherwise use Q-P. + if (!bodyIsAscii && !useQuotedPrintable && !useBase64) { + QString b = body(); + int nonAscii = 0; + int ct = b.length(); + for (int i = 0; i < ct && i < 100; i++) { + if (QXT_MUST_QP(b[i])) + nonAscii++; + } + useQuotedPrintable = !(nonAscii > 20); + useBase64 = !useQuotedPrintable; + } + } + + if (attach.count()) { + if (qxt_d->boundary.isEmpty()) + qxt_d->boundary = QUuid::createUuid().toString().toLatin1().replace("{", "").replace("}", ""); + if (!hasExtraHeader("MIME-Version")) + rv += "MIME-Version: 1.0\r\n"; + if (!hasExtraHeader("Content-Type")) + rv += "Content-Type: multipart/mixed; boundary=" + qxt_d->boundary + "\r\n"; + } else if (!bodyIsAscii && !hasExtraHeader("Content-Transfer-Encoding")) { + if (!useQuotedPrintable) { + // base64 + rv += "Content-Transfer-Encoding: base64\r\n"; + } else { + // quoted-printable + rv += "Content-Transfer-Encoding: quoted-printable\r\n"; + } + } + + for (const QString &r : qxt_d->extraHeaders.keys()) { + if ((r.toLower() == "content-type" || r.toLower() == "content-transfer-encoding") && attach.count()) { + // Since we're in multipart mode, we'll be outputting this later + continue; + } + rv += qxt_fold_mime_header(r.toLatin1(), extraHeader(r)); + } + + rv += "\r\n"; + + if (attach.count()) { + // we're going to have attachments, so output the lead-in for the message body + rv += "This is a message with multiple parts in MIME format.\r\n"; + rv += "--" + qxt_d->boundary + "\r\nContent-Type: "; + if (hasExtraHeader("Content-Type")) + rv += extraHeader("Content-Type") + "\r\n"; + else + rv += "text/plain; charset=UTF-8\r\n"; + if (hasExtraHeader("Content-Transfer-Encoding")) { + rv += "Content-Transfer-Encoding: " + extraHeader("Content-Transfer-Encoding") + "\r\n"; + } else if (!bodyIsAscii) { + if (!useQuotedPrintable) { + // base64 + rv += "Content-Transfer-Encoding: base64\r\n"; + } else { + // quoted-printable + rv += "Content-Transfer-Encoding: quoted-printable\r\n"; + } + } + rv += "\r\n"; + } + + if (bodyIsAscii) { + QByteArray b = body().toLatin1(); + int len = b.length(); + QByteArray line = ""; + QByteArray word = ""; + for (int i = 0; i < len; i++) { + if (b[i] == '\n' || b[i] == '\r') { + if (line.isEmpty()) { + line = word; + word = ""; + } else if (line.length() + word.length() + 1 <= 78) { + line = line + ' ' + word; + word = ""; + } + if (line.isEmpty()) + continue; + if (line[0] == '.') + rv += "."; + rv += line + "\r\n"; + if ((b[i + 1] == '\n' || b[i + 1] == '\r') && b[i] != b[i + 1]) { + // If we're looking at a CRLF pair, skip the second half + i++; + } + line = word; + } else if (b[i] == ' ') { + if (line.length() + word.length() + 1 > 78) { + if (line[0] == '.') + rv += "."; + rv += line + "\r\n"; + line = word; + } else if (line.isEmpty()) { + line = word; + } else { + line = line + ' ' + word; + } + word = ""; + } else { + word += b[i]; + } + } + if (line.length() + word.length() + 1 > 78) { + if (line[0] == '.') + rv += "."; + rv += line + "\r\n"; + line = word; + } else if (!word.isEmpty()) { + line += ' ' + word; + } + if (!line.isEmpty()) { + if (line[0] == '.') + rv += "."; + rv += line + "\r\n"; + } + } else if (useQuotedPrintable) { + QByteArray b = body().toUtf8(); + int ct = b.length(); + QByteArray line; + for (int i = 0; i < ct; i++) { + if (b[i] == '\n' || b[i] == '\r') { + if (line[0] == '.') + rv += "."; + rv += line + "\r\n"; + line = ""; + if ((b[i + 1] == '\n' || b[i + 1] == '\r') && b[i] != b[i + 1]) { + // If we're looking at a CRLF pair, skip the second half + i++; + } + } else if (line.length() > 74) { + rv += line + "=\r\n"; + line = ""; + } + if (QXT_MUST_QP(b[i])) { + line += "=" + b.mid(i, 1).toHex().toUpper(); + } else { + line += b[i]; + } + } + if (!line.isEmpty()) { + if (line[0] == '.') + rv += "."; + rv += line + "\r\n"; + } + } else /* base64 */ + { + QByteArray b = body().toUtf8().toBase64(); + int ct = b.length(); + for (int i = 0; i < ct; i += 78) { + rv += b.mid(i, 78) + "\r\n"; + } + } + + if (attach.count()) { + for (const QString &filename : attach.keys()) { + rv += "--" + qxt_d->boundary + "\r\n"; + rv += + qxt_fold_mime_header("Content-Disposition", QDir(filename).dirName(), "attachment; filename="); + rv += attach[filename].mimeData(); + } + rv += "--" + qxt_d->boundary + "--\r\n"; + } + + return rv; +} diff --git a/servatrice/src/smtp/qxtmailmessage.h b/servatrice/src/smtp/qxtmailmessage.h new file mode 100644 index 000000000..584e8fcc5 --- /dev/null +++ b/servatrice/src/smtp/qxtmailmessage.h @@ -0,0 +1,85 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ +#ifndef QXTMAILMESSAGE_H +#define QXTMAILMESSAGE_H + +#include "qxtglobal.h" +#include "qxtmailattachment.h" + +#include +#include +#include +#include + +struct QxtMailMessagePrivate; +class QXT_NETWORK_EXPORT QxtMailMessage +{ +public: + enum RecipientType + { + To, + Cc, + Bcc + }; + + QxtMailMessage(); + QxtMailMessage(const QxtMailMessage& other); + QxtMailMessage(const QString& sender, const QString& recipient); + QxtMailMessage& operator=(const QxtMailMessage& other); + ~QxtMailMessage(); + + QString sender() const; + void setSender(const QString&); + + QString subject() const; + void setSubject(const QString&); + + QString body() const; + void setBody(const QString&); + + QStringList recipients(RecipientType type = To) const; + void addRecipient(const QString&, RecipientType type = To); + void removeRecipient(const QString&); + + QHash extraHeaders() const; + QByteArray extraHeader(const QString&) const; + bool hasExtraHeader(const QString&) const; + void setExtraHeader(const QString& key, const QString& value); + void setExtraHeaders(const QHash&); + void removeExtraHeader(const QString& key); + + QHash attachments() const; + QxtMailAttachment attachment(const QString& filename) const; + void addAttachment(const QString& filename, const QxtMailAttachment& attach); + void removeAttachment(const QString& filename); + + QByteArray rfc2822() const; + +private: + QSharedDataPointer qxt_d; +}; +Q_DECLARE_TYPEINFO(QxtMailMessage, Q_MOVABLE_TYPE); + +#endif // QXTMAIL_H diff --git a/servatrice/src/smtp/qxtsmtp.cpp b/servatrice/src/smtp/qxtsmtp.cpp new file mode 100644 index 000000000..6326b101d --- /dev/null +++ b/servatrice/src/smtp/qxtsmtp.cpp @@ -0,0 +1,536 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +/*! + * \class QxtSmtp + * \inmodule QxtNetwork + * \brief The QxtSmtp class implements the SMTP protocol for sending email + */ + +#include "qxtsmtp.h" + +#include "qxthmac.h" +#include "qxtsmtp_p.h" + +#include +#include +#include +#include + +QxtSmtpPrivate::QxtSmtpPrivate() : QObject(0) +{ + // empty ctor +} + +QxtSmtp::QxtSmtp(QObject *parent) : QObject(parent) +{ + QXT_INIT_PRIVATE(QxtSmtp); + qxt_d().state = QxtSmtpPrivate::Disconnected; + qxt_d().nextID = 0; + qxt_d().socket = new QSslSocket(this); + QObject::connect(socket(), SIGNAL(encrypted()), this, SIGNAL(encrypted())); + // QObject::connect(socket(), SIGNAL(encrypted()), &qxt_d(), SLOT(ehlo())); + QObject::connect(socket(), SIGNAL(connected()), this, SIGNAL(connected())); + QObject::connect(socket(), SIGNAL(disconnected()), this, SIGNAL(disconnected())); +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + QObject::connect(socket(), SIGNAL(errorOccurred(QAbstractSocket::SocketError)), &qxt_d(), + SLOT(socketError(QAbstractSocket::SocketError))); +#else + QObject::connect(socket(), SIGNAL(error(QAbstractSocket::SocketError)), &qxt_d(), + SLOT(socketError(QAbstractSocket::SocketError))); +#endif + QObject::connect(this, SIGNAL(authenticated()), &qxt_d(), SLOT(sendNext())); + QObject::connect(socket(), SIGNAL(readyRead()), &qxt_d(), SLOT(socketRead())); +} + +QByteArray QxtSmtp::username() const +{ + return qxt_d().username; +} + +void QxtSmtp::setUsername(const QByteArray &username) +{ + qxt_d().username = username; +} + +QByteArray QxtSmtp::password() const +{ + return qxt_d().password; +} + +void QxtSmtp::setPassword(const QByteArray &password) +{ + qxt_d().password = password; +} + +int QxtSmtp::send(const QxtMailMessage &message) +{ + int messageID = ++qxt_d().nextID; + qxt_d().pending.append(qMakePair(messageID, message)); + if (qxt_d().state == QxtSmtpPrivate::Waiting) + qxt_d().sendNext(); + return messageID; +} + +int QxtSmtp::pendingMessages() const +{ + return qxt_d().pending.count(); +} + +QTcpSocket *QxtSmtp::socket() const +{ + return qxt_d().socket; +} + +void QxtSmtp::connectToHost(const QString &hostName, quint16 port) +{ + qxt_d().useSecure = false; + qxt_d().state = QxtSmtpPrivate::StartState; + socket()->connectToHost(hostName, port); +} + +void QxtSmtp::connectToHost(const QHostAddress &address, quint16 port) +{ + connectToHost(address.toString(), port); +} + +void QxtSmtp::disconnectFromHost() +{ + socket()->disconnectFromHost(); +} + +bool QxtSmtp::startTlsDisabled() const +{ + return qxt_d().disableStartTLS; +} + +void QxtSmtp::setStartTlsDisabled(bool disable) +{ + qxt_d().disableStartTLS = disable; +} + +QSslSocket *QxtSmtp::sslSocket() const +{ + return qxt_d().socket; +} + +void QxtSmtp::connectToSecureHost(const QString &hostName, quint16 port) +{ + qxt_d().useSecure = true; + qxt_d().state = QxtSmtpPrivate::StartState; + sslSocket()->connectToHostEncrypted(hostName, port); +} + +void QxtSmtp::connectToSecureHost(const QHostAddress &address, quint16 port) +{ + connectToSecureHost(address.toString(), port); +} + +bool QxtSmtp::hasExtension(const QString &extension) +{ + return qxt_d().extensions.contains(extension); +} + +QString QxtSmtp::extensionData(const QString &extension) +{ + return qxt_d().extensions[extension]; +} + +void QxtSmtpPrivate::socketError(QAbstractSocket::SocketError err) +{ + if (err == QAbstractSocket::SslHandshakeFailedError) { + emit qxt_p().encryptionFailed(); + emit qxt_p().encryptionFailed(socket->errorString().toLatin1()); + } else if (state == StartState) { + emit qxt_p().connectionFailed(); + emit qxt_p().connectionFailed(socket->errorString().toLatin1()); + } +} + +void QxtSmtpPrivate::socketRead() +{ + buffer += socket->readAll(); + while (true) { + int pos = buffer.indexOf("\r\n"); + if (pos < 0) + return; + QByteArray line = buffer.left(pos); + buffer = buffer.mid(pos + 2); + QByteArray code = line.left(3); + switch (state) { + case StartState: + if (code[0] != '2') { + socket->disconnectFromHost(); + } else { + ehlo(); + } + break; + case HeloSent: + case EhloSent: + case EhloGreetReceived: + parseEhlo(code, (line[3] != ' '), line.mid(4)); + break; + case StartTLSSent: + if (code == "220") { + socket->startClientEncryption(); + ehlo(); + } else { + authenticate(); + } + break; + case AuthRequestSent: + case AuthUsernameSent: + if (authType == AuthPlain) + authPlain(); + else if (authType == AuthLogin) + authLogin(); + else + authCramMD5(line.mid(4)); + break; + case AuthSent: + if (code[0] == '2') { + state = Authenticated; + emit qxt_p().authenticated(); + } else { + state = Disconnected; + emit qxt_p().authenticationFailed(); + emit qxt_p().authenticationFailed(line); + emit socket->disconnectFromHost(); + } + break; + case MailToSent: + case RcptAckPending: + if (code[0] != '2') { + emit qxt_p().mailFailed(pending.first().first, code.toInt()); + emit qxt_p().mailFailed(pending.first().first, code.toInt(), line); + // pending.removeFirst(); + // DO NOT remove it, the body sent state needs this message to assigned the next mail failed message + // that will the sendNext a reset will be sent to clear things out + sendNext(); + state = BodySent; + } else + sendNextRcpt(code, line); + break; + case SendingBody: + sendBody(code, line); + break; + case BodySent: + if (pending.count()) { + // if you removeFirst in RcpActpending/MailToSent on an error, and the queue is now empty, + // you will get into this state and then crash because no check is done. CHeck added but shouldnt + // be necessary since I commented out the removeFirst + if (code[0] != '2') { + emit qxt_p().mailFailed(pending.first().first, code.toInt()); + emit qxt_p().mailFailed(pending.first().first, code.toInt(), line); + } else + emit qxt_p().mailSent(pending.first().first); + pending.removeFirst(); + } + sendNext(); + break; + case Resetting: + if (code[0] != '2') { + emit qxt_p().connectionFailed(); + emit qxt_p().connectionFailed(line); + } else { + state = Waiting; + sendNext(); + } + break; + case Disconnected: + case EhloExtensionsReceived: + case EhloDone: + case Authenticated: + case Waiting: + // only to make compiler happy + break; + } + } +} + +void QxtSmtpPrivate::ehlo() +{ + QByteArray address = "127.0.0.1"; + for (const QHostAddress &addr : QNetworkInterface::allAddresses()) { + if (addr == QHostAddress::LocalHost || addr == QHostAddress::LocalHostIPv6) + continue; + address = addr.toString().toLatin1(); + break; + } + socket->write("ehlo " + address + "\r\n"); + extensions.clear(); + state = EhloSent; +} + +void QxtSmtpPrivate::parseEhlo(const QByteArray &code, bool cont, const QString &line) +{ + if (code != "250") { + // error! + if (state != HeloSent) { + // maybe let's try HELO + socket->write("helo\r\n"); + state = HeloSent; + } else { + // nope + socket->write("QUIT\r\n"); + socket->flush(); + socket->disconnectFromHost(); + } + return; + } else if (state != EhloGreetReceived) { + if (!cont) { + // greeting only, no extensions + state = EhloDone; + } else { + // greeting followed by extensions + state = EhloGreetReceived; + return; + } + } else { + extensions[line.section(' ', 0, 0).toUpper()] = line.section(' ', 1); + if (!cont) + state = EhloDone; + } + if (state != EhloDone) + return; + if (extensions.contains("STARTTLS") && !disableStartTLS) { + startTLS(); + } else { + authenticate(); + } +} + +void QxtSmtpPrivate::startTLS() +{ + socket->write("starttls\r\n"); + state = StartTLSSent; +} + +void QxtSmtpPrivate::authenticate() +{ + if (!extensions.contains("AUTH") || username.isEmpty() || password.isEmpty()) { + state = Authenticated; + emit qxt_p().authenticated(); + } else { + QStringList auth = extensions["AUTH"].toUpper().split(' ', Qt::SkipEmptyParts); + if (auth.contains("CRAM-MD5")) { + authCramMD5(); + } else if (auth.contains("PLAIN")) { + authPlain(); + } else if (auth.contains("LOGIN")) { + authLogin(); + } else { + state = Authenticated; + emit qxt_p().authenticated(); + } + } +} + +void QxtSmtpPrivate::authCramMD5(const QByteArray &challenge) +{ + if (state != AuthRequestSent) { + socket->write("auth cram-md5\r\n"); + authType = AuthCramMD5; + state = AuthRequestSent; + } else { + QxtHmac hmac(QCryptographicHash::Md5); + hmac.setKey(password); + hmac.addData(QByteArray::fromBase64(challenge)); + QByteArray response = username + ' ' + hmac.result().toHex(); + socket->write(response.toBase64() + "\r\n"); + state = AuthSent; + } +} + +void QxtSmtpPrivate::authPlain() +{ + if (state != AuthRequestSent) { + socket->write("auth plain\r\n"); + authType = AuthPlain; + state = AuthRequestSent; + } else { + QByteArray auth; + auth += '\0'; + auth += username; + auth += '\0'; + auth += password; + socket->write(auth.toBase64() + "\r\n"); + state = AuthSent; + } +} + +void QxtSmtpPrivate::authLogin() +{ + if (state != AuthRequestSent && state != AuthUsernameSent) { + socket->write("auth login\r\n"); + authType = AuthLogin; + state = AuthRequestSent; + } else if (state == AuthRequestSent) { + socket->write(username.toBase64() + "\r\n"); + state = AuthUsernameSent; + } else { + socket->write(password.toBase64() + "\r\n"); + state = AuthSent; + } +} + +static QByteArray qxt_extract_address(const QString &address) +{ + int parenDepth = 0; + int addrStart = -1; + bool inQuote = false; + int ct = address.length(); + + for (int i = 0; i < ct; i++) { + QChar ch = address[i]; + if (inQuote) { + if (ch == '"') + inQuote = false; + } else if (addrStart != -1) { + if (ch == '>') + return address.mid(addrStart, (i - addrStart)).toLatin1(); + } else if (ch == '(') { + parenDepth++; + } else if (ch == ')') { + parenDepth--; + if (parenDepth < 0) + parenDepth = 0; + } else if (ch == '"') { + if (parenDepth == 0) + inQuote = true; + } else if (ch == '<') { + if (!inQuote && parenDepth == 0) + addrStart = i + 1; + } + } + return address.toLatin1(); +} + +void QxtSmtpPrivate::sendNext() +{ + if (state == Disconnected) { + // leave the mail in the queue if not ready to send + return; + } + + if (pending.isEmpty()) { + // if there are no additional mails to send, finish up + state = Waiting; + emit qxt_p().finished(); + return; + } + + if (state != Waiting) { + state = Resetting; + socket->write("rset\r\n"); + return; + } + const QxtMailMessage &msg = pending.first().second; + rcptNumber = rcptAck = mailAck = 0; + recipients = + msg.recipients(QxtMailMessage::To) + msg.recipients(QxtMailMessage::Cc) + msg.recipients(QxtMailMessage::Bcc); + if (recipients.count() == 0) { + // can't send an e-mail with no recipients + emit qxt_p().mailFailed(pending.first().first, QxtSmtp::NoRecipients); + emit qxt_p().mailFailed(pending.first().first, QxtSmtp::NoRecipients, QByteArray("e-mail has no recipients")); + pending.removeFirst(); + sendNext(); + return; + } + // We explicitly use lowercase keywords because for some reason gmail + // interprets any string starting with an uppercase R as a request + // to renegotiate the SSL connection. + socket->write("mail from:<" + qxt_extract_address(msg.sender()) + ">\r\n"); + if (extensions.contains("PIPELINING")) // almost all do nowadays + { + for (const QString &rcpt : recipients) { + socket->write("rcpt to:<" + qxt_extract_address(rcpt) + ">\r\n"); + } + state = RcptAckPending; + } else { + state = MailToSent; + } +} + +void QxtSmtpPrivate::sendNextRcpt(const QByteArray &code, const QByteArray &line) +{ + int messageID = pending.first().first; + const QxtMailMessage &msg = pending.first().second; + + if (code[0] != '2') { + // on failure, emit a warning signal + if (!mailAck) { + emit qxt_p().senderRejected(messageID, msg.sender()); + emit qxt_p().senderRejected(messageID, msg.sender(), line); + } else { + emit qxt_p().recipientRejected(messageID, msg.sender()); + emit qxt_p().recipientRejected(messageID, msg.sender(), line); + } + } else if (!mailAck) { + mailAck = true; + } else { + rcptAck++; + } + + if (rcptNumber == recipients.count()) { + // all recipients have been sent + if (rcptAck == 0) { + // no recipients were considered valid + emit qxt_p().mailFailed(messageID, code.toInt()); + emit qxt_p().mailFailed(messageID, code.toInt(), line); + pending.removeFirst(); + sendNext(); + } else { + // at least one recipient was acknowledged, send mail body + socket->write("data\r\n"); + state = SendingBody; + } + } else if (state != RcptAckPending) { + // send the next recipient unless we're only waiting on acks + socket->write("rcpt to:<" + qxt_extract_address(recipients[rcptNumber]) + ">\r\n"); + rcptNumber++; + } else { + // If we're only waiting on acks, just count them + rcptNumber++; + } +} + +void QxtSmtpPrivate::sendBody(const QByteArray &code, const QByteArray &line) +{ + int messageID = pending.first().first; + const QxtMailMessage &msg = pending.first().second; + + if (code[0] != '3') { + emit qxt_p().mailFailed(messageID, code.toInt()); + emit qxt_p().mailFailed(messageID, code.toInt(), line); + pending.removeFirst(); + sendNext(); + return; + } + + socket->write(msg.rfc2822()); + socket->write(".\r\n"); + state = BodySent; +} diff --git a/servatrice/src/smtp/qxtsmtp.h b/servatrice/src/smtp/qxtsmtp.h new file mode 100644 index 000000000..747e22df9 --- /dev/null +++ b/servatrice/src/smtp/qxtsmtp.h @@ -0,0 +1,111 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ +#ifndef QXTSMTP_H +#define QXTSMTP_H + +#include +#include +#include + +#include "qxtglobal.h" +#include "qxtmailmessage.h" + +class QTcpSocket; +class QSslSocket; + +class QxtSmtpPrivate; +class QXT_NETWORK_EXPORT QxtSmtp : public QObject +{ + Q_OBJECT +public: + enum SmtpError + { + NoError, + NoRecipients, + CommandUnrecognized = 500, + SyntaxError, + CommandNotImplemented, + BadSequence, + ParameterNotImplemented, + MailboxUnavailable = 550, + UserNotLocal, + MessageTooLarge, + InvalidMailboxName, + TransactionFailed + }; + + QxtSmtp(QObject* parent = 0); + + QByteArray username() const; + void setUsername(const QByteArray& name); + + QByteArray password() const; + void setPassword(const QByteArray& password); + + int send(const QxtMailMessage& message); + int pendingMessages() const; + + QTcpSocket* socket() const; + void connectToHost(const QString& hostName, quint16 port = 25); + void connectToHost(const QHostAddress& address, quint16 port = 25); + void disconnectFromHost(); + + bool startTlsDisabled() const; + void setStartTlsDisabled(bool disable); + + QSslSocket* sslSocket() const; + void connectToSecureHost(const QString& hostName, quint16 port = 465); + void connectToSecureHost(const QHostAddress& address, quint16 port = 465); + + bool hasExtension(const QString& extension); + QString extensionData(const QString& extension); + +Q_SIGNALS: + void connected(); + void connectionFailed(); + void connectionFailed( const QByteArray & msg ); + void encrypted(); + void encryptionFailed(); + void encryptionFailed( const QByteArray & msg ); + void authenticated(); + void authenticationFailed(); + void authenticationFailed( const QByteArray & msg ); + + void senderRejected(int mailID, const QString& address ); + void senderRejected(int mailID, const QString& address, const QByteArray & msg ); + void recipientRejected(int mailID, const QString& address ); + void recipientRejected(int mailID, const QString& address, const QByteArray & msg ); + void mailFailed(int mailID, int errorCode); + void mailFailed(int mailID, int errorCode, const QByteArray & msg); + void mailSent(int mailID); + + void finished(); + void disconnected(); + +private: + QXT_DECLARE_PRIVATE(QxtSmtp) +}; + +#endif // QXTSMTP_H diff --git a/servatrice/src/smtp/qxtsmtp_p.h b/servatrice/src/smtp/qxtsmtp_p.h new file mode 100644 index 000000000..a8e234b13 --- /dev/null +++ b/servatrice/src/smtp/qxtsmtp_p.h @@ -0,0 +1,102 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ +#ifndef QXTSMTP_P_H +#define QXTSMTP_P_H + +#include "qxtsmtp.h" +#include +#include +#include +#include + +class QxtSmtpPrivate : public QObject, public QxtPrivate +{ + Q_OBJECT +public: + QxtSmtpPrivate(); + + QXT_DECLARE_PUBLIC(QxtSmtp) + + enum SmtpState + { + Disconnected, + StartState, + EhloSent, + EhloGreetReceived, + EhloExtensionsReceived, + EhloDone, + HeloSent, + StartTLSSent, + AuthRequestSent, + AuthUsernameSent, + AuthSent, + Authenticated, + MailToSent, + RcptAckPending, + SendingBody, + BodySent, + Waiting, + Resetting + }; + + enum AuthType + { + AuthPlain, + AuthLogin, + AuthCramMD5 + }; + + bool useSecure, disableStartTLS; + SmtpState state;// rather then an int use the enum. makes sure invalid states are entered at compile time, and makes debugging easier + AuthType authType; + QByteArray buffer, username, password; + QHash extensions; + QList > pending; + QStringList recipients; + int nextID, rcptNumber, rcptAck; + bool mailAck; + + QSslSocket* socket; + + void parseEhlo(const QByteArray& code, bool cont, const QString& line); + void startTLS(); + void authenticate(); + + void authCramMD5(const QByteArray& challenge = QByteArray()); + void authPlain(); + void authLogin(); + + void sendNextRcpt(const QByteArray& code, const QByteArray & line); + void sendBody(const QByteArray& code, const QByteArray & line); + +public slots: + void socketError(QAbstractSocket::SocketError err); + void socketRead(); + + void ehlo(); + void sendNext(); +}; + +#endif // QXTSMTP_P_H diff --git a/servatrice/src/smtpclient.cpp b/servatrice/src/smtpclient.cpp new file mode 100644 index 000000000..ee7214904 --- /dev/null +++ b/servatrice/src/smtpclient.cpp @@ -0,0 +1,214 @@ +#include "smtpclient.h" + +#include "settingscache.h" +#include "smtp/qxtsmtp.h" + +#include +#include + +SmtpClient::SmtpClient(QObject *parent) : QObject(parent) +{ + smtp = new QxtSmtp(this); + + connect(smtp, SIGNAL(authenticated()), this, SLOT(authenticated())); + connect(smtp, SIGNAL(authenticationFailed(const QByteArray &)), this, + SLOT(authenticationFailed(const QByteArray &))); + connect(smtp, SIGNAL(connected()), this, SLOT(connected())); + connect(smtp, SIGNAL(connectionFailed(const QByteArray &)), this, SLOT(connectionFailed(const QByteArray &))); + connect(smtp, SIGNAL(disconnected()), this, SLOT(disconnected())); + connect(smtp, SIGNAL(encrypted()), this, SLOT(encrypted())); + connect(smtp, SIGNAL(encryptionFailed(const QByteArray &)), this, SLOT(encryptionFailed(const QByteArray &))); + connect(smtp, SIGNAL(finished()), this, SLOT(finished())); + connect(smtp, SIGNAL(mailFailed(int, int, const QByteArray &)), this, + SLOT(mailFailed(int, int, const QByteArray &))); + connect(smtp, SIGNAL(mailSent(int)), this, SLOT(mailSent(int))); + connect(smtp, SIGNAL(recipientRejected(int, const QString &, const QByteArray &)), this, + SLOT(recipientRejected(int, const QString &, const QByteArray &))); + connect(smtp, SIGNAL(senderRejected(int, const QString &, const QByteArray &)), this, + SLOT(senderRejected(int, const QString &, const QByteArray &))); +} + +SmtpClient::~SmtpClient() +{ + if (smtp) { + delete smtp; + smtp = 0; + } +} + +bool SmtpClient::enqueueActivationTokenMail(const QString &nickname, const QString &recipient, const QString &token) +{ + QString email = settingsCache->value("smtp/email", "").toString(); + QString name = settingsCache->value("smtp/name", "").toString(); + QString subject = settingsCache->value("smtp/subject", "").toString(); + QString body = settingsCache->value("smtp/body", "").toString(); + + if (email.isEmpty()) { + qDebug() << "[MAIL] Missing sender email in configuration"; + return false; + } + + if (subject.isEmpty()) { + qDebug() << "[MAIL] Missing subject field in configuration"; + return false; + } + + if (body.isEmpty()) { + qDebug() << "[MAIL] Missing body field in configuration"; + return false; + } + + if (recipient.isEmpty()) { + qDebug() << "[MAIL] Missing recipient field for user " << nickname; + return false; + } + + if (token.isEmpty()) { + qDebug() << "[MAIL] Missing token field for user " << nickname; + return false; + } + + QxtMailMessage message; + message.setSender(name + " <" + email + ">"); + message.addRecipient(recipient); + message.setSubject(subject); + message.setBody(body.replace("%username", nickname).replace("%token", token)); + + int id = smtp->send(message); + qDebug() << "[MAIL] Enqueued mail to" << recipient << "as" << id; + return true; +} + +bool SmtpClient::enqueueForgotPasswordTokenMail(const QString &nickname, const QString &recipient, const QString &token) +{ + QString email = settingsCache->value("smtp/email", "").toString(); + QString name = settingsCache->value("smtp/name", "").toString(); + QString subject = settingsCache->value("forgotpassword/subject", "").toString(); + QString body = settingsCache->value("forgotpassword/body", "").toString(); + + if (email.isEmpty()) { + qDebug() << "[MAIL] Missing sender email in configuration"; + return false; + } + + if (subject.isEmpty()) { + qDebug() << "[MAIL] Missing subject field in configuration"; + return false; + } + + if (body.isEmpty()) { + qDebug() << "[MAIL] Missing body field in configuration"; + return false; + } + + if (recipient.isEmpty()) { + qDebug() << "[MAIL] Missing recipient field for user " << nickname; + return false; + } + + if (token.isEmpty()) { + qDebug() << "[MAIL] Missing token field for user " << nickname; + return false; + } + + QxtMailMessage message; + message.setSender(name + " <" + email + ">"); + message.addRecipient(recipient); + message.setSubject(subject); + message.setBody(body.replace("%username", nickname).replace("%token", token)); + + int id = smtp->send(message); + qDebug() << "[MAIL] Enqueued mail to" << recipient << "as" << id; + return true; +} + +void SmtpClient::sendAllEmails() +{ + // still connected from the previous round + if (smtp->socket()->state() == QAbstractSocket::ConnectedState) + return; + + if (smtp->pendingMessages() == 0) + return; + + QString connectionType = settingsCache->value("smtp/connection", "tcp").toString(); + QString host = settingsCache->value("smtp/host", "localhost").toString(); + int port = settingsCache->value("smtp/port", 25).toInt(); + QByteArray username = settingsCache->value("smtp/username", "").toByteArray(); + QByteArray password = settingsCache->value("smtp/password", "").toByteArray(); + bool acceptAllCerts = settingsCache->value("smtp/acceptallcerts", false).toBool(); + + smtp->setUsername(username); + smtp->setPassword(password); + + // Connect + if (connectionType == "ssl") { + if (acceptAllCerts) + smtp->sslSocket()->setPeerVerifyMode(QSslSocket::QueryPeer); + smtp->connectToSecureHost(host, port); + } else { + smtp->connectToHost(host, port); + } +} + +void SmtpClient::authenticated() +{ + qDebug() << "[MAIL] authenticated"; +} + +void SmtpClient::authenticationFailed(const QByteArray &msg) +{ + qDebug() << "[MAIL] authenticationFailed" << QString(msg); +} + +void SmtpClient::connected() +{ + qDebug() << "[MAIL] connected"; +} + +void SmtpClient::connectionFailed(const QByteArray &msg) +{ + qDebug() << "[MAIL] connectionFailed" << QString(msg); +} + +void SmtpClient::disconnected() +{ + qDebug() << "[MAIL] disconnected"; +} + +void SmtpClient::encrypted() +{ + qDebug() << "[MAIL] encrypted"; +} + +void SmtpClient::encryptionFailed(const QByteArray &msg) +{ + qDebug() << "[MAIL] encryptionFailed" << QString(msg); + qDebug() << "[MAIL] Try enabling the \"acceptallcerts\" option in servatrice.ini"; +} + +void SmtpClient::finished() +{ + qDebug() << "[MAIL] finished"; + smtp->disconnectFromHost(); +} + +void SmtpClient::mailFailed(int mailID, int errorCode, const QByteArray &msg) +{ + qDebug() << "[MAIL] mailFailed id=" << mailID << " errorCode=" << errorCode << "msg=" << QString(msg); +} + +void SmtpClient::mailSent(int mailID) +{ + qDebug() << "[MAIL] mailSent" << mailID; +} + +void SmtpClient::recipientRejected(int mailID, const QString &address, const QByteArray &msg) +{ + qDebug() << "[MAIL] recipientRejected id=" << mailID << " address=" << address << "msg=" << QString(msg); +} + +void SmtpClient::senderRejected(int mailID, const QString &address, const QByteArray &msg) +{ + qDebug() << "[MAIL] senderRejected id=" << mailID << " address=" << address << "msg=" << QString(msg); +} diff --git a/servatrice/src/smtpclient.h b/servatrice/src/smtpclient.h new file mode 100644 index 000000000..be97ed44d --- /dev/null +++ b/servatrice/src/smtpclient.h @@ -0,0 +1,37 @@ +#ifndef SMTPCLIENT_H +#define SMTPCLIENT_H + +#include + +class QxtSmtp; +class QxtMailMessage; + +class SmtpClient : public QObject +{ + Q_OBJECT +public: + SmtpClient(QObject *parent = 0); + ~SmtpClient(); + +protected: + QxtSmtp *smtp; +public slots: + bool enqueueActivationTokenMail(const QString &nickname, const QString &recipient, const QString &token); + bool enqueueForgotPasswordTokenMail(const QString &nickname, const QString &recipient, const QString &token); + void sendAllEmails(); +protected slots: + void authenticated(); + void authenticationFailed(const QByteArray &msg); + void connected(); + void connectionFailed(const QByteArray &msg); + void disconnected(); + void encrypted(); + void encryptionFailed(const QByteArray &msg); + void finished(); + void mailFailed(int mailID, int errorCode, const QByteArray &msg); + void mailSent(int mailID); + void recipientRejected(int mailID, const QString &address, const QByteArray &msg); + void senderRejected(int mailID, const QString &address, const QByteArray &msg); +}; + +#endif \ No newline at end of file diff --git a/shell.nix b/shell.nix new file mode 100644 index 000000000..404e9e1b6 --- /dev/null +++ b/shell.nix @@ -0,0 +1,32 @@ +{ pkgs ? import {} }: + pkgs.mkShell { + nativeBuildInputs = with pkgs.buildPackages; [ + # Build tools + cmake + cmake-format + ninja + bash + curl + git + qtcreator + + # Debug / Test + valgrind + gdb + clang-tools + + # Compiler + gcc + + # Libraries + openssl + protobuf + qt6.qtbase + qt6.full + qt6.wrapQtAppsHook + ]; + + # Make debug builds work + # https://github.com/NixOS/nixpkgs/issues/18995 + hardeningDisable = [ "fortify" ]; + } diff --git a/sounds/CMakeLists.txt b/sounds/CMakeLists.txt deleted file mode 100644 index cd9f6c331..000000000 --- a/sounds/CMakeLists.txt +++ /dev/null @@ -1,16 +0,0 @@ -# CMakeLists for sounds/ directory -# -# Installs default sound files - -FILE(GLOB sounds "${CMAKE_CURRENT_SOURCE_DIR}/*.raw") - -if(UNIX) - if(APPLE) - INSTALL(FILES ${sounds} DESTINATION cockatrice.app/Contents/Resources/sounds/) - else() - # Assume linux - INSTALL(FILES ${sounds} DESTINATION share/cockatrice/sounds/) - endif() -elseif(WIN32) - INSTALL(FILES ${sounds} DESTINATION sounds/) -endif() \ No newline at end of file diff --git a/sounds/cuckoo.raw b/sounds/cuckoo.raw deleted file mode 100644 index 52f0cbb60..000000000 Binary files a/sounds/cuckoo.raw and /dev/null differ diff --git a/sounds/cuckoo.wav b/sounds/cuckoo.wav deleted file mode 100644 index 5eba46f97..000000000 Binary files a/sounds/cuckoo.wav and /dev/null differ diff --git a/sounds/draw.raw b/sounds/draw.raw deleted file mode 100644 index 3732125f2..000000000 Binary files a/sounds/draw.raw and /dev/null differ diff --git a/sounds/notification.raw b/sounds/notification.raw deleted file mode 100644 index 19906b48d..000000000 Binary files a/sounds/notification.raw and /dev/null differ diff --git a/sounds/playcard.raw b/sounds/playcard.raw deleted file mode 100644 index c9fb802dc..000000000 Binary files a/sounds/playcard.raw and /dev/null differ diff --git a/sounds/shuffle.raw b/sounds/shuffle.raw deleted file mode 100644 index f092e5678..000000000 Binary files a/sounds/shuffle.raw and /dev/null differ diff --git a/sounds/tap.raw b/sounds/tap.raw deleted file mode 100644 index db5290a87..000000000 Binary files a/sounds/tap.raw and /dev/null differ diff --git a/sounds/untap.raw b/sounds/untap.raw deleted file mode 100644 index 938efe260..000000000 Binary files a/sounds/untap.raw and /dev/null differ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 000000000..fffaf1bda --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,69 @@ +# NOTE: Qt modules for tests are defined centrally in cmake/FindQtRuntime.cmake (the _TEST_NEEDED variable). +# If a new test needs additional Qt modules, add them there — not in individual test CMakeLists.txt files. +enable_testing() + +add_test(NAME dummy_test COMMAND dummy_test) +add_test(NAME expression_test COMMAND expression_test) +add_test(NAME test_age_formatting COMMAND test_age_formatting) +add_test(NAME password_hash_test COMMAND password_hash_test) + +add_test(NAME deck_hash_performance_test COMMAND deck_hash_performance_test) +set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 5) + +# Find GTest + +add_executable(dummy_test dummy_test.cpp) +add_executable(expression_test expression_test.cpp) +add_executable(test_age_formatting test_age_formatting.cpp) +add_executable(password_hash_test password_hash_test.cpp) +add_executable(deck_hash_performance_test deck_hash_performance_test.cpp) + +find_package(GTest) + +if(NOT GTEST_FOUND) + if(NOT EXISTS "${CMAKE_BINARY_DIR}/gtest-build") + message(STATUS "Downloading googletest") + configure_file( + "${CMAKE_SOURCE_DIR}/cmake/gtest-CMakeLists.txt.in" "${CMAKE_BINARY_DIR}/gtest-download/CMakeLists.txt" + ) + execute_process( + COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" . WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/gtest-download + ) + execute_process(COMMAND ${CMAKE_COMMAND} --build . WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/gtest-download) + else() + message(STATUS "GoogleTest directory exists") + endif() + + # Add gtest directly to our build + add_subdirectory(${CMAKE_BINARY_DIR}/gtest-src ${CMAKE_BINARY_DIR}/gtest-build EXCLUDE_FROM_ALL) + + # Add the gtest include directory, since gtest + # doesn't add that dependency to its gtest target + target_include_directories(gtest INTERFACE "$") + + set(GTEST_INCLUDE_DIRS "${CMAKE_BINARY_DIR}/gtest-src/include") + set(GTEST_BOTH_LIBRARIES gtest) + add_dependencies(dummy_test gtest) + add_dependencies(expression_test gtest) + add_dependencies(test_age_formatting gtest) + add_dependencies(password_hash_test gtest) + add_dependencies(deck_hash_performance_test gtest) +endif() + +include_directories(${GTEST_INCLUDE_DIRS}) +target_link_libraries(dummy_test Threads::Threads ${GTEST_BOTH_LIBRARIES}) +target_link_libraries(expression_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) +target_link_libraries(test_age_formatting Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) +target_link_libraries( + password_hash_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} +) +target_link_libraries( + deck_hash_performance_test libcockatrice_deck_list libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} + ${TEST_QT_MODULES} +) + +add_subdirectory(card_zone_algorithms) +add_subdirectory(carddatabase) +add_subdirectory(loading_from_clipboard) +add_subdirectory(movecard_tests) +add_subdirectory(oracle) diff --git a/tests/card_zone_algorithms/CMakeLists.txt b/tests/card_zone_algorithms/CMakeLists.txt new file mode 100644 index 000000000..889e92eaa --- /dev/null +++ b/tests/card_zone_algorithms/CMakeLists.txt @@ -0,0 +1,15 @@ +add_executable(card_zone_algorithms_test card_zone_algorithms_test.cpp) + +target_include_directories(card_zone_algorithms_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src/game/zones/logic) + +target_link_libraries( + card_zone_algorithms_test + PRIVATE Threads::Threads + PRIVATE ${GTEST_BOTH_LIBRARIES} +) + +add_test(NAME card_zone_algorithms_test COMMAND card_zone_algorithms_test) + +if(NOT GTEST_FOUND) + add_dependencies(card_zone_algorithms_test gtest) +endif() diff --git a/tests/card_zone_algorithms/card_zone_algorithms_test.cpp b/tests/card_zone_algorithms/card_zone_algorithms_test.cpp new file mode 100644 index 000000000..cc098cae9 --- /dev/null +++ b/tests/card_zone_algorithms/card_zone_algorithms_test.cpp @@ -0,0 +1,159 @@ +#include "card_zone_algorithms.h" + +#include +#include + +struct MockCardRef +{ +}; + +struct MockCard +{ + int idSet = 0; + bool idWasCalled = false; + MockCardRef cardRefSet{}; + bool cardRefWasCalled = false; + bool resetStateCalled = false; + bool resetStateKeepAnnotations = false; + bool visibleSet = false; + + void setId(int id) + { + idSet = id; + idWasCalled = true; + } + + void setCardRef(MockCardRef ref) + { + cardRefSet = ref; + cardRefWasCalled = true; + } + + void resetState(bool keepAnnotations) + { + resetStateCalled = true; + resetStateKeepAnnotations = keepAnnotations; + } + + void setVisible(bool visible) + { + visibleSet = visible; + } +}; + +class MockCardList +{ + std::vector cards; + bool contentsKnown; + +public: + explicit MockCardList(bool _contentsKnown) : contentsKnown(_contentsKnown) + { + } + + int size() const + { + return static_cast(cards.size()); + } + + void insert(int index, MockCard *card) + { + cards.insert(cards.begin() + index, card); + } + + bool getContentsKnown() const + { + return contentsKnown; + } + + MockCard *at(int index) const + { + return cards.at(index); + } +}; + +class AddCardAlgorithmTest : public ::testing::Test +{ +protected: + MockCardList knownList{true}; + MockCardList unknownList{false}; +}; + +TEST_F(AddCardAlgorithmTest, NegativeIndexClampsToEnd) +{ + MockCard a, b; + CardZoneAlgorithms::addCardToList(knownList, &a, 0, false); + CardZoneAlgorithms::addCardToList(knownList, &b, -1, false); + + EXPECT_EQ(knownList.at(0), &a); + EXPECT_EQ(knownList.at(1), &b); + EXPECT_EQ(knownList.size(), 2); +} + +TEST_F(AddCardAlgorithmTest, IndexBeyondSizeClampsToEnd) +{ + MockCard a, b; + CardZoneAlgorithms::addCardToList(knownList, &a, 0, false); + CardZoneAlgorithms::addCardToList(knownList, &b, 999, false); + + EXPECT_EQ(knownList.at(0), &a); + EXPECT_EQ(knownList.at(1), &b); + EXPECT_EQ(knownList.size(), 2); +} + +TEST_F(AddCardAlgorithmTest, ContentsKnownPreservesIdentity) +{ + MockCard card; + CardZoneAlgorithms::addCardToList(knownList, &card, 0, false); + + EXPECT_FALSE(card.idWasCalled); + EXPECT_FALSE(card.cardRefWasCalled); + EXPECT_TRUE(card.visibleSet); +} + +TEST_F(AddCardAlgorithmTest, ContentsUnknownClearsIdentity) +{ + MockCard card; + CardZoneAlgorithms::addCardToList(unknownList, &card, 0, false); + + EXPECT_TRUE(card.idWasCalled); + EXPECT_EQ(card.idSet, -1); + EXPECT_TRUE(card.cardRefWasCalled); +} + +TEST_F(AddCardAlgorithmTest, MidListInsertionPreservesOrder) +{ + MockCard a, b, c; + CardZoneAlgorithms::addCardToList(knownList, &a, 0, false); + CardZoneAlgorithms::addCardToList(knownList, &b, 1, false); + CardZoneAlgorithms::addCardToList(knownList, &c, 1, false); + + EXPECT_EQ(knownList.size(), 3); + EXPECT_EQ(knownList.at(0), &a); + EXPECT_EQ(knownList.at(1), &c); + EXPECT_EQ(knownList.at(2), &b); +} + +TEST_F(AddCardAlgorithmTest, KeepAnnotationsFalsePassedThrough) +{ + MockCard card; + CardZoneAlgorithms::addCardToList(knownList, &card, 0, false); + + EXPECT_TRUE(card.resetStateCalled); + EXPECT_FALSE(card.resetStateKeepAnnotations); +} + +TEST_F(AddCardAlgorithmTest, KeepAnnotationsTruePassedThrough) +{ + MockCard card; + CardZoneAlgorithms::addCardToList(knownList, &card, 0, true); + + EXPECT_TRUE(card.resetStateCalled); + EXPECT_TRUE(card.resetStateKeepAnnotations); +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/carddatabase/CMakeLists.txt b/tests/carddatabase/CMakeLists.txt new file mode 100644 index 000000000..987e23cd5 --- /dev/null +++ b/tests/carddatabase/CMakeLists.txt @@ -0,0 +1,51 @@ +cmake_minimum_required(VERSION 3.16) +project(CardDatabaseTests VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") + +# ------------------------ +# Definitions +# ------------------------ +add_definitions("-DCARDDB_DATADIR=\"${CMAKE_CURRENT_SOURCE_DIR}/data/\"") + +# ------------------------ +# Card Database Test +# ------------------------ +add_executable(carddatabase_test ${MOCKS_SOURCES} ${VERSION_STRING_CPP} carddatabase_test.cpp mocks.cpp) + +target_link_libraries( + carddatabase_test + PRIVATE libcockatrice_card + PRIVATE Threads::Threads + PRIVATE ${GTEST_BOTH_LIBRARIES} + PRIVATE ${TEST_QT_MODULES} +) + +add_test(NAME carddatabase_test COMMAND carddatabase_test) + +# ------------------------ +# Filter String Test +# (guard must match the condition for libcockatrice_filters in the root CMakeLists.txt) +# ------------------------ +if(WITH_ORACLE OR WITH_CLIENT) + add_executable(filter_string_test ${MOCKS_SOURCES} ${VERSION_STRING_CPP} filter_string_test.cpp mocks.cpp) + + target_link_libraries( + filter_string_test + PRIVATE libcockatrice_filters + PRIVATE Threads::Threads + PRIVATE ${GTEST_BOTH_LIBRARIES} + PRIVATE ${TEST_QT_MODULES} + ) + + add_test(NAME filter_string_test COMMAND filter_string_test) + + if(NOT GTEST_FOUND) + add_dependencies(filter_string_test gtest) + endif() +endif() + +# ------------------------ +# Dependencies on gtest +# ------------------------ +if(NOT GTEST_FOUND) + add_dependencies(carddatabase_test gtest) +endif() diff --git a/tests/carddatabase/carddatabase_test.cpp b/tests/carddatabase/carddatabase_test.cpp new file mode 100644 index 000000000..3fa0e3834 --- /dev/null +++ b/tests/carddatabase/carddatabase_test.cpp @@ -0,0 +1,42 @@ +#include "mocks.h" +#include "test_card_database_path_provider.h" + +#include "gtest/gtest.h" +#include +#include + +namespace +{ + +TEST(CardDatabaseTest, LoadXml) +{ + CardDatabase *db = new CardDatabase(nullptr, new NoopCardPreferenceProvider(), new TestCardDatabasePathProvider(), + new NoopCardSetPriorityController()); + + // ensure the card database is empty at start + ASSERT_EQ(0, db->getCardList().size()) << "Cards not empty at start"; + ASSERT_EQ(0, db->getSetList().size()) << "Sets not empty at start"; + ASSERT_EQ(0, db->query()->getAllMainCardTypes().size()) << "Types not empty at start"; + ASSERT_EQ(NotLoaded, db->getLoadStatus()) << "Incorrect status at start"; + + // load dummy cards and test result + db->loadCardDatabases(); + ASSERT_EQ(9, db->getCardList().size()) << "Wrong card count after load"; + ASSERT_EQ(5, db->getSetList().size()) << "Wrong sets count after load"; + ASSERT_EQ(3, db->query()->getAllMainCardTypes().size()) << "Wrong types count after load"; + ASSERT_EQ(Ok, db->getLoadStatus()) << "Wrong status after load"; + + // ensure the card database is empty after clear() + db->clear(); + ASSERT_EQ(0, db->getCardList().size()) << "Cards not empty after clear"; + ASSERT_EQ(0, db->getSetList().size()) << "Sets not empty after clear"; + ASSERT_EQ(0, db->query()->getAllMainCardTypes().size()) << "Types not empty after clear"; + ASSERT_EQ(NotLoaded, db->getLoadStatus()) << "Incorrect status after clear"; +} +} // namespace + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/tests/carddatabase/data/cards.xml b/tests/carddatabase/data/cards.xml new file mode 100644 index 000000000..2c1c09ed8 --- /dev/null +++ b/tests/carddatabase/data/cards.xml @@ -0,0 +1,76 @@ + + + + + Cat + CAT + 0 + Meow! + + 111 + G + 2G + 2 + Creature — Cat + Creature + 3/3 + + + + Dog + DOG + 0 + Woof! + + 222 + R + 2RR + 4 + Creature — Dog + Creature + 4/4 + + + + Doctor + WHO + 0 + Why did wizards introduce two-word creature types + + 222 + R + 2RR + 4 + Creature — Human Time Lord Doctor + Creature + 4/4 + + + + Not Dead + Not a Card + 0 + Dead! + + 333 + B + B + 1 + Instant + + + + Truth + Not a Card + 0 + Truth! + + 444 + U + 2U + 2 + Instant + + + + diff --git a/tests/carddatabase/data/customsets/customset1.xml b/tests/carddatabase/data/customsets/customset1.xml new file mode 100644 index 000000000..b2f187d54 --- /dev/null +++ b/tests/carddatabase/data/customsets/customset1.xml @@ -0,0 +1,27 @@ + + + + + Sparrow + BRD + W + W + 1 + Creature + 1/1 + 0 + + + + Crow + BRD + B + 1B + 2 + Creature + 2/2 + 0 + + + + diff --git a/tests/carddatabase/data/spoilers.xml b/tests/carddatabase/data/spoilers.xml new file mode 100644 index 000000000..dacd358b3 --- /dev/null +++ b/tests/carddatabase/data/spoilers.xml @@ -0,0 +1,21 @@ + + + + + Fluffy + CAT + 0 + + 1 + + 311 + G + + + Token + Token + 0/1 + + + + diff --git a/tests/carddatabase/data/tokens.xml b/tests/carddatabase/data/tokens.xml new file mode 100644 index 000000000..41b376153 --- /dev/null +++ b/tests/carddatabase/data/tokens.xml @@ -0,0 +1,37 @@ + + + + + Kitten + CAT + 0 + + 1 + + 112 + G + + + Token + Token + 1/1 + + + + Puppy + DOG + 0 + + 1 + + 223 + R + + + Token + Token + 1/1 + + + + diff --git a/tests/carddatabase/filter_string_test.cpp b/tests/carddatabase/filter_string_test.cpp new file mode 100644 index 000000000..c6d68be1f --- /dev/null +++ b/tests/carddatabase/filter_string_test.cpp @@ -0,0 +1,82 @@ +#include "mocks.h" +#include "test_card_database_path_provider.h" + +#include "gtest/gtest.h" +#include +#include +#include + +#define QUERY(name, card, query, match) \ + TEST_F(CardQuery, name) \ + { \ + ASSERT_EQ(FilterString(query).check(card), match); \ + } + +namespace +{ + +class CardQuery : public ::testing::Test +{ +protected: + void SetUp() override + { + CardDatabase *db = new CardDatabase(nullptr, new NoopCardPreferenceProvider(), + new TestCardDatabasePathProvider(), new NoopCardSetPriorityController()); + db->loadCardDatabases(); + + cat = db->query()->getCardBySimpleName("Cat"); + notDeadAfterAll = db->query()->getCardBySimpleName("Not Dead"); + truth = db->query()->getCardBySimpleName("Truth"); + doctor = db->query()->getCardBySimpleName("Doctor"); + } + // void TearDown() override {} + + CardData cat; + CardData notDeadAfterAll; + CardData truth; + CardData doctor; +}; + +QUERY(Empty, cat, "", true) +QUERY(Typing, cat, "t", true) + +QUERY(NonMatchingType, cat, "t:kithkin", false) +QUERY(MatchingType, cat, "t:creature", true) +QUERY(MatchingCreatureType, cat, "t:cat", true) +QUERY(PartialMatchingType, cat, "t:ca", false) +QUERY(MatchingMultiWordType, doctor, "t:\"Time Lord\"", true) +QUERY(Not1, cat, "NOT t:kithkin", true) +QUERY(Not2, cat, "NOT t:creature", false) +QUERY(NonKeyword1, cat, "not t:kithkin", false) +QUERY(NonKeyword2, cat, "t:bat or t:creature", false) +QUERY(NonKeyword3, notDeadAfterAll, "not dead", true) +QUERY(NonKeyword4, truth, "truth or trail", false) +QUERY(Case, cat, "t:cReAtUrE", true) + +QUERY(And, cat, "t:creature t:creature", true) +QUERY(And2, cat, "t:creature t:sorcery", false) + +QUERY(Or, cat, "t:bat OR t:creature", true) + +QUERY(Cmc1, cat, "cmc=2", true) +QUERY(Cmc2, cat, "cmc>3", false) +QUERY(Cmc3, cat, "cmc>1", true) + +QUERY(Quotes, cat, "t:\"creature\"", true) + +QUERY(Field, cat, "pt:\"3/3\"", true) + +QUERY(Color1, cat, "c:g", true) +QUERY(Color2, cat, "c:gw", true) +QUERY(Color3, cat, "c!g", true) +QUERY(Color4, cat, "c!gw", false) + +QUERY(BracketNextToUnquotedString, cat, "(o:woof OR o:meow)", true) + +} // namespace + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/carddatabase/mocks.cpp b/tests/carddatabase/mocks.cpp new file mode 100644 index 000000000..5568ac84f --- /dev/null +++ b/tests/carddatabase/mocks.cpp @@ -0,0 +1,6 @@ + +#include "mocks.h" + +void CardPictureLoader::clearPixmapCache(CardInfoPtr /* card */) +{ +} diff --git a/tests/carddatabase/mocks.h b/tests/carddatabase/mocks.h new file mode 100644 index 000000000..016642005 --- /dev/null +++ b/tests/carddatabase/mocks.h @@ -0,0 +1,16 @@ +/* + * Beware of this preprocessor hack used to redefine the settingCache class + * instead of including it and all of its dependencies. + * Always set header guards of mocked objects before including any headers + * with mocked objects. + */ + +#define PICTURELOADER_H + +#include + +class CardPictureLoader +{ +public: + static void clearPixmapCache(CardInfoPtr card); +}; diff --git a/tests/carddatabase/test_card_database_path_provider.h b/tests/carddatabase/test_card_database_path_provider.h new file mode 100644 index 000000000..8fc8ef962 --- /dev/null +++ b/tests/carddatabase/test_card_database_path_provider.h @@ -0,0 +1,28 @@ +#ifndef COCKATRICE_TEST_CARD_DATABASE_PATH_PROVIDER_H +#define COCKATRICE_TEST_CARD_DATABASE_PATH_PROVIDER_H + +#include + +class TestCardDatabasePathProvider : public ICardDatabasePathProvider +{ + +public: + QString getCardDatabasePath() const override + { + return QString("%1/cards.xml").arg(CARDDB_DATADIR); + } + QString getCustomCardDatabasePath() const override + { + return QString("%1/customsets/").arg(CARDDB_DATADIR); + } + QString getTokenDatabasePath() const override + { + return QString("%1/tokens.xml").arg(CARDDB_DATADIR); + } + QString getSpoilerCardDatabasePath() const override + { + return QString("%1/spoiler.xml").arg(CARDDB_DATADIR); + } +}; + +#endif // COCKATRICE_TEST_CARD_DATABASE_PATH_PROVIDER_H diff --git a/tests/deck_hash_performance_test.cpp b/tests/deck_hash_performance_test.cpp new file mode 100644 index 000000000..154283e8e --- /dev/null +++ b/tests/deck_hash_performance_test.cpp @@ -0,0 +1,81 @@ +#include "gtest/gtest.h" +#include +#include + +static constexpr int amount = 1e5; +QString repeatDeck; +QString numberDeck; +QString uniquesDeck; +QString uniquesXorDeck; +QString duplicatesDeck; + +TEST(DeckHashTest, RepeatTest) +{ + DeckList decklist(repeatDeck); + for (int i = 0; i < amount; ++i) { + decklist.getDeckHash(); + decklist.refreshDeckHash(); + } + auto hash = decklist.getDeckHash().toStdString(); + ASSERT_EQ(hash, "5cac19qm") << "The hash does not match!"; +} + +TEST(DeckHashTest, NumberTest) +{ + DeckList decklist(numberDeck); + auto hash = decklist.getDeckHash().toStdString(); + ASSERT_EQ(hash, "e0m38p19") << "The hash does not match!"; +} + +TEST(DeckHashTest, UniquesTest) +{ + DeckList decklist(uniquesDeck); + auto hash = decklist.getDeckHash().toStdString(); + ASSERT_EQ(hash, "88prk025") << "The hash does not match!"; +} + +TEST(DeckHashTest, UniquesTestXor) +{ + DeckList decklist(uniquesXorDeck); + auto hash = decklist.getDeckHash().toStdString(); + ASSERT_EQ(hash, "hkn6q4pf") << "The hash does not match!"; +} + +TEST(DeckHashTest, DuplicatesTest) +{ + DeckList decklist(duplicatesDeck); + auto hash = decklist.getDeckHash().toStdString(); + ASSERT_EQ(hash, "ekt6tg1h") << "The hash does not match!"; +} + +int main(int argc, char **argv) +{ + const QString deckStart = + R"()"; + const QString deckEnd = R"()"; + + repeatDeck = + deckStart + + R"()" + + deckEnd; + numberDeck = deckStart + QString(R"()").arg(amount) + deckEnd; + + QStringList deckString{deckStart}; + QStringList deckStringXor = deckString; + int len = QString::number(amount).length(); + for (int i = 0; i < amount; ++i) { + // creates already sorted list + deckString << R"()"; + // xor in order to mess with sorting + deckStringXor << R"()"; + } + deckString << deckEnd; + deckStringXor << deckEnd; + uniquesDeck = deckString.join(""); + uniquesXorDeck = deckStringXor.join(""); + + duplicatesDeck = deckStart + QString(R"()").repeated(amount) + deckEnd; + + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/dummy_test.cpp b/tests/dummy_test.cpp new file mode 100644 index 000000000..31a01bcff --- /dev/null +++ b/tests/dummy_test.cpp @@ -0,0 +1,19 @@ +#include "gtest/gtest.h" + +namespace +{ +class FooTest : public ::testing::Test +{ +}; + +TEST(DummyTest, Works) +{ + ASSERT_EQ(1, 1) << "One is not equal to one"; +} +} // namespace + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/tests/expression_test.cpp b/tests/expression_test.cpp new file mode 100644 index 000000000..4fbe98cb9 --- /dev/null +++ b/tests/expression_test.cpp @@ -0,0 +1,31 @@ +#include "gtest/gtest.h" +#include +#include + +#define TEST_EXPR(name, a, b) \ + TEST(ExpressionTest, name) \ + { \ + Expression exp(8); \ + ASSERT_EQ(exp.parse(a), b) << a; \ + } + +namespace +{ + +TEST_EXPR(Number, "1", 1) +TEST_EXPR(Multiply, "2*2", 4) +TEST_EXPR(Whitespace, "3 * 3", 9) +TEST_EXPR(Powers, "2^8", 256) +TEST_EXPR(OrderOfOperations, "2+2*2", 6) +TEST_EXPR(Fn, "2*cos(1)", 2 * qCos(1)) +TEST_EXPR(Variable, "x / 2", 4) +TEST_EXPR(Negative, "-2 * 2", -4) +TEST_EXPR(UnknownFnReturnsZero, "blah(22)", 0) + +} // namespace + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/loading_from_clipboard/CMakeLists.txt b/tests/loading_from_clipboard/CMakeLists.txt new file mode 100644 index 000000000..719d62f45 --- /dev/null +++ b/tests/loading_from_clipboard/CMakeLists.txt @@ -0,0 +1,12 @@ +add_definitions("-DCARDDB_DATADIR=\"${CMAKE_CURRENT_SOURCE_DIR}/data/\"") +add_executable(loading_from_clipboard_test ${VERSION_STRING_CPP} clipboard_testing.cpp loading_from_clipboard_test.cpp) + +if(NOT GTEST_FOUND) + add_dependencies(loading_from_clipboard_test gtest) +endif() + +target_link_libraries( + loading_from_clipboard_test libcockatrice_deck_list libcockatrice_card Threads::Threads ${GTEST_BOTH_LIBRARIES} + ${TEST_QT_MODULES} +) +add_test(NAME loading_from_clipboard_test COMMAND loading_from_clipboard_test) diff --git a/tests/loading_from_clipboard/clipboard_testing.cpp b/tests/loading_from_clipboard/clipboard_testing.cpp new file mode 100644 index 000000000..a9977b800 --- /dev/null +++ b/tests/loading_from_clipboard/clipboard_testing.cpp @@ -0,0 +1,55 @@ +#include "clipboard_testing.h" + +#include +#include +#include + +DeckList getDeckList(const QString &clipboard) +{ + DeckList deckList; + QString cp(clipboard); + QTextStream stream(&cp); // text stream requires local copy + deckList.loadFromStream_Plain(stream, false, CardNameNormalizer()); + return deckList; +} + +void testEmpty(const QString &clipboard) +{ + DeckList deckList = getDeckList(clipboard); + + ASSERT_TRUE(deckList.getCardList().isEmpty()); +} + +void testHash(const QString &clipboard, const std::string &hash) +{ + DeckList deckList = getDeckList(clipboard); + + ASSERT_EQ(deckList.getDeckHash().toStdString(), hash); +} + +void testDeck(const QString &clipboard, const Result &result) +{ + DeckList deckList = getDeckList(clipboard); + + ASSERT_EQ(result.name, deckList.getName().toStdString()); + ASSERT_EQ(result.comments, deckList.getComments().toStdString()); + + CardRows mainboard; + CardRows sideboard; + + auto extractCards = [&mainboard, &sideboard](const InnerDecklistNode *innerDecklistNode, + const DecklistCardNode *card) { + if (innerDecklistNode->getName() == DECK_ZONE_MAIN) { + mainboard.append({card->getName().toStdString(), card->getNumber()}); + } else if (innerDecklistNode->getName() == DECK_ZONE_SIDE) { + sideboard.append({card->getName().toStdString(), card->getNumber()}); + } else { + FAIL(); + } + }; + + deckList.forEachCard(extractCards); + + ASSERT_EQ(result.mainboard, mainboard); + ASSERT_EQ(result.sideboard, sideboard); +} diff --git a/tests/loading_from_clipboard/clipboard_testing.h b/tests/loading_from_clipboard/clipboard_testing.h new file mode 100644 index 000000000..41d8ea794 --- /dev/null +++ b/tests/loading_from_clipboard/clipboard_testing.h @@ -0,0 +1,27 @@ +#ifndef CLIPBOARD_TESTING_H +#define CLIPBOARD_TESTING_H + +#include "gtest/gtest.h" +#include + +// using std types because qt types aren't understood by gtest (without this you'll get less nice errors) +using CardRows = QVector>; + +struct Result +{ + std::string name; + std::string comments; + CardRows mainboard; + CardRows sideboard; + + Result(std::string _name, std::string _comments, CardRows _mainboard, CardRows _sideboard) + : name(_name), comments(_comments), mainboard(_mainboard), sideboard(_sideboard) + { + } +}; + +void testEmpty(const QString &clipboard); +void testHash(const QString &clipboard, const std::string &hash); +void testDeck(const QString &clipboard, const Result &result); + +#endif // CLIPBOARD_TESTING_H diff --git a/tests/loading_from_clipboard/loading_from_clipboard_test.cpp b/tests/loading_from_clipboard/loading_from_clipboard_test.cpp new file mode 100644 index 000000000..fcfbb22db --- /dev/null +++ b/tests/loading_from_clipboard/loading_from_clipboard_test.cpp @@ -0,0 +1,226 @@ +#include "clipboard_testing.h" + +// Testing is done by using the DeckList::loadFromString_Plain function in common/decklist.h +// It does not check if cards are in the database at all, so no comparisons to the database will be made. + +TEST(LoadingFromClipboardTest, EmptyDeck) +{ + testEmpty(""); +} + +TEST(LoadingFromClipboardTest, EmptySideboard) +{ + testEmpty("Sideboard"); +} + +TEST(LoadingFromClipboardTest, QuantityPrefixed) +{ + QString clipboard("1 Mountain\n" + "2x Island\n" + "3x Forest\n"); + Result result("", "", {{"Mountain", 1}, {"Island", 2}, {"Forest", 3}}, {}); + testDeck(clipboard, result); +} + +TEST(LoadingFromClipboardTest, CommentsAreIgnored) +{ + QString clipboard("//1 Mountain\n" + "//2x Island\n" + "//SB:2x Island\n"); + testEmpty(clipboard); +} + +TEST(LoadingFromClipboardTest, SideboardPrefix) +{ + QString clipboard("1 Mountain\n" + "SB: 1 Mountain\n" + "sb: 2x Island\n" + "2 Swamp\n" + "\n" + "3 Plains\n"); + Result result("", "", {{"Mountain", 1}, {"Swamp", 2}, {"Plains", 3}}, {{"Mountain", 1}, {"Island", 2}}); + testDeck(clipboard, result); +} + +TEST(LoadingFromClipboardTest, SideboardLine) +{ + QString clipboard("1 Mountain\n" + "2 Swamp\n" + "\n" + "3 Plains\n" + "sideboard\n" + "1 Mountain\n" + "2x Island\n"); + Result result("", "", {{"Mountain", 1}, {"Swamp", 2}, {"Plains", 3}}, {{"Mountain", 1}, {"Island", 2}}); + testDeck(clipboard, result); +} + +TEST(LoadingFromClipboardTest, UnknownCardsAreNotDiscarded) +{ + QString clipboard("1 CardThatDoesNotExistInCardsXml\n"); + Result result("", "", {{"CardThatDoesNotExistInCardsXml", 1}}, {}); + testDeck(clipboard, result); +} + +TEST(LoadingFromClipboardTest, WeirdWhitespaceIsIgnored) +{ + QString clipboard( + "\t\tSb:\t1\tOur Market Research Shows That Players Like Really Long Card Names So We Made " + " This Card to Have\tthe Absolute \t Longest Card Name \tEver Elemental\t\n\t"); + Result result("", "", {}, + {{"Our Market Research Shows That Players Like Really Long Card Names So We Made This Card to Have " + "the Absolute Longest Card Name Ever Elemental", + 1}}); + testDeck(clipboard, result); +} + +TEST(LoadingFromClipboardTest, RemoveBlankEntriesFromBeginningAndEnd) +{ + QString clipboard("\n" + "\n" + "\n" + "1x Algae Gharial\n" + "3x CardThatDoesNotExistInCardsXml\n" + "2x Phelddagrif\n" + "\n" + "\n"); + + Result result("", "", {{"Algae Gharial", 1}, {"CardThatDoesNotExistInCardsXml", 3}, {"Phelddagrif", 2}}, {}); + testDeck(clipboard, result); +} + +TEST(LoadingFromClipboardTest, UseFirstBlankIfOnlyOneBlankToSplitSideboard) +{ + QString clipboard("1x Algae Gharial\n" + "3x CardThatDoesNotExistInCardsXml\n" + "\n" + "2x Phelddagrif\n"); + + Result result("", "", {{"Algae Gharial", 1}, {"CardThatDoesNotExistInCardsXml", 3}}, {{"Phelddagrif", 2}}); + testDeck(clipboard, result); +} + +TEST(LoadingFromClipboardTest, IfMultipleScatteredBlanksAllMainBoard) +{ + QString clipboard("1x Algae Gharial\n" + "3x CardThatDoesNotExistInCardsXml\n" + "\n" + "2x Phelddagrif\n" + "\n" + "3 Giant Growth\n"); + + Result result( + "", "", {{"Algae Gharial", 1}, {"CardThatDoesNotExistInCardsXml", 3}, {"Phelddagrif", 2}, {"Giant Growth", 3}}, + {}); + testDeck(clipboard, result); +} + +TEST(LoadingFromClipboardTest, EdgeCaseTesting) +{ + QString clipboard(R"( +// DeckName + + // Comment 1 + +// +//Comment [two] +//(test) Æ ’ | / (3) + + +// Mainboard (11 cards) +Æther Adept +2x Fire // Ice +1 Minsc & Boo, Timeless Heroes +3 Pain/Suffering +4X [B] Forest (3) + + +// Sideboard (11 cards) + +5x [WTH] Nature’s Resurgence +6X Gaea's Skyfolk +7 B.F.M. (Big Furry Monster) + + + +)"); + + Result result("DeckName", "Comment 1\n\nComment [two]\n(test) Æ ’ | / (3)", + {{"Aether Adept", 1}, + {"Fire // Ice", 2}, + {"Minsc & Boo, Timeless Heroes", 1}, + {"Pain // Suffering", 3}, + {"Forest", 4}}, + {{"Nature's Resurgence", 5}, {"Gaea's Skyfolk", 6}, {"B.F.M. (Big Furry Monster)", 7}}); + testDeck(clipboard, result); +} + +TEST(LoadingFromClipboardTest, CommentsBeforeCardsTesting) +{ + QString clipboard("// Title from website.com\n" + "// A nice deck\n" + "// With nice cards\n" + "\n" + "// Mainboard\n" + "1 test1\n" + "Sideboard\n" + "2 test2\n"); + + Result result("Title from website.com", "A nice deck\nWith nice cards", {{"test1", 1}}, {{"test2", 2}}); + testDeck(clipboard, result); +} + +TEST(LoadingFromClipboardTest, mainboardAsLine) +{ + QString clipboard("// Deck Name\n" + "\n" + "MainBoard: 3 cards\n" + "3 card\n" + "\n" + "SideBoard: 2 cards\n" + "2 sidecard\n"); + + Result result("Deck Name", "", {{"card", 3}}, {{"sidecard", 2}}); + testDeck(clipboard, result); +} + +TEST(LoadingFromClipboardTest, deckAsCard) +{ + QString clipboard("6 Deck of Cards But Animated\n" + "\n" + "7 Sideboard Card\n"); + + Result result("", "", {{"Deck of Cards But Animated", 6}}, {{"Sideboard Card", 7}}); + testDeck(clipboard, result); +} + +TEST(LoadingFromClipboardTest, emptyMainBoard) +{ + QString clipboard("deck\n" + "\n" + "sideboard\n"); + + testEmpty(clipboard); +} + +TEST(LoadingFromClipboardTest, emptyHash) +{ + QString clipboard(""); + + testHash(clipboard, "r8sq7riu"); +} + +TEST(LoadingFromClipboardTest, deckHash) +{ + QString clipboard("1 Mountain\n" + "2 Island\n" + "SB: 3 Forest\n"); + + testHash(clipboard, "5cac19qm"); +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/movecard_tests/CMakeLists.txt b/tests/movecard_tests/CMakeLists.txt new file mode 100755 index 000000000..769047148 --- /dev/null +++ b/tests/movecard_tests/CMakeLists.txt @@ -0,0 +1,16 @@ +add_executable(reverse_card_move_test reverse_card_move_test.cpp) + +if(NOT GTEST_FOUND) + add_dependencies(reverse_card_move_test gtest) +endif() + +target_link_libraries( + reverse_card_move_test + PRIVATE libcockatrice_network_server_remote + PRIVATE libcockatrice_rng + PRIVATE Threads::Threads + PRIVATE ${GTEST_BOTH_LIBRARIES} + PRIVATE ${TEST_QT_MODULES} +) + +add_test(NAME reverse_card_move_test COMMAND reverse_card_move_test) diff --git a/tests/movecard_tests/reverse_card_move_test.cpp b/tests/movecard_tests/reverse_card_move_test.cpp new file mode 100644 index 000000000..2231a7e3b --- /dev/null +++ b/tests/movecard_tests/reverse_card_move_test.cpp @@ -0,0 +1,91 @@ +#include "game/server_abstract_player.h" +#include "game/server_card.h" +#include "game/server_cardzone.h" +#include "game/server_game.h" +#include "server_response_containers.h" +#include "server_room.h" +#include "server_test_helpers.h" + +#include +#include +#include +#include +#include + +RNG_Abstract *rng = nullptr; // this needs to be defined due to other functions in server + +TEST(ReverseCardMoveTest, MoveCardFromBottomTest) +{ + ServerInfo_User user; + user.set_name("test-user"); + + // instantiate a fake server instance + FakeServer server; + Server_Room room(0, 0, "", "", "", "", false, "", {}, &server); + Server_Game game(user, 1, "", "", 2, QList(), false, false, false, false, false, false, 20, false, &room); + Server_AbstractPlayer player(&game, 1, user, false, nullptr); + Server_CardZone deckZone(&player, ZoneNames::DECK, true, ServerInfo_Zone::PublicZone); + Server_CardZone exileZone(&player, ZoneNames::EXILE, true, ServerInfo_Zone::PublicZone); + + // setup the deck with 20 useless cards + for (int i = 0; i < 20; i++) { + auto *cardUseless = new Server_Card({"Card Useless", "card-Useless"}, player.newCardId(), i, 0); + deckZone.insertCard(cardUseless, i, 0); + } + + // add 4 cards to the end of it + auto *cardA = new Server_Card({"Card A", "card-a"}, player.newCardId(), 20, 0); + auto *cardB = new Server_Card({"Card B", "card-b"}, player.newCardId(), 21, 0); + auto *cardC = new Server_Card({"Card C", "card-c"}, player.newCardId(), 22, 0); + auto *cardD = new Server_Card({"Card D", "card-d"}, player.newCardId(), 23, 0); + + deckZone.insertCard(cardA, 20, 0); + deckZone.insertCard(cardB, 21, 0); + deckZone.insertCard(cardC, 22, 0); + deckZone.insertCard(cardD, 23, 0); + + // try to move them, with the expected client given order (n-3, n-2, n-1, n) + CardToMove moveA; + moveA.set_card_id(cardA->getId()); + CardToMove moveB; + moveB.set_card_id(cardB->getId()); + CardToMove moveC; + moveC.set_card_id(cardC->getId()); + CardToMove moveD; + moveD.set_card_id(cardD->getId()); + + QList cardsToMove = {&moveA, &moveB, &moveC, &moveD}; + GameEventStorage ges; + + const auto response = player.moveCard(ges, &deckZone, cardsToMove, &exileZone, 0, 0, false, false, false); + + EXPECT_EQ(response, Response::RespOk); + + int positionA; + int positionB; + int positionC; + int positionD; + // find the cards in the destination zone and check they are the right card + EXPECT_EQ(exileZone.getCard(cardA->getId(), &positionA), cardA); + EXPECT_EQ(exileZone.getCard(cardB->getId(), &positionB), cardB); + EXPECT_EQ(exileZone.getCard(cardC->getId(), &positionC), cardC); + EXPECT_EQ(exileZone.getCard(cardD->getId(), &positionD), cardD); + + // check that they are at the expected index + EXPECT_EQ(cardA->getX(), 3); + EXPECT_EQ(cardB->getX(), 2); + EXPECT_EQ(cardC->getX(), 1); + EXPECT_EQ(cardD->getX(), 0); + + // also check if the given positions are correct + EXPECT_EQ(positionA, 3); + EXPECT_EQ(positionB, 2); + EXPECT_EQ(positionC, 1); + EXPECT_EQ(positionD, 0); +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/movecard_tests/server_test_helpers.h b/tests/movecard_tests/server_test_helpers.h new file mode 100644 index 000000000..fd2ed6c17 --- /dev/null +++ b/tests/movecard_tests/server_test_helpers.h @@ -0,0 +1,42 @@ +#include "server.h" +#include "server_database_interface.h" + +class MockDatabaseInterface : public Server_DatabaseInterface +{ +public: + AuthenticationResult checkUserPassword(Server_ProtocolHandler *, + const QString &, + const QString &, + const QString &, + QString &, + int &, + bool) override + { + return NotLoggedIn; + } + ServerInfo_User getUserData(const QString &, bool) override + { + return ServerInfo_User(); + } + int getNextGameId() override + { + return 1; + } + int getNextReplayId() override + { + return 1; + } + int getActiveUserCount(QString) override + { + return 1; + } +}; + +class FakeServer : public Server +{ +public: + FakeServer() + { + setDatabaseInterface(new MockDatabaseInterface()); + } +}; diff --git a/tests/oracle/CMakeLists.txt b/tests/oracle/CMakeLists.txt new file mode 100644 index 000000000..c5c1e9097 --- /dev/null +++ b/tests/oracle/CMakeLists.txt @@ -0,0 +1,9 @@ +add_executable(parse_cipt_test ../../oracle/src/parsehelpers.cpp parse_cipt_test.cpp) + +if(NOT GTEST_FOUND) + add_dependencies(parse_cipt_test gtest) +endif() + +target_link_libraries(parse_cipt_test Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) + +add_test(NAME parse_cipt_test COMMAND parse_cipt_test) diff --git a/tests/oracle/parse_cipt_test.cpp b/tests/oracle/parse_cipt_test.cpp new file mode 100644 index 000000000..f5036f962 --- /dev/null +++ b/tests/oracle/parse_cipt_test.cpp @@ -0,0 +1,219 @@ +#include "../../oracle/src/parsehelpers.h" + +#include "gtest/gtest.h" + +TEST(ParseCiptTest, parsesThisEntersTapped) +{ + auto name = "Boring Fields"; + auto text = "This land enters tapped."; + + ASSERT_TRUE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, parsesThisEntersTheBattlefieldTapped) +{ + auto name = "Boring Fields"; + auto text = "This land enters the battlefield tapped.\n" + "{T}: Add {G}."; + + ASSERT_TRUE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, parsesItEntersTappedAtEndOfSentence) +{ + auto name = "Shocking Fields"; + auto text = "As this land enters, you may pay 2 life. If you don't, it enters tapped.\n" + "{T}: Add {G}."; + + ASSERT_TRUE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, parsesThisEntersTappedWhenNotOnFirstLine) +{ + auto name = "Boring Fields"; + auto text = "Flying\n" + "This land enters tapped.\n" + "{T}: Add {G}."; + + ASSERT_TRUE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, parsesFullNameWithUnderscoreAppendedText) +{ + auto name = "Boring Fields_SL50"; + auto text = "Boring Fields enters tapped.\n" + "{T}: Add {G}."; + + ASSERT_TRUE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, parsesFullNameWithBracketsAppendedText) +{ + auto name = "Boring Fields (SL50)"; + auto text = "Boring Fields enters tapped.\n" + "{T}: Add {G}."; + + ASSERT_TRUE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, parsesFullNameWithComma) +{ + auto name = "Bob, the Legend"; + auto text = "Bob, the Legend enters tapped.\n" + "Whenever Bob attacks, you win the game."; + + ASSERT_TRUE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, parsesFullNameWithCommaAtEndOfSentence) +{ + auto name = "Bob, the Legend"; + auto text = "As Bob, the Legend enters, you may pay 2 life. If you don't, Bob, the Legend enters tapped.\n" + "Whenever Bob attacks, you win the game."; + + ASSERT_TRUE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, parsesFullNameWithApostropheAtEndOfSentence) +{ + auto name = "Bob's Bobber"; + auto text = "As Bob's Bobber enters, you may pay 2 life. If you don't, Bob's Bobber enters tapped.\n" + "Whenever Bob attacks, you win the game."; + + ASSERT_TRUE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, parsesShortnameEndingWithComma) +{ + auto name = "Bob, the Legend"; + auto text = "Bob enters tapped.\n" + "Whenever Bob attacks, you win the game."; + + ASSERT_TRUE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, parsesShortnameEndingWithSpace) +{ + auto name = "Bob the Legend"; + auto text = "Bob enters tapped.\n" + "Whenever Bob attacks, you win the game."; + + ASSERT_TRUE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, parsesMultiWordShortnameEndingWithComma) +{ + auto name = "Bob Dod, the Legend"; + auto text = "Bob Dod enters tapped.\n" + "Whenever Bob Dod attacks, you win the game."; + + ASSERT_TRUE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, parsesMultiWordShortnameEndingWithSpace) +{ + auto name = "Bob Dod the Legend"; + auto text = "Bob Dod enters tapped.\n" + "Whenever Bob Dod attacks, you win the game."; + + ASSERT_TRUE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, parsesShortnameEndingWithSpaceWithUnderscoreAppendedText) +{ + auto name = "Bob the Legend_SL50"; + auto text = "Bob enters tapped.\n" + "Whenever Bob attacks, you win the game."; + + ASSERT_TRUE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, parsesShortnameEndingWithSpaceWithBracketsAppendedText) +{ + auto name = "Bob the Legend (SL50)"; + auto text = "Bob enters tapped.\n" + "Whenever Bob attacks, you win the game."; + + ASSERT_TRUE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, parsesMultiWordShortnameEndingWithSpaceWithUnderscoreAppendedText) +{ + auto name = "Bob Dod the Legend_SL50"; + auto text = "Bob Dod enters tapped.\n" + "Whenever Bob Dod attacks, you win the game."; + + ASSERT_TRUE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, parsesMultiWordShortnameEndingWithSpaceWithBracketsAppendedText) +{ + auto name = "Bob Dod the Legend (SL50)"; + auto text = "Bob Dod enters tapped.\n" + "Whenever Bob Dod attacks, you win the game."; + + ASSERT_TRUE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, rejectsEmptyText) +{ + auto name = "Vanilla Dude"; + auto text = ""; + + ASSERT_FALSE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, rejectsEntersTappedUnless) +{ + auto name = "Fast Fields"; + auto text = "This land enters tapped unless you control another land."; + + ASSERT_FALSE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, rejectsWhenNameIsDifferent) +{ + auto name = "Boring Fields"; + auto text = "Fast Fields enters tapped."; + + ASSERT_FALSE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, rejectsOtherCreaturesEnterTapped) +{ + auto name = "Imposing Guy"; + auto text = "Other creatures enter tapped."; + + ASSERT_FALSE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, rejectsAbilityGrantingEntersTapped) +{ + auto name = "Imposing Guy"; + auto text = "Other creatures have \"This creature enters tapped\"."; + + ASSERT_FALSE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, parsesEntersTappedAndAbilityGrantingEntersTappedOnSameCard) +{ + auto name = "Imposing Guy"; + auto text = "This creature enters tapped." + "Other creatures have \"This creature enters tapped\"."; + + ASSERT_TRUE(parseCipt(name, text)); +} + +TEST(ParseCiptTest, rejectsItEntersTappedAndAttacking) +{ + auto name = "Token Maker"; + auto text = "When Token Maker attacks, create a token. It enters tapped and attacking."; + + ASSERT_FALSE(parseCipt(name, text)); +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/password_hash_test.cpp b/tests/password_hash_test.cpp new file mode 100644 index 000000000..38d9b6315 --- /dev/null +++ b/tests/password_hash_test.cpp @@ -0,0 +1,38 @@ +#include "gtest/gtest.h" +#include +#include +#include + +RNG_Abstract *rng; + +namespace +{ +class PasswordHashTest : public ::testing::Test +{ +protected: + void SetUp() override + { + rng = new RNG_SFMT; + } + + void TearDown() override + { + delete rng; + } +}; + +TEST(PasswordHashTest, RegressionTest) +{ + QString salt = "saltsaltsaltsalt"; + QString password = "password"; + QString expected = "vmKoWv975yf+WT2QCXhW48JNzZ2ghGxdgNvuKLBU0h7s6AQHSG72J6QO4ZswuSeqvBbAXbmgJSRBaSJrgc55WA=="; + QString hash = PasswordHasher::computeHash(password, salt); + ASSERT_EQ(hash, salt + expected) << "The computed hash value remains the same"; +} +} // namespace + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/test_age_formatting.cpp b/tests/test_age_formatting.cpp new file mode 100644 index 000000000..e4fc64cf9 --- /dev/null +++ b/tests/test_age_formatting.cpp @@ -0,0 +1,44 @@ +#include "../cockatrice/src/interface/widgets/server/user/user_info_box.h" + +#include "gtest/gtest.h" + +namespace +{ +using dayyear = QPair; + +TEST(AgeFormatting, Zero) +{ + auto got = UserInfoBox::getDaysAndYearsBetween(QDate(2000, 1, 1), QDate(2000, 1, 1)); + ASSERT_EQ(got, dayyear(0, 0)) << "these are the same day"; +} + +TEST(AgeFormatting, LeapDay) +{ + auto got = UserInfoBox::getDaysAndYearsBetween(QDate(2000, 2, 28), QDate(2000, 3, 1)); + ASSERT_EQ(got, dayyear(2, 0)) << "there is a leap day in between these days"; +} + +TEST(AgeFormatting, LeapYear) +{ + auto got = UserInfoBox::getDaysAndYearsBetween(QDate(2000, 1, 1), QDate(2001, 1, 1)); + ASSERT_EQ(got, dayyear(0, 1)) << "there is a leap day in between these dates, but that's fine"; +} + +TEST(AgeFormatting, LeapDayWithYear) +{ + auto got = UserInfoBox::getDaysAndYearsBetween(QDate(2000, 2, 28), QDate(2001, 3, 1)); + ASSERT_EQ(got, dayyear(1, 1)) << "there is a leap day in between these days but not in the last year"; +} + +TEST(AgeFormatting, LeapDayThisYear) +{ + auto got = UserInfoBox::getDaysAndYearsBetween(QDate(2003, 2, 28), QDate(2004, 3, 1)); + ASSERT_EQ(got, dayyear(2, 1)) << "there is a leap day in between these days this year"; +} +} // namespace + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/travis-dependencies.sh b/travis-dependencies.sh deleted file mode 100755 index 533a213b1..000000000 --- a/travis-dependencies.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -if [[ $TRAVIS_OS_NAME == "osx" ]] ; then - brew update - brew install qt cmake protobuf -else - sudo apt-get update -qq - sudo apt-get install -y qtmobility-dev libprotobuf-dev protobuf-compiler libqt4-dev -fi diff --git a/vcpkg b/vcpkg new file mode 160000 index 000000000..74e653621 --- /dev/null +++ b/vcpkg @@ -0,0 +1 @@ +Subproject commit 74e6536215718009aae747d86d84b78376bf9e09 diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 000000000..ac7b75d07 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", + "dependencies": [ + "gtest", + "liblzma", + "openssl", + "protobuf", + "pthreads", + "zlib" + ] +} diff --git a/webclient/.env b/webclient/.env new file mode 100644 index 000000000..8592b28b4 --- /dev/null +++ b/webclient/.env @@ -0,0 +1 @@ +# Future template for server admin configuration diff --git a/webclient/.env.development b/webclient/.env.development new file mode 100644 index 000000000..2accbba81 --- /dev/null +++ b/webclient/.env.development @@ -0,0 +1 @@ +ESLINT_NO_DEV_ERRORS=true diff --git a/webclient/.env.production b/webclient/.env.production new file mode 100644 index 000000000..02269f00d --- /dev/null +++ b/webclient/.env.production @@ -0,0 +1 @@ +DISABLE_ESLINT_PLUGIN=true diff --git a/webclient/.env.test b/webclient/.env.test new file mode 100644 index 000000000..8711f95ab --- /dev/null +++ b/webclient/.env.test @@ -0,0 +1 @@ +CI=true diff --git a/webclient/.eslintrc.js b/webclient/.eslintrc.js new file mode 100644 index 000000000..98ce44430 --- /dev/null +++ b/webclient/.eslintrc.js @@ -0,0 +1,48 @@ +module.exports = { + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": {"project": ["./tsconfig.json"]}, + "plugins": [ + "@typescript-eslint" + ], + "ignorePatterns": ["node_modules/*", "build/*", "public/pb/*"], + "env": { + "jest": true + }, + "rules": { + "array-bracket-spacing": ["error", "never"], + "arrow-spacing": ["error", {"before": true, "after": true}], + "block-spacing": ["error", "always"], + "brace-style": ["error", "1tbs", {"allowSingleLine": false}], + "comma-spacing": ["error", {"before": false, "after": true}], + "comma-style": ["error", "last"], + "computed-property-spacing": ["error", "never"], + "curly": ["error", "all"], + "dot-location": ["error", "property"], + "eol-last": ["error"], + "func-names": ["warn"], + "indent": ["error", 2, {"SwitchCase": 1}], + "key-spacing": ["error", {"beforeColon": false, "afterColon": true}], + "keyword-spacing": ["error"], + "linebreak-style": ["error", (process.platform === "win32" ? "windows" : "unix")], + "max-len": ["error", {"code": 140}], + "no-eq-null": ["off"], + "no-func-assign": ["error"], + "no-inline-comments": ["error"], + "no-mixed-spaces-and-tabs": ["error"], + "no-multi-spaces": ["error"], + "no-spaced-func": ["error"], + "no-trailing-spaces": ["error"], + "no-var": ["error"], + "object-curly-spacing": ["error", "always"], + "one-var": ["error", "never"], + "one-var-declaration-per-line": ["error"], + "quotes": ["error", "single"], + "semi-spacing": ["error", {"before": false, "after": true}], + "space-before-blocks": ["error"], + "space-before-function-paren": ["error", {"asyncArrow": "always", "anonymous": "never", "named": "never"}], + "space-in-parens": ["error", "never"], + "space-infix-ops": ["error"], + "space-unary-ops": ["error", {"words": true, "nonwords": false}] + } +} \ No newline at end of file diff --git a/webclient/.gitignore b/webclient/.gitignore new file mode 100644 index 000000000..2b30e2c8d --- /dev/null +++ b/webclient/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# generated ./src files +/src/proto-files.json +/src/server-props.json + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webclient/.npmrc b/webclient/.npmrc new file mode 100644 index 000000000..521a9f7c0 --- /dev/null +++ b/webclient/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/webclient/README.md b/webclient/README.md new file mode 100644 index 000000000..436ab4fad --- /dev/null +++ b/webclient/README.md @@ -0,0 +1,73 @@ +## Application Architecture +![Application Architecture](architecture.png?raw=true "Application Architecture") + +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `npm start` + +Runs the app in the development mode.
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.
+You will also see any lint errors in the console. + +### `npm test` + +Launches the test runner in the interactive watch mode.
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `npm run build` + +Builds the app for production to the `build` folder.
+It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.
+Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `npm run eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). + +## To-Do List + +1) RefreshGuard modal + - there is no browser support for displaying custom output to window.onbeforeunload + - we should also display a custom modal explaining why they shouldnt refresh or navigate from the site + - ideally, the custom popup can be synced with the alert, so when the alert is closed, the modal closes too + +2) Disable AutoScrollToBottom when the user has scrolled up + - when the user scrolls back to bottom, it should renable + - renable after a period of inactivity (3 minutes?) + +3) Figure out how to type components w/ RouteComponentProps + - Component> + +4) clear input onSubmit + +5) figure out how to reflect server status changes in the ui + +6) Account page + +7) Register/Reset Password forms + +8) Message User + +9) Main Nav scheme diff --git a/webclient/architecture.png b/webclient/architecture.png new file mode 100644 index 000000000..0226eb201 Binary files /dev/null and b/webclient/architecture.png differ diff --git a/webclient/package-lock.json b/webclient/package-lock.json new file mode 100644 index 000000000..6b12b4ad6 --- /dev/null +++ b/webclient/package-lock.json @@ -0,0 +1,34786 @@ +{ + "name": "webclient", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "webclient", + "version": "1.0.0", + "dependencies": { + "@emotion/react": "^11.8.2", + "@emotion/styled": "^11.8.1", + "@mui/icons-material": "^5.5.1", + "@mui/material": "^5.5.1", + "crypto-js": "^4.2.0", + "dexie": "^3.2.2", + "final-form": "^4.20.6", + "final-form-set-field-touched": "^1.0.1", + "i18next": "^22.0.4", + "i18next-browser-languagedetector": "^7.0.0", + "i18next-icu": "^2.0.3", + "intl-messageformat": "^10.2.1", + "lodash": "^4.17.21", + "prop-types": "^15.8.1", + "protobufjs": "^7.2.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-final-form": "^6.5.8", + "react-final-form-listeners": "^1.0.3", + "react-i18next": "^12.0.0", + "react-redux": "^8.0.4", + "react-router-dom": "^6.2.2", + "react-scripts": "5.0.1", + "react-virtualized-auto-sizer": "^1.0.6", + "react-window": "^1.8.6", + "redux": "^4.1.2", + "redux-form": "^8.3.8", + "redux-thunk": "^2.4.1", + "rxjs": "^7.5.4", + "sanitize-html": "^2.7.3" + }, + "devDependencies": { + "@babel/core": "^7.17.5", + "@mui/types": "^7.1.3", + "@testing-library/jest-dom": "^5.16.2", + "@testing-library/react": "^13.4.0", + "@types/jest": "29.2.0", + "@types/jquery": "^3.5.14", + "@types/lodash": "^4.14.179", + "@types/node": "18.11.7", + "@types/prop-types": "^15.7.4", + "@types/react": "18.0.24", + "@types/react-dom": "18.0.8", + "@types/react-redux": "^7.1.23", + "@types/react-router-dom": "^5.3.3", + "@types/react-virtualized-auto-sizer": "^1.0.1", + "@types/react-window": "^1.8.5", + "@types/redux-form": "^8.3.3", + "@typescript-eslint/eslint-plugin": "^5.14.0", + "@typescript-eslint/parser": "^5.14.0", + "fs-extra": "^10.0.1", + "husky": "^8.0.1", + "typescript": "^4.6.2" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz", + "integrity": "sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==", + "dev": true + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@apideck/better-ajv-errors": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", + "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "dependencies": { + "json-schema": "^0.4.0", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.0.tgz", + "integrity": "sha512-Gt9jszFJYq7qzXVK4slhc6NzJXnOVmRECWcVjF/T23rNXD9NtWQ0W3qxdg+p9wWIB+VQw3GYV/U2Ha9bRTfs4w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.6.tgz", + "integrity": "sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg==", + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.6", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-module-transforms": "^7.19.6", + "@babel/helpers": "^7.19.4", + "@babel/parser": "^7.19.6", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.6", + "@babel/types": "^7.19.4", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/eslint-parser": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.19.1.tgz", + "integrity": "sha512-AqNf2QWt1rtu2/1rLswy6CDP7H9Oh3mMhk177Y67Rg8d7RD9WfOLLv8CGn6tisFvS2htm86yIe1yLF6I1UDaGQ==", + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.11.0", + "eslint": "^7.5.0 || ^8.0.0" + } + }, + "node_modules/@babel/eslint-parser/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dependencies": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", + "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "dependencies": { + "@babel/helper-explode-assignable-expression": "^7.18.6", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz", + "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==", + "dependencies": { + "@babel/compat-data": "^7.20.0", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.19.0.tgz", + "integrity": "sha512-NRz8DwF4jT3UfrmUoZjd0Uph9HQnP30t7Ash+weACcyNkiYTywpIjDBgReJMKgr+n86sn2nPVVmJ28Dm053Kqw==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-member-expression-to-functions": "^7.18.9", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.9", + "@babel/helper-split-export-declaration": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.19.0.tgz", + "integrity": "sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "regexpu-core": "^5.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "dependencies": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-explode-assignable-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", + "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz", + "integrity": "sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==", + "dependencies": { + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.6.tgz", + "integrity": "sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.19.4", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.6", + "@babel/types": "^7.19.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz", + "integrity": "sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", + "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-wrap-function": "^7.18.9", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.19.1.tgz", + "integrity": "sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-member-expression-to-functions": "^7.18.9", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/traverse": "^7.19.1", + "@babel/types": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.19.4.tgz", + "integrity": "sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg==", + "dependencies": { + "@babel/types": "^7.19.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", + "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "dependencies": { + "@babel/types": "^7.20.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.19.0.tgz", + "integrity": "sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg==", + "dependencies": { + "@babel/helper-function-name": "^7.19.0", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.0", + "@babel/types": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", + "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz", + "integrity": "sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", + "@babel/plugin-proposal-optional-chaining": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.19.1.tgz", + "integrity": "sha512-0yu8vNATgLy4ivqMNBIwb1HebCelqN7YX8SL3FDXORv/RqT0zEEWUCH4GH44JsSrvCu6GqnAdR5EBFAPeNBB4Q==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-static-block": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz", + "integrity": "sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.20.0.tgz", + "integrity": "sha512-vnuRRS20ygSxclEYikHzVrP9nZDFXaSzvJxGLQNAiBX041TmhS4hOUHWNIpq/q4muENuEP9XPJFXTNFejhemkg==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.19.0", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-replace-supers": "^7.19.1", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/plugin-syntax-decorators": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz", + "integrity": "sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.19.4.tgz", + "integrity": "sha512-wHmj6LDxVDnL+3WhXteUBaoM1aVILZODAUjg11kHqG4cOlfgMQGxw6aCgvrXrmaJR3Bn14oZhImyCPZzRpC93Q==", + "dependencies": { + "@babel/compat-data": "^7.19.4", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.18.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz", + "integrity": "sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz", + "integrity": "sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.19.0.tgz", + "integrity": "sha512-xaBZUEDntt4faL1yN8oIFlhfXeQAWJW7CLKYsHTUqriCUbj8xOra8bfxxKGi/UwExPFBuPdH4XfHc9rGQhrVkQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz", + "integrity": "sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", + "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", + "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz", + "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz", + "integrity": "sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz", + "integrity": "sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-remap-async-to-generator": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", + "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.0.tgz", + "integrity": "sha512-sXOohbpHZSk7GjxK9b3dKB7CfqUD5DwOH+DggKzOQ7TXYP+RCSbRykfjQmn/zq+rBjycVRtLf9pYhAaEJA786w==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.19.0.tgz", + "integrity": "sha512-YfeEE9kCjqTS9IitkgfJuxjcEtLUHMqa8yUJ6zdz8vR7hKuo6mOy2C05P0F1tdMmDCeuyidKnlrw/iTppHcr2A==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-compilation-targets": "^7.19.0", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-replace-supers": "^7.18.9", + "@babel/helper-split-export-declaration": "^7.18.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz", + "integrity": "sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.0.tgz", + "integrity": "sha512-1dIhvZfkDVx/zn2S1aFwlruspTt4189j7fEkH0Y0VyuDM6bQt7bD6kLcz3l4IlLG+e5OReaBz9ROAbttRtUHqA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", + "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", + "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", + "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.19.0.tgz", + "integrity": "sha512-sgeMlNaQVbCSpgLSKP4ZZKfsJVnFnNQlUSk6gPYzR/q7tzCgQF2t8RBKAP6cKJeZdveei7Q7Jm527xepI8lNLg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/plugin-syntax-flow": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.18.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", + "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", + "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "dependencies": { + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", + "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", + "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.19.6.tgz", + "integrity": "sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg==", + "dependencies": { + "@babel/helper-module-transforms": "^7.19.6", + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.19.6.tgz", + "integrity": "sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ==", + "dependencies": { + "@babel/helper-module-transforms": "^7.19.6", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-simple-access": "^7.19.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.6.tgz", + "integrity": "sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ==", + "dependencies": { + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-module-transforms": "^7.19.6", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-validator-identifier": "^7.19.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", + "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "dependencies": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.19.1.tgz", + "integrity": "sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.19.0", + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", + "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", + "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.18.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.18.8.tgz", + "integrity": "sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", + "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-constant-elements": { + "version": "7.18.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.18.12.tgz", + "integrity": "sha512-Q99U9/ttiu+LMnRU8psd23HhvwXmKWDQIpocm0JKaICcZHnw+mdQbHm6xnSy7dOl8I5PELakYtNBubNQlBXbZw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz", + "integrity": "sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.19.0.tgz", + "integrity": "sha512-UVEvX3tXie3Szm3emi1+G63jyw1w5IcMY0FSKM+CRnKRI5Mr1YbCNgsSTwoTwKphQEG9P+QqmuRFneJPZuHNhg==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/plugin-syntax-jsx": "^7.18.6", + "@babel/types": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz", + "integrity": "sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.18.6.tgz", + "integrity": "sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz", + "integrity": "sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "regenerator-transform": "^0.15.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", + "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz", + "integrity": "sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.19.0", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", + "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz", + "integrity": "sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", + "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", + "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", + "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.20.0.tgz", + "integrity": "sha512-xOAsAFaun3t9hCwZ13Qe7gq423UgMZ6zAgmLxeGGapFqlT/X3L5qT2btjiVLlFn7gWtMaVyceS5VxGAuKbgizw==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.19.0", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/plugin-syntax-typescript": "^7.20.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", + "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", + "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.19.4.tgz", + "integrity": "sha512-5QVOTXUdqTCjQuh2GGtdd7YEhoRXBMVGROAtsBeLGIbIz3obCBIfRMT1I3ZKkMgNzwkyCkftDXSSkHxnfVf4qg==", + "dependencies": { + "@babel/compat-data": "^7.19.4", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-async-generator-functions": "^7.19.1", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.18.6", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.19.4", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.18.6", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.18.6", + "@babel/plugin-transform-async-to-generator": "^7.18.6", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.19.4", + "@babel/plugin-transform-classes": "^7.19.0", + "@babel/plugin-transform-computed-properties": "^7.18.9", + "@babel/plugin-transform-destructuring": "^7.19.4", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.18.8", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.18.6", + "@babel/plugin-transform-modules-commonjs": "^7.18.6", + "@babel/plugin-transform-modules-systemjs": "^7.19.0", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.18.8", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.18.6", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.19.0", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.19.4", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "core-js-compat": "^3.25.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.18.6.tgz", + "integrity": "sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-transform-react-display-name": "^7.18.6", + "@babel/plugin-transform-react-jsx": "^7.18.6", + "@babel/plugin-transform-react-jsx-development": "^7.18.6", + "@babel/plugin-transform-react-pure-annotations": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz", + "integrity": "sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-transform-typescript": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.27.1.tgz", + "integrity": "sha512-909rVuj3phpjW6y0MCXAZ5iNeORePa6ldJvp2baWGcTjwqbBDDz6xoS5JHJ7lS88NlwLYj07ImL/8IUMtDZzTA==", + "dependencies": { + "core-js-pure": "^3.30.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" + }, + "node_modules/@csstools/normalize.css": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.0.0.tgz", + "integrity": "sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg==" + }, + "node_modules/@csstools/postcss-cascade-layers": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz", + "integrity": "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==", + "dependencies": { + "@csstools/selector-specificity": "^2.0.2", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-color-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz", + "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz", + "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-hwb-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz", + "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-ic-unit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz", + "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz", + "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==", + "dependencies": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz", + "integrity": "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", + "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-oklab-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz", + "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", + "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz", + "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz", + "integrity": "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz", + "integrity": "sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-unset-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", + "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz", + "integrity": "sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==", + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2", + "postcss-selector-parser": "^6.0.10" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.10.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz", + "integrity": "sha512-xE7/hyLHJac7D2Ve9dKroBBZqBT7WuPQmWcq7HSGb84sUuP4mlOWoB8dvVfD9yk5DHkU1m6RW7xSoDtnQHNQeA==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/plugin-syntax-jsx": "^7.17.12", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.0", + "@emotion/memoize": "^0.8.0", + "@emotion/serialize": "^1.1.1", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.1.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.10.5", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz", + "integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==", + "dependencies": { + "@emotion/memoize": "^0.8.0", + "@emotion/sheet": "^1.2.1", + "@emotion/utils": "^1.2.0", + "@emotion/weak-memoize": "^0.3.0", + "stylis": "4.1.3" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", + "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", + "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", + "dependencies": { + "@emotion/memoize": "^0.8.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + }, + "node_modules/@emotion/react": { + "version": "11.10.5", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.5.tgz", + "integrity": "sha512-TZs6235tCJ/7iF6/rvTaOH4oxQg2gMAcdHemjwLKIjKz4rRuYe1HJ2TQJKnAcRAfOUDdU8XoDadCe1rl72iv8A==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.10.5", + "@emotion/cache": "^11.10.5", + "@emotion/serialize": "^1.1.1", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", + "@emotion/utils": "^1.2.0", + "@emotion/weak-memoize": "^0.3.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==", + "dependencies": { + "@emotion/hash": "^0.9.0", + "@emotion/memoize": "^0.8.0", + "@emotion/unitless": "^0.8.0", + "@emotion/utils": "^1.2.0", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz", + "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==" + }, + "node_modules/@emotion/styled": { + "version": "11.10.5", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.5.tgz", + "integrity": "sha512-8EP6dD7dMkdku2foLoruPCNkRevzdcBaY6q0l0OsbyJK+x8D9HWjX27ARiSIKNF634hY9Zdoedh8bJCiva8yZw==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.10.5", + "@emotion/is-prop-valid": "^1.2.0", + "@emotion/serialize": "^1.1.1", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", + "@emotion/utils": "^1.2.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz", + "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", + "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", + "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" + }, + "node_modules/@eslint/eslintrc": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", + "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.13.0.tgz", + "integrity": "sha512-CQ8Ykd51jYD1n05dtoX6ns6B9n/+6ZAxnWUAonvHC4kkuAemROYBhHkEB4tm1uVrRlE7gLDqXkAnY51Y0pRCWQ==", + "dependencies": { + "@formatjs/intl-localematcher": "0.2.31", + "tslib": "2.4.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.6.tgz", + "integrity": "sha512-9CWZ3+wCkClKHX+i5j+NyoBVqGf0pIskTo6Xl6ihGokYM2yqSSS68JIgeo+99UIHc+7vi9L3/SDSz/dWI9SNlA==", + "dependencies": { + "tslib": "2.4.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.10.tgz", + "integrity": "sha512-KkRMxhifWkRC45dhM9tqm0GXbb6NPYTGVYY3xx891IKc6p++DQrZTnmkVSNNO47OEERLfuP2KkPFPJBuu8z/wg==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.13.0", + "@formatjs/icu-skeleton-parser": "1.3.14", + "tslib": "2.4.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.14.tgz", + "integrity": "sha512-7bv60HQQcBb3+TSj+45tOb/CHV5z1hOpwdtS50jsSBXfB+YpGhnoRsZxSRksXeCxMy6xn6tA6VY2601BrrK+OA==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.13.0", + "tslib": "2.4.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.2.31", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.31.tgz", + "integrity": "sha512-9QTjdSBpQ7wHShZgsNzNig5qT3rCPvmZogS/wXZzKotns5skbXgs0I7J8cuN0PPqXyynvNVuN+iOKhNS2eb+ZA==", + "dependencies": { + "tslib": "2.4.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", + "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==", + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", + "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/console/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/console/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/console/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/console/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@jest/console/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console/node_modules/jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/console/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/console/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz", + "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==", + "dependencies": { + "@jest/console": "^27.5.1", + "@jest/reporters": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^27.5.1", + "jest-config": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-resolve-dependencies": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "jest-watcher": "^27.5.1", + "micromatch": "^4.0.4", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/core/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@jest/core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", + "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", + "dependencies": { + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/environment/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/environment/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/environment/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/environment/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/environment/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/environment/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@jest/environment/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.2.2.tgz", + "integrity": "sha512-vwnVmrVhTmGgQzyvcpze08br91OL61t9O0lJMDyb6Y/D8EKQ9V7rGUb/p7PDt0GPzK0zFYqXWFo4EO2legXmkg==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.2.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", + "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", + "dependencies": { + "@jest/types": "^27.5.1", + "@sinonjs/fake-timers": "^8.0.1", + "@types/node": "*", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/fake-timers/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/fake-timers/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/fake-timers/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@jest/fake-timers/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/fake-timers/node_modules/jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/globals": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", + "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/types": "^27.5.1", + "expect": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/globals/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/globals/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/globals/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/globals/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/globals/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/globals/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@jest/globals/node_modules/diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/globals/node_modules/expect": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", + "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", + "dependencies": { + "@jest/types": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/globals/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/globals/node_modules/jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/globals/node_modules/jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/globals/node_modules/jest-matcher-utils": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/globals/node_modules/jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/globals/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", + "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-haste-map": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^4.0.1", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^8.1.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/reporters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@jest/reporters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.0.0.tgz", + "integrity": "sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.24.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", + "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", + "dependencies": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9", + "source-map": "^0.6.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/source-map/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@jest/test-result": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", + "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", + "dependencies": { + "@jest/console": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/test-result/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/test-result/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/test-result/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/test-result/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/test-result/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/test-result/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@jest/test-result/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/test-result/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz", + "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==", + "dependencies": { + "@jest/test-result": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-runtime": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", + "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", + "dependencies": { + "@babel/core": "^7.1.0", + "@jest/types": "^27.5.1", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-util": "^27.5.1", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/transform/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/transform/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/transform/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/transform/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@jest/transform/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/transform/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@jest/transform/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types": { + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.2.1.tgz", + "integrity": "sha512-O/QNDQODLnINEPAI0cl9U6zUIDXEWXt6IC1o2N2QENuos7hlGUIthlKyV4p6ki3TvXFX071blj8HUhgLGquPjw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.0.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" + }, + "node_modules/@mui/base": { + "version": "5.0.0-alpha.103", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.103.tgz", + "integrity": "sha512-fJIyB2df3CHn7D26WHnutnY7vew6aytTlhmRJz6GX7ag19zU2GcOUhJAzY5qwWcrXKnlYgzimhEjaEnuiUWU4g==", + "dependencies": { + "@babel/runtime": "^7.19.0", + "@emotion/is-prop-valid": "^1.2.0", + "@mui/types": "^7.2.0", + "@mui/utils": "^5.10.9", + "@popperjs/core": "^2.11.6", + "clsx": "^1.2.1", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.10.11", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.10.11.tgz", + "integrity": "sha512-u5ff+UCFDHcR8MoQ8tuJR4c35vt7T/ki3aMEE2O3XQoGs8KJSrBiisFpFKyldg9/W2NSyoZxN+kxEGIfRxh+9Q==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + } + }, + "node_modules/@mui/icons-material": { + "version": "5.10.9", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.10.9.tgz", + "integrity": "sha512-sqClXdEM39WKQJOQ0ZCPTptaZgqwibhj2EFV9N0v7BU1PO8y4OcX/a2wIQHn4fNuDjIZktJIBrmU23h7aqlGgg==", + "dependencies": { + "@babel/runtime": "^7.19.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "5.10.11", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.10.11.tgz", + "integrity": "sha512-KJ0wPCTbv6sFzwA3dgg0gowdfF+SRl7D510J9l6Nl/KFX0EawcewQudqKY4slYGFXniKa5PykqokpaWXsCCPqg==", + "dependencies": { + "@babel/runtime": "^7.19.0", + "@mui/base": "5.0.0-alpha.103", + "@mui/core-downloads-tracker": "^5.10.11", + "@mui/system": "^5.10.10", + "@mui/types": "^7.2.0", + "@mui/utils": "^5.10.9", + "@types/react-transition-group": "^4.4.5", + "clsx": "^1.2.1", + "csstype": "^3.1.1", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "5.10.9", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.10.9.tgz", + "integrity": "sha512-BN7/CnsVPVyBaQpDTij4uV2xGYHHHhOgpdxeYLlIu+TqnsVM7wUeF+37kXvHovxM6xmL5qoaVUD98gDC0IZnHg==", + "dependencies": { + "@babel/runtime": "^7.19.0", + "@mui/utils": "^5.10.9", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "5.10.8", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.10.8.tgz", + "integrity": "sha512-w+y8WI18EJV6zM/q41ug19cE70JTeO6sWFsQ7tgePQFpy6ToCVPh0YLrtqxUZXSoMStW5FMw0t9fHTFAqPbngw==", + "dependencies": { + "@babel/runtime": "^7.19.0", + "@emotion/cache": "^11.10.3", + "csstype": "^3.1.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "5.10.10", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.10.10.tgz", + "integrity": "sha512-TXwtKN0adKpBrZmO+eilQWoPf2veh050HLYrN78Kps9OhlvO70v/2Kya0+mORFhu9yhpAwjHXO8JII/R4a5ZLA==", + "dependencies": { + "@babel/runtime": "^7.19.0", + "@mui/private-theming": "^5.10.9", + "@mui/styled-engine": "^5.10.8", + "@mui/types": "^7.2.0", + "@mui/utils": "^5.10.9", + "clsx": "^1.2.1", + "csstype": "^3.1.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.0.tgz", + "integrity": "sha512-lGXtFKe5lp3UxTBGqKI1l7G8sE2xBik8qCfrLHD5olwP/YU0/ReWoWT7Lp1//ri32dK39oPMrJN8TgbkCSbsNA==", + "peerDependencies": { + "@types/react": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.10.9", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.10.9.tgz", + "integrity": "sha512-2tdHWrq3+WCy+G6TIIaFx3cg7PorXZ71P375ExuX61od1NOAJP1mK90VxQ8N4aqnj2vmO3AQDkV4oV2Ktvt4bA==", + "dependencies": { + "@babel/runtime": "^7.19.0", + "@types/prop-types": "^15.7.5", + "@types/react-is": "^16.7.1 || ^17.0.0", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.8.tgz", + "integrity": "sha512-wxXRwf+IQ6zvHSJZ+5T2RQNEsq+kx4jKRXfFvdt3nBIUzJUAvXEFsUeoaohDe/Kr84MTjGwcuIUPNcstNJORsA==", + "dependencies": { + "ansi-html-community": "^0.0.8", + "common-path-prefix": "^3.0.0", + "core-js-pure": "^3.23.3", + "error-stack-parser": "^2.0.6", + "find-up": "^5.0.0", + "html-entities": "^2.1.0", + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "@types/webpack": "4.x || 5.x", + "react-refresh": ">=0.10.0 <1.0.0", + "sockjs-client": "^1.4.0", + "type-fest": ">=0.17.0 <4.0.0", + "webpack": ">=4.43.0 <6.0.0", + "webpack-dev-server": "3.x || 4.x", + "webpack-hot-middleware": "2.x", + "webpack-plugin-serve": "0.x || 1.x" + }, + "peerDependenciesMeta": { + "@types/webpack": { + "optional": true + }, + "sockjs-client": { + "optional": true + }, + "type-fest": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + }, + "webpack-hot-middleware": { + "optional": true + }, + "webpack-plugin-serve": { + "optional": true + } + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", + "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@remix-run/router": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.2.tgz", + "integrity": "sha512-GRSOFhJzjGN+d4sKHTMSvNeUPoZiDHWmRnXfzaxrqe7dE/Nzlc8BiMSJdLDESZlndM7jIUrZ/F4yWqVYlI0rwQ==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "dependencies": { + "@babel/helper-module-imports": "^7.10.4", + "@rollup/pluginutils": "^3.1.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", + "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/pluginutils/node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", + "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==" + }, + "node_modules/@sinclair/typebox": { + "version": "0.24.51", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", + "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==" + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", + "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@surma/rollup-plugin-off-main-thread": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", + "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "dependencies": { + "ejs": "^3.1.6", + "json5": "^2.2.0", + "magic-string": "^0.25.0", + "string.prototype.matchall": "^4.0.6" + } + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz", + "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz", + "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz", + "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz", + "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz", + "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz", + "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz", + "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz", + "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz", + "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0", + "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "^5.0.1", + "@svgr/babel-plugin-replace-jsx-attribute-value": "^5.0.1", + "@svgr/babel-plugin-svg-dynamic-title": "^5.4.0", + "@svgr/babel-plugin-svg-em-dimensions": "^5.4.0", + "@svgr/babel-plugin-transform-react-native-svg": "^5.4.0", + "@svgr/babel-plugin-transform-svg-component": "^5.5.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/core": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz", + "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==", + "dependencies": { + "@svgr/plugin-jsx": "^5.5.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz", + "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==", + "dependencies": { + "@babel/types": "^7.12.6" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", + "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", + "dependencies": { + "@babel/core": "^7.12.3", + "@svgr/babel-preset": "^5.5.0", + "@svgr/hast-util-to-babel-ast": "^5.5.0", + "svg-parser": "^2.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-svgo": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz", + "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==", + "dependencies": { + "cosmiconfig": "^7.0.0", + "deepmerge": "^4.2.2", + "svgo": "^1.2.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/webpack": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz", + "integrity": "sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/plugin-transform-react-constant-elements": "^7.12.1", + "@babel/preset-env": "^7.12.1", + "@babel/preset-react": "^7.12.5", + "@svgr/core": "^5.5.0", + "@svgr/plugin-jsx": "^5.5.0", + "@svgr/plugin-svgo": "^5.5.0", + "loader-utils": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@testing-library/dom": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.19.0.tgz", + "integrity": "sha512-6YWYPPpxG3e/xOo6HIWwB/58HukkwIVTOaZ0VwdMVjhRUX/01E4FtQbck9GazOOj7MXHc5RBzMrU86iBJHbI+A==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^4.2.0", + "aria-query": "^5.0.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.4.4", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@testing-library/dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "5.16.5", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz", + "integrity": "sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.0.1", + "@babel/runtime": "^7.9.2", + "@types/testing-library__jest-dom": "^5.9.1", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.5.6", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=8", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@testing-library/jest-dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/react": { + "version": "13.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.4.0.tgz", + "integrity": "sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^8.5.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@types/aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.1.19", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz", + "integrity": "sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.2.tgz", + "integrity": "sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==", + "dependencies": { + "@babel/types": "^7.3.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", + "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", + "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.4.9", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.9.tgz", + "integrity": "sha512-jFCSo4wJzlHQLCpceUhUnXdrPuCNOjGFMQ8Eg6JXxlz3QaCKOb7eGi2cephQdM4XTYsNej69P9JDJ1zqNIbncQ==", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" + }, + "node_modules/@types/express": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", + "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.31", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", + "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", + "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "dev": true + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.9", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", + "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.2.0.tgz", + "integrity": "sha512-KO7bPV21d65PKwv3LLsD8Jn3E05pjNjRZvkm+YTacWhVmykAb07wW6IkZUmQAltwQafNcDUEUrMO2h3jeBSisg==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.2.1.tgz", + "integrity": "sha512-Y41Sa4aLCtKAXvwuIpTvcFBkyeYp2gdFWzXGA+ZNES3VwURIB165XO/z7CjETwzCCS53MjW/rLMyyqEnTtaOfA==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.0.0", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jquery": { + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz", + "integrity": "sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==", + "dev": true, + "dependencies": { + "@types/sizzle": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" + }, + "node_modules/@types/lodash": { + "version": "4.14.186", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.186.tgz", + "integrity": "sha512-eHcVlLXP0c2FlMPm56ITode2AgLMSa6aJ05JTTbYbI+7EMkCEE5qk2E41d5g2lCVTqRe0GnnRFurmlCsDODrPw==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" + }, + "node_modules/@types/node": { + "version": "18.11.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.7.tgz", + "integrity": "sha512-LhFTglglr63mNXUSRYD8A+ZAIu5sFqNJ4Y2fPuY7UlrySJH87rRRlhtVmMHplmfk5WkoJGmDjE9oiTfyX94CpQ==" + }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + }, + "node_modules/@types/prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + }, + "node_modules/@types/q": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", + "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==" + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "node_modules/@types/react": { + "version": "18.0.24", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.24.tgz", + "integrity": "sha512-wRJWT6ouziGUy+9uX0aW4YOJxAY0bG6/AOk5AW5QSvZqI7dk6VBIbXvcVgIw/W5Jrl24f77df98GEKTJGOLx7Q==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.8.tgz", + "integrity": "sha512-C3GYO0HLaOkk9dDAz3Dl4sbe4AKUGTCfFIZsz3n/82dPNN8Du533HzKatDxeUYWu24wJgMP1xICqkWk1YOLOIw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-is": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz", + "integrity": "sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-redux": { + "version": "7.1.24", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz", + "integrity": "sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==", + "dev": true, + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.19", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.19.tgz", + "integrity": "sha512-Fv/5kb2STAEMT3wHzdKQK2z8xKq38EDIGVrutYLmQVVLe+4orDFquU52hQrULnEHinMKv9FSA6lf9+uNT1ITtA==", + "dev": true, + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dev": true, + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-virtualized-auto-sizer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz", + "integrity": "sha512-GH8sAnBEM5GV9LTeiz56r4ZhMOUSrP43tAQNSRVxNexDjcNKLCEtnxusAItg1owFUFE6k0NslV26gqVClVvong==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-window": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz", + "integrity": "sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/redux-form": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/@types/redux-form/-/redux-form-8.3.5.tgz", + "integrity": "sha512-SchB4i7nxgWNbJS4cXEZducztkvHzVrb5xlAXwfLpbrLPo6tMY06+kx1GqMv42+YnGy9TpCAkF51a21HatqWBA==", + "dev": true, + "dependencies": { + "@types/react": "*", + "redux": "^3.6.0 || ^4.0.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + }, + "node_modules/@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + }, + "node_modules/@types/semver": { + "version": "7.3.13", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==" + }, + "node_modules/@types/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "dependencies": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/sizzle": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", + "dev": true + }, + "node_modules/@types/sockjs": { + "version": "0.3.33", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", + "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" + }, + "node_modules/@types/testing-library__jest-dom": { + "version": "5.14.5", + "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz", + "integrity": "sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==", + "dev": true, + "dependencies": { + "@types/jest": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", + "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, + "node_modules/@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.13", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", + "integrity": "sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.41.0.tgz", + "integrity": "sha512-DXUS22Y57/LAFSg3x7Vi6RNAuLpTXwxB9S2nIA7msBb/Zt8p7XqMwdpdc1IU7CkOQUPgAqR5fWvxuKCbneKGmA==", + "dependencies": { + "@typescript-eslint/scope-manager": "5.41.0", + "@typescript-eslint/type-utils": "5.41.0", + "@typescript-eslint/utils": "5.41.0", + "debug": "^4.3.4", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "5.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.41.0.tgz", + "integrity": "sha512-/qxT2Kd2q/A22JVIllvws4rvc00/3AT4rAo/0YgEN28y+HPhbJbk6X4+MAHEoZzpNyAOugIT7D/OLnKBW8FfhA==", + "dependencies": { + "@typescript-eslint/utils": "5.41.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.41.0.tgz", + "integrity": "sha512-HQVfix4+RL5YRWZboMD1pUfFN8MpRH4laziWkkAzyO1fvNOY/uinZcvo3QiFJVS/siNHupV8E5+xSwQZrl6PZA==", + "dependencies": { + "@typescript-eslint/scope-manager": "5.41.0", + "@typescript-eslint/types": "5.41.0", + "@typescript-eslint/typescript-estree": "5.41.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.41.0.tgz", + "integrity": "sha512-xOxPJCnuktUkY2xoEZBKXO5DBCugFzjrVndKdUnyQr3+9aDWZReKq9MhaoVnbL+maVwWJu/N0SEtrtEUNb62QQ==", + "dependencies": { + "@typescript-eslint/types": "5.41.0", + "@typescript-eslint/visitor-keys": "5.41.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.41.0.tgz", + "integrity": "sha512-L30HNvIG6A1Q0R58e4hu4h+fZqaO909UcnnPbwKiN6Rc3BUEx6ez2wgN7aC0cBfcAjZfwkzE+E2PQQ9nEuoqfA==", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.41.0", + "@typescript-eslint/utils": "5.41.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.41.0.tgz", + "integrity": "sha512-5BejraMXMC+2UjefDvrH0Fo/eLwZRV6859SXRg+FgbhA0R0l6lDqDGAQYhKbXhPN2ofk2kY5sgGyLNL907UXpA==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.41.0.tgz", + "integrity": "sha512-SlzFYRwFSvswzDSQ/zPkIWcHv8O5y42YUskko9c4ki+fV6HATsTODUPbRbcGDFYP86gaJL5xohUEytvyNNcXWg==", + "dependencies": { + "@typescript-eslint/types": "5.41.0", + "@typescript-eslint/visitor-keys": "5.41.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.41.0.tgz", + "integrity": "sha512-QlvfwaN9jaMga9EBazQ+5DDx/4sAdqDkcs05AsQHMaopluVCUyu1bTRUVKzXbgjDlrRAQrYVoi/sXJ9fmG+KLQ==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.41.0", + "@typescript-eslint/types": "5.41.0", + "@typescript-eslint/typescript-estree": "5.41.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.41.0.tgz", + "integrity": "sha512-vilqeHj267v8uzzakbm13HkPMl7cbYpKVjgFWZPIOHIJHZtinvypUhJ5xBXfWYg4eFKqztbMMpOgFpT9Gfx4fw==", + "dependencies": { + "@typescript-eslint/types": "5.41.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "dependencies": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + } + }, + "node_modules/acorn-globals/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dependencies": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "node_modules/acorn-node/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/address": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/address/-/address-1.2.1.tgz", + "integrity": "sha512-B+6bi5D34+fDYENiH5qOlA0cV2rAGKuWZ9LeyUUehbXy8e0VS9e498yO0Jeeh+iM+6KbfudHTFjXw2MmJD4QRA==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aria-query": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.2.tgz", + "integrity": "sha512-JWydkr9MirMg2jGJstDqDgzoHqaFbv7n1ghfXYdtEgXWgdq3jz7IU3SQvtj9k3mAszQBiTpQhFdlH+JIRuGTzg==", + "dev": true, + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" + }, + "node_modules/array-includes": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", + "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", + "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz", + "integrity": "sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.reduce": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.4.tgz", + "integrity": "sha512-WnM+AjG/DvLRLo4DDl+r+SvCzYtD2Jd9oeBYMcEaI7t3fFrHY9M53/wdLcTvmZNQ70IU6Htj0emFkZ5TS+lrdw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "node_modules/ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==" + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.13", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", + "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-lite": "^1.0.30001426", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.5.0.tgz", + "integrity": "sha512-4+rr8eQ7+XXS5nZrKcMO/AikHL0hVqy+lHWAnE3xdHl+aguag8SOQ6eEqLexwLNWgXIMfunGuD3ON1/6Kyet0A==", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", + "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==" + }, + "node_modules/babel-jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", + "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==", + "dependencies": { + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^27.5.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-jest/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/babel-jest/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/babel-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/babel-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/babel-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-loader": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.5.tgz", + "integrity": "sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==", + "dependencies": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^2.0.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "engines": { + "node": ">= 8.9" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "webpack": ">=2" + } + }, + "node_modules/babel-loader/node_modules/schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dependencies": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz", + "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-named-asset-import": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz", + "integrity": "sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==", + "peerDependencies": { + "@babel/core": "^7.1.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "dependencies": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.3", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", + "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3", + "core-js-compat": "^3.25.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-transform-react-remove-prop-types": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", + "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==" + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", + "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==", + "dependencies": { + "babel-plugin-jest-hoist": "^27.5.1", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-react-app": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz", + "integrity": "sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg==", + "dependencies": { + "@babel/core": "^7.16.0", + "@babel/plugin-proposal-class-properties": "^7.16.0", + "@babel/plugin-proposal-decorators": "^7.16.4", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", + "@babel/plugin-proposal-numeric-separator": "^7.16.0", + "@babel/plugin-proposal-optional-chaining": "^7.16.0", + "@babel/plugin-proposal-private-methods": "^7.16.0", + "@babel/plugin-transform-flow-strip-types": "^7.16.0", + "@babel/plugin-transform-react-display-name": "^7.16.0", + "@babel/plugin-transform-runtime": "^7.16.4", + "@babel/preset-env": "^7.16.4", + "@babel/preset-react": "^7.16.0", + "@babel/preset-typescript": "^7.16.0", + "@babel/runtime": "^7.16.3", + "babel-plugin-macros": "^3.1.0", + "babel-plugin-transform-react-remove-prop-types": "^0.4.24" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==" + }, + "node_modules/bfj": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.0.2.tgz", + "integrity": "sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw==", + "dependencies": { + "bluebird": "^3.5.5", + "check-types": "^11.1.1", + "hoopy": "^0.1.4", + "tryer": "^1.0.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/bonjour-service": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.0.14.tgz", + "integrity": "sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ==", + "dependencies": { + "array-flatten": "^2.1.2", + "dns-equal": "^1.0.0", + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/case-sensitive-paths-webpack-plugin": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", + "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/check-types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz", + "integrity": "sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ==" + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", + "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==" + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", + "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==" + }, + "node_modules/clean-css": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.1.tgz", + "integrity": "sha512-lCr8OHhiWCTw4v8POJovCoh4T7I9U11yVsPjMWWnnMmp9ZowCxyad1Pathle/9HjaDp+fdQKjO9fQydE6RHTZg==", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "dependencies": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==" + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" + }, + "node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==" + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==" + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/core-js": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz", + "integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.26.0.tgz", + "integrity": "sha512-piOX9Go+Z4f9ZiBFLnZ5VrOpBl0h7IGCkiFUN11QTe6LjAvOT3ifL/5TdoizMh99hcGy5SoLyWbapIY/PIb/3A==", + "dependencies": { + "browserslist": "^4.21.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-pure": { + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.42.0.tgz", + "integrity": "sha512-007bM04u91fF4kMgwom2I5cQxAFIy8jVulgr9eozILl/SZE53QOqnW/+vviC+wQWLv+AunBG+8Q0TLoeSsSxRQ==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/css-blank-pseudo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", + "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "bin": { + "css-blank-pseudo": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-declaration-sorter": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz", + "integrity": "sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==", + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-has-pseudo": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", + "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "bin": { + "css-has-pseudo": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-loader": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz", + "integrity": "sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.7", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/css-minimizer-webpack-plugin": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz", + "integrity": "sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==", + "dependencies": { + "cssnano": "^5.0.6", + "jest-worker": "^27.0.2", + "postcss": "^8.3.5", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@parcel/css": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", + "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", + "bin": { + "css-prefers-color-scheme": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" + }, + "node_modules/css-tree": { + "version": "1.0.0-alpha.37", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", + "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", + "dependencies": { + "mdn-data": "2.0.4", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssdb": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.0.2.tgz", + "integrity": "sha512-Vm4b6P/PifADu0a76H0DKRNVWq3Rq9xa/Nx6oEMUBJlwTUuZoZ3dkZxo8Gob3UEL53Cq+Ma1GBgISed6XEBs3w==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "5.1.14", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.14.tgz", + "integrity": "sha512-Oou7ihiTocbKqi0J1bB+TRJIQX5RMR3JghA8hcWSw9mjBLQ5Y3RWqEDoYG3sRNlAbCIXpqMoZGbq5KDR3vdzgw==", + "dependencies": { + "cssnano-preset-default": "^5.2.13", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-preset-default": { + "version": "5.2.13", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.13.tgz", + "integrity": "sha512-PX7sQ4Pb+UtOWuz8A1d+Rbi+WimBIxJTRyBdgGp1J75VU0r/HFQeLnMYgHiCAp6AR4rqrc7Y4R+1Rjk3KJz6DQ==", + "dependencies": { + "css-declaration-sorter": "^6.3.1", + "cssnano-utils": "^3.1.0", + "postcss-calc": "^8.2.3", + "postcss-colormin": "^5.3.0", + "postcss-convert-values": "^5.1.3", + "postcss-discard-comments": "^5.1.2", + "postcss-discard-duplicates": "^5.1.0", + "postcss-discard-empty": "^5.1.1", + "postcss-discard-overridden": "^5.1.0", + "postcss-merge-longhand": "^5.1.7", + "postcss-merge-rules": "^5.1.3", + "postcss-minify-font-values": "^5.1.0", + "postcss-minify-gradients": "^5.1.1", + "postcss-minify-params": "^5.1.4", + "postcss-minify-selectors": "^5.2.1", + "postcss-normalize-charset": "^5.1.0", + "postcss-normalize-display-values": "^5.1.0", + "postcss-normalize-positions": "^5.1.1", + "postcss-normalize-repeat-style": "^5.1.1", + "postcss-normalize-string": "^5.1.0", + "postcss-normalize-timing-functions": "^5.1.0", + "postcss-normalize-unicode": "^5.1.1", + "postcss-normalize-url": "^5.1.0", + "postcss-normalize-whitespace": "^5.1.1", + "postcss-ordered-values": "^5.1.3", + "postcss-reduce-initial": "^5.1.1", + "postcss-reduce-transforms": "^5.1.0", + "postcss-svgo": "^5.1.0", + "postcss-unique-selectors": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "node_modules/csso/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + }, + "node_modules/csstype": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" + }, + "node_modules/data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dependencies": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.2.tgz", + "integrity": "sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA==" + }, + "node_modules/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==" + }, + "node_modules/deep-equal": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.5.tgz", + "integrity": "sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "es-get-iterator": "^1.1.1", + "get-intrinsic": "^1.0.1", + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.2", + "is-regex": "^1.1.1", + "isarray": "^2.0.5", + "object-is": "^1.1.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.3.0", + "side-channel": "^1.0.3", + "which-boxed-primitive": "^1.0.1", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "node_modules/deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defined": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" + }, + "node_modules/detect-port-alt": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", + "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", + "dependencies": { + "address": "^1.0.1", + "debug": "^2.6.0" + }, + "bin": { + "detect": "bin/detect-port", + "detect-port": "bin/detect-port" + }, + "engines": { + "node": ">= 4.2.1" + } + }, + "node_modules/detect-port-alt/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/detect-port-alt/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/detective": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", + "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", + "dependencies": { + "acorn-node": "^1.8.2", + "defined": "^1.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "detective": "bin/detective.js" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/dexie": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-3.2.2.tgz", + "integrity": "sha512-q5dC3HPmir2DERlX+toCBbHQXW5MsyrFqPFcovkH9N2S/UW/H3H5AWAB6iEOExeraAu+j+zRDG+zg/D7YhH0qg==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + }, + "node_modules/diff-sequences": { + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.2.0.tgz", + "integrity": "sha512-413SY5JpYeSBZxmenGEmCVQ8mCgtFJF0w9PROdaS6z987XC2Pd2GOKqOITLtMftmyFZqgtCOb/QA7/Z3ZXfzIw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + }, + "node_modules/dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==" + }, + "node_modules/dns-packet": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.4.0.tgz", + "integrity": "sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.14", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz", + "integrity": "sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==", + "dev": true + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "dependencies": { + "webidl-conversions": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/domexception/node_modules/webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==" + }, + "node_modules/emittery": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", + "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-abstract": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", + "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.2.tgz", + "integrity": "sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.0", + "has-symbols": "^1.0.1", + "is-arguments": "^1.1.0", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==" + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", + "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/escodegen/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/escodegen/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.26.0.tgz", + "integrity": "sha512-kzJkpaw1Bfwheq4VXUezFriD1GxszX6dUekM7Z3aC2o4hju+tsR/XyTC3RcoSD7jmy9VkPU3+N6YjVU2e96Oyg==", + "dependencies": { + "@eslint/eslintrc": "^1.3.3", + "@humanwhocodes/config-array": "^0.11.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.15.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-react-app": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", + "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==", + "dependencies": { + "@babel/core": "^7.16.0", + "@babel/eslint-parser": "^7.16.3", + "@rushstack/eslint-patch": "^1.1.0", + "@typescript-eslint/eslint-plugin": "^5.5.0", + "@typescript-eslint/parser": "^5.5.0", + "babel-preset-react-app": "^10.0.1", + "confusing-browser-globals": "^1.0.11", + "eslint-plugin-flowtype": "^8.0.3", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jest": "^25.3.0", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.27.1", + "eslint-plugin-react-hooks": "^4.3.0", + "eslint-plugin-testing-library": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "eslint": "^8.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "dependencies": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", + "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-flowtype": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", + "integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==", + "dependencies": { + "lodash": "^4.17.21", + "string-natural-compare": "^3.0.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@babel/plugin-syntax-flow": "^7.14.5", + "@babel/plugin-transform-react-jsx": "^7.14.9", + "eslint": "^8.1.0" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", + "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "dependencies": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.3", + "has": "^1.0.3", + "is-core-module": "^2.8.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.5", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/eslint-plugin-jest": { + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz", + "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==", + "dependencies": { + "@typescript-eslint/experimental-utils": "^5.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^4.0.0 || ^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz", + "integrity": "sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q==", + "dependencies": { + "@babel/runtime": "^7.18.9", + "aria-query": "^4.2.2", + "array-includes": "^3.1.5", + "ast-types-flow": "^0.0.7", + "axe-core": "^4.4.3", + "axobject-query": "^2.2.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "has": "^1.0.3", + "jsx-ast-utils": "^3.3.2", + "language-tags": "^1.0.5", + "minimatch": "^3.1.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "dependencies": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.31.10", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.10.tgz", + "integrity": "sha512-e4N/nc6AAlg4UKW/mXeYWd3R++qUano5/o+t+wnWxIf+bLsOaH3a4q74kX3nDjYym3VBN4HyO9nEn1GcAqgQOA==", + "dependencies": { + "array-includes": "^3.1.5", + "array.prototype.flatmap": "^1.3.0", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.5", + "object.fromentries": "^2.0.5", + "object.hasown": "^1.1.1", + "object.values": "^1.1.5", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.3", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", + "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-testing-library": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.9.1.tgz", + "integrity": "sha512-6BQp3tmb79jLLasPHJmy8DnxREe+2Pgf7L+7o09TSWPfdqqtQfRZmZNetr5mOs3yqZk/MRNxpN3RUpJe0wB4LQ==", + "dependencies": { + "@typescript-eslint/utils": "^5.13.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0", + "npm": ">=6" + }, + "peerDependencies": { + "eslint": "^7.5.0 || ^8.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz", + "integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==", + "dependencies": { + "@types/eslint": "^7.29.0 || ^8.4.1", + "jest-worker": "^28.0.2", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/jest-worker": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", + "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/eslint-webpack-plugin/node_modules/schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", + "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.2.2.tgz", + "integrity": "sha512-hE09QerxZ5wXiOhqkXy5d2G9ar+EqOyifnCXCpMNu+vZ6DG9TJ6CO2c2kPDSLqERTTWrO7OZj8EkYHQqSd78Yw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.2.2", + "jest-get-type": "^29.2.0", + "jest-matcher-utils": "^29.2.2", + "jest-message-util": "^29.2.1", + "jest-util": "^29.2.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/filesize": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", + "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/final-form": { + "version": "4.20.7", + "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.7.tgz", + "integrity": "sha512-ii3X9wNfyBYFnDPunYN5jh1/HAvtOZ9aJI/TVk0MB86hZuOeYkb+W5L3icgwW9WWNztZR6MDU3En6eoZTUoFPg==", + "dependencies": { + "@babel/runtime": "^7.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/final-form" + } + }, + "node_modules/final-form-set-field-touched": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/final-form-set-field-touched/-/final-form-set-field-touched-1.0.1.tgz", + "integrity": "sha512-yvE5AAs9U3OgJQ9YF8NhSF0I0mJEECvOpkaXNqovloxji5Q6gOZ0DCIAyLAKHluGSpsXKUGORyBm8Hq0beZIqQ==", + "peerDependencies": { + "final-form": ">=1.2.0" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.2.tgz", + "integrity": "sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA==", + "dependencies": { + "@babel/code-frame": "^7.8.3", + "@types/json-schema": "^7.0.5", + "chalk": "^4.1.0", + "chokidar": "^3.4.2", + "cosmiconfig": "^6.0.0", + "deepmerge": "^4.2.2", + "fs-extra": "^9.0.0", + "glob": "^7.1.6", + "memfs": "^3.1.2", + "minimatch": "^3.0.4", + "schema-utils": "2.7.0", + "semver": "^7.3.2", + "tapable": "^1.0.0" + }, + "engines": { + "node": ">=10", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "eslint": ">= 6", + "typescript": ">= 2.7", + "vue-template-compiler": "*", + "webpack": ">= 4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + } + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "dependencies": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==" + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==" + }, + "node_modules/harmony-reflect": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", + "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==" + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/hoopy": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", + "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "dependencies": { + "whatwg-encoding": "^1.0.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", + "integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==", + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "webpack": "^5.20.0" + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/husky": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz", + "integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/i18next": { + "version": "22.0.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.0.4.tgz", + "integrity": "sha512-TOp7BTMKDbUkOHMzDlVsCYWpyaFkKakrrO3HNXfSz4EeJaWwnBScRmgQSTaWHScXVHBUFXTvShrCW8uryBYFcg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.17.2" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.0.tgz", + "integrity": "sha512-RrH7z5/DbhzhgCLDFIKXBTZlb2aXi38ZHa5e5oZaPt9zGLWmgAX49mzkQL/E7R6Y9fTE8QbZFuyMV0ronu4H/Q==", + "dependencies": { + "@babel/runtime": "^7.19.4" + } + }, + "node_modules/i18next-icu": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/i18next-icu/-/i18next-icu-2.0.3.tgz", + "integrity": "sha512-sZ0VCWDnHysUYQL8j/0rVOxv6rLR+SBoaqQQ2UVNfLyJCuf/bAjYPkoUQgyuDkWFo1xZjeCf4G6GBNr7gD61bQ==", + "peerDependencies": { + "intl-messageformat": "^9.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/idb": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.0.tgz", + "integrity": "sha512-Wsk07aAxDsntgYJY4h0knZJuTxM73eQ4reRAO+Z1liOh8eMCJ/MoDS8fCui1vGT9mnjtl1sOu3I2i/W1swPYZg==" + }, + "node_modules/identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", + "dependencies": { + "harmony-reflect": "^1.4.6" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "9.0.16", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.16.tgz", + "integrity": "sha512-qenGE7CstVm1NrHQbMh8YaSzTZTFNP3zPqr3YU0S0UY441j4bJTg4A2Hh5KAhwgaiU6ZZ1Ar6y/2f4TblnMReQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/intl-messageformat": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.2.1.tgz", + "integrity": "sha512-1lrJG2qKzcC1TVzYu1VuB1yiY68LU5rwpbHa2THCzA67Vutkz7+1lv5U20K3Lz5RAiH78zxNztMEtchokMWv8A==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.13.0", + "@formatjs/fast-memoize": "1.2.6", + "@formatjs/icu-messageformat-parser": "2.1.10", + "tslib": "2.4.0" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", + "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-root": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", + "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.9.tgz", + "integrity": "sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-abstract": "^1.20.0", + "for-each": "^0.3.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.8.5", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", + "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", + "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", + "dependencies": { + "@jest/core": "^27.5.1", + "import-local": "^3.0.2", + "jest-cli": "^27.5.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz", + "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==", + "dependencies": { + "@jest/types": "^27.5.1", + "execa": "^5.0.0", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-changed-files/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-changed-files/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-changed-files/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-changed-files/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-changed-files/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz", + "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "expect": "^27.5.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-circus/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-circus/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-circus/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-circus/node_modules/diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-circus/node_modules/expect": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", + "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", + "dependencies": { + "@jest/types": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-circus/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus/node_modules/jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-matcher-utils": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-circus/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", + "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", + "dependencies": { + "@jest/core": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "prompts": "^2.0.1", + "yargs": "^16.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-cli/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", + "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==", + "dependencies": { + "@babel/core": "^7.8.0", + "@jest/test-sequencer": "^27.5.1", + "@jest/types": "^27.5.1", + "babel-jest": "^27.5.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.9", + "jest-circus": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-jasmine2": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-config/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-config/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-config/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-config/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-config/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.2.1.tgz", + "integrity": "sha512-gfh/SMNlQmP3MOUgdzxPOd4XETDJifADpT937fN1iUGz+9DgOu2eUPHH25JDkLVcLwwqxv3GzVyK4VBUr9fjfA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.2.0", + "jest-get-type": "^29.2.0", + "pretty-format": "^29.2.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.2.1.tgz", + "integrity": "sha512-Y41Sa4aLCtKAXvwuIpTvcFBkyeYp2gdFWzXGA+ZNES3VwURIB165XO/z7CjETwzCCS53MjW/rLMyyqEnTtaOfA==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.0.0", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-docblock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz", + "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-each": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", + "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", + "dependencies": { + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "jest-get-type": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-each/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-each/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-each/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-each/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-each/node_modules/jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-each/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-each/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", + "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1", + "jsdom": "^16.6.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-environment-jsdom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-environment-jsdom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-node": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", + "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-environment-node/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-environment-node/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-environment-node/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-environment-node/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-node/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-get-type": { + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.2.0.tgz", + "integrity": "sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", + "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^27.5.1", + "jest-serializer": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "micromatch": "^4.0.4", + "walker": "^1.0.7" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-haste-map/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-haste-map/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-haste-map/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-haste-map/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-haste-map/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-haste-map/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-jasmine2": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz", + "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/source-map": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "expect": "^27.5.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-jasmine2/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-jasmine2/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-jasmine2/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-jasmine2/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-jasmine2/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-jasmine2/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-jasmine2/node_modules/diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-jasmine2/node_modules/expect": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", + "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", + "dependencies": { + "@jest/types": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-jasmine2/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-jasmine2/node_modules/jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-jasmine2/node_modules/jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-jasmine2/node_modules/jest-matcher-utils": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-jasmine2/node_modules/jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-jasmine2/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-jasmine2/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-leak-detector": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", + "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==", + "dependencies": { + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.2.2.tgz", + "integrity": "sha512-4DkJ1sDPT+UX2MR7Y3od6KtvRi9Im1ZGLGgdLFLm4lPexbTaCgJW5NN3IOXlQHF7NSHY/VHhflQ+WoKtD/vyCw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.2.1", + "jest-get-type": "^29.2.0", + "pretty-format": "^29.2.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-matcher-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.2.1.tgz", + "integrity": "sha512-Y41Sa4aLCtKAXvwuIpTvcFBkyeYp2gdFWzXGA+ZNES3VwURIB165XO/z7CjETwzCCS53MjW/rLMyyqEnTtaOfA==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.0.0", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util": { + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.2.1.tgz", + "integrity": "sha512-Dx5nEjw9V8C1/Yj10S/8ivA8F439VS8vTq1L7hEgwHFn9ovSKNpYW/kwNh7UglaEgXO42XxzKJB+2x0nSglFVw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.2.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.2.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-message-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-message-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.2.1.tgz", + "integrity": "sha512-Y41Sa4aLCtKAXvwuIpTvcFBkyeYp2gdFWzXGA+ZNES3VwURIB165XO/z7CjETwzCCS53MjW/rLMyyqEnTtaOfA==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.0.0", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", + "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-mock/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-mock/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-mock/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-mock/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-mock/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-mock/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-mock/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", + "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", + "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz", + "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==", + "dependencies": { + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "resolve": "^1.20.0", + "resolve.exports": "^1.1.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz", + "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==", + "dependencies": { + "@jest/types": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-snapshot": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-resolve-dependencies/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-resolve/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-resolve/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-resolve/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-resolve/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-resolve/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-resolve/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-resolve/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz", + "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==", + "dependencies": { + "@jest/console": "^27.5.1", + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-leak-detector": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "source-map-support": "^0.5.6", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-runner/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-runner/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-runner/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runner/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-runner/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner/node_modules/jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-runner/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", + "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/globals": "^27.5.1", + "@jest/source-map": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "execa": "^5.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-runtime/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-runtime/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-runtime/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runtime/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-runtime/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/node_modules/jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-runtime/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-runtime/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-serializer": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", + "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", + "dependencies": { + "@types/node": "*", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", + "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", + "dependencies": { + "@babel/core": "^7.7.2", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.0.0", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__traverse": "^7.0.4", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^27.5.1", + "semver": "^7.3.2" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-snapshot/node_modules/diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/expect": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", + "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", + "dependencies": { + "@jest/types": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot/node_modules/jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-matcher-utils": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util": { + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.2.1.tgz", + "integrity": "sha512-P5VWDj25r7kj7kl4pN2rG/RN2c1TLfYYYZYULnS/35nFDjBai+hBeo3MDrYZS7p6IoY3YHZnt2vq4L6mKnLk0g==", + "dev": true, + "dependencies": { + "@jest/types": "^29.2.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz", + "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==", + "dependencies": { + "@jest/types": "^27.5.1", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^27.5.1", + "leven": "^3.1.0", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-validate/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-validate/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-validate/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-validate/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate/node_modules/jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-validate/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-1.1.0.tgz", + "integrity": "sha512-Va5nLSJTN7YFtC2jd+7wsoe1pNe5K4ShLux/E5iHEwlB9AxaxmggY7to9KUqKojhaJw3aXqt5WAb4jGPOolpEw==", + "dependencies": { + "ansi-escapes": "^4.3.1", + "chalk": "^4.0.0", + "jest-regex-util": "^28.0.0", + "jest-watcher": "^28.0.0", + "slash": "^4.0.0", + "string-length": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "jest": "^27.0.0 || ^28.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@jest/console": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz", + "integrity": "sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==", + "dependencies": { + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^28.1.3", + "jest-util": "^28.1.3", + "slash": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@jest/console/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@jest/schemas": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", + "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", + "dependencies": { + "@sinclair/typebox": "^0.24.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@jest/test-result": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz", + "integrity": "sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==", + "dependencies": { + "@jest/console": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@jest/types": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz", + "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==", + "dependencies": { + "@jest/schemas": "^28.1.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-watch-typeahead/node_modules/emittery": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", + "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-message-util": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", + "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^28.1.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^28.1.3", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-message-util/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-regex-util": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", + "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-util": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", + "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", + "dependencies": { + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-watcher": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz", + "integrity": "sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==", + "dependencies": { + "@jest/test-result": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.10.2", + "jest-util": "^28.1.3", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead/node_modules/pretty-format": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", + "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", + "dependencies": { + "@jest/schemas": "^28.1.3", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watch-typeahead/node_modules/string-length": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", + "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==", + "dependencies": { + "char-regex": "^2.0.0", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watch-typeahead/node_modules/string-length/node_modules/char-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.1.tgz", + "integrity": "sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/jest-watch-typeahead/node_modules/strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz", + "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==", + "dependencies": { + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "jest-util": "^27.5.1", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-watcher/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-watcher/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-watcher/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-watcher/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-watcher/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-watcher/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-sdsl": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", + "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", + "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "dependencies": { + "abab": "^2.0.5", + "acorn": "^8.2.4", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^3.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.6", + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", + "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==", + "dependencies": { + "array-includes": "^3.1.5", + "object.assign": "^4.1.3" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "engines": { + "node": ">=6" + } + }, + "node_modules/klona": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", + "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==" + }, + "node_modules/language-tags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", + "dependencies": { + "language-subtag-registry": "~0.3.2" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", + "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" + }, + "node_modules/long": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz", + "integrity": "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lz-string": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", + "integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.9.tgz", + "integrity": "sha512-3rm8kbrzpUGRyPKSGuk387NZOwQ90O4rI9tsWQkzNW7BLSnKGp23RsEsKK8N8QVCrtJoAMqy3spxHC4os4G6PQ==", + "dependencies": { + "fs-monkey": "^1.0.3" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.1.tgz", + "integrity": "sha512-wd+SD57/K6DiV7jIR34P+s3uckTRuQvx0tKPcvjFlrEylk6P4mQ2KSWk1hblj1Kxaqok7LogKOieygXqBczNlg==", + "dependencies": { + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", + "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", + "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", + "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.getownpropertydescriptors": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.4.tgz", + "integrity": "sha512-sccv3L/pMModT6dJAYF3fzGMVcb38ysQ0tEE6ixv2yXJDtEIPph268OlAdJj5/qZMZDq2g/jqvwppt36uS/uQQ==", + "dependencies": { + "array.prototype.reduce": "^1.0.4", + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.hasown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.1.tgz", + "integrity": "sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==", + "dependencies": { + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz", + "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-browser-comments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz", + "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==", + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "browserslist": ">=4", + "postcss": ">=8" + } + }, + "node_modules/postcss-calc": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", + "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "dependencies": { + "postcss-selector-parser": "^6.0.9", + "postcss-value-parser": "^4.2.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz", + "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-color-hex-alpha": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz", + "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz", + "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-colormin": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.0.tgz", + "integrity": "sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg==", + "dependencies": { + "browserslist": "^4.16.6", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-convert-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", + "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-custom-media": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz", + "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/postcss-custom-properties": { + "version": "12.1.10", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.10.tgz", + "integrity": "sha512-U3BHdgrYhCrwTVcByFHs9EOBoqcKq4Lf3kXwbTi4hhq0qWhl/pDWq2THbv/ICX/Fl9KqeHBb8OVrTf2OaYF07A==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-custom-selectors": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz", + "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==", + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz", + "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-discard-comments": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-empty": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz", + "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-env-function": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", + "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-flexbugs-fixes": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", + "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==", + "peerDependencies": { + "postcss": "^8.1.4" + } + }, + "node_modules/postcss-focus-visible": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", + "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-within": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", + "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-gap-properties": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", + "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-image-set-function": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz", + "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-initial": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", + "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-lab-function": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz", + "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-loader": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", + "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", + "dependencies": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-loader/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postcss-logical": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", + "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-media-minmax": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", + "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", + "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-merge-rules": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.3.tgz", + "integrity": "sha512-LbLd7uFC00vpOuMvyZop8+vvhnfRGpp2S+IMQKeuOZZapPRY4SMq5ErjQeHbHsjCUgJkRNrlU+LmxsKIqPKQlA==", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^3.1.0", + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", + "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", + "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", + "dependencies": { + "colord": "^2.9.1", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-params": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", + "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", + "dependencies": { + "browserslist": "^4.21.4", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", + "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nesting": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz", + "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==", + "dependencies": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-normalize": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz", + "integrity": "sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA==", + "dependencies": { + "@csstools/normalize.css": "*", + "postcss-browser-comments": "^4", + "sanitize.css": "*" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "browserslist": ">= 4", + "postcss": ">= 8" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", + "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", + "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", + "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-string": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", + "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", + "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", + "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", + "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", + "dependencies": { + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", + "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-opacity-percentage": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.2.tgz", + "integrity": "sha512-lyUfF7miG+yewZ8EAk9XUBIlrHyUE6fijnesuz+Mj5zrIHIEw6KcIZSOk/elVMqzLvREmXB83Zi/5QpNRYd47w==", + "funding": [ + { + "type": "kofi", + "url": "https://ko-fi.com/mrcgrtz" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/mrcgrtz" + } + ], + "engines": { + "node": "^12 || ^14 || >=16" + } + }, + "node_modules/postcss-ordered-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", + "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", + "dependencies": { + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-overflow-shorthand": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz", + "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz", + "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-preset-env": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.2.tgz", + "integrity": "sha512-rSMUEaOCnovKnwc5LvBDHUDzpGP+nrUeWZGWt9M72fBvckCi45JmnJigUr4QG4zZeOHmOCNCZnd2LKDvP++ZuQ==", + "dependencies": { + "@csstools/postcss-cascade-layers": "^1.1.0", + "@csstools/postcss-color-function": "^1.1.1", + "@csstools/postcss-font-format-keywords": "^1.0.1", + "@csstools/postcss-hwb-function": "^1.0.2", + "@csstools/postcss-ic-unit": "^1.0.1", + "@csstools/postcss-is-pseudo-class": "^2.0.7", + "@csstools/postcss-nested-calc": "^1.0.0", + "@csstools/postcss-normalize-display-values": "^1.0.1", + "@csstools/postcss-oklab-function": "^1.1.1", + "@csstools/postcss-progressive-custom-properties": "^1.3.0", + "@csstools/postcss-stepped-value-functions": "^1.0.1", + "@csstools/postcss-text-decoration-shorthand": "^1.0.0", + "@csstools/postcss-trigonometric-functions": "^1.0.2", + "@csstools/postcss-unset-value": "^1.0.2", + "autoprefixer": "^10.4.11", + "browserslist": "^4.21.3", + "css-blank-pseudo": "^3.0.3", + "css-has-pseudo": "^3.0.4", + "css-prefers-color-scheme": "^6.0.3", + "cssdb": "^7.0.1", + "postcss-attribute-case-insensitive": "^5.0.2", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^4.2.4", + "postcss-color-hex-alpha": "^8.0.4", + "postcss-color-rebeccapurple": "^7.1.1", + "postcss-custom-media": "^8.0.2", + "postcss-custom-properties": "^12.1.9", + "postcss-custom-selectors": "^6.0.3", + "postcss-dir-pseudo-class": "^6.0.5", + "postcss-double-position-gradients": "^3.1.2", + "postcss-env-function": "^4.0.6", + "postcss-focus-visible": "^6.0.4", + "postcss-focus-within": "^5.0.4", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^3.0.5", + "postcss-image-set-function": "^4.0.7", + "postcss-initial": "^4.0.1", + "postcss-lab-function": "^4.2.1", + "postcss-logical": "^5.0.4", + "postcss-media-minmax": "^5.0.0", + "postcss-nesting": "^10.2.0", + "postcss-opacity-percentage": "^1.1.2", + "postcss-overflow-shorthand": "^3.0.4", + "postcss-page-break": "^3.0.4", + "postcss-place": "^7.0.5", + "postcss-pseudo-class-any-link": "^7.1.6", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz", + "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.1.tgz", + "integrity": "sha512-//jeDqWcHPuXGZLoolFrUXBDyuEGbr9S2rMo19bkTIjBQ4PqkaO+oI8wua5BOUxpfi97i3PCoInsiFIEBfkm9w==", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", + "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-selector-not": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz", + "integrity": "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", + "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^2.7.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/postcss-svgo/node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/postcss-svgo/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "node_modules/postcss-svgo/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss-svgo/node_modules/svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", + "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-app-polyfill": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz", + "integrity": "sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w==", + "dependencies": { + "core-js": "^3.19.2", + "object-assign": "^4.1.1", + "promise": "^8.1.0", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.9", + "whatwg-fetch": "^3.6.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/react-dev-utils": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", + "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", + "dependencies": { + "@babel/code-frame": "^7.16.0", + "address": "^1.1.2", + "browserslist": "^4.18.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "detect-port-alt": "^1.1.6", + "escape-string-regexp": "^4.0.0", + "filesize": "^8.0.6", + "find-up": "^5.0.0", + "fork-ts-checker-webpack-plugin": "^6.5.0", + "global-modules": "^2.0.0", + "globby": "^11.0.4", + "gzip-size": "^6.0.0", + "immer": "^9.0.7", + "is-root": "^2.1.0", + "loader-utils": "^3.2.0", + "open": "^8.4.0", + "pkg-up": "^3.1.0", + "prompts": "^2.4.2", + "react-error-overlay": "^6.0.11", + "recursive-readdir": "^2.2.2", + "shell-quote": "^1.7.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/react-dev-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/react-dev-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/react-dev-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/react-dev-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/react-dev-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/react-dev-utils/node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/react-dev-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-error-overlay": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", + "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" + }, + "node_modules/react-final-form": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.9.tgz", + "integrity": "sha512-x3XYvozolECp3nIjly+4QqxdjSSWfcnpGEL5K8OBT6xmGrq5kBqbA6+/tOqoom9NwqIPPbxPNsOViFlbKgowbA==", + "dependencies": { + "@babel/runtime": "^7.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/final-form" + }, + "peerDependencies": { + "final-form": "^4.20.4", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-final-form-listeners": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/react-final-form-listeners/-/react-final-form-listeners-1.0.3.tgz", + "integrity": "sha512-OrdCNxSS4JQS/EXD+R530kZKFqaPfa+WcXPgVro/h4BpaBDF/Ja+BtHyCzDezCIb5rWaGGdOJIj+tN2YdtvrXg==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "final-form": ">=4.0.0", + "prop-types": "^15.6.0", + "react": "^15.3.0 || ^16.0.0 || ^17.0.0", + "react-final-form": ">=3.0.0" + } + }, + "node_modules/react-i18next": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.0.0.tgz", + "integrity": "sha512-/O7N6aIEAl1FaWZBNvhdIo9itvF/MO/nRKr9pYqRc9LhuC1u21SlfwpiYQqvaeNSEW3g3qUXLREOWMt+gxrWbg==", + "dependencies": { + "@babel/runtime": "^7.14.5", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 19.0.0", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/react-redux": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.4.tgz", + "integrity": "sha512-yMfQ7mX6bWuicz2fids6cR1YT59VTuT8MKyyE310wJQlINKENCeT1UcPdEiX6znI5tF8zXyJ/VYvDgeGuaaNwQ==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", + "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.4.2.tgz", + "integrity": "sha512-Rb0BAX9KHhVzT1OKhMvCDMw776aTYM0DtkxqUBP8dNBom3mPXlfNs76JNGK8wKJ1IZEY1+WGj+cvZxHVk/GiKw==", + "dependencies": { + "@remix-run/router": "1.0.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.4.2.tgz", + "integrity": "sha512-yM1kjoTkpfjgczPrcyWrp+OuQMyB1WleICiiGfstnQYo/S8hPEEnVjr/RdmlH6yKK4Tnj1UGXFSa7uwAtmDoLQ==", + "dependencies": { + "@remix-run/router": "1.0.2", + "react-router": "6.4.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-scripts": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", + "integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==", + "dependencies": { + "@babel/core": "^7.16.0", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", + "@svgr/webpack": "^5.5.0", + "babel-jest": "^27.4.2", + "babel-loader": "^8.2.3", + "babel-plugin-named-asset-import": "^0.3.8", + "babel-preset-react-app": "^10.0.1", + "bfj": "^7.0.2", + "browserslist": "^4.18.1", + "camelcase": "^6.2.1", + "case-sensitive-paths-webpack-plugin": "^2.4.0", + "css-loader": "^6.5.1", + "css-minimizer-webpack-plugin": "^3.2.0", + "dotenv": "^10.0.0", + "dotenv-expand": "^5.1.0", + "eslint": "^8.3.0", + "eslint-config-react-app": "^7.0.1", + "eslint-webpack-plugin": "^3.1.1", + "file-loader": "^6.2.0", + "fs-extra": "^10.0.0", + "html-webpack-plugin": "^5.5.0", + "identity-obj-proxy": "^3.0.0", + "jest": "^27.4.3", + "jest-resolve": "^27.4.2", + "jest-watch-typeahead": "^1.0.0", + "mini-css-extract-plugin": "^2.4.5", + "postcss": "^8.4.4", + "postcss-flexbugs-fixes": "^5.0.2", + "postcss-loader": "^6.2.1", + "postcss-normalize": "^10.0.1", + "postcss-preset-env": "^7.0.1", + "prompts": "^2.4.2", + "react-app-polyfill": "^3.0.0", + "react-dev-utils": "^12.0.1", + "react-refresh": "^0.11.0", + "resolve": "^1.20.0", + "resolve-url-loader": "^4.0.0", + "sass-loader": "^12.3.0", + "semver": "^7.3.5", + "source-map-loader": "^3.0.0", + "style-loader": "^3.3.1", + "tailwindcss": "^3.0.2", + "terser-webpack-plugin": "^5.2.5", + "webpack": "^5.64.4", + "webpack-dev-server": "^4.6.0", + "webpack-manifest-plugin": "^4.0.2", + "workbox-webpack-plugin": "^6.4.1" + }, + "bin": { + "react-scripts": "bin/react-scripts.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + }, + "peerDependencies": { + "react": ">= 16", + "typescript": "^3.2.1 || ^4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/react-scripts/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/react-virtualized-auto-sizer": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.7.tgz", + "integrity": "sha512-Mxi6lwOmjwIjC1X4gABXMJcKHsOo0xWl3E3ugOgufB8GJU+MqrtY35aBuvCYv/razQ1Vbp7h1gWJjGjoNN5pmA==", + "engines": { + "node": ">8.0.0" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc", + "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc" + } + }, + "node_modules/react-window": { + "version": "1.8.7", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.7.tgz", + "integrity": "sha512-JHEZbPXBpKMmoNO1bNhoXOOLg/ujhL/BU4IqVU9r8eQPcy5KQnGHIHDRkJ0ns9IM5+Aq5LNwt3j8t3tIrePQzA==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + }, + "engines": { + "node": ">8.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recursive-readdir": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", + "dependencies": { + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redux": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz", + "integrity": "sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/redux-form": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/redux-form/-/redux-form-8.3.8.tgz", + "integrity": "sha512-PzXhA0d+awIc4PkuhbDa6dCEiraMrGMyyDlYEVNX6qEyW/G2SqZXrjav5zrpXb0CCeqQSc9iqwbMtYQXbJbOAQ==", + "dependencies": { + "@babel/runtime": "^7.9.2", + "es6-error": "^4.1.1", + "hoist-non-react-statics": "^3.3.2", + "invariant": "^2.2.4", + "is-promise": "^2.1.0", + "lodash": "^4.17.15", + "prop-types": "^15.6.1", + "react-is": "^16.4.2" + }, + "engines": { + "node": ">=8.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/redux-form" + }, + "peerDependencies": { + "immutable": "^3.8.2 || ^4.0.0", + "react": "^16.4.2 || ^17.0.0", + "react-redux": "^6.0.1 || ^7.0.0", + "redux": "^3.7.2 || ^4.0.0" + }, + "peerDependenciesMeta": { + "immutable": { + "optional": true + } + } + }, + "node_modules/redux-form/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/redux-thunk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", + "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==", + "peerDependencies": { + "redux": "^4" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz", + "integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==" + }, + "node_modules/regenerator-transform": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.0.tgz", + "integrity": "sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==", + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", + "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/regexpu-core": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.1.tgz", + "integrity": "sha512-HrnlNtpvqP1Xkb28tMhBUO2EbyUHdQlsnlAhzWcwHy8WJR53UWr7/MAvqrsQKMbV4qdpv03oTMG8iIhfsPFktQ==", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsgen": "^0.7.1", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", + "integrity": "sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==" + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-url-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz", + "integrity": "sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==", + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^7.0.35", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=8.9" + }, + "peerDependencies": { + "rework": "1.0.1", + "rework-visit": "1.0.0" + }, + "peerDependenciesMeta": { + "rework": { + "optional": true + }, + "rework-visit": { + "optional": true + } + } + }, + "node_modules/resolve-url-loader/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" + }, + "node_modules/resolve-url-loader/node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve.exports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz", + "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-terser": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "jest-worker": "^26.2.1", + "serialize-javascript": "^4.0.0", + "terser": "^5.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0" + } + }, + "node_modules/rollup-plugin-terser/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup-plugin-terser/node_modules/jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/rollup-plugin-terser/node_modules/serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/rollup-plugin-terser/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", + "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sanitize-html": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.0.tgz", + "integrity": "sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/sanitize-html/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/sanitize.css": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", + "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==" + }, + "node_modules/sass-loader": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", + "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", + "dependencies": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==" + }, + "node_modules/selfsigned": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", + "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "dependencies": { + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz", + "integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.2.tgz", + "integrity": "sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg==", + "dependencies": { + "abab": "^2.0.5", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility" + }, + "node_modules/stack-utils": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", + "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-natural-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", + "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", + "integrity": "sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.1", + "side-channel": "^1.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-loader": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", + "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/stylehacks": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", + "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/stylis": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", + "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" + }, + "node_modules/svgo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", + "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", + "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", + "dependencies": { + "chalk": "^2.4.1", + "coa": "^2.0.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "^0.1.1", + "css-tree": "1.0.0-alpha.37", + "csso": "^4.0.2", + "js-yaml": "^3.13.1", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/svgo/node_modules/css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "node_modules/svgo/node_modules/css-what": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", + "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/svgo/node_modules/dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dependencies": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "node_modules/svgo/node_modules/domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/svgo/node_modules/domutils/node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "node_modules/svgo/node_modules/nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dependencies": { + "boolbase": "~1.0.0" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, + "node_modules/tailwindcss": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.1.tgz", + "integrity": "sha512-Uw+GVSxp5CM48krnjHObqoOwlCt5Qo6nw1jlCRwfGy68dSYb/LwS9ZFidYGRiM+w6rMawkZiu1mEMAsHYAfoLg==", + "dependencies": { + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "color-name": "^1.1.4", + "detective": "^5.2.1", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.12", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "lilconfig": "^2.0.6", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.17", + "postcss-import": "^14.1.0", + "postcss-js": "^4.0.0", + "postcss-load-config": "^3.1.4", + "postcss-nested": "6.0.0", + "postcss-selector-parser": "^6.0.10", + "postcss-value-parser": "^4.2.0", + "quick-lru": "^5.1.1", + "resolve": "^1.22.1" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/tailwindcss/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dependencies": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.39.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz", + "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + }, + "node_modules/throat": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", + "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==" + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tryer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", + "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" + }, + "node_modules/tsconfig-paths": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz", + "integrity": "sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==" + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/util.promisify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", + "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.2", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", + "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", + "dependencies": { + "browser-process-hrtime": "^1.0.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "dependencies": { + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "engines": { + "node": ">=10.4" + } + }, + "node_modules/webpack": { + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/webpack-dev-middleware/node_modules/schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz", + "integrity": "sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==", + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.1", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.4.2" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-server/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-manifest-plugin": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz", + "integrity": "sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow==", + "dependencies": { + "tapable": "^2.0.0", + "webpack-sources": "^2.2.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "peerDependencies": { + "webpack": "^4.44.2 || ^5.47.0" + } + }, + "node_modules/webpack-manifest-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-manifest-plugin/node_modules/webpack-sources": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", + "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", + "dependencies": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dependencies": { + "iconv-lite": "0.4.24" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + }, + "node_modules/whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" + }, + "node_modules/whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "dependencies": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.8.tgz", + "integrity": "sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-abstract": "^1.20.0", + "for-each": "^0.3.3", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workbox-background-sync": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz", + "integrity": "sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g==", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.5.4.tgz", + "integrity": "sha512-I/lBERoH1u3zyBosnpPEtcAVe5lwykx9Yg1k6f8/BGEPGaMMgZrwVrqL1uA9QZ1NGGFoyE6t9i7lBjOlDhFEEw==", + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-build": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.5.4.tgz", + "integrity": "sha512-kgRevLXEYvUW9WS4XoziYqZ8Q9j/2ziJYEtTrjdz5/L/cTUa2XfyMP2i7c3p34lgqJ03+mTiz13SdFef2POwbA==", + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.11.1", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^5.2.0", + "@rollup/plugin-node-resolve": "^11.2.1", + "@rollup/plugin-replace": "^2.4.1", + "@surma/rollup-plugin-off-main-thread": "^2.2.3", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^7.1.6", + "lodash": "^4.17.20", + "pretty-bytes": "^5.3.0", + "rollup": "^2.43.1", + "rollup-plugin-terser": "^7.0.0", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "6.5.4", + "workbox-broadcast-update": "6.5.4", + "workbox-cacheable-response": "6.5.4", + "workbox-core": "6.5.4", + "workbox-expiration": "6.5.4", + "workbox-google-analytics": "6.5.4", + "workbox-navigation-preload": "6.5.4", + "workbox-precaching": "6.5.4", + "workbox-range-requests": "6.5.4", + "workbox-recipes": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4", + "workbox-streams": "6.5.4", + "workbox-sw": "6.5.4", + "workbox-window": "6.5.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/workbox-build/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/workbox-build/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/workbox-build/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/workbox-build/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workbox-build/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/workbox-build/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + }, + "node_modules/workbox-build/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.5.4.tgz", + "integrity": "sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug==", + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-core": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.5.4.tgz", + "integrity": "sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q==" + }, + "node_modules/workbox-expiration": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.5.4.tgz", + "integrity": "sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ==", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-google-analytics": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.5.4.tgz", + "integrity": "sha512-8AU1WuaXsD49249Wq0B2zn4a/vvFfHkpcFfqAFHNHwln3jK9QUYmzdkKXGIZl9wyKNP+RRX30vcgcyWMcZ9VAg==", + "dependencies": { + "workbox-background-sync": "6.5.4", + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "node_modules/workbox-navigation-preload": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.5.4.tgz", + "integrity": "sha512-IIwf80eO3cr8h6XSQJF+Hxj26rg2RPFVUmJLUlM0+A2GzB4HFbQyKkrgD5y2d84g2IbJzP4B4j5dPBRzamHrng==", + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-precaching": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.5.4.tgz", + "integrity": "sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg==", + "dependencies": { + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "node_modules/workbox-range-requests": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.5.4.tgz", + "integrity": "sha512-Je2qR1NXCFC8xVJ/Lux6saH6IrQGhMpDrPXWZWWS8n/RD+WZfKa6dSZwU+/QksfEadJEr/NfY+aP/CXFFK5JFg==", + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-recipes": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.5.4.tgz", + "integrity": "sha512-QZNO8Ez708NNwzLNEXTG4QYSKQ1ochzEtRLGaq+mr2PyoEIC1xFW7MrWxrONUxBFOByksds9Z4//lKAX8tHyUA==", + "dependencies": { + "workbox-cacheable-response": "6.5.4", + "workbox-core": "6.5.4", + "workbox-expiration": "6.5.4", + "workbox-precaching": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "node_modules/workbox-routing": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.5.4.tgz", + "integrity": "sha512-apQswLsbrrOsBUWtr9Lf80F+P1sHnQdYodRo32SjiByYi36IDyL2r7BH1lJtFX8fwNHDa1QOVY74WKLLS6o5Pg==", + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-strategies": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.5.4.tgz", + "integrity": "sha512-DEtsxhx0LIYWkJBTQolRxG4EI0setTJkqR4m7r4YpBdxtWJH1Mbg01Cj8ZjNOO8etqfA3IZaOPHUxCs8cBsKLw==", + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-streams": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.5.4.tgz", + "integrity": "sha512-FXKVh87d2RFXkliAIheBojBELIPnWbQdyDvsH3t74Cwhg0fDheL1T8BqSM86hZvC0ZESLsznSYWw+Va+KVbUzg==", + "dependencies": { + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4" + } + }, + "node_modules/workbox-sw": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.5.4.tgz", + "integrity": "sha512-vo2RQo7DILVRoH5LjGqw3nphavEjK4Qk+FenXeUsknKn14eCNedHOXWbmnvP4ipKhlE35pvJ4yl4YYf6YsJArA==" + }, + "node_modules/workbox-webpack-plugin": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.5.4.tgz", + "integrity": "sha512-LmWm/zoaahe0EGmMTrSLUi+BjyR3cdGEfU3fS6PN1zKFYbqAKuQ+Oy/27e4VSXsyIwAw8+QDfk1XHNGtZu9nQg==", + "dependencies": { + "fast-json-stable-stringify": "^2.1.0", + "pretty-bytes": "^5.4.1", + "upath": "^1.2.0", + "webpack-sources": "^1.4.3", + "workbox-build": "6.5.4" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "webpack": "^4.4.0 || ^5.9.0" + } + }, + "node_modules/workbox-webpack-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workbox-webpack-plugin/node_modules/webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "node_modules/workbox-window": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.5.4.tgz", + "integrity": "sha512-HnLZJDwYBE+hpG25AQBO8RUWBJRaCsI9ksQJEp3aCOFCaG5kqaToAYXFRAHxzRluM2cQbGzdQF5rjKPWPA1fug==", + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "6.5.4" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@adobe/css-tools": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz", + "integrity": "sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==", + "dev": true + }, + "@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "requires": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@apideck/better-ajv-errors": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", + "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "requires": { + "json-schema": "^0.4.0", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + } + }, + "@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "requires": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, + "@babel/compat-data": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.0.tgz", + "integrity": "sha512-Gt9jszFJYq7qzXVK4slhc6NzJXnOVmRECWcVjF/T23rNXD9NtWQ0W3qxdg+p9wWIB+VQw3GYV/U2Ha9bRTfs4w==" + }, + "@babel/core": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.6.tgz", + "integrity": "sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg==", + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.6", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-module-transforms": "^7.19.6", + "@babel/helpers": "^7.19.4", + "@babel/parser": "^7.19.6", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.6", + "@babel/types": "^7.19.4", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + } + }, + "@babel/eslint-parser": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.19.1.tgz", + "integrity": "sha512-AqNf2QWt1rtu2/1rLswy6CDP7H9Oh3mMhk177Y67Rg8d7RD9WfOLLv8CGn6tisFvS2htm86yIe1yLF6I1UDaGQ==", + "requires": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==" + } + } + }, + "@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "requires": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + } + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", + "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "requires": { + "@babel/helper-explode-assignable-expression": "^7.18.6", + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz", + "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==", + "requires": { + "@babel/compat-data": "^7.20.0", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "semver": "^6.3.0" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.19.0.tgz", + "integrity": "sha512-NRz8DwF4jT3UfrmUoZjd0Uph9HQnP30t7Ash+weACcyNkiYTywpIjDBgReJMKgr+n86sn2nPVVmJ28Dm053Kqw==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-member-expression-to-functions": "^7.18.9", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.9", + "@babel/helper-split-export-declaration": "^7.18.6" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.19.0.tgz", + "integrity": "sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "regexpu-core": "^5.1.0" + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "requires": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==" + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", + "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz", + "integrity": "sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==", + "requires": { + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-module-transforms": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.6.tgz", + "integrity": "sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw==", + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.19.4", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.6", + "@babel/types": "^7.19.4" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz", + "integrity": "sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==" + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", + "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-wrap-function": "^7.18.9", + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-replace-supers": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.19.1.tgz", + "integrity": "sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==", + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-member-expression-to-functions": "^7.18.9", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/traverse": "^7.19.1", + "@babel/types": "^7.19.0" + } + }, + "@babel/helper-simple-access": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.19.4.tgz", + "integrity": "sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg==", + "requires": { + "@babel/types": "^7.19.4" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", + "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "requires": { + "@babel/types": "^7.20.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==" + }, + "@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==" + }, + "@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==" + }, + "@babel/helper-wrap-function": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.19.0.tgz", + "integrity": "sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg==", + "requires": { + "@babel/helper-function-name": "^7.19.0", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.0", + "@babel/types": "^7.19.0" + } + }, + "@babel/helpers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "requires": { + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" + } + }, + "@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "requires": { + "@babel/types": "^7.27.1" + } + }, + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", + "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz", + "integrity": "sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", + "@babel/plugin-proposal-optional-chaining": "^7.18.9" + } + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.19.1.tgz", + "integrity": "sha512-0yu8vNATgLy4ivqMNBIwb1HebCelqN7YX8SL3FDXORv/RqT0zEEWUCH4GH44JsSrvCu6GqnAdR5EBFAPeNBB4Q==", + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-proposal-class-static-block": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz", + "integrity": "sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-proposal-decorators": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.20.0.tgz", + "integrity": "sha512-vnuRRS20ygSxclEYikHzVrP9nZDFXaSzvJxGLQNAiBX041TmhS4hOUHWNIpq/q4muENuEP9XPJFXTNFejhemkg==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.19.0", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-replace-supers": "^7.19.1", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/plugin-syntax-decorators": "^7.19.0" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz", + "integrity": "sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.19.4.tgz", + "integrity": "sha512-wHmj6LDxVDnL+3WhXteUBaoM1aVILZODAUjg11kHqG4cOlfgMQGxw6aCgvrXrmaJR3Bn14oZhImyCPZzRpC93Q==", + "requires": { + "@babel/compat-data": "^7.19.4", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.18.8" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz", + "integrity": "sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz", + "integrity": "sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-decorators": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.19.0.tgz", + "integrity": "sha512-xaBZUEDntt4faL1yN8oIFlhfXeQAWJW7CLKYsHTUqriCUbj8xOra8bfxxKGi/UwExPFBuPdH4XfHc9rGQhrVkQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.19.0" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-flow": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz", + "integrity": "sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-import-assertions": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", + "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.19.0" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", + "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz", + "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.19.0" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz", + "integrity": "sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz", + "integrity": "sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==", + "requires": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-remap-async-to-generator": "^7.18.6" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", + "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.0.tgz", + "integrity": "sha512-sXOohbpHZSk7GjxK9b3dKB7CfqUD5DwOH+DggKzOQ7TXYP+RCSbRykfjQmn/zq+rBjycVRtLf9pYhAaEJA786w==", + "requires": { + "@babel/helper-plugin-utils": "^7.19.0" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.19.0.tgz", + "integrity": "sha512-YfeEE9kCjqTS9IitkgfJuxjcEtLUHMqa8yUJ6zdz8vR7hKuo6mOy2C05P0F1tdMmDCeuyidKnlrw/iTppHcr2A==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-compilation-targets": "^7.19.0", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-replace-supers": "^7.18.9", + "@babel/helper-split-export-declaration": "^7.18.6", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz", + "integrity": "sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.0.tgz", + "integrity": "sha512-1dIhvZfkDVx/zn2S1aFwlruspTt4189j7fEkH0Y0VyuDM6bQt7bD6kLcz3l4IlLG+e5OReaBz9ROAbttRtUHqA==", + "requires": { + "@babel/helper-plugin-utils": "^7.19.0" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", + "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", + "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", + "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-flow-strip-types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.19.0.tgz", + "integrity": "sha512-sgeMlNaQVbCSpgLSKP4ZZKfsJVnFnNQlUSk6gPYzR/q7tzCgQF2t8RBKAP6cKJeZdveei7Q7Jm527xepI8lNLg==", + "requires": { + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/plugin-syntax-flow": "^7.18.6" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.18.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", + "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", + "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "requires": { + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", + "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", + "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.19.6.tgz", + "integrity": "sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg==", + "requires": { + "@babel/helper-module-transforms": "^7.19.6", + "@babel/helper-plugin-utils": "^7.19.0" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.19.6.tgz", + "integrity": "sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ==", + "requires": { + "@babel/helper-module-transforms": "^7.19.6", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-simple-access": "^7.19.4" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.6.tgz", + "integrity": "sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ==", + "requires": { + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-module-transforms": "^7.19.6", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-validator-identifier": "^7.19.1" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", + "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "requires": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.19.1.tgz", + "integrity": "sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.19.0", + "@babel/helper-plugin-utils": "^7.19.0" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", + "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", + "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.6" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.18.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.18.8.tgz", + "integrity": "sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", + "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-react-constant-elements": { + "version": "7.18.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.18.12.tgz", + "integrity": "sha512-Q99U9/ttiu+LMnRU8psd23HhvwXmKWDQIpocm0JKaICcZHnw+mdQbHm6xnSy7dOl8I5PELakYtNBubNQlBXbZw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-react-display-name": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz", + "integrity": "sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.19.0.tgz", + "integrity": "sha512-UVEvX3tXie3Szm3emi1+G63jyw1w5IcMY0FSKM+CRnKRI5Mr1YbCNgsSTwoTwKphQEG9P+QqmuRFneJPZuHNhg==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/plugin-syntax-jsx": "^7.18.6", + "@babel/types": "^7.19.0" + } + }, + "@babel/plugin-transform-react-jsx-development": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz", + "integrity": "sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==", + "requires": { + "@babel/plugin-transform-react-jsx": "^7.18.6" + } + }, + "@babel/plugin-transform-react-pure-annotations": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.18.6.tgz", + "integrity": "sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz", + "integrity": "sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "regenerator-transform": "^0.15.0" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", + "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-runtime": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz", + "integrity": "sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==", + "requires": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.19.0", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "semver": "^6.3.0" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", + "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz", + "integrity": "sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==", + "requires": { + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", + "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", + "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", + "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-typescript": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.20.0.tgz", + "integrity": "sha512-xOAsAFaun3t9hCwZ13Qe7gq423UgMZ6zAgmLxeGGapFqlT/X3L5qT2btjiVLlFn7gWtMaVyceS5VxGAuKbgizw==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.19.0", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/plugin-syntax-typescript": "^7.20.0" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", + "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", + "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/preset-env": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.19.4.tgz", + "integrity": "sha512-5QVOTXUdqTCjQuh2GGtdd7YEhoRXBMVGROAtsBeLGIbIz3obCBIfRMT1I3ZKkMgNzwkyCkftDXSSkHxnfVf4qg==", + "requires": { + "@babel/compat-data": "^7.19.4", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-async-generator-functions": "^7.19.1", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.18.6", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.19.4", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.18.6", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.18.6", + "@babel/plugin-transform-async-to-generator": "^7.18.6", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.19.4", + "@babel/plugin-transform-classes": "^7.19.0", + "@babel/plugin-transform-computed-properties": "^7.18.9", + "@babel/plugin-transform-destructuring": "^7.19.4", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.18.8", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.18.6", + "@babel/plugin-transform-modules-commonjs": "^7.18.6", + "@babel/plugin-transform-modules-systemjs": "^7.19.0", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.18.8", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.18.6", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.19.0", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.19.4", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "core-js-compat": "^3.25.1", + "semver": "^6.3.0" + } + }, + "@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/preset-react": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.18.6.tgz", + "integrity": "sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-transform-react-display-name": "^7.18.6", + "@babel/plugin-transform-react-jsx": "^7.18.6", + "@babel/plugin-transform-react-jsx-development": "^7.18.6", + "@babel/plugin-transform-react-pure-annotations": "^7.18.6" + } + }, + "@babel/preset-typescript": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz", + "integrity": "sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-transform-typescript": "^7.18.6" + } + }, + "@babel/runtime": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==" + }, + "@babel/runtime-corejs3": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.27.1.tgz", + "integrity": "sha512-909rVuj3phpjW6y0MCXAZ5iNeORePa6ldJvp2baWGcTjwqbBDDz6xoS5JHJ7lS88NlwLYj07ImL/8IUMtDZzTA==", + "requires": { + "core-js-pure": "^3.30.2" + } + }, + "@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + } + }, + "@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "requires": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" + }, + "@csstools/normalize.css": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.0.0.tgz", + "integrity": "sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg==" + }, + "@csstools/postcss-cascade-layers": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz", + "integrity": "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==", + "requires": { + "@csstools/selector-specificity": "^2.0.2", + "postcss-selector-parser": "^6.0.10" + } + }, + "@csstools/postcss-color-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz", + "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==", + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-font-format-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz", + "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-hwb-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz", + "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-ic-unit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz", + "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==", + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-is-pseudo-class": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz", + "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==", + "requires": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + } + }, + "@csstools/postcss-nested-calc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz", + "integrity": "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-normalize-display-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", + "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-oklab-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz", + "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==", + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-progressive-custom-properties": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", + "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-stepped-value-functions": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz", + "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-text-decoration-shorthand": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz", + "integrity": "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-trigonometric-functions": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz", + "integrity": "sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-unset-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", + "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==" + }, + "@csstools/selector-specificity": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz", + "integrity": "sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==" + }, + "@emotion/babel-plugin": { + "version": "11.10.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz", + "integrity": "sha512-xE7/hyLHJac7D2Ve9dKroBBZqBT7WuPQmWcq7HSGb84sUuP4mlOWoB8dvVfD9yk5DHkU1m6RW7xSoDtnQHNQeA==", + "requires": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/plugin-syntax-jsx": "^7.17.12", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.0", + "@emotion/memoize": "^0.8.0", + "@emotion/serialize": "^1.1.1", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.1.3" + } + }, + "@emotion/cache": { + "version": "11.10.5", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz", + "integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==", + "requires": { + "@emotion/memoize": "^0.8.0", + "@emotion/sheet": "^1.2.1", + "@emotion/utils": "^1.2.0", + "@emotion/weak-memoize": "^0.3.0", + "stylis": "4.1.3" + } + }, + "@emotion/hash": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", + "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" + }, + "@emotion/is-prop-valid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", + "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", + "requires": { + "@emotion/memoize": "^0.8.0" + } + }, + "@emotion/memoize": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + }, + "@emotion/react": { + "version": "11.10.5", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.5.tgz", + "integrity": "sha512-TZs6235tCJ/7iF6/rvTaOH4oxQg2gMAcdHemjwLKIjKz4rRuYe1HJ2TQJKnAcRAfOUDdU8XoDadCe1rl72iv8A==", + "requires": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.10.5", + "@emotion/cache": "^11.10.5", + "@emotion/serialize": "^1.1.1", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", + "@emotion/utils": "^1.2.0", + "@emotion/weak-memoize": "^0.3.0", + "hoist-non-react-statics": "^3.3.1" + } + }, + "@emotion/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==", + "requires": { + "@emotion/hash": "^0.9.0", + "@emotion/memoize": "^0.8.0", + "@emotion/unitless": "^0.8.0", + "@emotion/utils": "^1.2.0", + "csstype": "^3.0.2" + } + }, + "@emotion/sheet": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz", + "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==" + }, + "@emotion/styled": { + "version": "11.10.5", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.5.tgz", + "integrity": "sha512-8EP6dD7dMkdku2foLoruPCNkRevzdcBaY6q0l0OsbyJK+x8D9HWjX27ARiSIKNF634hY9Zdoedh8bJCiva8yZw==", + "requires": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.10.5", + "@emotion/is-prop-valid": "^1.2.0", + "@emotion/serialize": "^1.1.1", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", + "@emotion/utils": "^1.2.0" + } + }, + "@emotion/unitless": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + }, + "@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz", + "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==" + }, + "@emotion/utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", + "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" + }, + "@emotion/weak-memoize": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", + "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" + }, + "@eslint/eslintrc": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", + "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==", + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "globals": { + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "requires": { + "type-fest": "^0.20.2" + } + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" + } + } + }, + "@formatjs/ecma402-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.13.0.tgz", + "integrity": "sha512-CQ8Ykd51jYD1n05dtoX6ns6B9n/+6ZAxnWUAonvHC4kkuAemROYBhHkEB4tm1uVrRlE7gLDqXkAnY51Y0pRCWQ==", + "requires": { + "@formatjs/intl-localematcher": "0.2.31", + "tslib": "2.4.0" + } + }, + "@formatjs/fast-memoize": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.6.tgz", + "integrity": "sha512-9CWZ3+wCkClKHX+i5j+NyoBVqGf0pIskTo6Xl6ihGokYM2yqSSS68JIgeo+99UIHc+7vi9L3/SDSz/dWI9SNlA==", + "requires": { + "tslib": "2.4.0" + } + }, + "@formatjs/icu-messageformat-parser": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.10.tgz", + "integrity": "sha512-KkRMxhifWkRC45dhM9tqm0GXbb6NPYTGVYY3xx891IKc6p++DQrZTnmkVSNNO47OEERLfuP2KkPFPJBuu8z/wg==", + "requires": { + "@formatjs/ecma402-abstract": "1.13.0", + "@formatjs/icu-skeleton-parser": "1.3.14", + "tslib": "2.4.0" + } + }, + "@formatjs/icu-skeleton-parser": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.14.tgz", + "integrity": "sha512-7bv60HQQcBb3+TSj+45tOb/CHV5z1hOpwdtS50jsSBXfB+YpGhnoRsZxSRksXeCxMy6xn6tA6VY2601BrrK+OA==", + "requires": { + "@formatjs/ecma402-abstract": "1.13.0", + "tslib": "2.4.0" + } + }, + "@formatjs/intl-localematcher": { + "version": "0.2.31", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.31.tgz", + "integrity": "sha512-9QTjdSBpQ7wHShZgsNzNig5qT3rCPvmZogS/wXZzKotns5skbXgs0I7J8cuN0PPqXyynvNVuN+iOKhNS2eb+ZA==", + "requires": { + "tslib": "2.4.0" + } + }, + "@humanwhocodes/config-array": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", + "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==", + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==" + }, + "@jest/console": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", + "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", + "slash": "^3.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/core": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz", + "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==", + "requires": { + "@jest/console": "^27.5.1", + "@jest/reporters": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^27.5.1", + "jest-config": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-resolve-dependencies": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "jest-watcher": "^27.5.1", + "micromatch": "^4.0.4", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/environment": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", + "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", + "requires": { + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/expect-utils": { + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.2.2.tgz", + "integrity": "sha512-vwnVmrVhTmGgQzyvcpze08br91OL61t9O0lJMDyb6Y/D8EKQ9V7rGUb/p7PDt0GPzK0zFYqXWFo4EO2legXmkg==", + "dev": true, + "requires": { + "jest-get-type": "^29.2.0" + } + }, + "@jest/fake-timers": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", + "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", + "requires": { + "@jest/types": "^27.5.1", + "@sinonjs/fake-timers": "^8.0.1", + "@types/node": "*", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/globals": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", + "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", + "requires": { + "@jest/environment": "^27.5.1", + "@jest/types": "^27.5.1", + "expect": "^27.5.1" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==" + }, + "expect": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", + "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", + "requires": { + "@jest/types": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + } + }, + "jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==" + }, + "jest-matcher-utils": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + } + }, + "jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/reporters": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", + "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==", + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-haste-map": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^4.0.1", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^8.1.0" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/schemas": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.0.0.tgz", + "integrity": "sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.24.1" + } + }, + "@jest/source-map": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", + "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", + "requires": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "@jest/test-result": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", + "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", + "requires": { + "@jest/console": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/test-sequencer": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz", + "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==", + "requires": { + "@jest/test-result": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-runtime": "^27.5.1" + } + }, + "@jest/transform": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", + "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^27.5.1", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-util": "^27.5.1", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/types": { + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.2.1.tgz", + "integrity": "sha512-O/QNDQODLnINEPAI0cl9U6zUIDXEWXt6IC1o2N2QENuos7hlGUIthlKyV4p6ki3TvXFX071blj8HUhgLGquPjw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.0.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==" + }, + "@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + } + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" + }, + "@mui/base": { + "version": "5.0.0-alpha.103", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.103.tgz", + "integrity": "sha512-fJIyB2df3CHn7D26WHnutnY7vew6aytTlhmRJz6GX7ag19zU2GcOUhJAzY5qwWcrXKnlYgzimhEjaEnuiUWU4g==", + "requires": { + "@babel/runtime": "^7.19.0", + "@emotion/is-prop-valid": "^1.2.0", + "@mui/types": "^7.2.0", + "@mui/utils": "^5.10.9", + "@popperjs/core": "^2.11.6", + "clsx": "^1.2.1", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + } + }, + "@mui/core-downloads-tracker": { + "version": "5.10.11", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.10.11.tgz", + "integrity": "sha512-u5ff+UCFDHcR8MoQ8tuJR4c35vt7T/ki3aMEE2O3XQoGs8KJSrBiisFpFKyldg9/W2NSyoZxN+kxEGIfRxh+9Q==" + }, + "@mui/icons-material": { + "version": "5.10.9", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.10.9.tgz", + "integrity": "sha512-sqClXdEM39WKQJOQ0ZCPTptaZgqwibhj2EFV9N0v7BU1PO8y4OcX/a2wIQHn4fNuDjIZktJIBrmU23h7aqlGgg==", + "requires": { + "@babel/runtime": "^7.19.0" + } + }, + "@mui/material": { + "version": "5.10.11", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.10.11.tgz", + "integrity": "sha512-KJ0wPCTbv6sFzwA3dgg0gowdfF+SRl7D510J9l6Nl/KFX0EawcewQudqKY4slYGFXniKa5PykqokpaWXsCCPqg==", + "requires": { + "@babel/runtime": "^7.19.0", + "@mui/base": "5.0.0-alpha.103", + "@mui/core-downloads-tracker": "^5.10.11", + "@mui/system": "^5.10.10", + "@mui/types": "^7.2.0", + "@mui/utils": "^5.10.9", + "@types/react-transition-group": "^4.4.5", + "clsx": "^1.2.1", + "csstype": "^3.1.1", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "react-transition-group": "^4.4.5" + } + }, + "@mui/private-theming": { + "version": "5.10.9", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.10.9.tgz", + "integrity": "sha512-BN7/CnsVPVyBaQpDTij4uV2xGYHHHhOgpdxeYLlIu+TqnsVM7wUeF+37kXvHovxM6xmL5qoaVUD98gDC0IZnHg==", + "requires": { + "@babel/runtime": "^7.19.0", + "@mui/utils": "^5.10.9", + "prop-types": "^15.8.1" + } + }, + "@mui/styled-engine": { + "version": "5.10.8", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.10.8.tgz", + "integrity": "sha512-w+y8WI18EJV6zM/q41ug19cE70JTeO6sWFsQ7tgePQFpy6ToCVPh0YLrtqxUZXSoMStW5FMw0t9fHTFAqPbngw==", + "requires": { + "@babel/runtime": "^7.19.0", + "@emotion/cache": "^11.10.3", + "csstype": "^3.1.1", + "prop-types": "^15.8.1" + } + }, + "@mui/system": { + "version": "5.10.10", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.10.10.tgz", + "integrity": "sha512-TXwtKN0adKpBrZmO+eilQWoPf2veh050HLYrN78Kps9OhlvO70v/2Kya0+mORFhu9yhpAwjHXO8JII/R4a5ZLA==", + "requires": { + "@babel/runtime": "^7.19.0", + "@mui/private-theming": "^5.10.9", + "@mui/styled-engine": "^5.10.8", + "@mui/types": "^7.2.0", + "@mui/utils": "^5.10.9", + "clsx": "^1.2.1", + "csstype": "^3.1.1", + "prop-types": "^15.8.1" + } + }, + "@mui/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.0.tgz", + "integrity": "sha512-lGXtFKe5lp3UxTBGqKI1l7G8sE2xBik8qCfrLHD5olwP/YU0/ReWoWT7Lp1//ri32dK39oPMrJN8TgbkCSbsNA==" + }, + "@mui/utils": { + "version": "5.10.9", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.10.9.tgz", + "integrity": "sha512-2tdHWrq3+WCy+G6TIIaFx3cg7PorXZ71P375ExuX61od1NOAJP1mK90VxQ8N4aqnj2vmO3AQDkV4oV2Ktvt4bA==", + "requires": { + "@babel/runtime": "^7.19.0", + "@types/prop-types": "^15.7.5", + "@types/react-is": "^16.7.1 || ^17.0.0", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + } + }, + "@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "requires": { + "eslint-scope": "5.1.1" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.8.tgz", + "integrity": "sha512-wxXRwf+IQ6zvHSJZ+5T2RQNEsq+kx4jKRXfFvdt3nBIUzJUAvXEFsUeoaohDe/Kr84MTjGwcuIUPNcstNJORsA==", + "requires": { + "ansi-html-community": "^0.0.8", + "common-path-prefix": "^3.0.0", + "core-js-pure": "^3.23.3", + "error-stack-parser": "^2.0.6", + "find-up": "^5.0.0", + "html-entities": "^2.1.0", + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==" + } + } + }, + "@popperjs/core": { + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", + "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==" + }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "@remix-run/router": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.2.tgz", + "integrity": "sha512-GRSOFhJzjGN+d4sKHTMSvNeUPoZiDHWmRnXfzaxrqe7dE/Nzlc8BiMSJdLDESZlndM7jIUrZ/F4yWqVYlI0rwQ==" + }, + "@rollup/plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "requires": { + "@babel/helper-module-imports": "^7.10.4", + "@rollup/pluginutils": "^3.1.0" + } + }, + "@rollup/plugin-node-resolve": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", + "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", + "requires": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + } + }, + "@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "requires": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "dependencies": { + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" + } + } + }, + "@rushstack/eslint-patch": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", + "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==" + }, + "@sinclair/typebox": { + "version": "0.24.51", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", + "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==" + }, + "@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", + "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@surma/rollup-plugin-off-main-thread": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", + "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "requires": { + "ejs": "^3.1.6", + "json5": "^2.2.0", + "magic-string": "^0.25.0", + "string.prototype.matchall": "^4.0.6" + } + }, + "@svgr/babel-plugin-add-jsx-attribute": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz", + "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==" + }, + "@svgr/babel-plugin-remove-jsx-attribute": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz", + "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==" + }, + "@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz", + "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==" + }, + "@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz", + "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==" + }, + "@svgr/babel-plugin-svg-dynamic-title": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz", + "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==" + }, + "@svgr/babel-plugin-svg-em-dimensions": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz", + "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==" + }, + "@svgr/babel-plugin-transform-react-native-svg": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz", + "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==" + }, + "@svgr/babel-plugin-transform-svg-component": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz", + "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==" + }, + "@svgr/babel-preset": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz", + "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==", + "requires": { + "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0", + "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "^5.0.1", + "@svgr/babel-plugin-replace-jsx-attribute-value": "^5.0.1", + "@svgr/babel-plugin-svg-dynamic-title": "^5.4.0", + "@svgr/babel-plugin-svg-em-dimensions": "^5.4.0", + "@svgr/babel-plugin-transform-react-native-svg": "^5.4.0", + "@svgr/babel-plugin-transform-svg-component": "^5.5.0" + } + }, + "@svgr/core": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz", + "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==", + "requires": { + "@svgr/plugin-jsx": "^5.5.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^7.0.0" + } + }, + "@svgr/hast-util-to-babel-ast": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz", + "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==", + "requires": { + "@babel/types": "^7.12.6" + } + }, + "@svgr/plugin-jsx": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", + "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", + "requires": { + "@babel/core": "^7.12.3", + "@svgr/babel-preset": "^5.5.0", + "@svgr/hast-util-to-babel-ast": "^5.5.0", + "svg-parser": "^2.0.2" + } + }, + "@svgr/plugin-svgo": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz", + "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==", + "requires": { + "cosmiconfig": "^7.0.0", + "deepmerge": "^4.2.2", + "svgo": "^1.2.2" + } + }, + "@svgr/webpack": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz", + "integrity": "sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==", + "requires": { + "@babel/core": "^7.12.3", + "@babel/plugin-transform-react-constant-elements": "^7.12.1", + "@babel/preset-env": "^7.12.1", + "@babel/preset-react": "^7.12.5", + "@svgr/core": "^5.5.0", + "@svgr/plugin-jsx": "^5.5.0", + "@svgr/plugin-svgo": "^5.5.0", + "loader-utils": "^2.0.0" + } + }, + "@testing-library/dom": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.19.0.tgz", + "integrity": "sha512-6YWYPPpxG3e/xOo6HIWwB/58HukkwIVTOaZ0VwdMVjhRUX/01E4FtQbck9GazOOj7MXHc5RBzMrU86iBJHbI+A==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^4.2.0", + "aria-query": "^5.0.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.4.4", + "pretty-format": "^27.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@testing-library/jest-dom": { + "version": "5.16.5", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz", + "integrity": "sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==", + "dev": true, + "requires": { + "@adobe/css-tools": "^4.0.1", + "@babel/runtime": "^7.9.2", + "@types/testing-library__jest-dom": "^5.9.1", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.5.6", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@testing-library/react": { + "version": "13.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.4.0.tgz", + "integrity": "sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^8.5.0", + "@types/react-dom": "^18.0.0" + } + }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" + }, + "@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==" + }, + "@types/aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==", + "dev": true + }, + "@types/babel__core": { + "version": "7.1.19", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz", + "integrity": "sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==", + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.2.tgz", + "integrity": "sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==", + "requires": { + "@babel/types": "^7.3.0" + } + }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/bonjour": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", + "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "requires": { + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/connect-history-api-fallback": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", + "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", + "requires": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "@types/eslint": { + "version": "8.4.9", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.9.tgz", + "integrity": "sha512-jFCSo4wJzlHQLCpceUhUnXdrPuCNOjGFMQ8Eg6JXxlz3QaCKOb7eGi2cephQdM4XTYsNej69P9JDJ1zqNIbncQ==", + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" + }, + "@types/express": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", + "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.31", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", + "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/graceful-fs": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", + "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", + "requires": { + "@types/node": "*" + } + }, + "@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "dev": true + }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==" + }, + "@types/http-proxy": { + "version": "1.17.9", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", + "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==" + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.2.0.tgz", + "integrity": "sha512-KO7bPV21d65PKwv3LLsD8Jn3E05pjNjRZvkm+YTacWhVmykAb07wW6IkZUmQAltwQafNcDUEUrMO2h3jeBSisg==", + "dev": true, + "requires": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.2.1.tgz", + "integrity": "sha512-Y41Sa4aLCtKAXvwuIpTvcFBkyeYp2gdFWzXGA+ZNES3VwURIB165XO/z7CjETwzCCS53MjW/rLMyyqEnTtaOfA==", + "dev": true, + "requires": { + "@jest/schemas": "^29.0.0", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + } + } + }, + "@types/jquery": { + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz", + "integrity": "sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==", + "dev": true, + "requires": { + "@types/sizzle": "*" + } + }, + "@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" + }, + "@types/lodash": { + "version": "4.14.186", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.186.tgz", + "integrity": "sha512-eHcVlLXP0c2FlMPm56ITode2AgLMSa6aJ05JTTbYbI+7EMkCEE5qk2E41d5g2lCVTqRe0GnnRFurmlCsDODrPw==", + "dev": true + }, + "@types/mime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" + }, + "@types/node": { + "version": "18.11.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.7.tgz", + "integrity": "sha512-LhFTglglr63mNXUSRYD8A+ZAIu5sFqNJ4Y2fPuY7UlrySJH87rRRlhtVmMHplmfk5WkoJGmDjE9oiTfyX94CpQ==" + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + }, + "@types/prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==" + }, + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + }, + "@types/q": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", + "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==" + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "@types/react": { + "version": "18.0.24", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.24.tgz", + "integrity": "sha512-wRJWT6ouziGUy+9uX0aW4YOJxAY0bG6/AOk5AW5QSvZqI7dk6VBIbXvcVgIw/W5Jrl24f77df98GEKTJGOLx7Q==", + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.8.tgz", + "integrity": "sha512-C3GYO0HLaOkk9dDAz3Dl4sbe4AKUGTCfFIZsz3n/82dPNN8Du533HzKatDxeUYWu24wJgMP1xICqkWk1YOLOIw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react-is": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz", + "integrity": "sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==", + "requires": { + "@types/react": "*" + } + }, + "@types/react-redux": { + "version": "7.1.24", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz", + "integrity": "sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==", + "dev": true, + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, + "@types/react-router": { + "version": "5.1.19", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.19.tgz", + "integrity": "sha512-Fv/5kb2STAEMT3wHzdKQK2z8xKq38EDIGVrutYLmQVVLe+4orDFquU52hQrULnEHinMKv9FSA6lf9+uNT1ITtA==", + "dev": true, + "requires": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dev": true, + "requires": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "@types/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==", + "requires": { + "@types/react": "*" + } + }, + "@types/react-virtualized-auto-sizer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz", + "integrity": "sha512-GH8sAnBEM5GV9LTeiz56r4ZhMOUSrP43tAQNSRVxNexDjcNKLCEtnxusAItg1owFUFE6k0NslV26gqVClVvong==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react-window": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz", + "integrity": "sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/redux-form": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/@types/redux-form/-/redux-form-8.3.5.tgz", + "integrity": "sha512-SchB4i7nxgWNbJS4cXEZducztkvHzVrb5xlAXwfLpbrLPo6tMY06+kx1GqMv42+YnGy9TpCAkF51a21HatqWBA==", + "dev": true, + "requires": { + "@types/react": "*", + "redux": "^3.6.0 || ^4.0.0" + } + }, + "@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "requires": { + "@types/node": "*" + } + }, + "@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + }, + "@types/semver": { + "version": "7.3.13", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==" + }, + "@types/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "requires": { + "@types/express": "*" + } + }, + "@types/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "requires": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "@types/sizzle": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", + "dev": true + }, + "@types/sockjs": { + "version": "0.3.33", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", + "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "requires": { + "@types/node": "*" + } + }, + "@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" + }, + "@types/testing-library__jest-dom": { + "version": "5.14.5", + "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz", + "integrity": "sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==", + "dev": true, + "requires": { + "@types/jest": "*" + } + }, + "@types/trusted-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", + "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==" + }, + "@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, + "@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "requires": { + "@types/node": "*" + } + }, + "@types/yargs": { + "version": "17.0.13", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", + "integrity": "sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" + }, + "@typescript-eslint/eslint-plugin": { + "version": "5.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.41.0.tgz", + "integrity": "sha512-DXUS22Y57/LAFSg3x7Vi6RNAuLpTXwxB9S2nIA7msBb/Zt8p7XqMwdpdc1IU7CkOQUPgAqR5fWvxuKCbneKGmA==", + "requires": { + "@typescript-eslint/scope-manager": "5.41.0", + "@typescript-eslint/type-utils": "5.41.0", + "@typescript-eslint/utils": "5.41.0", + "debug": "^4.3.4", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "5.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.41.0.tgz", + "integrity": "sha512-/qxT2Kd2q/A22JVIllvws4rvc00/3AT4rAo/0YgEN28y+HPhbJbk6X4+MAHEoZzpNyAOugIT7D/OLnKBW8FfhA==", + "requires": { + "@typescript-eslint/utils": "5.41.0" + } + }, + "@typescript-eslint/parser": { + "version": "5.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.41.0.tgz", + "integrity": "sha512-HQVfix4+RL5YRWZboMD1pUfFN8MpRH4laziWkkAzyO1fvNOY/uinZcvo3QiFJVS/siNHupV8E5+xSwQZrl6PZA==", + "requires": { + "@typescript-eslint/scope-manager": "5.41.0", + "@typescript-eslint/types": "5.41.0", + "@typescript-eslint/typescript-estree": "5.41.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.41.0.tgz", + "integrity": "sha512-xOxPJCnuktUkY2xoEZBKXO5DBCugFzjrVndKdUnyQr3+9aDWZReKq9MhaoVnbL+maVwWJu/N0SEtrtEUNb62QQ==", + "requires": { + "@typescript-eslint/types": "5.41.0", + "@typescript-eslint/visitor-keys": "5.41.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.41.0.tgz", + "integrity": "sha512-L30HNvIG6A1Q0R58e4hu4h+fZqaO909UcnnPbwKiN6Rc3BUEx6ez2wgN7aC0cBfcAjZfwkzE+E2PQQ9nEuoqfA==", + "requires": { + "@typescript-eslint/typescript-estree": "5.41.0", + "@typescript-eslint/utils": "5.41.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/types": { + "version": "5.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.41.0.tgz", + "integrity": "sha512-5BejraMXMC+2UjefDvrH0Fo/eLwZRV6859SXRg+FgbhA0R0l6lDqDGAQYhKbXhPN2ofk2kY5sgGyLNL907UXpA==" + }, + "@typescript-eslint/typescript-estree": { + "version": "5.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.41.0.tgz", + "integrity": "sha512-SlzFYRwFSvswzDSQ/zPkIWcHv8O5y42YUskko9c4ki+fV6HATsTODUPbRbcGDFYP86gaJL5xohUEytvyNNcXWg==", + "requires": { + "@typescript-eslint/types": "5.41.0", + "@typescript-eslint/visitor-keys": "5.41.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/utils": { + "version": "5.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.41.0.tgz", + "integrity": "sha512-QlvfwaN9jaMga9EBazQ+5DDx/4sAdqDkcs05AsQHMaopluVCUyu1bTRUVKzXbgjDlrRAQrYVoi/sXJ9fmG+KLQ==", + "requires": { + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.41.0", + "@typescript-eslint/types": "5.41.0", + "@typescript-eslint/typescript-estree": "5.41.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0", + "semver": "^7.3.7" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.41.0.tgz", + "integrity": "sha512-vilqeHj267v8uzzakbm13HkPMl7cbYpKVjgFWZPIOHIJHZtinvypUhJ5xBXfWYg4eFKqztbMMpOgFpT9Gfx4fw==", + "requires": { + "@typescript-eslint/types": "5.41.0", + "eslint-visitor-keys": "^3.3.0" + } + }, + "@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "requires": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==" + }, + "@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==" + }, + "@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==" + }, + "@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==" + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==" + }, + "@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + }, + "abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==" + }, + "acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "requires": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + } + } + }, + "acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==" + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==" + }, + "acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "requires": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + } + } + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" + }, + "address": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/address/-/address-1.2.1.tgz", + "integrity": "sha512-B+6bi5D34+fDYENiH5qOlA0cV2rAGKuWZ9LeyUUehbXy8e0VS9e498yO0Jeeh+iM+6KbfudHTFjXw2MmJD4QRA==" + }, + "adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "requires": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + } + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==" + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "aria-query": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.2.tgz", + "integrity": "sha512-JWydkr9MirMg2jGJstDqDgzoHqaFbv7n1ghfXYdtEgXWgdq3jz7IU3SQvtj9k3mAszQBiTpQhFdlH+JIRuGTzg==", + "dev": true, + "requires": { + "deep-equal": "^2.0.5" + } + }, + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" + }, + "array-includes": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", + "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" + }, + "array.prototype.flat": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", + "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.flatmap": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz", + "integrity": "sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.reduce": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.4.tgz", + "integrity": "sha512-WnM+AjG/DvLRLo4DDl+r+SvCzYtD2Jd9oeBYMcEaI7t3fFrHY9M53/wdLcTvmZNQ70IU6Htj0emFkZ5TS+lrdw==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + } + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==" + }, + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" + }, + "autoprefixer": { + "version": "10.4.13", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", + "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==", + "requires": { + "browserslist": "^4.21.4", + "caniuse-lite": "^1.0.30001426", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + } + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true + }, + "axe-core": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.5.0.tgz", + "integrity": "sha512-4+rr8eQ7+XXS5nZrKcMO/AikHL0hVqy+lHWAnE3xdHl+aguag8SOQ6eEqLexwLNWgXIMfunGuD3ON1/6Kyet0A==" + }, + "axobject-query": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", + "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==" + }, + "babel-jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", + "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==", + "requires": { + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^27.5.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "babel-loader": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.5.tgz", + "integrity": "sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==", + "requires": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^2.0.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "dependencies": { + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz", + "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==", + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "requires": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + } + }, + "babel-plugin-named-asset-import": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz", + "integrity": "sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==" + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "requires": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.3", + "semver": "^6.1.1" + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", + "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.3", + "core-js-compat": "^3.25.1" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.3" + } + }, + "babel-plugin-transform-react-remove-prop-types": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", + "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==" + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", + "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==", + "requires": { + "babel-plugin-jest-hoist": "^27.5.1", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, + "babel-preset-react-app": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz", + "integrity": "sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg==", + "requires": { + "@babel/core": "^7.16.0", + "@babel/plugin-proposal-class-properties": "^7.16.0", + "@babel/plugin-proposal-decorators": "^7.16.4", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", + "@babel/plugin-proposal-numeric-separator": "^7.16.0", + "@babel/plugin-proposal-optional-chaining": "^7.16.0", + "@babel/plugin-proposal-private-methods": "^7.16.0", + "@babel/plugin-transform-flow-strip-types": "^7.16.0", + "@babel/plugin-transform-react-display-name": "^7.16.0", + "@babel/plugin-transform-runtime": "^7.16.4", + "@babel/preset-env": "^7.16.4", + "@babel/preset-react": "^7.16.0", + "@babel/preset-typescript": "^7.16.0", + "@babel/runtime": "^7.16.3", + "babel-plugin-macros": "^3.1.0", + "babel-plugin-transform-react-remove-prop-types": "^0.4.24" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==" + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==" + }, + "bfj": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.0.2.tgz", + "integrity": "sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw==", + "requires": { + "bluebird": "^3.5.5", + "check-types": "^11.1.1", + "hoopy": "^0.1.4", + "tryer": "^1.0.1" + } + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, + "body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "bonjour-service": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.0.14.tgz", + "integrity": "sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ==", + "requires": { + "array-flatten": "^2.1.2", + "dns-equal": "^1.0.0", + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "requires": { + "fill-range": "^7.1.1" + } + }, + "browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" + }, + "browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "requires": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==" + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==" + }, + "call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "requires": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" + }, + "camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" + }, + "caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "requires": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==" + }, + "case-sensitive-paths-webpack-plugin": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", + "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==" + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + } + } + }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==" + }, + "check-types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz", + "integrity": "sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ==" + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" + }, + "ci-info": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", + "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==" + }, + "cjs-module-lexer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", + "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==" + }, + "clean-css": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.1.tgz", + "integrity": "sha512-lCr8OHhiWCTw4v8POJovCoh4T7I9U11yVsPjMWWnnMmp9ZowCxyad1Pathle/9HjaDp+fdQKjO9fQydE6RHTZg==", + "requires": { + "source-map": "~0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==" + }, + "coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "requires": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + } + }, + "collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==" + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" + }, + "colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==" + }, + "common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==" + }, + "common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==" + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==" + }, + "connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==" + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "core-js": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz", + "integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw==" + }, + "core-js-compat": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.26.0.tgz", + "integrity": "sha512-piOX9Go+Z4f9ZiBFLnZ5VrOpBl0h7IGCkiFUN11QTe6LjAvOT3ifL/5TdoizMh99hcGy5SoLyWbapIY/PIb/3A==", + "requires": { + "browserslist": "^4.21.4" + } + }, + "core-js-pure": { + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.42.0.tgz", + "integrity": "sha512-007bM04u91fF4kMgwom2I5cQxAFIy8jVulgr9eozILl/SZE53QOqnW/+vviC+wQWLv+AunBG+8Q0TLoeSsSxRQ==" + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" + }, + "css-blank-pseudo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", + "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "css-declaration-sorter": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz", + "integrity": "sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==" + }, + "css-has-pseudo": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", + "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "css-loader": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz", + "integrity": "sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==", + "requires": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.7", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.5" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "css-minimizer-webpack-plugin": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz", + "integrity": "sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==", + "requires": { + "cssnano": "^5.0.6", + "jest-worker": "^27.0.2", + "postcss": "^8.3.5", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "css-prefers-color-scheme": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", + "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==" + }, + "css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + } + }, + "css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" + }, + "css-tree": { + "version": "1.0.0-alpha.37", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", + "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", + "requires": { + "mdn-data": "2.0.4", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" + }, + "css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "cssdb": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.0.2.tgz", + "integrity": "sha512-Vm4b6P/PifADu0a76H0DKRNVWq3Rq9xa/Nx6oEMUBJlwTUuZoZ3dkZxo8Gob3UEL53Cq+Ma1GBgISed6XEBs3w==" + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" + }, + "cssnano": { + "version": "5.1.14", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.14.tgz", + "integrity": "sha512-Oou7ihiTocbKqi0J1bB+TRJIQX5RMR3JghA8hcWSw9mjBLQ5Y3RWqEDoYG3sRNlAbCIXpqMoZGbq5KDR3vdzgw==", + "requires": { + "cssnano-preset-default": "^5.2.13", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + } + }, + "cssnano-preset-default": { + "version": "5.2.13", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.13.tgz", + "integrity": "sha512-PX7sQ4Pb+UtOWuz8A1d+Rbi+WimBIxJTRyBdgGp1J75VU0r/HFQeLnMYgHiCAp6AR4rqrc7Y4R+1Rjk3KJz6DQ==", + "requires": { + "css-declaration-sorter": "^6.3.1", + "cssnano-utils": "^3.1.0", + "postcss-calc": "^8.2.3", + "postcss-colormin": "^5.3.0", + "postcss-convert-values": "^5.1.3", + "postcss-discard-comments": "^5.1.2", + "postcss-discard-duplicates": "^5.1.0", + "postcss-discard-empty": "^5.1.1", + "postcss-discard-overridden": "^5.1.0", + "postcss-merge-longhand": "^5.1.7", + "postcss-merge-rules": "^5.1.3", + "postcss-minify-font-values": "^5.1.0", + "postcss-minify-gradients": "^5.1.1", + "postcss-minify-params": "^5.1.4", + "postcss-minify-selectors": "^5.2.1", + "postcss-normalize-charset": "^5.1.0", + "postcss-normalize-display-values": "^5.1.0", + "postcss-normalize-positions": "^5.1.1", + "postcss-normalize-repeat-style": "^5.1.1", + "postcss-normalize-string": "^5.1.0", + "postcss-normalize-timing-functions": "^5.1.0", + "postcss-normalize-unicode": "^5.1.1", + "postcss-normalize-url": "^5.1.0", + "postcss-normalize-whitespace": "^5.1.1", + "postcss-ordered-values": "^5.1.3", + "postcss-reduce-initial": "^5.1.1", + "postcss-reduce-transforms": "^5.1.0", + "postcss-svgo": "^5.1.0", + "postcss-unique-selectors": "^5.1.1" + } + }, + "cssnano-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==" + }, + "csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "requires": { + "css-tree": "^1.1.2" + }, + "dependencies": { + "css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "requires": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + } + }, + "mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==" + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + } + } + }, + "csstype": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" + }, + "damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" + }, + "data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "requires": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "decimal.js": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.2.tgz", + "integrity": "sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA==" + }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==" + }, + "deep-equal": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.5.tgz", + "integrity": "sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "es-get-iterator": "^1.1.1", + "get-intrinsic": "^1.0.1", + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.2", + "is-regex": "^1.1.1", + "isarray": "^2.0.5", + "object-is": "^1.1.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.3.0", + "side-channel": "^1.0.3", + "which-boxed-primitive": "^1.0.1", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.2" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" + }, + "default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "requires": { + "execa": "^5.0.0" + } + }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==" + }, + "define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "defined": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==" + }, + "detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" + }, + "detect-port-alt": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", + "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", + "requires": { + "address": "^1.0.1", + "debug": "^2.6.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "detective": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", + "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", + "requires": { + "acorn-node": "^1.8.2", + "defined": "^1.0.0", + "minimist": "^1.2.6" + } + }, + "dexie": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-3.2.2.tgz", + "integrity": "sha512-q5dC3HPmir2DERlX+toCBbHQXW5MsyrFqPFcovkH9N2S/UW/H3H5AWAB6iEOExeraAu+j+zRDG+zg/D7YhH0qg==" + }, + "didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + }, + "diff-sequences": { + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.2.0.tgz", + "integrity": "sha512-413SY5JpYeSBZxmenGEmCVQ8mCgtFJF0w9PROdaS6z987XC2Pd2GOKqOITLtMftmyFZqgtCOb/QA7/Z3ZXfzIw==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "requires": { + "path-type": "^4.0.0" + } + }, + "dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==" + }, + "dns-packet": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.4.0.tgz", + "integrity": "sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==", + "requires": { + "@leichtgewicht/ip-codec": "^2.0.1" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-accessibility-api": { + "version": "0.5.14", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz", + "integrity": "sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==", + "dev": true + }, + "dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "requires": { + "utila": "~0.4" + } + }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, + "domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "requires": { + "webidl-conversions": "^5.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==" + } + } + }, + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" + }, + "dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" + }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "requires": { + "jake": "^10.8.5" + } + }, + "electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==" + }, + "emittery": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", + "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==" + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" + }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, + "enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "requires": { + "stackframe": "^1.3.4" + } + }, + "es-abstract": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", + "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + } + }, + "es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" + }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-get-iterator": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.2.tgz", + "integrity": "sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.0", + "has-symbols": "^1.0.1", + "is-arguments": "^1.1.0", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + } + }, + "es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==" + }, + "es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "requires": { + "has": "^1.0.3" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==" + }, + "escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "escodegen": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", + "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "requires": { + "prelude-ls": "~1.1.2" + } + } + } + }, + "eslint": { + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.26.0.tgz", + "integrity": "sha512-kzJkpaw1Bfwheq4VXUezFriD1GxszX6dUekM7Z3aC2o4hju+tsR/XyTC3RcoSD7jmy9VkPU3+N6YjVU2e96Oyg==", + "requires": { + "@eslint/eslintrc": "^1.3.3", + "@humanwhocodes/config-array": "^0.11.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.15.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "requires": { + "type-fest": "^0.20.2" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" + } + } + }, + "eslint-config-react-app": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", + "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==", + "requires": { + "@babel/core": "^7.16.0", + "@babel/eslint-parser": "^7.16.3", + "@rushstack/eslint-patch": "^1.1.0", + "@typescript-eslint/eslint-plugin": "^5.5.0", + "@typescript-eslint/parser": "^5.5.0", + "babel-preset-react-app": "^10.0.1", + "confusing-browser-globals": "^1.0.11", + "eslint-plugin-flowtype": "^8.0.3", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jest": "^25.3.0", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.27.1", + "eslint-plugin-react-hooks": "^4.3.0", + "eslint-plugin-testing-library": "^5.0.1" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "requires": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-module-utils": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", + "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", + "requires": { + "debug": "^3.2.7" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-plugin-flowtype": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", + "integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==", + "requires": { + "lodash": "^4.17.21", + "string-natural-compare": "^3.0.1" + } + }, + "eslint-plugin-import": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", + "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "requires": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.3", + "has": "^1.0.3", + "is-core-module": "^2.8.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.5", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "requires": { + "esutils": "^2.0.2" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "eslint-plugin-jest": { + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz", + "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==", + "requires": { + "@typescript-eslint/experimental-utils": "^5.0.0" + } + }, + "eslint-plugin-jsx-a11y": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz", + "integrity": "sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q==", + "requires": { + "@babel/runtime": "^7.18.9", + "aria-query": "^4.2.2", + "array-includes": "^3.1.5", + "ast-types-flow": "^0.0.7", + "axe-core": "^4.4.3", + "axobject-query": "^2.2.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "has": "^1.0.3", + "jsx-ast-utils": "^3.3.2", + "language-tags": "^1.0.5", + "minimatch": "^3.1.2", + "semver": "^6.3.0" + }, + "dependencies": { + "aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "requires": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + } + } + } + }, + "eslint-plugin-react": { + "version": "7.31.10", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.10.tgz", + "integrity": "sha512-e4N/nc6AAlg4UKW/mXeYWd3R++qUano5/o+t+wnWxIf+bLsOaH3a4q74kX3nDjYym3VBN4HyO9nEn1GcAqgQOA==", + "requires": { + "array-includes": "^3.1.5", + "array.prototype.flatmap": "^1.3.0", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.5", + "object.fromentries": "^2.0.5", + "object.hasown": "^1.1.1", + "object.values": "^1.1.5", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.3", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.7" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "requires": { + "esutils": "^2.0.2" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + }, + "resolve": { + "version": "2.0.0-next.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", + "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + } + } + }, + "eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==" + }, + "eslint-plugin-testing-library": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.9.1.tgz", + "integrity": "sha512-6BQp3tmb79jLLasPHJmy8DnxREe+2Pgf7L+7o09TSWPfdqqtQfRZmZNetr5mOs3yqZk/MRNxpN3RUpJe0wB4LQ==", + "requires": { + "@typescript-eslint/utils": "^5.13.0" + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==" + } + } + }, + "eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==" + }, + "eslint-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz", + "integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==", + "requires": { + "@types/eslint": "^7.29.0 || ^8.4.1", + "jest-worker": "^28.0.2", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-worker": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", + "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "espree": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", + "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", + "requires": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + }, + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==" + }, + "expect": { + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.2.2.tgz", + "integrity": "sha512-hE09QerxZ5wXiOhqkXy5d2G9ar+EqOyifnCXCpMNu+vZ6DG9TJ6CO2c2kPDSLqERTTWrO7OZj8EkYHQqSd78Yw==", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.2.2", + "jest-get-type": "^29.2.0", + "jest-matcher-utils": "^29.2.2", + "jest-message-util": "^29.2.1", + "jest-util": "^29.2.1" + } + }, + "express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==" + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "requires": { + "reusify": "^1.0.4" + } + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "requires": { + "bser": "2.1.1" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "requires": { + "flat-cache": "^3.0.4" + } + }, + "file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + } + }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "filesize": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", + "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==" + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "final-form": { + "version": "4.20.7", + "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.7.tgz", + "integrity": "sha512-ii3X9wNfyBYFnDPunYN5jh1/HAvtOZ9aJI/TVk0MB86hZuOeYkb+W5L3icgwW9WWNztZR6MDU3En6eoZTUoFPg==", + "requires": { + "@babel/runtime": "^7.10.0" + } + }, + "final-form-set-field-touched": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/final-form-set-field-touched/-/final-form-set-field-touched-1.0.1.tgz", + "integrity": "sha512-yvE5AAs9U3OgJQ9YF8NhSF0I0mJEECvOpkaXNqovloxji5Q6gOZ0DCIAyLAKHluGSpsXKUGORyBm8Hq0beZIqQ==" + }, + "finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" + }, + "follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==" + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "requires": { + "is-callable": "^1.1.3" + } + }, + "fork-ts-checker-webpack-plugin": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.2.tgz", + "integrity": "sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA==", + "requires": { + "@babel/code-frame": "^7.8.3", + "@types/json-schema": "^7.0.5", + "chalk": "^4.1.0", + "chokidar": "^3.4.2", + "cosmiconfig": "^6.0.0", + "deepmerge": "^4.2.2", + "fs-extra": "^9.0.0", + "glob": "^7.1.6", + "memfs": "^3.1.2", + "minimatch": "^3.0.4", + "schema-utils": "2.7.0", + "semver": "^7.3.2", + "tapable": "^1.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + } + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "requires": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" + } + } + }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==" + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==" + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "requires": { + "global-prefix": "^3.0.0" + } + }, + "global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "requires": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "dependencies": { + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" + }, + "gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "requires": { + "duplexer": "^0.1.2" + } + }, + "handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==" + }, + "harmony-reflect": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", + "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==" + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, + "hoopy": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", + "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==" + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "requires": { + "whatwg-encoding": "^1.0.5" + } + }, + "html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==" + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + }, + "html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "requires": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + } + }, + "html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "requires": { + "void-elements": "3.1.0" + } + }, + "html-webpack-plugin": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", + "integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==", + "requires": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + } + }, + "htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==" + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, + "http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "requires": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" + }, + "husky": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz", + "integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==", + "dev": true + }, + "i18next": { + "version": "22.0.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.0.4.tgz", + "integrity": "sha512-TOp7BTMKDbUkOHMzDlVsCYWpyaFkKakrrO3HNXfSz4EeJaWwnBScRmgQSTaWHScXVHBUFXTvShrCW8uryBYFcg==", + "requires": { + "@babel/runtime": "^7.17.2" + } + }, + "i18next-browser-languagedetector": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.0.tgz", + "integrity": "sha512-RrH7z5/DbhzhgCLDFIKXBTZlb2aXi38ZHa5e5oZaPt9zGLWmgAX49mzkQL/E7R6Y9fTE8QbZFuyMV0ronu4H/Q==", + "requires": { + "@babel/runtime": "^7.19.4" + } + }, + "i18next-icu": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/i18next-icu/-/i18next-icu-2.0.3.tgz", + "integrity": "sha512-sZ0VCWDnHysUYQL8j/0rVOxv6rLR+SBoaqQQ2UVNfLyJCuf/bAjYPkoUQgyuDkWFo1xZjeCf4G6GBNr7gD61bQ==" + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==" + }, + "idb": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.0.tgz", + "integrity": "sha512-Wsk07aAxDsntgYJY4h0knZJuTxM73eQ4reRAO+Z1liOh8eMCJ/MoDS8fCui1vGT9mnjtl1sOu3I2i/W1swPYZg==" + }, + "identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", + "requires": { + "harmony-reflect": "^1.4.6" + } + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" + }, + "immer": { + "version": "9.0.16", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.16.tgz", + "integrity": "sha512-qenGE7CstVm1NrHQbMh8YaSzTZTFNP3zPqr3YU0S0UY441j4bJTg4A2Hh5KAhwgaiU6ZZ1Ar6y/2f4TblnMReQ==" + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "intl-messageformat": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.2.1.tgz", + "integrity": "sha512-1lrJG2qKzcC1TVzYu1VuB1yiY68LU5rwpbHa2THCzA67Vutkz7+1lv5U20K3Lz5RAiH78zxNztMEtchokMWv8A==", + "requires": { + "@formatjs/ecma402-abstract": "1.13.0", + "@formatjs/fast-memoize": "1.2.6", + "@formatjs/icu-messageformat-parser": "2.1.10", + "tslib": "2.4.0" + } + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, + "ipaddr.js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", + "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==" + }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" + }, + "is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==" + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==" + }, + "is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==" + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, + "is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==" + }, + "is-root": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", + "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==" + }, + "is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.9.tgz", + "integrity": "sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-abstract": "^1.20.0", + "for-each": "^0.3.3", + "has-tostringtag": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==" + }, + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jake": { + "version": "10.8.5", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", + "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", + "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", + "requires": { + "@jest/core": "^27.5.1", + "import-local": "^3.0.2", + "jest-cli": "^27.5.1" + } + }, + "jest-changed-files": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz", + "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==", + "requires": { + "@jest/types": "^27.5.1", + "execa": "^5.0.0", + "throat": "^6.0.1" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-circus": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz", + "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==", + "requires": { + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "expect": "^27.5.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3", + "throat": "^6.0.1" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==" + }, + "expect": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", + "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", + "requires": { + "@jest/types": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + } + }, + "jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==" + }, + "jest-matcher-utils": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + } + }, + "jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-cli": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", + "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", + "requires": { + "@jest/core": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "prompts": "^2.0.1", + "yargs": "^16.2.0" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-config": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", + "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==", + "requires": { + "@babel/core": "^7.8.0", + "@jest/test-sequencer": "^27.5.1", + "@jest/types": "^27.5.1", + "babel-jest": "^27.5.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.9", + "jest-circus": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-jasmine2": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==" + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-diff": { + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.2.1.tgz", + "integrity": "sha512-gfh/SMNlQmP3MOUgdzxPOd4XETDJifADpT937fN1iUGz+9DgOu2eUPHH25JDkLVcLwwqxv3GzVyK4VBUr9fjfA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.2.0", + "jest-get-type": "^29.2.0", + "pretty-format": "^29.2.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "pretty-format": { + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.2.1.tgz", + "integrity": "sha512-Y41Sa4aLCtKAXvwuIpTvcFBkyeYp2gdFWzXGA+ZNES3VwURIB165XO/z7CjETwzCCS53MjW/rLMyyqEnTtaOfA==", + "dev": true, + "requires": { + "@jest/schemas": "^29.0.0", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-docblock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz", + "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==", + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", + "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", + "requires": { + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "jest-get-type": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==" + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-environment-jsdom": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", + "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", + "requires": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1", + "jsdom": "^16.6.0" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-environment-node": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", + "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==", + "requires": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-get-type": { + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.2.0.tgz", + "integrity": "sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA==", + "dev": true + }, + "jest-haste-map": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", + "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", + "requires": { + "@jest/types": "^27.5.1", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^27.5.1", + "jest-serializer": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "micromatch": "^4.0.4", + "walker": "^1.0.7" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-jasmine2": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz", + "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==", + "requires": { + "@jest/environment": "^27.5.1", + "@jest/source-map": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "expect": "^27.5.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1", + "throat": "^6.0.1" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==" + }, + "expect": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", + "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", + "requires": { + "@jest/types": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + } + }, + "jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==" + }, + "jest-matcher-utils": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + } + }, + "jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-leak-detector": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", + "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==", + "requires": { + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "dependencies": { + "jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==" + } + } + }, + "jest-matcher-utils": { + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.2.2.tgz", + "integrity": "sha512-4DkJ1sDPT+UX2MR7Y3od6KtvRi9Im1ZGLGgdLFLm4lPexbTaCgJW5NN3IOXlQHF7NSHY/VHhflQ+WoKtD/vyCw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.2.1", + "jest-get-type": "^29.2.0", + "pretty-format": "^29.2.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "pretty-format": { + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.2.1.tgz", + "integrity": "sha512-Y41Sa4aLCtKAXvwuIpTvcFBkyeYp2gdFWzXGA+ZNES3VwURIB165XO/z7CjETwzCCS53MjW/rLMyyqEnTtaOfA==", + "dev": true, + "requires": { + "@jest/schemas": "^29.0.0", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-message-util": { + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.2.1.tgz", + "integrity": "sha512-Dx5nEjw9V8C1/Yj10S/8ivA8F439VS8vTq1L7hEgwHFn9ovSKNpYW/kwNh7UglaEgXO42XxzKJB+2x0nSglFVw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.2.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.2.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "pretty-format": { + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.2.1.tgz", + "integrity": "sha512-Y41Sa4aLCtKAXvwuIpTvcFBkyeYp2gdFWzXGA+ZNES3VwURIB165XO/z7CjETwzCCS53MjW/rLMyyqEnTtaOfA==", + "dev": true, + "requires": { + "@jest/schemas": "^29.0.0", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-mock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", + "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-pnp-resolver": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", + "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==" + }, + "jest-regex-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", + "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==" + }, + "jest-resolve": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz", + "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==", + "requires": { + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "resolve": "^1.20.0", + "resolve.exports": "^1.1.0", + "slash": "^3.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-resolve-dependencies": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz", + "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==", + "requires": { + "@jest/types": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-snapshot": "^27.5.1" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-runner": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz", + "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==", + "requires": { + "@jest/console": "^27.5.1", + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-leak-detector": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "source-map-support": "^0.5.6", + "throat": "^6.0.1" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-runtime": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", + "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", + "requires": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/globals": "^27.5.1", + "@jest/source-map": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "execa": "^5.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-serializer": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", + "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", + "requires": { + "@types/node": "*", + "graceful-fs": "^4.2.9" + } + }, + "jest-snapshot": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", + "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", + "requires": { + "@babel/core": "^7.7.2", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.0.0", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__traverse": "^7.0.4", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^27.5.1", + "semver": "^7.3.2" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==" + }, + "expect": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", + "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", + "requires": { + "@jest/types": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + } + }, + "jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==" + }, + "jest-matcher-utils": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + } + }, + "jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-util": { + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.2.1.tgz", + "integrity": "sha512-P5VWDj25r7kj7kl4pN2rG/RN2c1TLfYYYZYULnS/35nFDjBai+hBeo3MDrYZS7p6IoY3YHZnt2vq4L6mKnLk0g==", + "dev": true, + "requires": { + "@jest/types": "^29.2.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-validate": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz", + "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==", + "requires": { + "@jest/types": "^27.5.1", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^27.5.1", + "leven": "^3.1.0", + "pretty-format": "^27.5.1" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-watch-typeahead": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-1.1.0.tgz", + "integrity": "sha512-Va5nLSJTN7YFtC2jd+7wsoe1pNe5K4ShLux/E5iHEwlB9AxaxmggY7to9KUqKojhaJw3aXqt5WAb4jGPOolpEw==", + "requires": { + "ansi-escapes": "^4.3.1", + "chalk": "^4.0.0", + "jest-regex-util": "^28.0.0", + "jest-watcher": "^28.0.0", + "slash": "^4.0.0", + "string-length": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "dependencies": { + "@jest/console": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz", + "integrity": "sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==", + "requires": { + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^28.1.3", + "jest-util": "^28.1.3", + "slash": "^3.0.0" + }, + "dependencies": { + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" + } + } + }, + "@jest/schemas": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", + "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", + "requires": { + "@sinclair/typebox": "^0.24.1" + } + }, + "@jest/test-result": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz", + "integrity": "sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==", + "requires": { + "@jest/console": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/types": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz", + "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==", + "requires": { + "@jest/schemas": "^28.1.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "emittery": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", + "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-message-util": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", + "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^28.1.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^28.1.3", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" + } + } + }, + "jest-regex-util": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", + "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==" + }, + "jest-util": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", + "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", + "requires": { + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jest-watcher": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz", + "integrity": "sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==", + "requires": { + "@jest/test-result": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.10.2", + "jest-util": "^28.1.3", + "string-length": "^4.0.1" + }, + "dependencies": { + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "pretty-format": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", + "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", + "requires": { + "@jest/schemas": "^28.1.3", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + } + } + }, + "slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==" + }, + "string-length": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", + "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==", + "requires": { + "char-regex": "^2.0.0", + "strip-ansi": "^7.0.1" + }, + "dependencies": { + "char-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.1.tgz", + "integrity": "sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==" + } + } + }, + "strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "requires": { + "ansi-regex": "^6.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + } + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-watcher": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz", + "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==", + "requires": { + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "jest-util": "^27.5.1", + "string-length": "^4.0.1" + }, + "dependencies": { + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-sdsl": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", + "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsdom": { + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", + "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "requires": { + "abab": "^2.0.5", + "acorn": "^8.2.4", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^3.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.6", + "xml-name-validator": "^3.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==" + }, + "jsx-ast-utils": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", + "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==", + "requires": { + "array-includes": "^3.1.5", + "object.assign": "^4.1.3" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" + }, + "klona": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", + "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==" + }, + "language-subtag-registry": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==" + }, + "language-tags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", + "requires": { + "language-subtag-registry": "~0.3.2" + } + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==" + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lilconfig": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", + "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==" + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==" + }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" + }, + "long": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz", + "integrity": "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w==" + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "requires": { + "tslib": "^2.0.3" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "lz-string": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", + "integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==", + "dev": true + }, + "magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "requires": { + "sourcemap-codec": "^1.4.8" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + } + }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "requires": { + "tmpl": "1.0.5" + } + }, + "mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "memfs": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.9.tgz", + "integrity": "sha512-3rm8kbrzpUGRyPKSGuk387NZOwQ90O4rI9tsWQkzNW7BLSnKGp23RsEsKK8N8QVCrtJoAMqy3spxHC4os4G6PQ==", + "requires": { + "fs-monkey": "^1.0.3" + } + }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, + "merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, + "mini-css-extract-plugin": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.1.tgz", + "integrity": "sha512-wd+SD57/K6DiV7jIR34P+s3uckTRuQvx0tKPcvjFlrEylk6P4mQ2KSWk1hblj1Kxaqok7LogKOieygXqBczNlg==", + "requires": { + "schema-utils": "^4.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + } + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "requires": { + "minimist": "^1.2.6" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "requires": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + } + }, + "nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" + }, + "node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" + }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "requires": { + "path-key": "^3.0.0" + } + }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "requires": { + "boolbase": "^1.0.0" + } + }, + "nwsapi": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", + "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" + }, + "object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" + }, + "object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", + "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "object.fromentries": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", + "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.4.tgz", + "integrity": "sha512-sccv3L/pMModT6dJAYF3fzGMVcb38ysQ0tEE6ixv2yXJDtEIPph268OlAdJj5/qZMZDq2g/jqvwppt36uS/uQQ==", + "requires": { + "array.prototype.reduce": "^1.0.4", + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.1" + } + }, + "object.hasown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.1.tgz", + "integrity": "sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==", + "requires": { + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "requires": { + "p-limit": "^3.0.2" + } + }, + "p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "requires": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" + }, + "pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==" + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + } + } + }, + "pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "requires": { + "find-up": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" + } + } + }, + "postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "requires": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + } + }, + "postcss-attribute-case-insensitive": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz", + "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==", + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-browser-comments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz", + "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==" + }, + "postcss-calc": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", + "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "requires": { + "postcss-selector-parser": "^6.0.9", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-color-functional-notation": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz", + "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-color-hex-alpha": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz", + "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-color-rebeccapurple": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz", + "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-colormin": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.0.tgz", + "integrity": "sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg==", + "requires": { + "browserslist": "^4.16.6", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-convert-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", + "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", + "requires": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-custom-media": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz", + "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-custom-properties": { + "version": "12.1.10", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.10.tgz", + "integrity": "sha512-U3BHdgrYhCrwTVcByFHs9EOBoqcKq4Lf3kXwbTi4hhq0qWhl/pDWq2THbv/ICX/Fl9KqeHBb8OVrTf2OaYF07A==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-custom-selectors": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz", + "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==", + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-dir-pseudo-class": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz", + "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==", + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-discard-comments": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==" + }, + "postcss-discard-duplicates": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==" + }, + "postcss-discard-empty": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==" + }, + "postcss-discard-overridden": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==" + }, + "postcss-double-position-gradients": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz", + "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==", + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-env-function": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", + "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-flexbugs-fixes": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", + "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==" + }, + "postcss-focus-visible": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", + "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "postcss-focus-within": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", + "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==" + }, + "postcss-gap-properties": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", + "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==" + }, + "postcss-image-set-function": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz", + "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "requires": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + } + }, + "postcss-initial": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==" + }, + "postcss-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", + "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "requires": { + "camelcase-css": "^2.0.1" + } + }, + "postcss-lab-function": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz", + "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==", + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "requires": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + } + }, + "postcss-loader": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", + "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", + "requires": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.5" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "postcss-logical": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", + "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==" + }, + "postcss-media-minmax": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", + "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==" + }, + "postcss-merge-longhand": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", + "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", + "requires": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^5.1.1" + } + }, + "postcss-merge-rules": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.3.tgz", + "integrity": "sha512-LbLd7uFC00vpOuMvyZop8+vvhnfRGpp2S+IMQKeuOZZapPRY4SMq5ErjQeHbHsjCUgJkRNrlU+LmxsKIqPKQlA==", + "requires": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^3.1.0", + "postcss-selector-parser": "^6.0.5" + } + }, + "postcss-minify-font-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", + "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-minify-gradients": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", + "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", + "requires": { + "colord": "^2.9.1", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-minify-params": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", + "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", + "requires": { + "browserslist": "^4.21.4", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-minify-selectors": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", + "requires": { + "postcss-selector-parser": "^6.0.5" + } + }, + "postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==" + }, + "postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "requires": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "requires": { + "icss-utils": "^5.0.0" + } + }, + "postcss-nested": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", + "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-nesting": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz", + "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==", + "requires": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-normalize": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz", + "integrity": "sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA==", + "requires": { + "@csstools/normalize.css": "*", + "postcss-browser-comments": "^4", + "sanitize.css": "*" + } + }, + "postcss-normalize-charset": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==" + }, + "postcss-normalize-display-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", + "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-positions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", + "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-repeat-style": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", + "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-string": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", + "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-timing-functions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", + "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-unicode": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", + "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", + "requires": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", + "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", + "requires": { + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-whitespace": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", + "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-opacity-percentage": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.2.tgz", + "integrity": "sha512-lyUfF7miG+yewZ8EAk9XUBIlrHyUE6fijnesuz+Mj5zrIHIEw6KcIZSOk/elVMqzLvREmXB83Zi/5QpNRYd47w==" + }, + "postcss-ordered-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", + "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", + "requires": { + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-overflow-shorthand": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz", + "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==" + }, + "postcss-place": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz", + "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-preset-env": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.2.tgz", + "integrity": "sha512-rSMUEaOCnovKnwc5LvBDHUDzpGP+nrUeWZGWt9M72fBvckCi45JmnJigUr4QG4zZeOHmOCNCZnd2LKDvP++ZuQ==", + "requires": { + "@csstools/postcss-cascade-layers": "^1.1.0", + "@csstools/postcss-color-function": "^1.1.1", + "@csstools/postcss-font-format-keywords": "^1.0.1", + "@csstools/postcss-hwb-function": "^1.0.2", + "@csstools/postcss-ic-unit": "^1.0.1", + "@csstools/postcss-is-pseudo-class": "^2.0.7", + "@csstools/postcss-nested-calc": "^1.0.0", + "@csstools/postcss-normalize-display-values": "^1.0.1", + "@csstools/postcss-oklab-function": "^1.1.1", + "@csstools/postcss-progressive-custom-properties": "^1.3.0", + "@csstools/postcss-stepped-value-functions": "^1.0.1", + "@csstools/postcss-text-decoration-shorthand": "^1.0.0", + "@csstools/postcss-trigonometric-functions": "^1.0.2", + "@csstools/postcss-unset-value": "^1.0.2", + "autoprefixer": "^10.4.11", + "browserslist": "^4.21.3", + "css-blank-pseudo": "^3.0.3", + "css-has-pseudo": "^3.0.4", + "css-prefers-color-scheme": "^6.0.3", + "cssdb": "^7.0.1", + "postcss-attribute-case-insensitive": "^5.0.2", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^4.2.4", + "postcss-color-hex-alpha": "^8.0.4", + "postcss-color-rebeccapurple": "^7.1.1", + "postcss-custom-media": "^8.0.2", + "postcss-custom-properties": "^12.1.9", + "postcss-custom-selectors": "^6.0.3", + "postcss-dir-pseudo-class": "^6.0.5", + "postcss-double-position-gradients": "^3.1.2", + "postcss-env-function": "^4.0.6", + "postcss-focus-visible": "^6.0.4", + "postcss-focus-within": "^5.0.4", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^3.0.5", + "postcss-image-set-function": "^4.0.7", + "postcss-initial": "^4.0.1", + "postcss-lab-function": "^4.2.1", + "postcss-logical": "^5.0.4", + "postcss-media-minmax": "^5.0.0", + "postcss-nesting": "^10.2.0", + "postcss-opacity-percentage": "^1.1.2", + "postcss-overflow-shorthand": "^3.0.4", + "postcss-page-break": "^3.0.4", + "postcss-place": "^7.0.5", + "postcss-pseudo-class-any-link": "^7.1.6", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^6.0.1", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-pseudo-class-any-link": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz", + "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==", + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-reduce-initial": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.1.tgz", + "integrity": "sha512-//jeDqWcHPuXGZLoolFrUXBDyuEGbr9S2rMo19bkTIjBQ4PqkaO+oI8wua5BOUxpfi97i3PCoInsiFIEBfkm9w==", + "requires": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0" + } + }, + "postcss-reduce-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", + "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==" + }, + "postcss-selector-not": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz", + "integrity": "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==", + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-svgo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", + "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", + "requires": { + "postcss-value-parser": "^4.2.0", + "svgo": "^2.7.0" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + }, + "css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "requires": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + } + }, + "mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "requires": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + } + } + } + }, + "postcss-unique-selectors": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", + "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", + "requires": { + "postcss-selector-parser": "^6.0.5" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" + }, + "pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==" + }, + "pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "requires": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "requires": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + } + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "requires": { + "asap": "~2.0.6" + } + }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, + "protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "dependencies": { + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + } + } + }, + "psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==" + }, + "qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "requires": { + "side-channel": "^1.0.6" + } + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" + }, + "raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "requires": { + "performance-now": "^2.1.0" + } + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-app-polyfill": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz", + "integrity": "sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w==", + "requires": { + "core-js": "^3.19.2", + "object-assign": "^4.1.1", + "promise": "^8.1.0", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.9", + "whatwg-fetch": "^3.6.2" + } + }, + "react-dev-utils": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", + "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", + "requires": { + "@babel/code-frame": "^7.16.0", + "address": "^1.1.2", + "browserslist": "^4.18.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "detect-port-alt": "^1.1.6", + "escape-string-regexp": "^4.0.0", + "filesize": "^8.0.6", + "find-up": "^5.0.0", + "fork-ts-checker-webpack-plugin": "^6.5.0", + "global-modules": "^2.0.0", + "globby": "^11.0.4", + "gzip-size": "^6.0.0", + "immer": "^9.0.7", + "is-root": "^2.1.0", + "loader-utils": "^3.2.0", + "open": "^8.4.0", + "pkg-up": "^3.1.0", + "prompts": "^2.4.2", + "react-error-overlay": "^6.0.11", + "recursive-readdir": "^2.2.2", + "shell-quote": "^1.7.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + } + }, + "react-error-overlay": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", + "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" + }, + "react-final-form": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.9.tgz", + "integrity": "sha512-x3XYvozolECp3nIjly+4QqxdjSSWfcnpGEL5K8OBT6xmGrq5kBqbA6+/tOqoom9NwqIPPbxPNsOViFlbKgowbA==", + "requires": { + "@babel/runtime": "^7.15.4" + } + }, + "react-final-form-listeners": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/react-final-form-listeners/-/react-final-form-listeners-1.0.3.tgz", + "integrity": "sha512-OrdCNxSS4JQS/EXD+R530kZKFqaPfa+WcXPgVro/h4BpaBDF/Ja+BtHyCzDezCIb5rWaGGdOJIj+tN2YdtvrXg==", + "requires": { + "@babel/runtime": "^7.12.5" + } + }, + "react-i18next": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.0.0.tgz", + "integrity": "sha512-/O7N6aIEAl1FaWZBNvhdIo9itvF/MO/nRKr9pYqRc9LhuC1u21SlfwpiYQqvaeNSEW3g3qUXLREOWMt+gxrWbg==", + "requires": { + "@babel/runtime": "^7.14.5", + "html-parse-stringify": "^3.0.1" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "react-redux": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.4.tgz", + "integrity": "sha512-yMfQ7mX6bWuicz2fids6cR1YT59VTuT8MKyyE310wJQlINKENCeT1UcPdEiX6znI5tF8zXyJ/VYvDgeGuaaNwQ==", + "requires": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + } + }, + "react-refresh": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", + "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==" + }, + "react-router": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.4.2.tgz", + "integrity": "sha512-Rb0BAX9KHhVzT1OKhMvCDMw776aTYM0DtkxqUBP8dNBom3mPXlfNs76JNGK8wKJ1IZEY1+WGj+cvZxHVk/GiKw==", + "requires": { + "@remix-run/router": "1.0.2" + } + }, + "react-router-dom": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.4.2.tgz", + "integrity": "sha512-yM1kjoTkpfjgczPrcyWrp+OuQMyB1WleICiiGfstnQYo/S8hPEEnVjr/RdmlH6yKK4Tnj1UGXFSa7uwAtmDoLQ==", + "requires": { + "@remix-run/router": "1.0.2", + "react-router": "6.4.2" + } + }, + "react-scripts": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", + "integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==", + "requires": { + "@babel/core": "^7.16.0", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", + "@svgr/webpack": "^5.5.0", + "babel-jest": "^27.4.2", + "babel-loader": "^8.2.3", + "babel-plugin-named-asset-import": "^0.3.8", + "babel-preset-react-app": "^10.0.1", + "bfj": "^7.0.2", + "browserslist": "^4.18.1", + "camelcase": "^6.2.1", + "case-sensitive-paths-webpack-plugin": "^2.4.0", + "css-loader": "^6.5.1", + "css-minimizer-webpack-plugin": "^3.2.0", + "dotenv": "^10.0.0", + "dotenv-expand": "^5.1.0", + "eslint": "^8.3.0", + "eslint-config-react-app": "^7.0.1", + "eslint-webpack-plugin": "^3.1.1", + "file-loader": "^6.2.0", + "fs-extra": "^10.0.0", + "fsevents": "^2.3.2", + "html-webpack-plugin": "^5.5.0", + "identity-obj-proxy": "^3.0.0", + "jest": "^27.4.3", + "jest-resolve": "^27.4.2", + "jest-watch-typeahead": "^1.0.0", + "mini-css-extract-plugin": "^2.4.5", + "postcss": "^8.4.4", + "postcss-flexbugs-fixes": "^5.0.2", + "postcss-loader": "^6.2.1", + "postcss-normalize": "^10.0.1", + "postcss-preset-env": "^7.0.1", + "prompts": "^2.4.2", + "react-app-polyfill": "^3.0.0", + "react-dev-utils": "^12.0.1", + "react-refresh": "^0.11.0", + "resolve": "^1.20.0", + "resolve-url-loader": "^4.0.0", + "sass-loader": "^12.3.0", + "semver": "^7.3.5", + "source-map-loader": "^3.0.0", + "style-loader": "^3.3.1", + "tailwindcss": "^3.0.2", + "terser-webpack-plugin": "^5.2.5", + "webpack": "^5.64.4", + "webpack-dev-server": "^4.6.0", + "webpack-manifest-plugin": "^4.0.2", + "workbox-webpack-plugin": "^6.4.1" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, + "react-virtualized-auto-sizer": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.7.tgz", + "integrity": "sha512-Mxi6lwOmjwIjC1X4gABXMJcKHsOo0xWl3E3ugOgufB8GJU+MqrtY35aBuvCYv/razQ1Vbp7h1gWJjGjoNN5pmA==" + }, + "react-window": { + "version": "1.8.7", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.7.tgz", + "integrity": "sha512-JHEZbPXBpKMmoNO1bNhoXOOLg/ujhL/BU4IqVU9r8eQPcy5KQnGHIHDRkJ0ns9IM5+Aq5LNwt3j8t3tIrePQzA==", + "requires": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + } + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "requires": { + "pify": "^2.3.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "requires": { + "picomatch": "^2.2.1" + } + }, + "recursive-readdir": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", + "requires": { + "minimatch": "^3.0.5" + } + }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, + "redux": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz", + "integrity": "sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, + "redux-form": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/redux-form/-/redux-form-8.3.8.tgz", + "integrity": "sha512-PzXhA0d+awIc4PkuhbDa6dCEiraMrGMyyDlYEVNX6qEyW/G2SqZXrjav5zrpXb0CCeqQSc9iqwbMtYQXbJbOAQ==", + "requires": { + "@babel/runtime": "^7.9.2", + "es6-error": "^4.1.1", + "hoist-non-react-statics": "^3.3.2", + "invariant": "^2.2.4", + "is-promise": "^2.1.0", + "lodash": "^4.17.15", + "prop-types": "^15.6.1", + "react-is": "^16.4.2" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, + "redux-thunk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", + "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==" + }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" + }, + "regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "requires": { + "regenerate": "^1.4.2" + } + }, + "regenerator-runtime": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz", + "integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==" + }, + "regenerator-transform": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.0.tgz", + "integrity": "sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==", + "requires": { + "@babel/runtime": "^7.8.4" + } + }, + "regex-parser": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", + "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==" + }, + "regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==" + }, + "regexpu-core": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.1.tgz", + "integrity": "sha512-HrnlNtpvqP1Xkb28tMhBUO2EbyUHdQlsnlAhzWcwHy8WJR53UWr7/MAvqrsQKMbV4qdpv03oTMG8iIhfsPFktQ==", + "requires": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsgen": "^0.7.1", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.0.0" + } + }, + "regjsgen": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", + "integrity": "sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==" + }, + "regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==" + } + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==" + }, + "renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "requires": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, + "resolve-url-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz", + "integrity": "sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==", + "requires": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^7.0.35", + "source-map": "0.6.1" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "resolve.exports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz", + "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==" + }, + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==" + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "rollup": { + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "requires": { + "fsevents": "~2.3.2" + } + }, + "rollup-plugin-terser": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", + "requires": { + "@babel/code-frame": "^7.10.4", + "jest-worker": "^26.2.1", + "serialize-javascript": "^4.0.0", + "terser": "^5.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "requires": { + "randombytes": "^2.1.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "rxjs": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", + "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", + "requires": { + "tslib": "^2.1.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sanitize-html": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.0.tgz", + "integrity": "sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==", + "requires": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + }, + "dependencies": { + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + } + } + }, + "sanitize.css": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", + "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==" + }, + "sass-loader": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", + "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", + "requires": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "requires": { + "xmlchars": "^2.2.0" + } + }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==" + }, + "selfsigned": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", + "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "requires": { + "node-forge": "^1" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + }, + "send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==" + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==" + } + } + }, + "serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "requires": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + } + }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "shell-quote": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz", + "integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==" + }, + "side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" + }, + "sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "requires": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==" + }, + "source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + }, + "source-map-loader": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.2.tgz", + "integrity": "sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg==", + "requires": { + "abab": "^2.0.5", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.1" + } + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + }, + "spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" + }, + "stack-utils": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", + "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" + } + } + }, + "stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "string-natural-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", + "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + } + } + }, + "string.prototype.matchall": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", + "integrity": "sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.1", + "side-channel": "^1.0.4" + } + }, + "string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "requires": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==" + }, + "strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==" + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" + }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + }, + "style-loader": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", + "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==" + }, + "stylehacks": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", + "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", + "requires": { + "browserslist": "^4.21.4", + "postcss-selector-parser": "^6.0.4" + } + }, + "stylis": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", + "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, + "svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" + }, + "svgo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", + "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", + "requires": { + "chalk": "^2.4.1", + "coa": "^2.0.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "^0.1.1", + "css-tree": "1.0.0-alpha.37", + "csso": "^4.0.2", + "js-yaml": "^3.13.1", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + }, + "dependencies": { + "css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-what": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", + "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==" + }, + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + }, + "dependencies": { + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + } + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "requires": { + "boolbase": "~1.0.0" + } + } + } + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, + "tailwindcss": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.1.tgz", + "integrity": "sha512-Uw+GVSxp5CM48krnjHObqoOwlCt5Qo6nw1jlCRwfGy68dSYb/LwS9ZFidYGRiM+w6rMawkZiu1mEMAsHYAfoLg==", + "requires": { + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "color-name": "^1.1.4", + "detective": "^5.2.1", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.12", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "lilconfig": "^2.0.6", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.17", + "postcss-import": "^14.1.0", + "postcss-js": "^4.0.0", + "postcss-load-config": "^3.1.4", + "postcss-nested": "6.0.0", + "postcss-selector-parser": "^6.0.10", + "postcss-value-parser": "^4.2.0", + "quick-lru": "^5.1.1", + "resolve": "^1.22.1" + }, + "dependencies": { + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "requires": { + "is-glob": "^4.0.3" + } + } + } + }, + "tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==" + }, + "temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==" + }, + "tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "requires": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "dependencies": { + "type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==" + } + } + }, + "terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "requires": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + } + }, + "terser": { + "version": "5.39.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz", + "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==", + "requires": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + } + } + }, + "terser-webpack-plugin": { + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "requires": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + } + } + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + }, + "throat": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", + "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==" + }, + "thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" + }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "dependencies": { + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==" + } + } + }, + "tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "requires": { + "punycode": "^2.1.1" + } + }, + "tryer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", + "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" + }, + "tsconfig-paths": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "requires": { + "minimist": "^1.2.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==" + } + } + }, + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "dev": true + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==" + }, + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "requires": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz", + "integrity": "sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==" + }, + "unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==" + }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "requires": { + "crypto-random-string": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==" + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==" + }, + "update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "requires": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + } + }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "util.promisify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", + "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.2", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.0" + } + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "v8-to-istanbul": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", + "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==" + } + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" + }, + "w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, + "w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "requires": { + "xml-name-validator": "^3.0.0" + } + }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "requires": { + "makeerror": "1.0.12" + } + }, + "watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==" + }, + "webpack": { + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "requires": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + } + } + } + }, + "webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "requires": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + } + } + }, + "webpack-dev-server": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz", + "integrity": "sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==", + "requires": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.1", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.4.2" + }, + "dependencies": { + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + }, + "ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==" + } + } + }, + "webpack-manifest-plugin": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz", + "integrity": "sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow==", + "requires": { + "tapable": "^2.0.0", + "webpack-sources": "^2.2.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "webpack-sources": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", + "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", + "requires": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + } + } + } + }, + "webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==" + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "requires": { + "iconv-lite": "0.4.24" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" + }, + "whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "requires": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "requires": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + } + }, + "which-typed-array": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.8.tgz", + "integrity": "sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-abstract": "^1.20.0", + "for-each": "^0.3.3", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.9" + } + }, + "word-wrap": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==" + }, + "workbox-background-sync": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz", + "integrity": "sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g==", + "requires": { + "idb": "^7.0.1", + "workbox-core": "6.5.4" + } + }, + "workbox-broadcast-update": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.5.4.tgz", + "integrity": "sha512-I/lBERoH1u3zyBosnpPEtcAVe5lwykx9Yg1k6f8/BGEPGaMMgZrwVrqL1uA9QZ1NGGFoyE6t9i7lBjOlDhFEEw==", + "requires": { + "workbox-core": "6.5.4" + } + }, + "workbox-build": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.5.4.tgz", + "integrity": "sha512-kgRevLXEYvUW9WS4XoziYqZ8Q9j/2ziJYEtTrjdz5/L/cTUa2XfyMP2i7c3p34lgqJ03+mTiz13SdFef2POwbA==", + "requires": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.11.1", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^5.2.0", + "@rollup/plugin-node-resolve": "^11.2.1", + "@rollup/plugin-replace": "^2.4.1", + "@surma/rollup-plugin-off-main-thread": "^2.2.3", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^7.1.6", + "lodash": "^4.17.20", + "pretty-bytes": "^5.3.0", + "rollup": "^2.43.1", + "rollup-plugin-terser": "^7.0.0", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "6.5.4", + "workbox-broadcast-update": "6.5.4", + "workbox-cacheable-response": "6.5.4", + "workbox-core": "6.5.4", + "workbox-expiration": "6.5.4", + "workbox-google-analytics": "6.5.4", + "workbox-navigation-preload": "6.5.4", + "workbox-precaching": "6.5.4", + "workbox-range-requests": "6.5.4", + "workbox-recipes": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4", + "workbox-streams": "6.5.4", + "workbox-sw": "6.5.4", + "workbox-window": "6.5.4" + }, + "dependencies": { + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "requires": { + "whatwg-url": "^7.0.0" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "requires": { + "punycode": "^2.1.0" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + } + }, + "workbox-cacheable-response": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.5.4.tgz", + "integrity": "sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug==", + "requires": { + "workbox-core": "6.5.4" + } + }, + "workbox-core": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.5.4.tgz", + "integrity": "sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q==" + }, + "workbox-expiration": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.5.4.tgz", + "integrity": "sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ==", + "requires": { + "idb": "^7.0.1", + "workbox-core": "6.5.4" + } + }, + "workbox-google-analytics": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.5.4.tgz", + "integrity": "sha512-8AU1WuaXsD49249Wq0B2zn4a/vvFfHkpcFfqAFHNHwln3jK9QUYmzdkKXGIZl9wyKNP+RRX30vcgcyWMcZ9VAg==", + "requires": { + "workbox-background-sync": "6.5.4", + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "workbox-navigation-preload": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.5.4.tgz", + "integrity": "sha512-IIwf80eO3cr8h6XSQJF+Hxj26rg2RPFVUmJLUlM0+A2GzB4HFbQyKkrgD5y2d84g2IbJzP4B4j5dPBRzamHrng==", + "requires": { + "workbox-core": "6.5.4" + } + }, + "workbox-precaching": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.5.4.tgz", + "integrity": "sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg==", + "requires": { + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "workbox-range-requests": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.5.4.tgz", + "integrity": "sha512-Je2qR1NXCFC8xVJ/Lux6saH6IrQGhMpDrPXWZWWS8n/RD+WZfKa6dSZwU+/QksfEadJEr/NfY+aP/CXFFK5JFg==", + "requires": { + "workbox-core": "6.5.4" + } + }, + "workbox-recipes": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.5.4.tgz", + "integrity": "sha512-QZNO8Ez708NNwzLNEXTG4QYSKQ1ochzEtRLGaq+mr2PyoEIC1xFW7MrWxrONUxBFOByksds9Z4//lKAX8tHyUA==", + "requires": { + "workbox-cacheable-response": "6.5.4", + "workbox-core": "6.5.4", + "workbox-expiration": "6.5.4", + "workbox-precaching": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "workbox-routing": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.5.4.tgz", + "integrity": "sha512-apQswLsbrrOsBUWtr9Lf80F+P1sHnQdYodRo32SjiByYi36IDyL2r7BH1lJtFX8fwNHDa1QOVY74WKLLS6o5Pg==", + "requires": { + "workbox-core": "6.5.4" + } + }, + "workbox-strategies": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.5.4.tgz", + "integrity": "sha512-DEtsxhx0LIYWkJBTQolRxG4EI0setTJkqR4m7r4YpBdxtWJH1Mbg01Cj8ZjNOO8etqfA3IZaOPHUxCs8cBsKLw==", + "requires": { + "workbox-core": "6.5.4" + } + }, + "workbox-streams": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.5.4.tgz", + "integrity": "sha512-FXKVh87d2RFXkliAIheBojBELIPnWbQdyDvsH3t74Cwhg0fDheL1T8BqSM86hZvC0ZESLsznSYWw+Va+KVbUzg==", + "requires": { + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4" + } + }, + "workbox-sw": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.5.4.tgz", + "integrity": "sha512-vo2RQo7DILVRoH5LjGqw3nphavEjK4Qk+FenXeUsknKn14eCNedHOXWbmnvP4ipKhlE35pvJ4yl4YYf6YsJArA==" + }, + "workbox-webpack-plugin": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.5.4.tgz", + "integrity": "sha512-LmWm/zoaahe0EGmMTrSLUi+BjyR3cdGEfU3fS6PN1zKFYbqAKuQ+Oy/27e4VSXsyIwAw8+QDfk1XHNGtZu9nQg==", + "requires": { + "fast-json-stable-stringify": "^2.1.0", + "pretty-bytes": "^5.4.1", + "upath": "^1.2.0", + "webpack-sources": "^1.4.3", + "workbox-build": "6.5.4" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + } + } + }, + "workbox-window": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.5.4.tgz", + "integrity": "sha512-HnLZJDwYBE+hpG25AQBO8RUWBJRaCsI9ksQJEp3aCOFCaG5kqaToAYXFRAHxzRluM2cQbGzdQF5rjKPWPA1fug==", + "requires": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "6.5.4" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==" + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + } + } +} diff --git a/webclient/package.json b/webclient/package.json new file mode 100644 index 000000000..a578fc9d9 --- /dev/null +++ b/webclient/package.json @@ -0,0 +1,94 @@ +{ + "name": "webclient", + "version": "1.0.0", + "private": true, + "scripts": { + "prebuild": "node prebuild.js", + "prestart": "node prebuild.js", + "build": "react-scripts build", + "start": "react-scripts start", + "test": "react-scripts test", + "test:watch": "react-scripts test", + "eject": "react-scripts eject", + "lint": "eslint \"./**/*.{ts,tsx}\"", + "lint:fix": "eslint \"./**/*.{ts,tsx}\" --fix", + "golden": "npm run lint && npm run test", + "prepare": "cd .. && husky install", + "translate": "node prebuild.js -i18nOnly" + }, + "dependencies": { + "@emotion/react": "^11.8.2", + "@emotion/styled": "^11.8.1", + "@mui/icons-material": "^5.5.1", + "@mui/material": "^5.5.1", + "crypto-js": "^4.2.0", + "dexie": "^3.2.2", + "final-form": "^4.20.6", + "final-form-set-field-touched": "^1.0.1", + "i18next": "^22.0.4", + "i18next-browser-languagedetector": "^7.0.0", + "i18next-icu": "^2.0.3", + "intl-messageformat": "^10.2.1", + "lodash": "^4.17.21", + "prop-types": "^15.8.1", + "protobufjs": "^7.2.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-final-form": "^6.5.8", + "react-final-form-listeners": "^1.0.3", + "react-i18next": "^12.0.0", + "react-redux": "^8.0.4", + "react-router-dom": "^6.2.2", + "react-scripts": "5.0.1", + "react-virtualized-auto-sizer": "^1.0.6", + "react-window": "^1.8.6", + "redux": "^4.1.2", + "redux-form": "^8.3.8", + "redux-thunk": "^2.4.1", + "rxjs": "^7.5.4", + "sanitize-html": "^2.7.3" + }, + "devDependencies": { + "@babel/core": "^7.17.5", + "@mui/types": "^7.1.3", + "@testing-library/jest-dom": "^5.16.2", + "@testing-library/react": "^13.4.0", + "@types/jest": "29.2.0", + "@types/jquery": "^3.5.14", + "@types/lodash": "^4.14.179", + "@types/node": "18.11.7", + "@types/prop-types": "^15.7.4", + "@types/react": "18.0.24", + "@types/react-dom": "18.0.8", + "@types/react-redux": "^7.1.23", + "@types/react-router-dom": "^5.3.3", + "@types/react-virtualized-auto-sizer": "^1.0.1", + "@types/react-window": "^1.8.5", + "@types/redux-form": "^8.3.3", + "@typescript-eslint/eslint-plugin": "^5.14.0", + "@typescript-eslint/parser": "^5.14.0", + "fs-extra": "^10.0.1", + "husky": "^8.0.1", + "typescript": "^4.6.2" + }, + "eslintConfig": { + "extends": "react-app" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "jest": { + "moduleNameMapper": { + "\\.(css|less)$": "identity-obj-proxy" + } + } +} diff --git a/webclient/prebuild.js b/webclient/prebuild.js new file mode 100644 index 000000000..3b420f32c --- /dev/null +++ b/webclient/prebuild.js @@ -0,0 +1,112 @@ +const fse = require('fs-extra'); +const path = require('path'); +const util = require('util'); +const exec = util.promisify(require('child_process').exec); + +const ROOT_DIR = './src'; +const PUBLIC_DIR = './public'; + +const protoFilesDir = `${PUBLIC_DIR}/pb`; +const i18nDefaultFile = `${ROOT_DIR}/i18n-default.json`; +const serverPropsFile = `${ROOT_DIR}/server-props.json`; +const masterProtoFile = `${ROOT_DIR}/proto-files.json`; + +const sharedFiles = [ + ['../libcockatrice_protocol/libcockatrice/protocol/pb', protoFilesDir], + ['../cockatrice/resources/countries', `${ROOT_DIR}/images/countries`], +]; + +const i18nFileRegex = /\.i18n\.json$/; + +const i18nOnly = process.argv.indexOf('-i18nOnly') > -1; + +(async () => { + if (i18nOnly) { + await createI18NDefault(); + return; + } + + // make sure these files finish copying before master file is created + await copySharedFiles(); + + await createMasterProtoFile(); + await createServerProps(); + await createI18NDefault(); +})(); + +async function copySharedFiles() { + try { + return await Promise.all(sharedFiles.map(([src, dest]) => fse.copy(src, dest, { overwrite: true }))); + } catch (e) { + console.error(e); + process.exitCode = 1; + } +} + +async function createMasterProtoFile() { + try { + fse.readdir(protoFilesDir, (err, files) => { + if (err) throw err; + + fse.outputFile(masterProtoFile, JSON.stringify(files.filter(file => /\.proto$/.test(file)))); + }); + } catch (e) { + console.error(e); + process.exitCode = 1; + } +} + +async function createServerProps() { + try { + fse.outputFile(serverPropsFile, JSON.stringify({ + REACT_APP_VERSION: await getCommitHash(), + })); + } catch (e) { + console.error(e); + process.exitCode = 1; + } +} + +async function createI18NDefault() { + try { + const files = getAllFiles(ROOT_DIR, i18nFileRegex); + const allJson = await Promise.all(files.map(file => fse.readJson(file))); + + const rollup = allJson.reduce((acc, json) => { + const newKeys = Object.keys(json); + + newKeys.forEach(key => { + if (acc[key]) { + throw new Error(`i18n key collision: ${key}\n${JSON.stringify(json)}`); + } + + acc[key] = json[key]; + }); + + return acc; + }, {}); + + fse.outputFile(i18nDefaultFile, JSON.stringify(rollup, null, 2)); + } catch (e) { + console.error(e); + process.exitCode = 1; + } +} + +async function getCommitHash() { + return (await exec('git rev-parse HEAD')).stdout.trim(); +} + +function getAllFiles(dirPath, regex = /./, allFiles = []) { + return fse.readdirSync(dirPath).reduce((files, file) => { + const filePath = dirPath + "/" + file; + + if (fse.statSync(filePath).isDirectory()) { + files.concat(getAllFiles(filePath, regex, files)); + } else if (regex.test(file)) { + files.push(path.join(__dirname, filePath)); + } + + return files; + }, allFiles); +} diff --git a/webclient/public/favicon.ico b/webclient/public/favicon.ico new file mode 100644 index 000000000..c2c86b859 Binary files /dev/null and b/webclient/public/favicon.ico differ diff --git a/webclient/public/index.html b/webclient/public/index.html new file mode 100644 index 000000000..c050e82f4 --- /dev/null +++ b/webclient/public/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + Webatrice + + + +
+ + + diff --git a/webclient/public/locales/de/translation.json b/webclient/public/locales/de/translation.json new file mode 100644 index 000000000..294f3fb9f --- /dev/null +++ b/webclient/public/locales/de/translation.json @@ -0,0 +1,382 @@ +{ + "Common": { + "language": "Deutsch (German)", + "disconnect": "Verbindung trennen", + "label": { + "confirmPassword": "Passwort bestätigen", + "confirmSure": "Sind Sie sicher?", + "country": "Land", + "delete": "Löschen", + "email": "E-Mail", + "hostName": "Name des Hosts", + "hostAddress": "Adresse des Hosts", + "password": "Passwort", + "passwordAgain": "Passwort Erneut", + "port": "Port", + "realName": "Richtiger Name", + "saveChanges": "Änderungen speichern", + "token": "Token", + "username": "Benutzername" + }, + "validation": { + "minChars": "Mindestens {count} {count, plural, one {Zeichen} other {Zeichen}} benötigt", + "passwordsMustMatch": "Die Passwörter stimmen nicht überein", + "required": "Benötigt" + }, + "countries": { + "AD": "Andorra", + "AE": "Vereinigte Arabische Emirate", + "AF": "Afghanistan", + "AG": "Antigua und Barbuda", + "AI": "Anguilla", + "AL": "Albanien", + "AM": "Armenien", + "AO": "Angola", + "AQ": "Antarktis", + "AR": "Argentinien", + "AS": "Amerikanisch-Samoa", + "AT": "Österreich", + "AU": "Australien", + "AW": "Aruba", + "AX": "Ålandinseln", + "AZ": "Aserbaidschan", + "BA": "Bosnien und Herzegowina", + "BB": "Barbados", + "BD": "Bangladesch", + "BE": "Belgien", + "BF": "Burkina Faso", + "BG": "Bulgarien", + "BH": "Bahrain", + "BI": "Burundi", + "BJ": "Benin", + "BL": "Saint-Barthélemy ", + "BM": "Bermuda", + "BN": "Brunei", + "BO": "Bolivien", + "BQ": "Bonaire, Sint Eustatius und Saba", + "BR": "Brasilien", + "BS": "Bahamas", + "BT": "Bhutan", + "BV": "Bouvetinsel", + "BW": "Botswana", + "BY": "Weißrussland", + "BZ": "Belize", + "CA": "Kanada", + "CC": "Kokosinseln", + "CD": "DR Kongo", + "CF": "Zentralafrikanische Republik", + "CG": "Republik Kongo", + "CH": "Schweiz", + "CI": "Elfenbeinküste", + "CK": "Cookinseln", + "CL": "Chile", + "CM": "Kamerun", + "CN": "China", + "CO": "Kolumbien", + "CR": "Costa Rica", + "CU": "Kuba", + "CV": "Kap Verde", + "CW": "Curaçao", + "CX": "Weihnachtsinsel", + "CY": "Zypern", + "CZ": "Tschechien", + "DE": "Deutschland", + "DJ": "Dschibuti", + "DK": "Dänemark", + "DM": "Dominica", + "DO": "Dominikanische Republik", + "DZ": "Algerien", + "EC": "Ecuador", + "EE": "Estland", + "EG": "Ägypten", + "EH": "Westsahara", + "ER": "Eritrea", + "ES": "Spanien", + "ET": "Äthiopien", + "FI": "Finnland", + "FJ": "Fiji", + "FK": "Falklandinseln", + "FM": "Mikronesien", + "FO": "Färöer", + "FR": "Frankreich", + "GA": "Gabun", + "GB": "Vereinigtes Königreich", + "GD": "Grenada", + "GE": "Georgien", + "GF": "Französisch-Guayana", + "GG": "Guernsey", + "GH": "Ghana", + "GI": "Gibraltar", + "GL": "Grönland", + "GM": "Gambia", + "GN": "Guinea", + "GP": "Guandeloupe", + "GQ": "Äquatorialguinea", + "GR": "Griechenland", + "GS": "Südgeorgien und die Südlichen Sandwichinseln", + "GT": "Guatemala", + "GU": "Guam", + "GW": "Guinea-Bissau", + "GY": "Guyana", + "HK": "Hongkong", + "HM": "Heard und McDonaldinseln", + "HN": "Honduras", + "HR": "Kroatien", + "HT": "Haiti", + "HU": "Ungarn", + "ID": "Indonesien", + "IE": "Irland", + "IL": "Israel", + "IM": "Insel Man", + "IN": "Indien", + "IO": " Britisches Territorium im Indischen Ozean", + "IQ": "Irak", + "IR": "Iran", + "IS": "Island", + "IT": "Italien", + "JE": "Jersey", + "JM": "Jamaika", + "JO": "Jordanien", + "JP": "Japan", + "KE": "Kenia", + "KG": "Kirgisistan", + "KH": "Kambodscha", + "KI": "Kiribati", + "KM": "Komoren", + "KN": "St. Kitts und Nevis", + "KP": "Nordkorea", + "KR": "Südkorea", + "KW": "Kuwait", + "KY": "Kaimaninseln", + "KZ": "Kasachstan", + "LA": "Laos", + "LB": "Libanon", + "LC": "St. Lucia", + "LI": "Liechtenstein", + "LK": "Sri Lanka", + "LR": "Liberia", + "LS": "Lesotho", + "LT": "Litauen", + "LU": "Luxemburg", + "LV": "Lettland", + "LY": "Libyen", + "MA": "Marokko", + "MC": "Monaco", + "MD": "Moldau", + "ME": "Montenegro", + "MF": "Saint-Martin (Französischer Teil)", + "MG": "Madagaskar", + "MH": "Marshallinseln", + "MK": "Nordmazedonien", + "ML": "Mali", + "MM": "Myanmar", + "MN": "Mongolei", + "MO": "Macau", + "MP": "Nördliche Marianen", + "MQ": "Martinique", + "MR": "Mauretanien", + "MS": "Montserrat", + "MT": "Malta", + "MU": "Mauritius", + "MV": "Malediven", + "MW": "Malawi", + "MX": "Mexiko", + "MY": "Malaysia", + "MZ": "Mosambik", + "NA": "Namibia", + "NC": "Neukaledonien", + "NE": "Niger", + "NF": "Norfolkinsel", + "NG": "Nigeria", + "NI": "Nicaragua", + "NL": "Niederlande", + "NO": "Norwegen", + "NP": "Nepal", + "NR": "Nauru", + "NU": "Niue", + "NZ": "Neuseeland", + "OM": "Oman", + "PA": "Panama", + "PE": "Peru", + "PF": "Französisch-Polynesien", + "PG": "Papua-Neuguinea", + "PH": "Philippinen", + "PK": "Pakistan", + "PL": "Polen", + "PM": "St. Pierre und Miquelon", + "PN": "Pitcairninseln", + "PR": "Puerto Rico", + "PS": "Palästina", + "PT": "Portugal", + "PW": "Palau", + "PY": "Paraguay", + "QA": "Katar", + "RE": "Réunion", + "RO": "Rumänien", + "RS": "Serbien", + "RU": "Russland", + "RW": "Ruanda", + "SA": "Saudi-Arabien", + "SB": "Salomonen", + "SC": "Seychellen", + "SD": "Sudan", + "SE": "Schweden", + "SG": "Singapur", + "SH": "St. Helena, Ascension und Tristan da Cunha", + "SI": "Slowenien", + "SJ": "Spitzbergen und Jan Mayen", + "SK": "Slowakei", + "SL": "Sierra Leone", + "SM": "San Marino", + "SN": "Senegal", + "SO": "Somalia", + "SR": "Suriname", + "SS": "Südsudan", + "ST": "São Tomé und Príncipe", + "SV": "El Salvador", + "SX": "Sint Maarten (Niederländischer Teil)", + "SY": "Syrien", + "SZ": "Eswatini", + "TC": "Turks- und Caicosinseln", + "TD": "Tschad", + "TF": "TAAF", + "TG": "Togo", + "TH": "Thailand", + "TJ": "Tadschikistan", + "TK": "Tokelau", + "TL": "Timor-Leste", + "TM": "Turkmenistan", + "TN": "Tunesien", + "TO": "Tonga", + "TR": "Türkei", + "TT": "Trinidad und Tobago", + "TV": "Tuvalu", + "TW": "Taiwan", + "TZ": "Tansania", + "UA": "Ukraine", + "UG": "Uganda", + "UM": "Kleinere abgelegene Inseln der Vereinigten Staaten", + "US": "Vereinigte Staaten von Amerika", + "UY": "Uruguay", + "UZ": "Usbekistan", + "VA": "Heiliger Stuhl", + "VC": "St. Vincent und die Grenadinen", + "VE": "Venezuela", + "VG": "Britische Jungferninseln", + "VI": "Amerikanische Jungferninseln", + "VN": "Vietnam", + "VU": "Vanuatu", + "WF": "Wallis und Futuna", + "WS": "Samoa", + "YE": "Jemen", + "YT": "Mayotte", + "XK": "Kosovo", + "ZA": "Südafrika", + "ZM": "Sambia", + "ZW": "Simbabwe", + "EU": "Europäische Union" + }, + "languages": { + "en-US": "Englisch - US", + "fr": "Französisch", + "nl": "Niederländisch", + "pt_BR": "Portugiesisch - Brasilien" + } + }, + "KnownHosts": { + "label": "Host", + "add": "Neuen Host hinzufügen", + "toast": "Host erfolgreich {mode, select, created {erstellt} deleted {gelöscht} other {bearbeitet}}." + }, + "InitializeContainer": { + "title": "WUSSTEN SIE SCHON", + "subtitle": "<1>Cockatrice wird von Freiwilligen geleitet <1>die Kartenspiele lieben!" + }, + "LoginContainer": { + "header": { + "title": "Login", + "subtitle": "Ein plattformübergreifender, virtueller Tabletop für Multiplayer-Kartenspiele." + }, + "footer": { + "registerPrompt": "Noch nicht registriert?", + "registerAction": "Einen Account erstellen", + "credit": "Cockatrice ist ein Open-Source-Projekt", + "version": "Version" + }, + "content": { + "subtitle1": "Spielen Sie Kartenspiele im Multiplayer online.", + "subtitle2": "Plattformübergreifender, virtueller Tabletop für Kartenspiele im Multiplayer. Für immer kostenlos." + }, + "toasts": { + "passwordResetSuccessToast": "Passwort erfolgreich zurückgesetzt", + "accountActivationSuccess": "Account erfolgreich aktiviert" + } + }, + "UnsupportedContainer": { + "title": "Browser wird nicht unterstützt", + "subtitle1": "Bitte updaten Sie Ihren Browser und/oder überprüfen Sie Ihre Berechtigungen.", + "subtitle2": "Hinweis: Beim privaten Surfen werden bei einigen Browsern bestimmte Berechtigungen oder Funktionen deaktiviert." + }, + "AccountActivationDialog": { + "title": "Aktivierung des Accounts", + "subtitle1": "Ihr Account wurde noch nicht aktiviert.", + "subtitle2": "Sie müssen das Aktivierungs-Token angeben, das Sie in der Aktivierungs-E-Mail erhalten haben." + }, + "KnownHostDialog": { + "title": "{mode, select, edit {Bearbeiten} other {Hinzufügen}} Bekannter Host", + "subtitle": "Wenn Sie einen neuen Host hinzufügen, können Sie sich mit verschiedenen Servern verbinden. Geben Sie die unten stehenden Details in Ihre Hostliste ein." + }, + "RegistrationDialog": { + "title": "Einen neuen Account erstellen" + }, + "RequestPasswordResetDialog": { + "title": "Passwort-Zurücksetzung anfordern" + }, + "ResetPasswordDialog": { + "title": "Passwort zurücksetzen" + }, + "AccountActivationForm": { + "error": { + "failed": "Account-Aktivierung fehlgeschlagen" + }, + "label": { + "activate": "Account aktivieren" + } + }, + "KnownHostForm": { + "help": "Brauchen Sie Hilfe beim Hinzufügen eines neuen Hosts?", + "label": { + "add": "Host hinzufügen", + "find": "Host finden" + } + }, + "LoginForm": { + "label": { + "autoConnect": "Automatisch verbinden", + "forgot": "Passwort vergessen", + "login": "Login", + "savePassword": "Passwort speichern", + "savedPassword": "Passwort gespeichert" + } + }, + "RegisterForm": { + "label": { + "register": "Registrieren" + }, + "toast": { + "registerSuccess": "Registrierung erfolgreich!" + } + }, + "RequestPasswordResetForm": { + "error": "Anfrage zum Zurücksetzen des Passworts fehlgeschlagen", + "mfaEnabled": "Der Server hat Multi-Faktor-Authentifizierung aktiviert", + "request": "Rücksetzungs-Token anfordern", + "skipRequest": "Ich habe bereits ein Rücksetzungs-Token" + }, + "ResetPasswordForm": { + "error": "Passwort-Zurücksetzung fehlgeschlagen", + "label": { + "reset": "Passwort zurücksetzen" + } + } +} \ No newline at end of file diff --git a/webclient/public/locales/en_US/translation.json b/webclient/public/locales/en_US/translation.json new file mode 100644 index 000000000..3c63e0abb --- /dev/null +++ b/webclient/public/locales/en_US/translation.json @@ -0,0 +1,382 @@ +{ + "Common": { + "language": "English", + "disconnect": "Disconnect", + "label": { + "confirmPassword": "Confirm Password", + "confirmSure": "Are you sure?", + "country": "Country", + "delete": "Delete", + "email": "Email", + "hostName": "Host Name", + "hostAddress": "Host Address", + "password": "Password", + "passwordAgain": "Password Again", + "port": "Port", + "realName": "Real Name", + "saveChanges": "Save Changes", + "token": "Token", + "username": "Username" + }, + "validation": { + "minChars": "Minimum of {count} {count, plural, one {character} other {characters}} required", + "passwordsMustMatch": "Passwords don't match", + "required": "Required" + }, + "countries": { + "AD": "Andorra", + "AE": "United Arab Emirates", + "AF": "Afghanistan", + "AG": "Antigua and Barbuda", + "AI": "Anguilla", + "AL": "Albania", + "AM": "Armenia", + "AO": "Angola", + "AQ": "Antarctica", + "AR": "Argentina", + "AS": "American Samoa", + "AT": "Austria", + "AU": "Australia", + "AW": "Aruba", + "AX": "Åland Islands", + "AZ": "Azerbaijan", + "BA": "Bosnia and Herzegovina", + "BB": "Barbados", + "BD": "Bangladesh", + "BE": "Belgium", + "BF": "Burkina Faso", + "BG": "Bulgaria", + "BH": "Bahrain", + "BI": "Burundi", + "BJ": "Benin", + "BL": "Saint Barthélemy", + "BM": "Bermuda", + "BN": "Brunei Darussalam", + "BO": "Bolivia", + "BQ": "Bonaire, Sint Eustatius and Saba", + "BR": "Brazil", + "BS": "Bahamas", + "BT": "Bhutan", + "BV": "Bouvet Island", + "BW": "Botswana", + "BY": "Belarus", + "BZ": "Belize", + "CA": "Canada", + "CC": "Cocos (Keeling) Islands", + "CD": "DR Congo", + "CF": "Central African Republic", + "CG": "Republic of the Congo", + "CH": "Switzerland", + "CI": "Ivory Coast", + "CK": "Cook Islands", + "CL": "Chile", + "CM": "Cameroon", + "CN": "China", + "CO": "Colombia", + "CR": "Costa Rica", + "CU": "Cuba", + "CV": "Cape Verde", + "CW": "Curaçao", + "CX": "Christmas Island", + "CY": "Cyprus", + "CZ": "Czechia", + "DE": "Germany", + "DJ": "Djibouti", + "DK": "Denmark", + "DM": "Dominica", + "DO": "Dominican Republic", + "DZ": "Algeria", + "EC": "Ecuador", + "EE": "Estonia", + "EG": "Egypt", + "EH": "Western Sahara", + "ER": "Eritrea", + "ES": "Spain", + "ET": "Ethiopia", + "FI": "Finland", + "FJ": "Fiji", + "FK": "Falkland Islands", + "FM": "Micronesia", + "FO": "Faroe Islands", + "FR": "France", + "GA": "Gabon", + "GB": "United Kingdom", + "GD": "Grenada", + "GE": "Georgia", + "GF": "French Guiana", + "GG": "Guernsey", + "GH": "Ghana", + "GI": "Gibraltar", + "GL": "Greenland", + "GM": "Gambia", + "GN": "Guinea", + "GP": "Guadeloupe", + "GQ": "Equatorial Guinea", + "GR": "Greece", + "GS": "South Georgia and the South Sandwich Islands", + "GT": "Guatemala", + "GU": "Guam", + "GW": "Guinea-Bissau", + "GY": "Guyana", + "HK": "Hong Kong", + "HM": "Heard Island and McDonald Islands", + "HN": "Honduras", + "HR": "Croatia", + "HT": "Haiti", + "HU": "Hungary", + "ID": "Indonesia", + "IE": "Ireland", + "IL": "Israel", + "IM": "Isle of Man", + "IN": "India", + "IO": "British Indian Ocean Territory", + "IQ": "Iraq", + "IR": "Iran", + "IS": "Iceland", + "IT": "Italy", + "JE": "Jersey", + "JM": "Jamaica", + "JO": "Jordan", + "JP": "Japan", + "KE": "Kenya", + "KG": "Kyrgyzstan", + "KH": "Cambodia", + "KI": "Kiribati", + "KM": "Comoros", + "KN": "Saint Kitts and Nevis", + "KP": "North Korea", + "KR": "South Korea", + "KW": "Kuwait", + "KY": "Cayman Islands", + "KZ": "Kazakhstan", + "LA": "Laos", + "LB": "Lebanon", + "LC": "Saint Lucia", + "LI": "Liechtenstein", + "LK": "Sri Lanka", + "LR": "Liberia", + "LS": "Lesotho", + "LT": "Lithuania", + "LU": "Luxembourg", + "LV": "Latvia", + "LY": "Libya", + "MA": "Morocco", + "MC": "Monaco", + "MD": "Moldova", + "ME": "Montenegro", + "MF": "Saint Martin (French part)", + "MG": "Madagascar", + "MH": "Marshall Islands", + "MK": "North Macedonia", + "ML": "Mali", + "MM": "Myanmar", + "MN": "Mongolia", + "MO": "Macao", + "MP": "Northern Mariana Islands", + "MQ": "Martinique", + "MR": "Mauritania", + "MS": "Montserrat", + "MT": "Malta", + "MU": "Mauritius", + "MV": "Maldives", + "MW": "Malawi", + "MX": "Mexico", + "MY": "Malaysia", + "MZ": "Mozambique", + "NA": "Namibia", + "NC": "New Caledonia", + "NE": "Niger", + "NF": "Norfolk Island", + "NG": "Nigeria", + "NI": "Nicaragua", + "NL": "Netherlands", + "NO": "Norway", + "NP": "Nepal", + "NR": "Nauru", + "NU": "Niue", + "NZ": "New Zealand", + "OM": "Oman", + "PA": "Panama", + "PE": "Peru", + "PF": "French Polynesia", + "PG": "Papua New Guinea", + "PH": "Philippines", + "PK": "Pakistan", + "PL": "Poland", + "PM": "Saint Pierre and Miquelon", + "PN": "Pitcairn", + "PR": "Puerto Rico", + "PS": "Palestine", + "PT": "Portugal", + "PW": "Palau", + "PY": "Paraguay", + "QA": "Qatar", + "RE": "Réunion", + "RO": "Romania", + "RS": "Serbia", + "RU": "Russia", + "RW": "Rwanda", + "SA": "Saudi Arabia", + "SB": "Solomon Islands", + "SC": "Seychelles", + "SD": "Sudan", + "SE": "Sweden", + "SG": "Singapore", + "SH": "Saint Helena, Ascension and Tristan da Cunha", + "SI": "Slovenia", + "SJ": "Svalbard and Jan Mayen", + "SK": "Slovakia", + "SL": "Sierra Leone", + "SM": "San Marino", + "SN": "Senegal", + "SO": "Somalia", + "SR": "Suriname", + "SS": "South Sudan", + "ST": "Sao Tome and Principe", + "SV": "El Salvador", + "SX": "Sint Maarten (Dutch part)", + "SY": "Syria", + "SZ": "Eswatini", + "TC": "Turks and Caicos Islands", + "TD": "Chad", + "TF": "TAAF", + "TG": "Togo", + "TH": "Thailand", + "TJ": "Tajikistan", + "TK": "Tokelau", + "TL": "Timor-Leste", + "TM": "Turkmenistan", + "TN": "Tunisia", + "TO": "Tonga", + "TR": "Turkey", + "TT": "Trinidad and Tobago", + "TV": "Tuvalu", + "TW": "Taiwan", + "TZ": "Tanzania", + "UA": "Ukraine", + "UG": "Uganda", + "UM": "United States Minor Outlying Islands", + "US": "United States", + "UY": "Uruguay", + "UZ": "Uzbekistan", + "VA": "Holy See", + "VC": "Saint Vincent and the Grenadines", + "VE": "Venezuela", + "VG": "British Virgin Islands", + "VI": "U.S. Virgin Islands", + "VN": "Viet Nam", + "VU": "Vanuatu", + "WF": "Wallis and Futuna", + "WS": "Samoa", + "YE": "Yemen", + "YT": "Mayotte", + "XK": "Kosovo", + "ZA": "South Africa", + "ZM": "Zambia", + "ZW": "Zimbabwe", + "EU": "European Union" + }, + "languages": { + "en-US": "English - US", + "fr": "French", + "nl": "Dutch", + "pt_BR": "Portuguese - Brazil" + } + }, + "KnownHosts": { + "label": "Host", + "add": "Add new host", + "toast": "Host successfully {mode, select, created {created} deleted {deleted} other {edited}}." + }, + "InitializeContainer": { + "title": "DID YOU KNOW", + "subtitle": "<1>Cockatrice is run by volunteers<1>that love card games!" + }, + "LoginContainer": { + "header": { + "title": "Login", + "subtitle": "A cross-platform virtual tabletop for multiplayer card games." + }, + "footer": { + "registerPrompt": "Not registered yet?", + "registerAction": "Create an account", + "credit": "Cockatrice is an open source project", + "version": "Version" + }, + "content": { + "subtitle1": "Play multiplayer card games online.", + "subtitle2": "Cross-platform virtual tabletop for multiplayer card games. Forever free." + }, + "toasts": { + "passwordResetSuccessToast": "Password Reset Successfully", + "accountActivationSuccess": "Account Activated Successfully" + } + }, + "UnsupportedContainer": { + "title": "Unsupported Browser", + "subtitle1": "Please update your browser and/or check your permissions.", + "subtitle2": "Note: Private browsing causes some browsers to disable certain permissions or features." + }, + "AccountActivationDialog": { + "title": "Account Activation", + "subtitle1": "Your account has not been activated yet.", + "subtitle2": "You need to provide the activation token received in the activation email." + }, + "KnownHostDialog": { + "title": "{mode, select, edit {Edit} other {Add}} Known Host", + "subtitle": "Adding a new host allows you to connect to different servers. Enter the details below to your host list." + }, + "RegistrationDialog": { + "title": "Create New Account" + }, + "RequestPasswordResetDialog": { + "title": "Request Password Reset" + }, + "ResetPasswordDialog": { + "title": "Reset Password" + }, + "AccountActivationForm": { + "error": { + "failed": "Account activation failed" + }, + "label": { + "activate": "Activate Account" + } + }, + "KnownHostForm": { + "help": "Need help adding a new host?", + "label": { + "add": "Add Host", + "find": "Find Host" + } + }, + "LoginForm": { + "label": { + "autoConnect": "Auto Connect", + "forgot": "Forgot Password", + "login": "Login", + "savePassword": "Save Password", + "savedPassword": "Saved Password" + } + }, + "RegisterForm": { + "label": { + "register": "Register" + }, + "toast": { + "registerSuccess": "Registration Successful!" + } + }, + "RequestPasswordResetForm": { + "error": "Request password reset failed", + "mfaEnabled": "Server has multi-factor authentication enabled", + "request": "Request Reset Token", + "skipRequest": "I already have a reset token" + }, + "ResetPasswordForm": { + "error": "Password reset failed", + "label": { + "reset": "Reset Password" + } + } +} \ No newline at end of file diff --git a/webclient/public/locales/es/translation.json b/webclient/public/locales/es/translation.json new file mode 100644 index 000000000..33a3db45c --- /dev/null +++ b/webclient/public/locales/es/translation.json @@ -0,0 +1,382 @@ +{ + "Common": { + "language": "Español (Spanish)", + "disconnect": "Desconectar", + "label": { + "confirmPassword": "Confirmar contraseña", + "confirmSure": "¿Estás seguro?", + "country": "País", + "delete": "Borrar", + "email": "e-mail", + "hostName": "Nombre del servidor", + "hostAddress": "Dirección del servidor", + "password": "Contraseña", + "passwordAgain": "Contraseña de nuevo", + "port": "Puerto", + "realName": "Nombre real", + "saveChanges": "Guardar cambios", + "token": "Ficha", + "username": "Nombre de usuario" + }, + "validation": { + "minChars": "Se requiere un mínimo de {conteo} {conteo, plural, un {carácter} otros {caracteres}}", + "passwordsMustMatch": "Las contraseñas no coinciden", + "required": "Requerido" + }, + "countries": { + "AD": "Andorra", + "AE": "Emiratos Árabes Unidos", + "AF": "Afganistán", + "AG": "Antigua y Barbuda", + "AI": "Anguila", + "AL": "Albania", + "AM": "Armenia", + "AO": "Angola", + "AQ": "Antártida", + "AR": "Argentina", + "AS": "Samoa americana", + "AT": "Austria", + "AU": "Australia", + "AW": "Aruba", + "AX": "Islas Aland", + "AZ": "Azerbaiján", + "BA": "Bosnia y Herzegovina", + "BB": "Barbados", + "BD": "Bangladés", + "BE": "Bélgica", + "BF": "Burkina Faso", + "BG": "Bulgaria", + "BH": "Baréin", + "BI": "Burundi", + "BJ": "Benín", + "BL": "San Bartolomé", + "BM": "Bermuda", + "BN": "Brunei Darussalam", + "BO": "Bolivia", + "BQ": "Bonaire, San Eustaquio y Saba", + "BR": "Brasil", + "BS": "Bahamas", + "BT": "Bután", + "BV": "Isla Bouvet", + "BW": "Botswana", + "BY": "Belorrusia", + "BZ": "Belice", + "CA": "Canadá", + "CC": "Islas (Keeling) Cocos", + "CD": "República Democrática del Congo", + "CF": "República Centroafricana", + "CG": "República del Congo", + "CH": "Suiza", + "CI": "Costa de Marfil", + "CK": "Islas Cook", + "CL": "Chile", + "CM": "Camerún", + "CN": "China", + "CO": "Colombia", + "CR": "Costa Rica", + "CU": "Cuba", + "CV": "Cabo Verde", + "CW": "Curazao", + "CX": "Isla de Navidad", + "CY": "Chipre", + "CZ": "República Checa", + "DE": "Alemania", + "DJ": "Djibouti", + "DK": "Dinamarca", + "DM": "Dominica", + "DO": "República Dominicana", + "DZ": "Argelia", + "EC": "Ecuador", + "EE": "Estonia", + "EG": "Egipto", + "EH": "Sáhara Occidental", + "ER": "Eritrea", + "ES": "España", + "ET": "Etiopía", + "FI": "Finlandia", + "FJ": "Fiji", + "FK": "Islas Falkland", + "FM": "Micronesia", + "FO": "Islas Faroe", + "FR": "Francia", + "GA": "Gabón", + "GB": "Reino Unido", + "GD": "Granada", + "GE": "Georgia", + "GF": "Guinea Francesa", + "GG": "Guernsey", + "GH": "Ghana", + "GI": "Gibraltar", + "GL": "Greenland", + "GM": "Gambía", + "GN": "Guinea", + "GP": "Guadalupe", + "GQ": "Guinea Ecuatorial", + "GR": "Grecia", + "GS": "Georgia del Sur y las islas Sandwich del sur", + "GT": "Guatemala", + "GU": "Guam", + "GW": "Guinea-Bisáu", + "GY": "Guyana", + "HK": "Hong Kong", + "HM": "Islas Heard y McDonald", + "HN": "Honduras", + "HR": "Croacia", + "HT": "Haití", + "HU": "Hungría", + "ID": "Indonesia", + "IE": "Irlanda", + "IL": "Israel", + "IM": "Isla de Man", + "IN": "India", + "IO": "Territorio Británico del Océano Índico", + "IQ": "Irak", + "IR": "Irán", + "IS": "Groenlandia", + "IT": "Italia", + "JE": "Jersey", + "JM": "Jamaica", + "JO": "Jordania", + "JP": "Japón", + "KE": "Kenya", + "KG": "Kirguistán", + "KH": "Camboya", + "KI": "Kiribati", + "KM": "Comoras", + "KN": "San Cristóbal y Nieves", + "KP": "Corea la Buena-Norte", + "KR": "Corea la Mala-Sur", + "KW": "Kuwait", + "KY": "Islas Caimán", + "KZ": "Kazajistán", + "LA": "Laos", + "LB": "Líbano", + "LC": "Santa Lucía", + "LI": "Principado de Liechtenstein", + "LK": "Sri Lanka", + "LR": "Liberia", + "LS": "Lesoto", + "LT": "Lituania", + "LU": "Luxemburgo", + "LV": "Letonia", + "LY": "Libia", + "MA": "Marruecos", + "MC": "Mónaci", + "MD": "Moldavia", + "ME": "Montenegro", + "MF": "San Martín (Parte francesa)", + "MG": "Madagascar", + "MH": "Islas Marshall", + "MK": "Macedonia del Norte", + "ML": "Malí", + "MM": "Birmania", + "MN": "Mongolia", + "MO": "Macao", + "MP": "Islas Marianas del Norte", + "MQ": "Martinica", + "MR": "Mauritania", + "MS": "Montserrat", + "MT": "Malta", + "MU": "Mauricio", + "MV": "Maldivas", + "MW": "Malawi", + "MX": "México", + "MY": "Malasia", + "MZ": "Mozambique", + "NA": "Namibia", + "NC": "Nueva Caledonia", + "NE": "Nigeria", + "NF": "Isla Norfolk", + "NG": "Nigeria", + "NI": "Nicaragua", + "NL": "Países Bajos (Holanda)", + "NO": "Noruega", + "NP": "Nepal", + "NR": "Nauru", + "NU": "Niue", + "NZ": "Nueva Zelanda", + "OM": "Omán", + "PA": "Panamá", + "PE": "Perú", + "PF": "Polinesia Francesa", + "PG": "Papúa Nueva Guinea", + "PH": "Filipinas", + "PK": "Pakistán", + "PL": "Polonia", + "PM": "San Pedro y Miquelón", + "PN": "Islas Pitcairn", + "PR": "Puerto Rico", + "PS": "Palestina", + "PT": "Portugal", + "PW": "Palaos", + "PY": "Paraguay", + "QA": "Catar", + "RE": "Reunión", + "RO": "Rumanía", + "RS": "Serbia", + "RU": "Rusia", + "RW": "Ruanda", + "SA": "Arabia Saudí", + "SB": "Islas Salomón", + "SC": "Islas Seychelles", + "SD": "Sudán", + "SE": "Suecia", + "SG": "Singapur", + "SH": "Santa Elena, Ascensión y Tristán de Acuña", + "SI": "Eslovenia", + "SJ": "Svalbard y Jan Mayen", + "SK": "Eslovaquia", + "SL": "Sierra Leona", + "SM": "San Marino", + "SN": "Senegal", + "SO": "Somalia", + "SR": "Surinam", + "SS": "Sudán del Sur", + "ST": "Santo Tomé y Príncipe", + "SV": "El Salvador", + "SX": "Sint Maarten (Países Bajos-Holanda)", + "SY": "Siria", + "SZ": "Esuatini", + "TC": "Islas Turcas y Caicos", + "TD": "Chad", + "TF": "Las Tierras Australes y Antárticas Francesas (TAAF)", + "TG": "Togo", + "TH": "Tailandia", + "TJ": "Tayikistán", + "TK": "Tokelau", + "TL": "Timor Oriental", + "TM": "Turkmenistán", + "TN": "Túnez", + "TO": "Tonga", + "TR": "Turquía", + "TT": "Trinidad y Tobago", + "TV": "Tuvalu", + "TW": "Taiwán", + "TZ": "Tanzania", + "UA": "Ucrania", + "UG": "Uganda", + "UM": "Islas Ultramarinas Menores de Estados Unidos", + "US": "Estados Unidos", + "UY": "Uruguay", + "UZ": "Uzbekistán", + "VA": "Ciudad del Vaticano", + "VC": "San Vicente y las Granadinas", + "VE": "Venezuela", + "VG": "Islas Vírgenes Británicas", + "VI": "Islas Vírgenes Estadounidenses", + "VN": "Vietnam", + "VU": "Vanuatu", + "WF": "Wallis y Futuna", + "WS": "Samoa", + "YE": "Yemen", + "YT": "Mayotte", + "XK": "Kósovo", + "ZA": "Sudáfrica", + "ZM": "Zambia", + "ZW": "Zimbabue", + "EU": "Unión Europea" + }, + "languages": { + "en-US": "Inglés - US", + "fr": "Francés", + "nl": "Holandés", + "pt_BR": " Portugués - Brasil" + } + }, + "KnownHosts": { + "label": "IP", + "add": "Añadir nueva dirección IP", + "toast": "Dirección IP correcta {modo, seleccionar, creado {creado} eliminado {eliminado} otro {editado}}." + }, + "InitializeContainer": { + "title": "¿LO SABÍAS?", + "subtitle": "<1>¡Cockatrice está buscando voluntarios<1> que amen los juegos de cartas!" + }, + "LoginContainer": { + "header": { + "title": "Conectar", + "subtitle": "Un tablero de mesa multiplataforma para juegos de cartas multijugador." + }, + "footer": { + "registerPrompt": "¿No estás registrado aún?", + "registerAction": "Crear una cuenta", + "credit": "Cockatrice es un proyecto de fuente abierta", + "version": "Versión" + }, + "content": { + "subtitle1": "Juega juegos de cartas multijugador en línea.", + "subtitle2": "Tablero multiplataforma virtual para juegos de cartas multijugador. Siempre gratuito." + }, + "toasts": { + "passwordResetSuccessToast": "Reinicio de contraseña exitoso", + "accountActivationSuccess": "Cuenta activada con éxito" + } + }, + "UnsupportedContainer": { + "title": "Navegador sin apoyo", + "subtitle1": "Por favor actualiza tu navegador y/o comprueba tus permisos.", + "subtitle2": "Nota: Los navegadores privados pueden causar que los navegadores deshabiliten ciertos permisos o funciones." + }, + "AccountActivationDialog": { + "title": "Activación de la cuenta", + "subtitle1": "Tu cuenta aún no ha sido activada.", + "subtitle2": "Necesitas comprobar el mensaje de activación recibido en el e-mail de activación." + }, + "KnownHostDialog": { + "title": "{modo, seleccionar, editar {Editar} otro {Agregar}} IP conocida", + "subtitle": "Añadir una nueva dirección IP te permite conectarte a diferentes servidores. Introduce los detalles a continuación a tu lista de IPs." + }, + "RegistrationDialog": { + "title": "Crear nueva cuenta" + }, + "RequestPasswordResetDialog": { + "title": "Requiere reinicio de contraseña" + }, + "ResetPasswordDialog": { + "title": "Reiniciar contraseña" + }, + "AccountActivationForm": { + "error": { + "failed": "La activación de la cuenta falló" + }, + "label": { + "activate": "Activar cuenta" + } + }, + "KnownHostForm": { + "help": "¿Necesitas ayuda al añadir una nueva dirección IP?", + "label": { + "add": "Añadir dirección IP", + "find": "Encontrar dirección IP" + } + }, + "LoginForm": { + "label": { + "autoConnect": "Conectarse automáticamente", + "forgot": "Contraseña olvidada", + "login": "Conectar", + "savePassword": "Guardar contraseña", + "savedPassword": "Contraseña guardada" + } + }, + "RegisterForm": { + "label": { + "register": "Registrarse" + }, + "toast": { + "registerSuccess": "¡Te has registrado correctamente!" + } + }, + "RequestPasswordResetForm": { + "error": "Solicitud de restablecimiento de contraseña fallida", + "mfaEnabled": "El servidor tiene habilitada la autenticación multifactor", + "request": "Se requiere reiniciar el token", + "skipRequest": "Ya tengo un reinicio de token" + }, + "ResetPasswordForm": { + "error": "El reinicio de la contraseña falló", + "label": { + "reset": "Reiniciar contraseña" + } + } +} \ No newline at end of file diff --git a/webclient/public/locales/fi/translation.json b/webclient/public/locales/fi/translation.json new file mode 100644 index 000000000..12737731a --- /dev/null +++ b/webclient/public/locales/fi/translation.json @@ -0,0 +1,382 @@ +{ + "Common": { + "language": "Suomi (Finnish)", + "disconnect": "Katkaise yhteys", + "label": { + "confirmPassword": "Vahvista salasana", + "confirmSure": "Oletko varma?", + "country": "Maa", + "delete": "Poista", + "email": "Sähköposti", + "hostName": "Isännän Nimi", + "hostAddress": "Isännän Osoite", + "password": "Salasana", + "passwordAgain": "Salasana uudelleen", + "port": "Portti", + "realName": "Oikea nimi", + "saveChanges": "Tallenna muutokset", + "token": "Tokeni", + "username": "Käyttäjänimi" + }, + "validation": { + "minChars": "Minimum of {count} {count, plural, one {character} other {characters}} required", + "passwordsMustMatch": "Passwords don't match", + "required": "Required" + }, + "countries": { + "AD": "Andorra", + "AE": "United Arab Emirates", + "AF": "Afghanistan", + "AG": "Antigua and Barbuda", + "AI": "Anguilla", + "AL": "Albania", + "AM": "Armenia", + "AO": "Angola", + "AQ": "Antarctica", + "AR": "Argentina", + "AS": "American Samoa", + "AT": "Austria", + "AU": "Australia", + "AW": "Aruba", + "AX": "Åland Islands", + "AZ": "Azerbaijan", + "BA": "Bosnia and Herzegovina", + "BB": "Barbados", + "BD": "Bangladesh", + "BE": "Belgium", + "BF": "Burkina Faso", + "BG": "Bulgaria", + "BH": "Bahrain", + "BI": "Burundi", + "BJ": "Benin", + "BL": "Saint Barthélemy", + "BM": "Bermuda", + "BN": "Brunei Darussalam", + "BO": "Bolivia", + "BQ": "Bonaire, Sint Eustatius and Saba", + "BR": "Brazil", + "BS": "Bahamas", + "BT": "Bhutan", + "BV": "Bouvet Island", + "BW": "Botswana", + "BY": "Belarus", + "BZ": "Belize", + "CA": "Canada", + "CC": "Cocos (Keeling) Islands", + "CD": "DR Congo", + "CF": "Central African Republic", + "CG": "Republic of the Congo", + "CH": "Switzerland", + "CI": "Ivory Coast", + "CK": "Cook Islands", + "CL": "Chile", + "CM": "Cameroon", + "CN": "China", + "CO": "Colombia", + "CR": "Costa Rica", + "CU": "Cuba", + "CV": "Cape Verde", + "CW": "Curaçao", + "CX": "Christmas Island", + "CY": "Cyprus", + "CZ": "Czechia", + "DE": "Germany", + "DJ": "Djibouti", + "DK": "Denmark", + "DM": "Dominica", + "DO": "Dominican Republic", + "DZ": "Algeria", + "EC": "Ecuador", + "EE": "Estonia", + "EG": "Egypt", + "EH": "Western Sahara", + "ER": "Eritrea", + "ES": "Spain", + "ET": "Ethiopia", + "FI": "Finland", + "FJ": "Fiji", + "FK": "Falkland Islands", + "FM": "Micronesia", + "FO": "Faroe Islands", + "FR": "France", + "GA": "Gabon", + "GB": "United Kingdom", + "GD": "Grenada", + "GE": "Georgia", + "GF": "French Guiana", + "GG": "Guernsey", + "GH": "Ghana", + "GI": "Gibraltar", + "GL": "Greenland", + "GM": "Gambia", + "GN": "Guinea", + "GP": "Guadeloupe", + "GQ": "Equatorial Guinea", + "GR": "Greece", + "GS": "South Georgia and the South Sandwich Islands", + "GT": "Guatemala", + "GU": "Guam", + "GW": "Guinea-Bissau", + "GY": "Guyana", + "HK": "Hong Kong", + "HM": "Heard Island and McDonald Islands", + "HN": "Honduras", + "HR": "Croatia", + "HT": "Haiti", + "HU": "Hungary", + "ID": "Indonesia", + "IE": "Ireland", + "IL": "Israel", + "IM": "Isle of Man", + "IN": "India", + "IO": "British Indian Ocean Territory", + "IQ": "Iraq", + "IR": "Iran", + "IS": "Iceland", + "IT": "Italy", + "JE": "Jersey", + "JM": "Jamaica", + "JO": "Jordan", + "JP": "Japan", + "KE": "Kenya", + "KG": "Kyrgyzstan", + "KH": "Cambodia", + "KI": "Kiribati", + "KM": "Comoros", + "KN": "Saint Kitts and Nevis", + "KP": "North Korea", + "KR": "South Korea", + "KW": "Kuwait", + "KY": "Cayman Islands", + "KZ": "Kazakhstan", + "LA": "Laos", + "LB": "Lebanon", + "LC": "Saint Lucia", + "LI": "Liechtenstein", + "LK": "Sri Lanka", + "LR": "Liberia", + "LS": "Lesotho", + "LT": "Lithuania", + "LU": "Luxembourg", + "LV": "Latvia", + "LY": "Libya", + "MA": "Morocco", + "MC": "Monaco", + "MD": "Moldova", + "ME": "Montenegro", + "MF": "Saint Martin (French part)", + "MG": "Madagascar", + "MH": "Marshall Islands", + "MK": "North Macedonia", + "ML": "Mali", + "MM": "Myanmar", + "MN": "Mongolia", + "MO": "Macao", + "MP": "Northern Mariana Islands", + "MQ": "Martinique", + "MR": "Mauritania", + "MS": "Montserrat", + "MT": "Malta", + "MU": "Mauritius", + "MV": "Maldives", + "MW": "Malawi", + "MX": "Mexico", + "MY": "Malaysia", + "MZ": "Mozambique", + "NA": "Namibia", + "NC": "New Caledonia", + "NE": "Niger", + "NF": "Norfolk Island", + "NG": "Nigeria", + "NI": "Nicaragua", + "NL": "Netherlands", + "NO": "Norway", + "NP": "Nepal", + "NR": "Nauru", + "NU": "Niue", + "NZ": "New Zealand", + "OM": "Oman", + "PA": "Panama", + "PE": "Peru", + "PF": "French Polynesia", + "PG": "Papua New Guinea", + "PH": "Philippines", + "PK": "Pakistan", + "PL": "Poland", + "PM": "Saint Pierre and Miquelon", + "PN": "Pitcairn", + "PR": "Puerto Rico", + "PS": "Palestine", + "PT": "Portugal", + "PW": "Palau", + "PY": "Paraguay", + "QA": "Qatar", + "RE": "Réunion", + "RO": "Romania", + "RS": "Serbia", + "RU": "Russia", + "RW": "Rwanda", + "SA": "Saudi Arabia", + "SB": "Solomon Islands", + "SC": "Seychelles", + "SD": "Sudan", + "SE": "Sweden", + "SG": "Singapore", + "SH": "Saint Helena, Ascension and Tristan da Cunha", + "SI": "Slovenia", + "SJ": "Svalbard and Jan Mayen", + "SK": "Slovakia", + "SL": "Sierra Leone", + "SM": "San Marino", + "SN": "Senegal", + "SO": "Somalia", + "SR": "Suriname", + "SS": "South Sudan", + "ST": "Sao Tome and Principe", + "SV": "El Salvador", + "SX": "Sint Maarten (Dutch part)", + "SY": "Syria", + "SZ": "Eswatini", + "TC": "Turks and Caicos Islands", + "TD": "Chad", + "TF": "TAAF", + "TG": "Togo", + "TH": "Thailand", + "TJ": "Tajikistan", + "TK": "Tokelau", + "TL": "Timor-Leste", + "TM": "Turkmenistan", + "TN": "Tunisia", + "TO": "Tonga", + "TR": "Turkey", + "TT": "Trinidad and Tobago", + "TV": "Tuvalu", + "TW": "Taiwan", + "TZ": "Tanzania", + "UA": "Ukraine", + "UG": "Uganda", + "UM": "United States Minor Outlying Islands", + "US": "United States", + "UY": "Uruguay", + "UZ": "Uzbekistan", + "VA": "Holy See", + "VC": "Saint Vincent and the Grenadines", + "VE": "Venezuela", + "VG": "British Virgin Islands", + "VI": "U.S. Virgin Islands", + "VN": "Viet Nam", + "VU": "Vanuatu", + "WF": "Wallis and Futuna", + "WS": "Samoa", + "YE": "Yemen", + "YT": "Mayotte", + "XK": "Kosovo", + "ZA": "South Africa", + "ZM": "Zambia", + "ZW": "Zimbabwe", + "EU": "European Union" + }, + "languages": { + "en-US": "English - US", + "fr": "French", + "nl": "Dutch", + "pt_BR": "Portuguese - Brazil" + } + }, + "KnownHosts": { + "label": "Isäntä", + "add": "Lisää isäntä", + "toast": "Host successfully {mode, select, created {created} deleted {deleted} other {edited}}." + }, + "InitializeContainer": { + "title": "TIESITKÖ", + "subtitle": "<1>Cockatricea pyörittävät vapaaehtoiset<1>jotka rakastavat korttipelejä!" + }, + "LoginContainer": { + "header": { + "title": "Kirjaudu sisään", + "subtitle": "Järjestelmäriippumaton virtuaalinen pöytä moninpelikorttipelejä varten." + }, + "footer": { + "registerPrompt": "Etkö ole vielä rekisteröitynyt?", + "registerAction": "Luo tili", + "credit": "Cockatrice on avoimen lähdekoodin projekti", + "version": "Versio" + }, + "content": { + "subtitle1": "Pelaa moninpelikorttipelejä verkossa.", + "subtitle2": "Järjestelmäriippumaton virtuaalinen pöytä moninpelikorttipelejä varten. Ikuisesti ilmainen." + }, + "toasts": { + "passwordResetSuccessToast": "Salasanan Nollaus Onnistui", + "accountActivationSuccess": "Tilin Aktivointi Onnistui" + } + }, + "UnsupportedContainer": { + "title": "Selainta ei tueta", + "subtitle1": "Päivitä selain ja/tai tarksita käyttöoikeudet.", + "subtitle2": "Huomautus: Yksityinen selaaminen saa jotkin selaimet poistamaan tietyt käyttöoikeudet tai ominaisuudet käytöstä." + }, + "AccountActivationDialog": { + "title": "Tilin Aktivointi", + "subtitle1": "Your account has not been activated yet.", + "subtitle2": "You need to provide the activation token received in the activation email." + }, + "KnownHostDialog": { + "title": "{mode, select, edit {Edit} other {Add}} Known Host", + "subtitle": "Uuden isännän lisääminen mahdollistaa yhdistämisen eri palvelimiin. Lisää alla olevat tiedot isäntäluetteloosi." + }, + "RegistrationDialog": { + "title": "Luo Tili" + }, + "RequestPasswordResetDialog": { + "title": "Request Password Reset" + }, + "ResetPasswordDialog": { + "title": "Nollaa Salasana" + }, + "AccountActivationForm": { + "error": { + "failed": "Tilin aktivointi epäonnistui" + }, + "label": { + "activate": "Aktivoi Tili" + } + }, + "KnownHostForm": { + "help": "Tarvitsetko apua isännän lisäämisessä?", + "label": { + "add": "Lisää Isäntä", + "find": "Etsi Isäntä" + } + }, + "LoginForm": { + "label": { + "autoConnect": "Auto Connect", + "forgot": "Forgot Password", + "login": "Kirjaudu sisään", + "savePassword": "Save Password", + "savedPassword": "Saved Password" + } + }, + "RegisterForm": { + "label": { + "register": "Register" + }, + "toast": { + "registerSuccess": "Registration Successful!" + } + }, + "RequestPasswordResetForm": { + "error": "Request password reset failed", + "mfaEnabled": "Server has multi-factor authentication enabled", + "request": "Request Reset Token", + "skipRequest": "I already have a reset token" + }, + "ResetPasswordForm": { + "error": "Password reset failed", + "label": { + "reset": "Nollaa Salasana" + } + } +} \ No newline at end of file diff --git a/webclient/public/locales/fr/translation.json b/webclient/public/locales/fr/translation.json new file mode 100644 index 000000000..aacd26d80 --- /dev/null +++ b/webclient/public/locales/fr/translation.json @@ -0,0 +1,382 @@ +{ + "Common": { + "language": "Français (French)", + "disconnect": "Déconnecter", + "label": { + "confirmPassword": "Confirmer le mot de passe", + "confirmSure": "Êtes-vous sûr ?", + "country": "Pays", + "delete": "Effacer", + "email": "E-mail", + "hostName": "Nom d'hôte", + "hostAddress": "Adresse d'hôte", + "password": "Mot de passe", + "passwordAgain": "Mot de passe à nouveau", + "port": "Port", + "realName": "Nom réel", + "saveChanges": "Sauvegarder les changements", + "token": "Jeton", + "username": "Nom d'utilisateur" + }, + "validation": { + "minChars": "Minimum de {count} {count, plural, one {caractère} other {caractères}} requis", + "passwordsMustMatch": "Les mots de passe ne correspondent pas.", + "required": "Requis" + }, + "countries": { + "AD": "Andorre", + "AE": "Émirats arabes unis", + "AF": "Afghanistan", + "AG": "Antigua-et-Barbuda", + "AI": "Anguilla", + "AL": "Albanie", + "AM": "Arménie", + "AO": "Angola", + "AQ": "Antarctique", + "AR": "Argentine", + "AS": "Samoa américaines", + "AT": "Autriche", + "AU": "Australie", + "AW": "Aruba", + "AX": "Åland", + "AZ": "Azerbaijan", + "BA": "Bosnie-Herzégovine", + "BB": "Barbade", + "BD": "Bangladesh", + "BE": "Belgique", + "BF": "Burkina Faso", + "BG": "Bulgarie", + "BH": "Bahrain", + "BI": "Burundi", + "BJ": "Bénin", + "BL": "Saint Barthélemy", + "BM": "Bermudes", + "BN": "Brunéi Darussalam", + "BO": "Bolivie", + "BQ": "Pays-Bas caribéens", + "BR": "Brésil", + "BS": "Bahamas", + "BT": "Bhoutan", + "BV": "Île Bouvet", + "BW": "Botswana", + "BY": "Biélorussie", + "BZ": "Belize", + "CA": "Canada", + "CC": "Îles Cocos (Keeling)", + "CD": "République Démocratique du Congo", + "CF": "République Centrafricaine", + "CG": "République du Congo", + "CH": "Suisse", + "CI": "Côte d'Ivoire", + "CK": "Îles Cook", + "CL": "Chili", + "CM": "Cameroun", + "CN": "Chine", + "CO": "Colombie", + "CR": "Costa Rica", + "CU": "Cuba", + "CV": "Cap-Vert", + "CW": "Curaçao", + "CX": "Île Christmas", + "CY": "Chypre", + "CZ": "Tchéquie", + "DE": "Allemagne", + "DJ": "Djibouti", + "DK": "Danemark", + "DM": "Dominique", + "DO": "République dominicaine", + "DZ": "Algérie", + "EC": "Équateur", + "EE": "Estonie", + "EG": "Égypte", + "EH": "Sahara Occidental", + "ER": "Érythrée", + "ES": "Espagne", + "ET": "Éthiopie", + "FI": "Finlande", + "FJ": "Fidji", + "FK": "Îles Falkland", + "FM": "Micronésie", + "FO": "Îles Féroé", + "FR": "France", + "GA": "Gabon", + "GB": "Royaume-Uni", + "GD": "Grenade", + "GE": "Géorgie", + "GF": "Guyane Française", + "GG": "Guernsey", + "GH": "Ghana", + "GI": "Gibraltar", + "GL": "Groenland", + "GM": "Gambie", + "GN": "Guinée", + "GP": "Guadeloupe", + "GQ": "Guinée équatoriale", + "GR": "Grèce", + "GS": "Géorgie du Sud et les Îles Sandwich du Sud", + "GT": "Guatemala", + "GU": "Guam", + "GW": "Guinée-Bissau", + "GY": "Guyana", + "HK": "Hong Kong", + "HM": "Îles Heard et McDonald", + "HN": "Honduras", + "HR": "Croatie", + "HT": "Haïti", + "HU": "Hongrie", + "ID": "Indonésie", + "IE": "Irlande", + "IL": "Israël", + "IM": "Île de Man", + "IN": "Inde", + "IO": "Territoire britannique de l'océan Indien", + "IQ": "Irak", + "IR": "Iran", + "IS": "Islande", + "IT": "Italie", + "JE": "Jersey", + "JM": "Jamaïque", + "JO": "Jordanie", + "JP": "Japon", + "KE": "Kenya", + "KG": "Kirghizistan", + "KH": "Cambodge", + "KI": "Kiribati", + "KM": "Comores", + "KN": "Saint-Kitts-et-Nevis", + "KP": "République Populaire et Démocratique de Corée", + "KR": "Corée du Sud", + "KW": "Koweït", + "KY": "Îles Caïmans", + "KZ": "Kazakhstan", + "LA": "Laos", + "LB": "Liban", + "LC": "Sainte-Lucie", + "LI": "Liechtenstein", + "LK": "Sri Lanka", + "LR": "Liberia", + "LS": "Lesotho", + "LT": "Lituanie", + "LU": "Luxembourg", + "LV": "Lettonie", + "LY": "Lybie", + "MA": "Maroc", + "MC": "Monaco", + "MD": "Moldavie", + "ME": "Montenegro", + "MF": "Saint-Martin (Territoire Français)", + "MG": "Madagascar", + "MH": "Îles Marshall", + "MK": "Macédoine du Nord", + "ML": "Mali", + "MM": "Myanmar", + "MN": "Mongolie", + "MO": "Macao", + "MP": "Îles Mariannes du Nord", + "MQ": "Martinique", + "MR": "Mauritanie", + "MS": "Montserrat", + "MT": "Malte", + "MU": "Maurice", + "MV": "Maldives", + "MW": "Malawi", + "MX": "Mexique", + "MY": "Malaisie", + "MZ": "Mozambique", + "NA": "Namibie", + "NC": "Nouvelle Calédonie", + "NE": "Niger", + "NF": "Île Norfolk", + "NG": "Nigeria", + "NI": "Nicaragua", + "NL": "Pays-Bas", + "NO": "Norvège", + "NP": "Népal", + "NR": "Nauru", + "NU": "Niué", + "NZ": "Nouvelle-Zélande", + "OM": "Oman", + "PA": "Panama", + "PE": "Pérou", + "PF": "Polynésie Française", + "PG": "Papouasie-Nouvelle-Guinée", + "PH": "Philippines", + "PK": "Pakistan", + "PL": "Pologne", + "PM": "Saint Pierre et Miquelon", + "PN": "Îles Pitcairn", + "PR": "Porto Rico", + "PS": "Palestine", + "PT": "Portugal", + "PW": "Palaos", + "PY": "Paraguay", + "QA": "Qatar", + "RE": "La Réunion", + "RO": "Roumanie", + "RS": "Serbie", + "RU": "Russie", + "RW": "Rwanda", + "SA": "Arabie Saoudite", + "SB": "Îles Salomon", + "SC": "Seychelles", + "SD": "Soudan", + "SE": "Suède", + "SG": "Singapour", + "SH": "Sainte-Hélène", + "SI": "Slovénie", + "SJ": "Svalbard et Jan Mayen", + "SK": "Slovaquie", + "SL": "Sierra Leone", + "SM": "Saint-Marin", + "SN": "Sénégal", + "SO": "Somalie", + "SR": "Suriname", + "SS": "Soudan du Sud", + "ST": "Sao Tome et Principe", + "SV": "El Salvador", + "SX": "Sint Maarten (territoire des Pays-Bas)", + "SY": "Syrie", + "SZ": "Eswatini", + "TC": "Îles Turks et Caïques", + "TD": "Tchad", + "TF": "Terres australes et antarctiques françaises", + "TG": "Togo", + "TH": "Thailande", + "TJ": "Tajikistan", + "TK": "Tokelau", + "TL": "Timor-Leste", + "TM": "Turkmenistan", + "TN": "Tunisie", + "TO": "Tonga", + "TR": "Turquie", + "TT": "Trinité-et-Tobago", + "TV": "Tuvalu", + "TW": "Taïwan", + "TZ": "Tanzanie", + "UA": "Ukraine", + "UG": "Ouganda", + "UM": "Îles mineures éloignées des États-Unis", + "US": "États-Unis", + "UY": "Uruguay", + "UZ": "Ouzbekistan", + "VA": "Saint-Siège", + "VC": "Saint-Vincent et les Grenadines", + "VE": "Venezuela", + "VG": "Îles Vierges britanniques", + "VI": "Îles Vierges des États-Unis", + "VN": "Vietnam", + "VU": "Vanuatu", + "WF": "Wallis et Futuna", + "WS": "Samoa", + "YE": "Yemen", + "YT": "Mayotte", + "XK": "Kosovo", + "ZA": "Afrique du Sud", + "ZM": "Zambie", + "ZW": "Zimbabwe", + "EU": "Union Européenne" + }, + "languages": { + "en-US": "Anglais - US", + "fr": "Français", + "nl": "Néerlandais", + "pt_BR": "Portugais - Brésil" + } + }, + "KnownHosts": { + "label": "Hôte", + "add": "Ajouter un nouvel hôte", + "toast": "Hôte {mode, select, created {créé} deleted {effacé} other {édité}} avec succès." + }, + "InitializeContainer": { + "title": "LE SAVIEZ-VOUS ?", + "subtitle": "<1>Cockatrice est géré par des volontaires<1>qui aiment les jeux de cartes !" + }, + "LoginContainer": { + "header": { + "title": "S'identifier", + "subtitle": "Un jeu de table virtuel multi-plate-forme pour jeux de cartes multijoueurs." + }, + "footer": { + "registerPrompt": "Pas encore inscrit ?", + "registerAction": "Créer un compte", + "credit": "Cockatrice est un projet open source.", + "version": "Version" + }, + "content": { + "subtitle1": "Jouer à des jeux de cartes multijoueurs en ligne.", + "subtitle2": "Jeu de table virtuel multi-plate-forme pour jeux de cartes multijoueurs. Gratuit pour toujours." + }, + "toasts": { + "passwordResetSuccessToast": "Mot de passe réinitialisé avec succès", + "accountActivationSuccess": "Compte activé avec succès" + } + }, + "UnsupportedContainer": { + "title": "Navigateur non supporté", + "subtitle1": "Veuillez mettre à jour votre navigateur et/ou vérifier vos permissions.", + "subtitle2": "Note : La navigation privée désactive certaines permissions ou fonctionnalités." + }, + "AccountActivationDialog": { + "title": "Activation du compte", + "subtitle1": "Votre compte n'a pas encore été activé.", + "subtitle2": "Vous devez fournir le jeton d'activation reçu dans l'e-mail d'activation." + }, + "KnownHostDialog": { + "title": "{mode, select, edit {Éditer} other {Ajouter}} un hôte connu", + "subtitle": "Ajouter un nouvel hôte vous permet de vous connecter à différents serveurs. Entrez les détails ci-dessous dans votre liste d'hôtes." + }, + "RegistrationDialog": { + "title": "Créer un nouveau compte" + }, + "RequestPasswordResetDialog": { + "title": "Demander une réinitialisation du mot de passe" + }, + "ResetPasswordDialog": { + "title": "Réinitialiser le mot de passe" + }, + "AccountActivationForm": { + "error": { + "failed": "L'activation du compte a échoué." + }, + "label": { + "activate": "Activer le compte" + } + }, + "KnownHostForm": { + "help": "Besoin d'aide pour ajouter un nouvel hôte ?", + "label": { + "add": "Ajouter un hôte", + "find": "Trouver un hôte" + } + }, + "LoginForm": { + "label": { + "autoConnect": "Connexion automatique", + "forgot": "Mot de passe oublié", + "login": "S'identifier", + "savePassword": "Enregistrer le mot de passe", + "savedPassword": "Mot de passe enregistré" + } + }, + "RegisterForm": { + "label": { + "register": "S'enregistrer" + }, + "toast": { + "registerSuccess": "Enregistrement réussi !" + } + }, + "RequestPasswordResetForm": { + "error": "La demande de réinitialisation du mot de passe a échoué.", + "mfaEnabled": "L'authentification multifacteur est activée sur ce serveur.", + "request": "Réinitialiser jeton", + "skipRequest": "Je possède déjà un jeton de réinitialisation." + }, + "ResetPasswordForm": { + "error": "La réinitialisation du mot de passe a échoué.", + "label": { + "reset": "Réinitialiser le mot de passe" + } + } +} \ No newline at end of file diff --git a/webclient/public/locales/it/translation.json b/webclient/public/locales/it/translation.json new file mode 100644 index 000000000..74734384b --- /dev/null +++ b/webclient/public/locales/it/translation.json @@ -0,0 +1,382 @@ +{ + "Common": { + "language": "Italiano (Italian)", + "disconnect": "Disconnetti", + "label": { + "confirmPassword": "Conferma Password", + "confirmSure": "Sei sicuro?", + "country": "Stato", + "delete": "Elimina", + "email": "Email", + "hostName": "Nome Host", + "hostAddress": "Indirizzo Host", + "password": "Password", + "passwordAgain": "Ripeti Password", + "port": "Porta", + "realName": "Nome reale", + "saveChanges": "Salva Cambiamenti", + "token": "Pedina", + "username": "Nome utente" + }, + "validation": { + "minChars": "Minimo di {%n} carattere(i) richiesti", + "passwordsMustMatch": "Le password non coincidono.", + "required": "Richiesto" + }, + "countries": { + "AD": "Andorra", + "AE": "Emirati Arabi Uniti", + "AF": "Afghanistan", + "AG": "Antigua e Barbuda", + "AI": "Anguilla", + "AL": "Albania", + "AM": "Armenia", + "AO": "Angola", + "AQ": "Antartide", + "AR": "Argentina", + "AS": "Samoa Americane", + "AT": "Austria", + "AU": "Australia", + "AW": "Aruba", + "AX": "Isole Åland", + "AZ": "Azerbaijan", + "BA": "Bosnia ed Erzegovina", + "BB": "Barbados", + "BD": "Bangledesh", + "BE": "Belgio", + "BF": "Burkina Faso", + "BG": "Bulgaria", + "BH": "Bahrein", + "BI": "Burundi", + "BJ": "Benin", + "BL": "Saint Barthélemy", + "BM": "Bermuda", + "BN": "Brunei", + "BO": "Bolivia", + "BQ": "Paesi Bassi Caraibici", + "BR": "Brasile", + "BS": "Bahamas", + "BT": "Bhutan", + "BV": "Isola Bouvet", + "BW": "Botswana", + "BY": "Bielorussia", + "BZ": "Belize", + "CA": "Canada", + "CC": "Isole Cocos (Keeling)", + "CD": "DR Congo", + "CF": "Repubblica Centrafricana", + "CG": "Repubblica del Congo", + "CH": "Svizzera", + "CI": "Costa D'Avorio", + "CK": "Isole Cook", + "CL": "Cile", + "CM": "Camerun", + "CN": "Cina", + "CO": "Colombia", + "CR": "Costa Rica", + "CU": "Cuba", + "CV": "Capo Verde", + "CW": "Curaçao", + "CX": "Isola Christmas", + "CY": "Cipro", + "CZ": "Repubblica Ceca", + "DE": "Germania", + "DJ": "Gibuti", + "DK": "Danimarca", + "DM": "Dominica", + "DO": "Repubblica Domenicana", + "DZ": "Algeria", + "EC": "Ecuador", + "EE": "Estonia", + "EG": "Egitto", + "EH": "Sahara Occidentale", + "ER": "Eritrea", + "ES": "Spagna", + "ET": "Etiopia", + "FI": "Finlandia", + "FJ": "Fiji", + "FK": "Isole Falkland", + "FM": "Micronesia", + "FO": "Isole Faroe", + "FR": "Francia", + "GA": "Gabon", + "GB": "Regno Unito", + "GD": "Grenada", + "GE": "Georgia", + "GF": "Guyana Francese", + "GG": "Guernsey", + "GH": "Ghana", + "GI": "Gibraltar", + "GL": "Groenlandia", + "GM": "Gambia", + "GN": "Guinea", + "GP": "Guadalupa", + "GQ": "Guinea Equatoriale", + "GR": "Grecia", + "GS": "Georgia del Sud e Isole Sandwich Australi", + "GT": "Guatemala", + "GU": "Guam", + "GW": "Guinea-Bissau", + "GY": "Guyana", + "HK": "Hong Kong", + "HM": "Isole Heard e McDonald", + "HN": "Honduras", + "HR": "Croazia", + "HT": "Haiti", + "HU": "Ungheria", + "ID": "Indonesia", + "IE": "Irlanda", + "IL": "Israele", + "IM": "Isola di Man", + "IN": "India", + "IO": "Territorio britannico dell'Oceano Indiano", + "IQ": "Iraq", + "IR": "Iran", + "IS": "Islanda", + "IT": "Italia", + "JE": "Jersey", + "JM": "Jamaica", + "JO": "Giordania", + "JP": "Giappone", + "KE": "Kenya", + "KG": "Kirghizistan", + "KH": "Cambogia", + "KI": "Kiribati", + "KM": "Comore", + "KN": "Saint Kitts e Nevis", + "KP": "Korea del Nord", + "KR": "Korea del Sud", + "KW": "Kuwait", + "KY": "Isole Cayman", + "KZ": "Kazakhstan", + "LA": "Laos", + "LB": "Lebano", + "LC": "Santa Lucia", + "LI": "Liechtenstein", + "LK": "Sri Lanka", + "LR": "Liberia", + "LS": "Lesotho", + "LT": "Lituania", + "LU": "Lussemburgo", + "LV": "Lettonia", + "LY": "Libia", + "MA": "Marocco", + "MC": "Monaco", + "MD": "Moldova", + "ME": "Montenegro", + "MF": "San Martino (parte Francese)", + "MG": "Madagascar", + "MH": "Isole Marshall", + "MK": "Macedonia del Nord", + "ML": "Mali", + "MM": "Birmania", + "MN": "Mongolia", + "MO": "Macao", + "MP": "Isole Marianne Settentrionali", + "MQ": "Martinica", + "MR": "Mauritania", + "MS": "Montserrat", + "MT": "Malta", + "MU": "Mauritius", + "MV": "Maldive", + "MW": "Malawi", + "MX": "Messico", + "MY": "Malesia", + "MZ": "Mozambico", + "NA": "Namibia", + "NC": "Nuova Caledonia", + "NE": "Niger", + "NF": "Isola Norfolk", + "NG": "Nigeria", + "NI": "Nicaragua", + "NL": "Paesi Bassi", + "NO": "Norvegia", + "NP": "Nepal", + "NR": "Nauru", + "NU": "Niue", + "NZ": "Nuova Zelanda", + "OM": "Oman", + "PA": "Panama", + "PE": "Peru", + "PF": "Polinesia Francese", + "PG": "Papua Nuova Guinea", + "PH": "Filippine", + "PK": "Pakistan", + "PL": "Polonia", + "PM": "Saint Pierre e Miquelon", + "PN": "Pitcairn", + "PR": "Porto Rico", + "PS": "Palestina", + "PT": "Portogallo", + "PW": "Palau", + "PY": "Paraguay", + "QA": "Qatar", + "RE": "La Riunione", + "RO": "Romania", + "RS": "Serbia", + "RU": "Russia", + "RW": "Rwanda", + "SA": "Arabia Saudita", + "SB": "Isole Salomone", + "SC": "Seychelles", + "SD": "Sudan", + "SE": "Svezia", + "SG": "Singapore", + "SH": "Sant'Elena, Ascensione e Tristan da Cunha", + "SI": "Slovenia", + "SJ": "Svalbard e Jan Mayen", + "SK": "Slovakia", + "SL": "Sierra Leone", + "SM": "San Marino", + "SN": "Senegal", + "SO": "Somalia", + "SR": "Suriname", + "SS": "Sud Sudan", + "ST": "Sao Tome e Principe", + "SV": "El Salvador", + "SX": "Sint Maarten (Parte Olandese)", + "SY": "Siria", + "SZ": "eSwatini", + "TC": "Turks e Caicos", + "TD": "Chad", + "TF": "TAAF", + "TG": "Togo", + "TH": "Thailand", + "TJ": "Tajikistan", + "TK": "Tokelau", + "TL": "Timor Est", + "TM": "Turkmenistan", + "TN": "Tunisia", + "TO": "Tonga", + "TR": "Turchia", + "TT": "Trinidad e Tobago", + "TV": "Tuvalu", + "TW": "Taiwan", + "TZ": "Tanzania", + "UA": "Ucraina", + "UG": "Uganda", + "UM": "Isole minori esterne degli Stati Uniti d'America", + "US": "Stati Uniti", + "UY": "Uruguay", + "UZ": "Uzbekistan", + "VA": "Vaticano", + "VC": "Saint Vincent e Grenadine", + "VE": "Venezuela", + "VG": "Isole Vergini britanniche", + "VI": "Isole Vergini americane", + "VN": "Viet Nam", + "VU": "Vanuatu", + "WF": "Wallis e Futuna", + "WS": "Samoa", + "YE": "Yemen", + "YT": "Mayotte", + "XK": "Kosovo", + "ZA": "Sud Africa", + "ZM": "Zambia", + "ZW": "Zimbabwe", + "EU": "Unione Europea" + }, + "languages": { + "en-US": "Inglese - US", + "fr": "Francese", + "nl": "Olandese", + "pt_BR": "Portoghese - Brasile" + } + }, + "KnownHosts": { + "label": "Host", + "add": "Aggiungi nuovo Host", + "toast": "Host {mode, select, created {creato} deleted {eliminato} other {modificato}} con successo." + }, + "InitializeContainer": { + "title": "LO SAPEVI", + "subtitle": "<1>Cockatrice è gestita da volontari<1>che amano i giochi di carte!" + }, + "LoginContainer": { + "header": { + "title": "Accesso", + "subtitle": "Un portale digitale multi-piattaforma per giochi di carte multigiocatore" + }, + "footer": { + "registerPrompt": "Non sei ancora registrato?", + "registerAction": "Crea un nuovo account", + "credit": "Cockatrice è un progetto open source", + "version": "Versione" + }, + "content": { + "subtitle1": "Gioca online giochi di carte multigiocatore.", + "subtitle2": "Portale digitale multi-piattaforma per giochi di carte multigiocatore. Sempre gratis." + }, + "toasts": { + "passwordResetSuccessToast": "Password reimpostata con successo", + "accountActivationSuccess": "Account attivato con successo" + } + }, + "UnsupportedContainer": { + "title": "Browser non supportato", + "subtitle1": "Aggiornare il browser e/o controllare i permessi.", + "subtitle2": "Nota: la navigazione Privata può far si che il browser disabiliti certi permessi o funzionalità." + }, + "AccountActivationDialog": { + "title": "Attivazione dell'account", + "subtitle1": "Il tuo account non è ancora stato attivato.", + "subtitle2": "È necessario fornire il codice ricevuto nell'e-mail di attivazione." + }, + "KnownHostDialog": { + "title": "{modalità, seleziona, modifica {Modifica} altro {Aggiungi}} Host conosciuto", + "subtitle": "L'aggiunta di un nuovo host consente di connettersi a server diversi. Inserisci i dettagli di seguito nella lista degli host." + }, + "RegistrationDialog": { + "title": "Crea un nuovo account" + }, + "RequestPasswordResetDialog": { + "title": "Richiedi la reimpostazione della password" + }, + "ResetPasswordDialog": { + "title": "Reimposta Password" + }, + "AccountActivationForm": { + "error": { + "failed": "Attivazione dell'account non riuscita" + }, + "label": { + "activate": "Attiva account" + } + }, + "KnownHostForm": { + "help": "Hai bisogno di aiuto per aggiungere un nuovo host?", + "label": { + "add": "Aggiungi Host", + "find": "Trova Host" + } + }, + "LoginForm": { + "label": { + "autoConnect": "Connessione automatica", + "forgot": "Password dimenticata", + "login": "Accesso", + "savePassword": "Salva Password", + "savedPassword": "Password salvata" + } + }, + "RegisterForm": { + "label": { + "register": "Iscriviti" + }, + "toast": { + "registerSuccess": "Iscrizione completata correttamente!" + } + }, + "RequestPasswordResetForm": { + "error": "Richiesta reimpostazione password non riuscita", + "mfaEnabled": "Il server ha l'autenticazione a più fattori abilitata", + "request": "Richiedi il token di ripristino", + "skipRequest": "Ho già un token di ripristino" + }, + "ResetPasswordForm": { + "error": "Reimpostazione della password non riuscita", + "label": { + "reset": "Reimposta Password" + } + } +} \ No newline at end of file diff --git a/webclient/public/locales/nl/translation.json b/webclient/public/locales/nl/translation.json new file mode 100644 index 000000000..fffb18989 --- /dev/null +++ b/webclient/public/locales/nl/translation.json @@ -0,0 +1,382 @@ +{ + "Common": { + "language": "Nederlands (Dutch)", + "disconnect": "Verbinding Verbreken", + "label": { + "confirmPassword": "Wachtwoord Bevestigen", + "confirmSure": "Weet je het zeker?", + "country": "Land", + "delete": "Verwijderen", + "email": "Email", + "hostName": "Hostnaam", + "hostAddress": "Hostaddres", + "password": "Wachtwoord", + "passwordAgain": "Wachtwoord Nogmaals", + "port": "Poort", + "realName": "Echte Naam", + "saveChanges": "Wijzigingen Opslaan", + "token": "Token", + "username": "Gebruikersnaam" + }, + "validation": { + "minChars": "Minimum van {count} {count, plural, one {karakter} other {karakters}} verplicht", + "passwordsMustMatch": "Wachtwoorden komen niet overeen", + "required": "Verplicht" + }, + "countries": { + "AD": "Andorra", + "AE": "United Arab Emirates", + "AF": "Afghanistan", + "AG": "Antigua and Barbuda", + "AI": "Anguilla", + "AL": "Albania", + "AM": "Armenia", + "AO": "Angola", + "AQ": "Antarctica", + "AR": "Argentina", + "AS": "American Samoa", + "AT": "Austria", + "AU": "Australia", + "AW": "Aruba", + "AX": "Åland Islands", + "AZ": "Azerbaijan", + "BA": "Bosnia and Herzegovina", + "BB": "Barbados", + "BD": "Bangladesh", + "BE": "Belgium", + "BF": "Burkina Faso", + "BG": "Bulgaria", + "BH": "Bahrain", + "BI": "Burundi", + "BJ": "Benin", + "BL": "Saint Barthélemy", + "BM": "Bermuda", + "BN": "Brunei Darussalam", + "BO": "Bolivia", + "BQ": "Bonaire, Sint Eustatius and Saba", + "BR": "Brazil", + "BS": "Bahamas", + "BT": "Bhutan", + "BV": "Bouvet Island", + "BW": "Botswana", + "BY": "Belarus", + "BZ": "Belize", + "CA": "Canada", + "CC": "Cocos (Keeling) Islands", + "CD": "DR Congo", + "CF": "Central African Republic", + "CG": "Republic of the Congo", + "CH": "Switzerland", + "CI": "Ivory Coast", + "CK": "Cook Islands", + "CL": "Chile", + "CM": "Cameroon", + "CN": "China", + "CO": "Colombia", + "CR": "Costa Rica", + "CU": "Cuba", + "CV": "Cape Verde", + "CW": "Curaçao", + "CX": "Christmas Island", + "CY": "Cyprus", + "CZ": "Czechia", + "DE": "Germany", + "DJ": "Djibouti", + "DK": "Denmark", + "DM": "Dominica", + "DO": "Dominican Republic", + "DZ": "Algeria", + "EC": "Ecuador", + "EE": "Estonia", + "EG": "Egypt", + "EH": "Western Sahara", + "ER": "Eritrea", + "ES": "Spain", + "ET": "Ethiopia", + "FI": "Finland", + "FJ": "Fiji", + "FK": "Falkland Islands", + "FM": "Micronesia", + "FO": "Faroe Islands", + "FR": "France", + "GA": "Gabon", + "GB": "United Kingdom", + "GD": "Grenada", + "GE": "Georgia", + "GF": "French Guiana", + "GG": "Guernsey", + "GH": "Ghana", + "GI": "Gibraltar", + "GL": "Greenland", + "GM": "Gambia", + "GN": "Guinea", + "GP": "Guadeloupe", + "GQ": "Equatorial Guinea", + "GR": "Greece", + "GS": "South Georgia and the South Sandwich Islands", + "GT": "Guatemala", + "GU": "Guam", + "GW": "Guinea-Bissau", + "GY": "Guyana", + "HK": "Hong Kong", + "HM": "Heard Island and McDonald Islands", + "HN": "Honduras", + "HR": "Croatia", + "HT": "Haiti", + "HU": "Hungary", + "ID": "Indonesia", + "IE": "Ireland", + "IL": "Israel", + "IM": "Isle of Man", + "IN": "India", + "IO": "British Indian Ocean Territory", + "IQ": "Iraq", + "IR": "Iran", + "IS": "Iceland", + "IT": "Italy", + "JE": "Jersey", + "JM": "Jamaica", + "JO": "Jordan", + "JP": "Japan", + "KE": "Kenya", + "KG": "Kyrgyzstan", + "KH": "Cambodia", + "KI": "Kiribati", + "KM": "Comoros", + "KN": "Saint Kitts and Nevis", + "KP": "North Korea", + "KR": "South Korea", + "KW": "Kuwait", + "KY": "Cayman Islands", + "KZ": "Kazakhstan", + "LA": "Laos", + "LB": "Lebanon", + "LC": "Saint Lucia", + "LI": "Liechtenstein", + "LK": "Sri Lanka", + "LR": "Liberia", + "LS": "Lesotho", + "LT": "Lithuania", + "LU": "Luxembourg", + "LV": "Latvia", + "LY": "Libya", + "MA": "Morocco", + "MC": "Monaco", + "MD": "Moldova", + "ME": "Montenegro", + "MF": "Saint Martin (French part)", + "MG": "Madagascar", + "MH": "Marshall Islands", + "MK": "North Macedonia", + "ML": "Mali", + "MM": "Myanmar", + "MN": "Mongolia", + "MO": "Macao", + "MP": "Northern Mariana Islands", + "MQ": "Martinique", + "MR": "Mauritania", + "MS": "Montserrat", + "MT": "Malta", + "MU": "Mauritius", + "MV": "Maldives", + "MW": "Malawi", + "MX": "Mexico", + "MY": "Malaysia", + "MZ": "Mozambique", + "NA": "Namibia", + "NC": "New Caledonia", + "NE": "Niger", + "NF": "Norfolk Island", + "NG": "Nigeria", + "NI": "Nicaragua", + "NL": "Netherlands", + "NO": "Norway", + "NP": "Nepal", + "NR": "Nauru", + "NU": "Niue", + "NZ": "New Zealand", + "OM": "Oman", + "PA": "Panama", + "PE": "Peru", + "PF": "French Polynesia", + "PG": "Papua New Guinea", + "PH": "Philippines", + "PK": "Pakistan", + "PL": "Poland", + "PM": "Saint Pierre and Miquelon", + "PN": "Pitcairn", + "PR": "Puerto Rico", + "PS": "Palestine", + "PT": "Portugal", + "PW": "Palau", + "PY": "Paraguay", + "QA": "Qatar", + "RE": "Réunion", + "RO": "Romania", + "RS": "Serbia", + "RU": "Russia", + "RW": "Rwanda", + "SA": "Saudi Arabia", + "SB": "Solomon Islands", + "SC": "Seychelles", + "SD": "Sudan", + "SE": "Sweden", + "SG": "Singapore", + "SH": "Saint Helena, Ascension and Tristan da Cunha", + "SI": "Slovenia", + "SJ": "Svalbard and Jan Mayen", + "SK": "Slovakia", + "SL": "Sierra Leone", + "SM": "San Marino", + "SN": "Senegal", + "SO": "Somalia", + "SR": "Suriname", + "SS": "South Sudan", + "ST": "Sao Tome and Principe", + "SV": "El Salvador", + "SX": "Sint Maarten (Dutch part)", + "SY": "Syria", + "SZ": "Eswatini", + "TC": "Turks and Caicos Islands", + "TD": "Chad", + "TF": "TAAF", + "TG": "Togo", + "TH": "Thailand", + "TJ": "Tajikistan", + "TK": "Tokelau", + "TL": "Timor-Leste", + "TM": "Turkmenistan", + "TN": "Tunisia", + "TO": "Tonga", + "TR": "Turkey", + "TT": "Trinidad and Tobago", + "TV": "Tuvalu", + "TW": "Taiwan", + "TZ": "Tanzania", + "UA": "Ukraine", + "UG": "Uganda", + "UM": "United States Minor Outlying Islands", + "US": "United States", + "UY": "Uruguay", + "UZ": "Uzbekistan", + "VA": "Holy See", + "VC": "Saint Vincent and the Grenadines", + "VE": "Venezuela", + "VG": "British Virgin Islands", + "VI": "U.S. Virgin Islands", + "VN": "Viet Nam", + "VU": "Vanuatu", + "WF": "Wallis and Futuna", + "WS": "Samoa", + "YE": "Yemen", + "YT": "Mayotte", + "XK": "Kosovo", + "ZA": "South Africa", + "ZM": "Zambia", + "ZW": "Zimbabwe", + "EU": "European Union" + }, + "languages": { + "en-US": "English - US", + "fr": "French", + "nl": "Dutch", + "pt_BR": "Portuguese - Brazil" + } + }, + "KnownHosts": { + "label": "Host", + "add": "Voeg nieuwe host toe", + "toast": "Host {mode, select created {toegevoegd} deleted {verwijderd} other {aangepast}}." + }, + "InitializeContainer": { + "title": "WIST JE DAT", + "subtitle": "<1>Cockatrice word gerund door vrijwilligers<1>die van kaartspellen houden!" + }, + "LoginContainer": { + "header": { + "title": "Login", + "subtitle": "Een cross-platform virtual tabletop voor multiplayer kaartspellen." + }, + "footer": { + "registerPrompt": "Nog niet geregistreerd?", + "registerAction": "Maak een account aan", + "credit": "Cockatrice is een open source project", + "version": "Versie" + }, + "content": { + "subtitle1": "Speel multiplayer kaartspellen online.", + "subtitle2": "Cross-platform virtual tabletop voor multiplayer kaartspellen. Gratis en open source." + }, + "toasts": { + "passwordResetSuccessToast": "Wachtwoord Opnieuw Ingesteld", + "accountActivationSuccess": "Account Succesvol Geactiveerd" + } + }, + "UnsupportedContainer": { + "title": "Niet-ondersteunde Browser", + "subtitle1": "Update je browser en/of check je instellingen.", + "subtitle2": "Let op: Private browsing zorgt er voor dat sommige browsers bepaalde toestemmingen of functies uitschakelen." + }, + "AccountActivationDialog": { + "title": "Account Activatie", + "subtitle1": "Je account is nog niet geactiveerd.", + "subtitle2": "Vul de activerings-token die je hebt ontvangen via de activerings-email in." + }, + "KnownHostDialog": { + "title": "{mode, select, edit {Aanpassen} other {Toevoegen}} Bekende Host", + "subtitle": "Door een nieuwe host toe te voegen kun je verbinden met andere servers. Vul hieronder de gegevens in." + }, + "RegistrationDialog": { + "title": "Maak Een Account Aan" + }, + "RequestPasswordResetDialog": { + "title": "Opniew Wachtwoord Aanvragen" + }, + "ResetPasswordDialog": { + "title": "Vraag Wachtwoord Opnieuw Aan" + }, + "AccountActivationForm": { + "error": { + "failed": "Activering account mislukt" + }, + "label": { + "activate": "Activeer Account" + } + }, + "KnownHostForm": { + "help": "Hulp nodig met het toevoegen van een nieuwe host?", + "label": { + "add": "Voeg Host Toe", + "find": "Vind Host" + } + }, + "LoginForm": { + "label": { + "autoConnect": "Verbind Automatisch", + "forgot": "Wachtwoord Vergeten", + "login": "Log in", + "savePassword": "Wachtwoord Opslaan", + "savedPassword": "Opgeslagen Wachtwoord" + } + }, + "RegisterForm": { + "label": { + "register": "Registreer" + }, + "toast": { + "registerSuccess": "Registratie Geslaagd!" + } + }, + "RequestPasswordResetForm": { + "error": "Opniew wachtwoord aanvragen mislukt", + "mfaEnabled": "Server heeft multi-factor authenticatie ingeschakeld", + "request": "Vraag Reset Token Aan", + "skipRequest": "Ik heb al een reset token" + }, + "ResetPasswordForm": { + "error": "Opnieuw wachtwoord aanvragen mislukt", + "label": { + "reset": "Vraag Wachtwoord Opnieuw Aan" + } + } +} \ No newline at end of file diff --git a/webclient/public/locales/pl/translation.json b/webclient/public/locales/pl/translation.json new file mode 100644 index 000000000..abe400792 --- /dev/null +++ b/webclient/public/locales/pl/translation.json @@ -0,0 +1,382 @@ +{ + "Common": { + "language": "Polski (Polish)", + "disconnect": "Rozłącz", + "label": { + "confirmPassword": "Potwierdź Hasło", + "confirmSure": "Czy na pewno?", + "country": "Kraj", + "delete": "Usuń", + "email": "Email", + "hostName": "Nazwa Hosta", + "hostAddress": "Adres Hosta", + "password": "Hasło", + "passwordAgain": "Hasło Ponownie:", + "port": "Port", + "realName": "Prawdziwe Imię", + "saveChanges": "Zapisz zmiany", + "token": "Token", + "username": "Nazwa użytkownika" + }, + "validation": { + "minChars": "Minimum of {count} {count, plural, one {character} other {characters}} required", + "passwordsMustMatch": "Hasła nie są zgodne.", + "required": "Wymagane" + }, + "countries": { + "AD": "Andora", + "AE": "Zjednoczone Emiraty Arabskie", + "AF": "Afganistan", + "AG": "Antigua i Barbuda", + "AI": "Anguilla", + "AL": "Albania", + "AM": "Armenia", + "AO": "Angola", + "AQ": "Antarktyka", + "AR": "Argentyna", + "AS": "Samoa Amerykańskie", + "AT": "Austria", + "AU": "Australia", + "AW": "Aruba", + "AX": "Wyspy Alandzkie", + "AZ": "Azerbejdżan", + "BA": "Bośnia i Hercegowina", + "BB": "Barbados", + "BD": "Bangladesz", + "BE": "Belgia", + "BF": "Burkina Faso", + "BG": "Bułgaria", + "BH": "Bahrajn", + "BI": "Burundi", + "BJ": "Benin", + "BL": "Saint-Barthélemy", + "BM": "Bermudy", + "BN": "Brunei", + "BO": "Boliwia", + "BQ": "Bonaire, Sint Eustatius i Saba", + "BR": "Brazylia", + "BS": "Bahamy", + "BT": "Bhutan", + "BV": "Wyspa Bouveta", + "BW": "Botwsana", + "BY": "Białoruś", + "BZ": "Belize", + "CA": "Kanada", + "CC": "Wyspy Kokosowe", + "CD": "Demokratyczna Republika Kongo", + "CF": "Republika Środkowoafrykańska", + "CG": "Kongo", + "CH": "Szwajcaria", + "CI": "Wybrzeże Kości Słoniowej", + "CK": "Wyspa Cooka", + "CL": "Chile", + "CM": "Kamerun", + "CN": "Chiny", + "CO": "Kolumbia", + "CR": "Kostaryka", + "CU": "Kuba", + "CV": "Republika Zielonego Przylądka", + "CW": "Curaçao", + "CX": "Wyspa Bożego Narodzenia", + "CY": "Cypr", + "CZ": "Czechy", + "DE": "Niemcy", + "DJ": "Dżibuti", + "DK": "Dania", + "DM": "Dominika", + "DO": "Dominikana", + "DZ": "Algieria", + "EC": "Ekwador", + "EE": "Estonia", + "EG": "Egipt", + "EH": "Sahara Zachodnia", + "ER": "Erytrea", + "ES": "Hiszpania", + "ET": "Etiopia", + "FI": "Finlandia", + "FJ": "Fidżi", + "FK": "Falklandy", + "FM": "Mikronezja", + "FO": "Wyspy Owcze", + "FR": "Francja", + "GA": "Gabon", + "GB": "Wielka Brytania", + "GD": "Grenada", + "GE": "Gruzja", + "GF": "Gujana Francuska", + "GG": "Guernsey", + "GH": "Ghana", + "GI": "Gibraltar", + "GL": "Grenlandia", + "GM": "Gambia", + "GN": "Gwinea", + "GP": "Gwadelupa", + "GQ": "Gwinea Równikowa", + "GR": "Grecja", + "GS": "Georgia Południowa i Sandwich Południowy", + "GT": "Gwatemala", + "GU": "Guam", + "GW": "Gwinea Bissau", + "GY": "Gujana", + "HK": "Hongkong", + "HM": "Wyspy Heard i McDonalda", + "HN": "Honduras", + "HR": "Chorwacja", + "HT": "Haiti", + "HU": "Węgry", + "ID": "Indonezja", + "IE": "Irlandia", + "IL": "Israel", + "IM": "Wyspa Man", + "IN": "Indie", + "IO": "Brytyjskie Terytorium Oceanu Indyjskiego", + "IQ": "Irak", + "IR": "Iran", + "IS": "Islandia", + "IT": "Włochy", + "JE": "Jersey", + "JM": "Jamajka", + "JO": "Jordania", + "JP": "Japonia", + "KE": "Kenia", + "KG": "Kirgistan", + "KH": "Kambodża", + "KI": "Kiribati", + "KM": "Komory", + "KN": "Saint Kitts i Nevis", + "KP": "Korea Północna", + "KR": "Korea Południowa", + "KW": "Kuwejt", + "KY": "Kajmany", + "KZ": "Kazachstan", + "LA": "Laos", + "LB": "Liban", + "LC": "Saint Lucia", + "LI": "Liechtenstein", + "LK": "Sri Lanka", + "LR": "Liberia", + "LS": "Lesotho", + "LT": "Litwa", + "LU": "Luksemburg", + "LV": "Łotwa", + "LY": "Libia", + "MA": "Maroko", + "MC": "Monako", + "MD": "Mołdawia", + "ME": "Czarnogóra", + "MF": "Saint-Martin", + "MG": "Madagaskar", + "MH": "Wyspy Marshalla", + "MK": "Macedonia Północna", + "ML": "Mali", + "MM": "Mjanma", + "MN": "Mongolia", + "MO": "Makau", + "MP": "Mariany Północne", + "MQ": "Martynika", + "MR": "Mauretania", + "MS": "Montserrat", + "MT": "Malta", + "MU": "Mauritius", + "MV": "Malediwy", + "MW": "Malawi", + "MX": "Meksyk", + "MY": "Malezja", + "MZ": "Mozambik", + "NA": "Namibia", + "NC": "Nowa Kaledonia", + "NE": "Niger", + "NF": "Wyspa Norfolk", + "NG": "Nigeria", + "NI": "Nikaragua", + "NL": "Holandia", + "NO": "Norwegia", + "NP": "Nepal", + "NR": "Nauru", + "NU": "Niue", + "NZ": "Nowa Zelandia", + "OM": "Oman", + "PA": "Panama", + "PE": "Peru", + "PF": "Polinezja Francuska", + "PG": "Papua-Nowa Gwinea", + "PH": "Filipiny", + "PK": "Pakistan", + "PL": "Polska", + "PM": "Saint-Pierre i Miquelon", + "PN": "Pitcairn", + "PR": "Portoryko", + "PS": "Palestyna", + "PT": "Portugalia", + "PW": "Palau", + "PY": "Paragwaj", + "QA": "Katar", + "RE": "Reunion", + "RO": "Rumunia", + "RS": "Serbia", + "RU": "Rosja", + "RW": "Rwanda", + "SA": "Arabia Saudyjska", + "SB": "Wyspy Solomona", + "SC": "Seszele", + "SD": "Sudan", + "SE": "Szwecja", + "SG": "Singapur", + "SH": "Wyspa Świętej Heleny, Wyspa Wniebowstąpienia i Tristan da Cunha", + "SI": "Słowenia", + "SJ": "Svalbard i Jan Mayen", + "SK": "Słowacja", + "SL": "Sierra Leone", + "SM": "San Marino", + "SN": "Senegal", + "SO": "Somalia", + "SR": "Surinam", + "SS": "Sudan Południowy", + "ST": "Wyspy Świętego Tomasza i Książęca", + "SV": "Salwador", + "SX": "Sint Maarten", + "SY": "Syria", + "SZ": "Eswatini", + "TC": "Turks i Caicos", + "TD": "Czad", + "TF": "FTPA", + "TG": "Togo", + "TH": "Tajlandia", + "TJ": "Tadżykistan", + "TK": "Tokelau", + "TL": "Timor Wschodni", + "TM": "Turkmenistan", + "TN": "Tunezja", + "TO": "Tonga", + "TR": "Turcja", + "TT": "Trynidad i Tobago", + "TV": "Tuvalu", + "TW": "Tajwan", + "TZ": "Tanzania", + "UA": "Ukraina", + "UG": "Uganda", + "UM": "Dalekie Wyspy Mniejsze Stanów Zjednoczonych", + "US": "Stany Zjednoczone", + "UY": "Urugwaj", + "UZ": "Uzbekistan", + "VA": "Watykan", + "VC": "Saint Vincent i Grenadyny", + "VE": "Wenezuela", + "VG": "Brytyjskie Wyspy Dziewicze", + "VI": "Wyspy Dziewicze Stanów Zjednoczonych", + "VN": "Wietnam", + "VU": "Vanuatu", + "WF": "Wallis i Futuna", + "WS": "Samoa", + "YE": "Jemen", + "YT": "Majotta", + "XK": "Kosowo", + "ZA": "Południowa Afryka", + "ZM": "Zambia", + "ZW": "Zimbambwe", + "EU": "Unia Europejska" + }, + "languages": { + "en-US": "Angielski - USA", + "fr": "Francuski", + "nl": "Holenderski", + "pt_BR": "Portugalski - Brazylia" + } + }, + "KnownHosts": { + "label": "Host", + "add": "Dodaj nowego hosta", + "toast": "Host successfully {mode, select, created {created} deleted {deleted} other {edited}}." + }, + "InitializeContainer": { + "title": "WIEDZIAŁEŚ", + "subtitle": "<1>Cockatrice jest prowadzony przez ochotników<1>którzy uwielbiają karcianki!" + }, + "LoginContainer": { + "header": { + "title": "Login", + "subtitle": "Wieloplatformowy wirtualny stół do wieloosobowych gier karcianych." + }, + "footer": { + "registerPrompt": "Jeszcze nie zarejestrowany?", + "registerAction": "Stwórz konto", + "credit": "Cockatrice jest projektem open source", + "version": "Wersja" + }, + "content": { + "subtitle1": "Graj wieloosobowe karcianki online.", + "subtitle2": "Wieloplatformowy wirtualny stół do wieloosobowych gier karcianych. Zawsze darmowy i wolny." + }, + "toasts": { + "passwordResetSuccessToast": "Hasło Pomyślnie Zresetowany", + "accountActivationSuccess": "Konto Pomyślnie Aktywowane" + } + }, + "UnsupportedContainer": { + "title": "Nieobsługiwana Przeglądarka", + "subtitle1": "Proszę zaktualizuj swoją przeglądarkę i/lub sprawdź jej uprawnienia.", + "subtitle2": "Przeglądanie prywatne może wyłączyć niektóre uprawnienia lub funkcje." + }, + "AccountActivationDialog": { + "title": "Aktywacja Konta", + "subtitle1": "Twoje konto nie zostało jeszcze aktywowane.", + "subtitle2": "Musisz podać token aktywujący, otrzymany w wiadomości email." + }, + "KnownHostDialog": { + "title": "{mode, select, edit {Edit} other {Add}} Known Host", + "subtitle": "Dodanie nowego hosta pozwoli Ci połączyć się z innymi serwerami. Podaj jego dane poniżej aby dodać go do listy." + }, + "RegistrationDialog": { + "title": "Stwórz Nowe Konto" + }, + "RequestPasswordResetDialog": { + "title": "Żądanie Resetu Hasła" + }, + "ResetPasswordDialog": { + "title": "Zresetuj hasło" + }, + "AccountActivationForm": { + "error": { + "failed": "Aktywacja konta zakończona niepowodzeniem" + }, + "label": { + "activate": "Aktywuj Konto" + } + }, + "KnownHostForm": { + "help": "Potrzebujesz pomocy z dodaniem nowego hosta?", + "label": { + "add": "Dodaj Hosta", + "find": "Znajdź Hosta" + } + }, + "LoginForm": { + "label": { + "autoConnect": "Połącz automatycznie", + "forgot": "Zapomniałem hasła", + "login": "Login", + "savePassword": "Zapisz hasło", + "savedPassword": "Zapisane Hasło" + } + }, + "RegisterForm": { + "label": { + "register": "Zarejestruj się" + }, + "toast": { + "registerSuccess": "Rejestracja powiodła się!" + } + }, + "RequestPasswordResetForm": { + "error": "Żądanie resetu hasła nie powiodło się.", + "mfaEnabled": "Serwer ma włączone uwierzytelnianie wielkoskładnikowe", + "request": "Zażądaj Token Resetu", + "skipRequest": "Mam już token resetu" + }, + "ResetPasswordForm": { + "error": "Żądanie resetu hasła nie powiodło się.", + "label": { + "reset": "Zresetuj hasło" + } + } +} \ No newline at end of file diff --git a/webclient/public/locales/pt_BR/translation.json b/webclient/public/locales/pt_BR/translation.json new file mode 100644 index 000000000..10ad08603 --- /dev/null +++ b/webclient/public/locales/pt_BR/translation.json @@ -0,0 +1,382 @@ +{ + "Common": { + "language": "Português do Brasil (Brazilian Portuguese)", + "disconnect": "Desconectar", + "label": { + "confirmPassword": "Confirmar senha", + "confirmSure": "Você tem certeza?", + "country": "País", + "delete": "Excluir", + "email": "Email", + "hostName": "Nome do host", + "hostAddress": "Endereço do host", + "password": "Senha", + "passwordAgain": "Senha novamente", + "port": "Porta", + "realName": "Nome Real", + "saveChanges": "Salvar Mudanças", + "token": "Ficha", + "username": "Nome de usuário" + }, + "validation": { + "minChars": "Mínimo de {count} {count, plural, one {caractere} other {caracteres}} necessários", + "passwordsMustMatch": "As senhas não correspondem", + "required": "Campo obrigatório" + }, + "countries": { + "AD": "Andorra", + "AE": "Emirados Árabes Unidos", + "AF": "Afeganistão", + "AG": "Antígua e Barbuda", + "AI": "Anguila", + "AL": "Albânia", + "AM": "Armênia", + "AO": "Angola", + "AQ": "Antártica", + "AR": "Argentina", + "AS": "Samoa Americana", + "AT": "Áustria", + "AU": "Austrália", + "AW": "Aruba", + "AX": "Ilhas Aland", + "AZ": "Azerbaijão", + "BA": "Bósnia e Herzegovina", + "BB": "Barbados", + "BD": "Bangladesh", + "BE": "Bélgica", + "BF": "Burquina Faso", + "BG": "Bulgária", + "BH": "Bahrein", + "BI": "Burundi", + "BJ": "Benim", + "BL": "São Bartolomeu", + "BM": "Bermudas", + "BN": "Brunei", + "BO": "Bolivia", + "BQ": "Bonaire, Santo Eustáquio e Saba", + "BR": "Brasil", + "BS": "Bahamas", + "BT": "Butão", + "BV": "Ilha Bouvet", + "BW": "Botsuana", + "BY": "Bielorrússia", + "BZ": "Belize", + "CA": "Canadá", + "CC": "Ilhas Cocos (Keeling)", + "CD": "RD Congo", + "CF": "República Centro-Africana", + "CG": "República do Congo", + "CH": "Suiça", + "CI": "Costa do Marfim", + "CK": "Ilhas Cook", + "CL": "Chile", + "CM": "Camarões", + "CN": "China", + "CO": "Colômbia", + "CR": "Costa Rica", + "CU": "Cuba", + "CV": "Cabo Verde", + "CW": "Curaçao", + "CX": "Ilha Christmas", + "CY": "Chipre", + "CZ": "Tchéquia", + "DE": "Alemanha", + "DJ": "Djibuti", + "DK": "Dinamarca", + "DM": "Dominica", + "DO": "República Dominicana", + "DZ": "Argélia", + "EC": "Equador", + "EE": "Estônia", + "EG": "Egito", + "EH": "Saara Ocidental", + "ER": "Eritreia", + "ES": "Espanha", + "ET": "Etiópia", + "FI": "Finlândia", + "FJ": "Fiji", + "FK": "Ilhas Malvinas", + "FM": "Micronésia", + "FO": "Ilhas Faroé", + "FR": "França", + "GA": "Gabão", + "GB": "Reino Unido", + "GD": "Granada", + "GE": "Geórgia", + "GF": "Guiana Francesa", + "GG": "Guernsey", + "GH": "Gana", + "GI": "Gibraltar", + "GL": "Groenlândia", + "GM": "Gâmbia", + "GN": "Guiné", + "GP": "Guadalupe", + "GQ": "Guiné Equatorial", + "GR": "Grécia", + "GS": "Ilhas Geórgia do Sul e Sandwich do Sul", + "GT": "Guatemala", + "GU": "Guam", + "GW": "Guiné-Bissau", + "GY": "Guiana", + "HK": "Hong Kong", + "HM": "Ilha Head e Ilhas McDonald", + "HN": "Honduras", + "HR": "Croácia", + "HT": "Haiti", + "HU": "Hungria", + "ID": "Indonésia", + "IE": "Irlanda", + "IL": "Israel", + "IM": "Ilha de Man", + "IN": "Índia", + "IO": "Território Britânico do Oceano Índico", + "IQ": "Iraque", + "IR": "Irã", + "IS": "Islândia", + "IT": "Itália", + "JE": "Jersey", + "JM": "Jamaica", + "JO": "Jordânia", + "JP": "Japão", + "KE": "Quênia", + "KG": "Quirguistão", + "KH": "Cambodja", + "KI": "Quiribati", + "KM": "Comores", + "KN": "São Cristóvão e Névis", + "KP": "Coréia do Norte", + "KR": "Coreia do Sul", + "KW": "Kuwait", + "KY": "Ilhas Cayman", + "KZ": "Cazaquistão", + "LA": "Laos", + "LB": "Líbano", + "LC": "Santa Lúcia", + "LI": "Liechtenstein", + "LK": "Sri Lanka", + "LR": "Libéria", + "LS": "Lesoto", + "LT": "Lituânia", + "LU": "Luxemburgo", + "LV": "Letônia", + "LY": "Líbia", + "MA": "Marrocos", + "MC": "Mônaco", + "MD": "Moldávia", + "ME": "Montenegro", + "MF": "São Martinho (França)", + "MG": "Madagáscar", + "MH": "Ilhas Marshall", + "MK": "Macedônia do Norte", + "ML": "Mali", + "MM": "Mianmar", + "MN": "Mongólia", + "MO": "Macau", + "MP": "Ilhas Marianas do Norte", + "MQ": "Martinica", + "MR": "Mauritânia", + "MS": "Monserrate", + "MT": "Malta", + "MU": "Ilhas Maurício", + "MV": "Maldivas", + "MW": "Malawi", + "MX": "México", + "MY": "Malásia", + "MZ": "Moçambique", + "NA": "Namíbia", + "NC": "Nova Caledônia", + "NE": "Níger", + "NF": "Ilha Norfolk", + "NG": "Nigéria", + "NI": "Nicarágua", + "NL": "Holanda", + "NO": "Noruega", + "NP": "Nepal", + "NR": "Nauru", + "NU": "Niue", + "NZ": "Nova Zelândia", + "OM": "Omã", + "PA": "Panamá", + "PE": "Peru", + "PF": "Polinésia Francesa", + "PG": "Papua Nova Guiné", + "PH": "Filipinas", + "PK": "Paquistão", + "PL": "Polônia", + "PM": "Saint-Pierre e Miquelon", + "PN": "Ilhas Pitcairn", + "PR": "Porto Rico", + "PS": "Palestina", + "PT": "Portugal", + "PW": "Palau", + "PY": "Paraguai", + "QA": "Catar", + "RE": "Ilha da Reunião", + "RO": "Romênia", + "RS": "Sérvia", + "RU": "Rússia", + "RW": "Ruanda", + "SA": "Arábia Saudita", + "SB": "Ilhas Salomão", + "SC": "Seychelles", + "SD": "Sudão", + "SE": "Suécia", + "SG": "Cingapura", + "SH": "Santa Helena, Ascensão e Tristão da Cunha", + "SI": "Eslovênia", + "SJ": "Svalbard e Jan Mayen", + "SK": "Eslováquia", + "SL": "Serra Leoa", + "SM": "San Marino", + "SN": "Senegal", + "SO": "Somália", + "SR": "Suriname", + "SS": "Sudão do Sul", + "ST": "São Tomé e Príncipe", + "SV": "El Salvador", + "SX": "São Martinho (Holanda)", + "SY": "Síria", + "SZ": "Essuatíni", + "TC": "Ilhas Turcas e Caicos", + "TD": "Chad", + "TF": "TAAF", + "TG": "Togo", + "TH": "Tailândia", + "TJ": "Tajiquistão", + "TK": "Tokelau", + "TL": "Timor-Leste", + "TM": "Turcomenistão", + "TN": "Tunísia", + "TO": "Tonga", + "TR": "Turquia", + "TT": "Trindade e Tobago", + "TV": "Tuvalu", + "TW": "Taiwan", + "TZ": "Tanzânia", + "UA": "Ucrânia", + "UG": "Uganda", + "UM": "Ilhas Menores Distantes dos Estados Unidos", + "US": "Estados Unidos", + "UY": "Uruguai", + "UZ": "Uzbequistão", + "VA": "Santa Sé", + "VC": "São Vicente e Granadinas", + "VE": "Venezuela", + "VG": "Ilhas Virgens Britânicas", + "VI": "Ilhas Virgens Americanas", + "VN": "Vietnã", + "VU": "Vanuatu", + "WF": "Wallis e Futuna", + "WS": "Samoa", + "YE": "Iêmen", + "YT": "Mayotte", + "XK": "Kosovo", + "ZA": "África do Sul", + "ZM": "Zâmbia", + "ZW": "Zimbábue", + "EU": "União Européia" + }, + "languages": { + "en-US": "Inglês - EUA", + "fr": "Francês", + "nl": "Holandês", + "pt_BR": "Português - Brasil" + } + }, + "KnownHosts": { + "label": "Servidor", + "add": "Adicionar novo servidor", + "toast": "Servidor {mode, select, created {criado} deleted {deletado} other {editado}} com sucesso." + }, + "InitializeContainer": { + "title": "VOCÊ SABIA?", + "subtitle": "<1>O Cockatrice é administrado por voluntários<1>que adoram jogos de cartas!" + }, + "LoginContainer": { + "header": { + "title": "Login", + "subtitle": "Uma mesa virtual multiplataforma para jogos multijogador de carta." + }, + "footer": { + "registerPrompt": "Não registrou ainda?", + "registerAction": "Crie uma conta", + "credit": "Cockatrice é um projeto de código aberto", + "version": "Versão" + }, + "content": { + "subtitle1": "Jogue jogos multijogador de cartas online.", + "subtitle2": "Mesa virtual multiplataforma para jogos multijogador de carta. Gratuito para sempre." + }, + "toasts": { + "passwordResetSuccessToast": "Senha redefinida com sucesso", + "accountActivationSuccess": "Conta ativada com sucesso" + } + }, + "UnsupportedContainer": { + "title": "Navegador não suportado", + "subtitle1": "Por favor atualize seu navegador de internet e/ou check as permissões", + "subtitle2": "Observação: a navegação privada faz com que alguns navegadores desativem determinadas permissões ou recursos." + }, + "AccountActivationDialog": { + "title": "Ativação da conta", + "subtitle1": "Sua conta ainda não foi ativada.", + "subtitle2": "Você precisa fornecer o código de ativação recebido por email." + }, + "KnownHostDialog": { + "title": "{mode, select, edit {Editar} other {Adicionar}} servidor conhecido", + "subtitle": "Adicionar um novo host permite que você se conecte a diferentes servidores. Insira os detalhes abaixo em sua lista de hosts." + }, + "RegistrationDialog": { + "title": "Criar Nova Conta" + }, + "RequestPasswordResetDialog": { + "title": "Solicitar redefinição de senha" + }, + "ResetPasswordDialog": { + "title": "Redefinir Senha" + }, + "AccountActivationForm": { + "error": { + "failed": "Falha na activação de conta" + }, + "label": { + "activate": "Ativar Conta" + } + }, + "KnownHostForm": { + "help": "Necessita de ajuda para adicionar um novo host?", + "label": { + "add": "Adicionar Host", + "find": "Buscar Host" + } + }, + "LoginForm": { + "label": { + "autoConnect": "Conectar Automaticamente", + "forgot": "Esqueci minha senha", + "login": "Login", + "savePassword": "Salvar Senha", + "savedPassword": "Salvar Senha" + } + }, + "RegisterForm": { + "label": { + "register": "Registrar" + }, + "toast": { + "registerSuccess": "Registro bem-sucedido" + } + }, + "RequestPasswordResetForm": { + "error": "Falha na solicitação de redefinição de senha", + "mfaEnabled": "O servidor tem autenticação de multi-fator ativada", + "request": "Solicitar código de redefinição", + "skipRequest": "Eu já tenho um código de redefinição" + }, + "ResetPasswordForm": { + "error": "Falha na redefinição de senha", + "label": { + "reset": "Redefinir Senha" + } + } +} \ No newline at end of file diff --git a/webclient/public/locales/ru/translation.json b/webclient/public/locales/ru/translation.json new file mode 100644 index 000000000..5e92f790e --- /dev/null +++ b/webclient/public/locales/ru/translation.json @@ -0,0 +1,382 @@ +{ + "Common": { + "language": "Русский (Russian)", + "disconnect": "Прервать подключение", + "label": { + "confirmPassword": "Подтверждение пароля:", + "confirmSure": "Вы уверены?", + "country": "Страна:", + "delete": "Удалить", + "email": "Адрес email:", + "hostName": "Наименование сервера", + "hostAddress": "Адрес сервера", + "password": "Пароль:", + "passwordAgain": "Подтверждение пароля:", + "port": "&Порт:", + "realName": "Настоящее имя:", + "saveChanges": "Сохранить изменения", + "token": "Фишка", + "username": "Имя пользователя:" + }, + "validation": { + "minChars": "Как минимум {count} {count, plural, one {character} other {characters}} необходимо", + "passwordsMustMatch": "Введенные пароли не совпадают.", + "required": "Необходимо" + }, + "countries": { + "AD": "Андорра", + "AE": "ОАЭ", + "AF": "Афганистан", + "AG": "Антигуа и Барбуда", + "AI": "Ангилья", + "AL": "Албания", + "AM": "Армения", + "AO": "Ангола", + "AQ": "Антарктика", + "AR": "Аргентина", + "AS": "Американское Самоа", + "AT": "Австрия", + "AU": "Австралия", + "AW": "Аруба", + "AX": "Аландские острова", + "AZ": "Азербайджан", + "BA": "Босния и Герцеговина", + "BB": "Барбадос", + "BD": "Бангладеш", + "BE": "Бельгия", + "BF": "Буркина Фасо", + "BG": "Болгария", + "BH": "Бахрейн", + "BI": "Бурунди", + "BJ": "Бенин", + "BL": "Сен-Бартелеми", + "BM": "Бермуды", + "BN": "Бруней", + "BO": "Боливия", + "BQ": "Бонэйр", + "BR": "Бразилия", + "BS": "Багамы", + "BT": "Бутан", + "BV": "Остров Буве", + "BW": "Ботсвана", + "BY": "Беларусь", + "BZ": "Белиз", + "CA": "Канада", + "CC": "Кокосовые острова", + "CD": "ДР Конго", + "CF": "ЦАР", + "CG": "Республика Конго", + "CH": "Швейцария", + "CI": "Кот-д'Ивуар", + "CK": "Острова Кука", + "CL": "Чили", + "CM": "Камерун", + "CN": "Китай", + "CO": "Колумбия", + "CR": "Коста-Рика", + "CU": "Куба", + "CV": "Кабо-Верде", + "CW": "Кюрасао", + "CX": "Остров Рождества", + "CY": "Кипр", + "CZ": "Чехия", + "DE": "Германия", + "DJ": "Джибути", + "DK": "Дания", + "DM": "Доминика", + "DO": "Доминиканская Республика", + "DZ": "Алжир", + "EC": "Эквадор", + "EE": "Эстония", + "EG": "Египет", + "EH": "Западная Сахара", + "ER": "Эритрея", + "ES": "Испания", + "ET": "Эфиопия", + "FI": "Финляндия", + "FJ": "Фиджи", + "FK": "Фолклендские острова", + "FM": "Микронезия", + "FO": "Фарерские о-ва", + "FR": "Франция", + "GA": "Габон", + "GB": "Великобритания", + "GD": "Гренада", + "GE": "Грузия", + "GF": "Французская Гвиана", + "GG": "Гернси", + "GH": "Гана", + "GI": "Гибралтар", + "GL": "Гренландия", + "GM": "Гамбия", + "GN": "Гвинея", + "GP": "Гваделупа", + "GQ": "Экваториальная Гвинея", + "GR": "Греция", + "GS": "Южная Георгия и Южные Сандвичевы острова", + "GT": "Гватемала", + "GU": "Гуам", + "GW": "Гвинея-Бисау", + "GY": "Гайана", + "HK": "Гонконг", + "HM": "Остров Херд и остров Макдональд", + "HN": "Гондурас", + "HR": "Хорватия", + "HT": "Гаити", + "HU": "Венгрия", + "ID": "Индонезия", + "IE": "Ирландия", + "IL": "Израиль", + "IM": "о-в Мэн", + "IN": "Индия", + "IO": "Британская территория в Индийском океане ", + "IQ": "Ирак", + "IR": "Иран", + "IS": "Исландия", + "IT": "Италия", + "JE": "Джерси", + "JM": "Ямайка", + "JO": "Иордания", + "JP": "Япония", + "KE": "Кения", + "KG": "Киргизия", + "KH": "Камбоджия", + "KI": "Кирибати", + "KM": "Коморы", + "KN": "Сент-Китс и Невис", + "KP": "Северная Корея", + "KR": "Южная Корея", + "KW": "Кувейт", + "KY": "Каймановы острова", + "KZ": "Казахстан", + "LA": "Лаос", + "LB": "Ливан", + "LC": "Сент-Люсия", + "LI": "Лихтенштейн", + "LK": "Шри-Ланка", + "LR": "Либерия", + "LS": "Лесото", + "LT": "Литва", + "LU": "Люксембург", + "LV": "Латвия", + "LY": "Ливия", + "MA": "Морокко", + "MC": "Монако", + "MD": "Молдавия", + "ME": "Черногория", + "MF": "Сен-Мартен (владение Франции)", + "MG": "Мадагарскар", + "MH": "Маршалловы о-ва", + "MK": "Северная Македония", + "ML": "Мали", + "MM": "Мьянма (Бирма)", + "MN": "Монголия", + "MO": "Макао", + "MP": "Северные Марианские о-ва", + "MQ": "Мартиника", + "MR": "Мавритания", + "MS": "Монтсеррат", + "MT": "Мальта", + "MU": "о. Маврикий", + "MV": "Мальдивы", + "MW": "Малави", + "MX": "Мексика", + "MY": "Малазия", + "MZ": "Мозамбик", + "NA": "Намибия", + "NC": "Новая Каледония", + "NE": "Нигер", + "NF": "Остров Норфолк", + "NG": "Нигерия", + "NI": "Никарагуа", + "NL": "Нидерланды", + "NO": "Норвегия", + "NP": "Непал", + "NR": "Науру", + "NU": "Ниуэ", + "NZ": "Новая Зеландия", + "OM": "Оман", + "PA": "Панама", + "PE": "Перу", + "PF": "Французская Полинезия", + "PG": "Папуа-Новая Гвинея", + "PH": "Филиппины", + "PK": "Пакистан", + "PL": "Польша", + "PM": "Сен-Пьер и Микелон", + "PN": "о-ва Питкэрн", + "PR": "Пуэрто-Рико", + "PS": "Палестина", + "PT": "Португалия", + "PW": "Палау", + "PY": "Парагва", + "QA": "Катар", + "RE": "Реюньон", + "RO": "Румыния", + "RS": "Сербия", + "RU": "Российская Федерация", + "RW": "Руанда", + "SA": "Саудовская Аравия", + "SB": "Соломоновы о-ва", + "SC": "Сейшелы", + "SD": "Судан", + "SE": "Швеция", + "SG": "Сингапур", + "SH": "о. Св. Елены", + "SI": "Словения", + "SJ": "Шпицберген и Ян-Майен", + "SK": "Словакия", + "SL": "Сьерра-Леоне", + "SM": "Сан-Марино", + "SN": "Сенегал", + "SO": "Сомали", + "SR": "Суринам", + "SS": "Южный Судан", + "ST": "Сан-Томе и Принсипи", + "SV": "Сальвадор", + "SX": "Синт-Мартен (Дания)", + "SY": "Сирия", + "SZ": "Эсватини", + "TC": "Острова Теркс и Кайкос", + "TD": "Чад", + "TF": "Французские Южные и Антарктические территории", + "TG": "Того", + "TH": "Тайланд", + "TJ": "Таджикистан", + "TK": "Токелау", + "TL": "Восточный Тимор", + "TM": "Туркмения", + "TN": "Тунис", + "TO": "Тонго", + "TR": "Турция", + "TT": "Тринидад и Тобаго", + "TV": "Тувалу", + "TW": "Тайвань", + "TZ": "Танзания", + "UA": "Украина", + "UG": "Уганда", + "UM": "Внешние малые о-ва (США)", + "US": "США", + "UY": "Уругвай", + "UZ": "Узбекистан", + "VA": "Святой Престол", + "VC": "Сент-Винсент и Гренадины", + "VE": "Венесуэла", + "VG": "Британские Виргинские острова", + "VI": "Американские Виргинские острова", + "VN": "Вьетнам", + "VU": "Вануату", + "WF": "о-ва Уоллис и Футуна", + "WS": "Самоа", + "YE": "Йемен", + "YT": "Майотта", + "XK": "Косово", + "ZA": "ЮАР", + "ZM": "Замбия", + "ZW": "Зимбабве", + "EU": "Европейский Союз" + }, + "languages": { + "en-US": "Английский - США", + "fr": "Французский", + "nl": "Датский", + "pt_BR": "Португальский - Бразилия" + } + }, + "KnownHosts": { + "label": "&Хост", + "add": "Добавить новый сервер", + "toast": "Сервер успешно {mode, select, created {created} deleted {deleted} other {edited}}." + }, + "InitializeContainer": { + "title": "ЗНАЕТЕ ЛИ ВЫ", + "subtitle": "<1>Cockatrice поддерживается энтузиастами<1>которые любят карточные игры!" + }, + "LoginContainer": { + "header": { + "title": "Логин", + "subtitle": "Кросс-платформенный виртуальный стол для мультиплеерных ККИ" + }, + "footer": { + "registerPrompt": "Ещё не зарегистрированы?", + "registerAction": "Создать учетную запись", + "credit": "Cockatrice - проект с открытым исходным кодом", + "version": "Версия" + }, + "content": { + "subtitle1": "Играйте в мультиплеерные ККИ онлайн", + "subtitle2": "Кросс-платформенный виртуальный стол для мультиплеерных ККИ. Всегда будет бесплатным." + }, + "toasts": { + "passwordResetSuccessToast": "Пароль сброшен успешно", + "accountActivationSuccess": "Учетная запись активирована успешно" + } + }, + "UnsupportedContainer": { + "title": "Браузер не поддерживается", + "subtitle1": "Обновите бразуер и/или проверьте права доступа", + "subtitle2": "Обратите внимание, что использование анонимных браузеров может привести к неработоспособности некоторых фич и проблемами с правами доступа" + }, + "AccountActivationDialog": { + "title": "Активация учетной записи", + "subtitle1": "Ваша учетная запись не активирована", + "subtitle2": "Ваш аккаунт пока не активирован. Вам необходимо следовать указаниям по активации, отправленным на ваш email" + }, + "KnownHostDialog": { + "title": "{модифицировать, выбрать, редактировать {Edit} другой {Add}} известный хост", + "subtitle": "Добавление нового хоста позволит вам присоединяться к различным серверам. Введите данные о сервере в ваш список хостов" + }, + "RegistrationDialog": { + "title": "Создать новый аккаунт" + }, + "RequestPasswordResetDialog": { + "title": "Запросить сброс пароля" + }, + "ResetPasswordDialog": { + "title": "Сбросить пароль" + }, + "AccountActivationForm": { + "error": { + "failed": "Не удалось активировать аккаунт" + }, + "label": { + "activate": "Активировать аккаунт" + } + }, + "KnownHostForm": { + "help": "Нужна помощь в добавлении нового хоста?", + "label": { + "add": "Добавить хост", + "find": "Найти хост" + } + }, + "LoginForm": { + "label": { + "autoConnect": "Автоматическое подключение", + "forgot": "Забыли пароль", + "login": "Логин", + "savePassword": "Сохранить пароль", + "savedPassword": "Сохранённый пароль" + } + }, + "RegisterForm": { + "label": { + "register": "Зарегистрироваться" + }, + "toast": { + "registerSuccess": "Регистрация прошла успешно!" + } + }, + "RequestPasswordResetForm": { + "error": "Не удалось запросить сброс пароля", + "mfaEnabled": "На сервере включена многофакторная аутентификация", + "request": "Запросить токен сброса", + "skipRequest": "У меня уже есть токен сброса" + }, + "ResetPasswordForm": { + "error": "Не удалось сбросить пароль", + "label": { + "reset": "Сбросить пароля" + } + } +} \ No newline at end of file diff --git a/webclient/public/locales/tok/translation.json b/webclient/public/locales/tok/translation.json new file mode 100644 index 000000000..46437f558 --- /dev/null +++ b/webclient/public/locales/tok/translation.json @@ -0,0 +1,382 @@ +{ + "Common": { + "language": "Toki Pona", + "disconnect": "mi o weka tan kulupu", + "label": { + "confirmPassword": "nimi ken li pona ala pona", + "confirmSure": "ni li pona ala pona", + "country": "ma", + "delete": "mi o weka e ni", + "email": "nimi Email", + "hostName": "nimi pi ilo lawa", + "hostAddress": "ma pi ilo lawa", + "password": "nimi ken pi kama sina", + "passwordAgain": "o pana sin e nimi ken pi kama sina", + "port": "nanpa pi ilo lawa", + "realName": "nimi sina lon", + "saveChanges": "mi o awen e ante sina", + "token": "ijo lili", + "username": "nimi sina" + }, + "validation": { + "minChars": "Minimum of {count} {count, plural, one {character} other {characters}} required", + "passwordsMustMatch": "nimi ken nanpa wan li sama ala nimi ken nanpa tu", + "required": "o ni" + }, + "countries": { + "AD": "ma Antola", + "AE": "United Arab Emirates", + "AF": "ma Akanisan", + "AG": "Antigua and Barbuda", + "AI": "Anguilla", + "AL": "ma Sipe", + "AM": "ma Aja", + "AO": "ma Ankola", + "AQ": "ma Antasika", + "AR": "ma Alensina", + "AS": "American Samoa", + "AT": "ma Esalasi", + "AU": "ma Oselija", + "AW": "Aruba", + "AX": "Åland Islands", + "AZ": "Azerbaijan", + "BA": "Bosnia and Herzegovina", + "BB": "ma Papeto", + "BD": "ma Panla", + "BE": "ma Pesije", + "BF": "ma Pukinapaso", + "BG": "ma Pokasi", + "BH": "ma Palani", + "BI": "Burundi", + "BJ": "ma Penen", + "BL": "Saint Barthélemy", + "BM": "ma Pemuta", + "BN": "Brunei Darussalam", + "BO": "Bolivia", + "BQ": "Bonaire, Sint Eustatius and Saba", + "BR": "ma Pasila", + "BS": "ma Pawama", + "BT": "ma Putan", + "BV": "Bouvet Island", + "BW": "ma Posuwana", + "BY": "ma Pelalusi", + "BZ": "Belize", + "CA": "ma Kanata", + "CC": "Cocos (Keeling) Islands", + "CD": "ma DR Konko", + "CF": "ma Santapiken", + "CG": "ma Konko", + "CH": "ma Suwasi", + "CI": "ma Kowisa", + "CK": "Cook Islands", + "CL": "ma Sile", + "CM": "ma Kamelun", + "CN": "ma Sonko", + "CO": "Colombia", + "CR": "ma Kosalika", + "CU": "ma Kupa", + "CV": "Cape Verde", + "CW": "Curaçao", + "CX": "Christmas Island", + "CY": "ma Kiposi", + "CZ": "ma Seki", + "DE": "ma Tosi", + "DJ": "ma Sipusi", + "DK": "ma Tansi", + "DM": "ma Watukupuli", + "DO": "ma Tominika", + "DZ": "ma Sasali", + "EC": "ma Ekato", + "EE": "ma Esi", + "EG": "ma Masu", + "EH": "Western Sahara", + "ER": "ma Eliteja", + "ES": "ma Epanja", + "ET": "ma Isijopija", + "FI": "ma Sumi", + "FJ": "ma Pisi", + "FK": "Falkland Islands", + "FM": "Micronesia", + "FO": "Faroe Islands", + "FR": "ma Kanse", + "GA": "ma Kapon", + "GB": "ma Juke", + "GD": "ma Kenata", + "GE": "ma Katelo", + "GF": "French Guiana", + "GG": "Guernsey", + "GH": "ma Kana", + "GI": "Gibraltar", + "GL": "ma Kalalinuna", + "GM": "ma Kanpija", + "GN": "ma Kine", + "GP": "Guadeloupe", + "GQ": "ma Kinejekatolija", + "GR": "ma Elena", + "GS": "South Georgia and the South Sandwich Islands", + "GT": "ma Katemala", + "GU": "Guam", + "GW": "ma Kinepisa", + "GY": "Guyana", + "HK": "Hong Kong", + "HM": "Heard Island and McDonald Islands", + "HN": "ma Ontula", + "HR": "ma Lowasi", + "HT": "ma Awisi", + "HU": "ma Mosijo", + "ID": "ma Intonesija", + "IE": "ma Alan", + "IL": "ma Isale", + "IM": "Isle of Man", + "IN": "ma Palata", + "IO": "British Indian Ocean Territory", + "IQ": "Iraq", + "IR": "Iran", + "IS": "ma Isilan", + "IT": "ma Italija", + "JE": "Jersey", + "JM": "ma Sameka", + "JO": "ma Utun", + "JP": "ma Nijon", + "KE": "ma Kenja", + "KG": "Kyrgyzstan", + "KH": "ma Kanpusi", + "KI": "ma Kilipasi", + "KM": "ma Komo", + "KN": "Saint Kitts and Nevis", + "KP": "North Korea", + "KR": "ma Anku", + "KW": "ma Kuwasi", + "KY": "Cayman Islands", + "KZ": "Kazakhstan", + "LA": "Laos", + "LB": "ma Lunpan", + "LC": "Saint Lucia", + "LI": "ma Lisensan", + "LK": "ma Lanka", + "LR": "ma Lapewija", + "LS": "ma Lesoto", + "LT": "ma Lijatuwa", + "LU": "ma Lusepu", + "LV": "ma Lawi", + "LY": "ma Lipija", + "MA": "ma Malipe", + "MC": "Monaco", + "MD": "ma Motowa", + "ME": "Montenegro", + "MF": "Saint Martin (French part)", + "MG": "Madagascar", + "MH": "Marshall Islands", + "MK": "ma Maketonija", + "ML": "ma Mali", + "MM": "ma Mijama", + "MN": "Mongolia", + "MO": "Macao", + "MP": "Northern Mariana Islands", + "MQ": "Martinique", + "MR": "ma Mulitanija", + "MS": "Montserrat", + "MT": "Malta", + "MU": "ma Mowisi", + "MV": "Maldives", + "MW": "Malawi", + "MX": "ma Mesiko", + "MY": "ma Malasija", + "MZ": "ma Mosanpi", + "NA": "ma Namipija", + "NC": "New Caledonia", + "NE": "ma Nise", + "NF": "Norfolk Island", + "NG": "ma Naselija", + "NI": "Nicaragua", + "NL": "ma Netelan", + "NO": "ma Nosiki", + "NP": "Nepal", + "NR": "Nauru", + "NU": "Niue", + "NZ": "ma Nusilan", + "OM": "ma Uman", + "PA": "ma Panama", + "PE": "ma Pelu", + "PF": "French Polynesia", + "PG": "ma Papuwanijukini", + "PH": "ma Pilipina", + "PK": "ma Pakisan", + "PL": " ma Posuka", + "PM": "Saint Pierre and Miquelon", + "PN": "Pitcairn", + "PR": "Puerto Rico", + "PS": "ma Pilisin", + "PT": "ma Potuke", + "PW": "Palau", + "PY": "ma Palakawi", + "QA": "Qatar", + "RE": "Réunion", + "RO": "ma Lomani", + "RS": "ma Sopisi", + "RU": "ma Losi", + "RW": "ma Luwanta", + "SA": "ma Sawusi", + "SB": "Solomon Islands", + "SC": "Seychelles", + "SD": "ma Sutan", + "SE": "ma Wensa", + "SG": "Singapore", + "SH": "Saint Helena, Ascension and Tristan da Cunha", + "SI": "ma Lowensina", + "SJ": "Svalbard and Jan Mayen", + "SK": "ma Lowenki", + "SL": "ma Sijelalijon", + "SM": "ma Samalino", + "SN": "ma Seneka", + "SO": "ma Somalija", + "SR": "Suriname", + "SS": "South Sudan", + "ST": "Sao Tome and Principe", + "SV": "El Salvador", + "SX": "Sint Maarten (Dutch part)", + "SY": "ma Sulija", + "SZ": "Eswatini", + "TC": "Turks and Caicos Islands", + "TD": "ma Sate", + "TF": "TAAF", + "TG": "ma Toko", + "TH": "ma Tawi", + "TJ": "Tajikistan", + "TK": "Tokelau", + "TL": "Timor-Leste", + "TM": "Turkmenistan", + "TN": "ma Tunisi", + "TO": " ma Tona", + "TR": "ma Tuki", + "TT": "ma Sinita", + "TV": "ma Tuwalu", + "TW": "ma Tawan", + "TZ": "ma Tansanija", + "UA": "ma Ukawina", + "UG": "ma Ukanta", + "UM": "United States Minor Outlying Islands", + "US": "ma Mewika", + "UY": "ma Ulukawi", + "UZ": "Uzbekistan", + "VA": "Holy See", + "VC": "ma SVG", + "VE": "ma Penesuwela", + "VG": "British Virgin Islands", + "VI": "U.S. Virgin Islands", + "VN": "ma Wije", + "VU": "ma Wanuwatu", + "WF": "Wallis and Futuna", + "WS": "ma Samowa", + "YE": "ma Jamanija", + "YT": "Mayotte", + "XK": "Kosovo", + "ZA": "ma Setapika", + "ZM": "ma Sanpija", + "ZW": "ma Sinpapuwe", + "EU": "ma kulupu Elopa" + }, + "languages": { + "en-US": "toki Inli", + "fr": "toki Kanse", + "nl": "toki Netelan", + "pt_BR": "toki Potuke pi ma Pasila" + } + }, + "KnownHosts": { + "label": "ilo lawa", + "add": "mi o ken e ilo lawa sin", + "toast": "Host successfully {mode, select, created {created} deleted {deleted} other {edited}}." + }, + "InitializeContainer": { + "title": "sina sona ala sona e ni:", + "subtitle": "<1>jan li pali e ilo Cockatrice lon wile taso! <1>musi lipu li pona tawa ona!" + }, + "LoginContainer": { + "header": { + "title": "mi o kama e sina lon kulupu", + "subtitle": "ilo Cockatrice li ken e musi lipu ale. nasin mute ilo li ken lon ona." + }, + "footer": { + "registerPrompt": "sina jo ala jo e sijelo kulupu?", + "registerAction": "mi o pali e sijelo kulupu sina", + "credit": "insa pi ilo Cockatrice li len ala", + "version": "nanpa ante" + }, + "content": { + "subtitle1": "Play multiplayer card games online.", + "subtitle2": "Cross-platform virtual tabletop for multiplayer card games. Forever free." + }, + "toasts": { + "passwordResetSuccessToast": "Password Reset Successfully", + "accountActivationSuccess": "Account Activated Successfully" + } + }, + "UnsupportedContainer": { + "title": "Unsupported Browser", + "subtitle1": "Please update your browser and/or check your permissions.", + "subtitle2": "Note: Private browsing causes some browsers to disable certain permissions or features." + }, + "AccountActivationDialog": { + "title": "Account Activation", + "subtitle1": "Your account has not been activated yet.", + "subtitle2": "You need to provide the activation token received in the activation email." + }, + "KnownHostDialog": { + "title": "{mode, select, edit {Edit} other {Add}} Known Host", + "subtitle": "Adding a new host allows you to connect to different servers. Enter the details below to your host list." + }, + "RegistrationDialog": { + "title": "Create New Account" + }, + "RequestPasswordResetDialog": { + "title": "Request Password Reset" + }, + "ResetPasswordDialog": { + "title": "mi o sin e nimi ken" + }, + "AccountActivationForm": { + "error": { + "failed": "Account activation failed" + }, + "label": { + "activate": "Activate Account" + } + }, + "KnownHostForm": { + "help": "Need help adding a new host?", + "label": { + "add": "Add Host", + "find": "Find Host" + } + }, + "LoginForm": { + "label": { + "autoConnect": "Auto Connect", + "forgot": "nimi ken pi kama sina li weka tan sona", + "login": "mi o kama e sina lon kulupu", + "savePassword": "mi o awen e nimi ken", + "savedPassword": "mi awen e nimi ken" + } + }, + "RegisterForm": { + "label": { + "register": "Register" + }, + "toast": { + "registerSuccess": "Registration Successful!" + } + }, + "RequestPasswordResetForm": { + "error": "wile ante pi nimi ken li pakala", + "mfaEnabled": "Server has multi-factor authentication enabled", + "request": "Request Reset Token", + "skipRequest": "I already have a reset token" + }, + "ResetPasswordForm": { + "error": "ante pi nimi ken li pakala", + "label": { + "reset": "mi o sin e nimi ken" + } + } +} \ No newline at end of file diff --git a/webclient/public/locales/yue/translation.json b/webclient/public/locales/yue/translation.json new file mode 100644 index 000000000..b14041496 --- /dev/null +++ b/webclient/public/locales/yue/translation.json @@ -0,0 +1,382 @@ +{ + "Common": { + "language": "", + "disconnect": "斷開連線", + "label": { + "confirmPassword": "確認新密碼:", + "confirmSure": "你確定嗎?", + "country": "國家:", + "delete": "删除", + "email": "電郵:", + "hostName": "主機名稱", + "hostAddress": "主機地址", + "password": "密碼:", + "passwordAgain": "再次輸入密碼:", + "port": "端口:", + "realName": "實名:", + "saveChanges": "儲存變更", + "token": "令牌", + "username": "用戶名稱:" + }, + "validation": { + "minChars": "最少需要{數目} {數目,眾數,單{字元}或其他{眾字元}}", + "passwordsMustMatch": "新舊密碼不一致.", + "required": "必須" + }, + "countries": { + "AD": "安道爾", + "AE": "阿拉伯聯合大公國", + "AF": "阿富汗", + "AG": "安地卡及巴布達", + "AI": "安圭拉", + "AL": "阿爾巴尼亞", + "AM": "亞美尼亞", + "AO": "安哥拉", + "AQ": "南極洲", + "AR": "阿根廷", + "AS": "美屬薩摩亞", + "AT": "奧地利", + "AU": "澳洲", + "AW": "阿魯巴", + "AX": "奧蘭群島", + "AZ": "亞塞拜然", + "BA": "波士尼亞與赫塞哥維納", + "BB": "巴貝多", + "BD": "孟加拉", + "BE": "比利時", + "BF": "布吉納法索", + "BG": "保加利亞", + "BH": "巴林", + "BI": "蒲隆地", + "BJ": "貝南", + "BL": "聖巴泰勒米", + "BM": "百慕達", + "BN": "汶萊達魯薩蘭國", + "BO": "玻利維亞", + "BQ": "博內爾島、聖尤斯特歇斯島和薩巴島", + "BR": "巴西", + "BS": "巴哈馬", + "BT": "不丹", + "BV": "布韋島", + "BW": "波札那", + "BY": "白俄羅斯", + "BZ": "貝里斯", + "CA": "加拿大", + "CC": "科科斯(基林)群島", + "CD": "剛果民主共和國", + "CF": "中非共和國", + "CG": "剛果共和國", + "CH": "瑞士", + "CI": "科特迪瓦", + "CK": "庫克群島", + "CL": "智利", + "CM": "喀麥隆", + "CN": "中國", + "CO": "哥倫比亞", + "CR": "哥斯大黎加", + "CU": "古巴", + "CV": "維德角", + "CW": "庫拉索", + "CX": "聖誕島", + "CY": "賽普勒斯", + "CZ": "捷克", + "DE": "德國", + "DJ": "吉布地", + "DK": "丹麥", + "DM": "多明尼加", + "DO": "多明尼加共和國", + "DZ": "阿爾及利亞", + "EC": "厄瓜多", + "EE": "愛沙尼亞", + "EG": "埃及", + "EH": "西撒哈拉", + "ER": "厄利垂亞", + "ES": "西班牙", + "ET": "衣索比亞", + "FI": "芬蘭", + "FJ": "斐濟", + "FK": "福克蘭群島", + "FM": "密克羅尼西亞", + "FO": "法羅群島", + "FR": "法國", + "GA": "加彭", + "GB": "英國", + "GD": "格瑞那達", + "GE": "喬治亞", + "GF": "法屬圭亞那", + "GG": "根西島", + "GH": "迦納", + "GI": "直布羅陀", + "GL": "格陵蘭", + "GM": "Gambia", + "GN": "Guinea", + "GP": "Guadeloupe", + "GQ": "Equatorial Guinea", + "GR": "Greece", + "GS": "South Georgia and the South Sandwich Islands", + "GT": "Guatemala", + "GU": "Guam", + "GW": "Guinea-Bissau", + "GY": "Guyana", + "HK": "Hong Kong", + "HM": "Heard Island and McDonald Islands", + "HN": "Honduras", + "HR": "Croatia", + "HT": "Haiti", + "HU": "Hungary", + "ID": "Indonesia", + "IE": "Ireland", + "IL": "Israel", + "IM": "Isle of Man", + "IN": "India", + "IO": "British Indian Ocean Territory", + "IQ": "Iraq", + "IR": "Iran", + "IS": "Iceland", + "IT": "Italy", + "JE": "Jersey", + "JM": "Jamaica", + "JO": "Jordan", + "JP": "Japan", + "KE": "Kenya", + "KG": "Kyrgyzstan", + "KH": "Cambodia", + "KI": "Kiribati", + "KM": "Comoros", + "KN": "Saint Kitts and Nevis", + "KP": "North Korea", + "KR": "South Korea", + "KW": "Kuwait", + "KY": "Cayman Islands", + "KZ": "Kazakhstan", + "LA": "Laos", + "LB": "Lebanon", + "LC": "Saint Lucia", + "LI": "Liechtenstein", + "LK": "Sri Lanka", + "LR": "Liberia", + "LS": "Lesotho", + "LT": "Lithuania", + "LU": "Luxembourg", + "LV": "Latvia", + "LY": "Libya", + "MA": "Morocco", + "MC": "Monaco", + "MD": "Moldova", + "ME": "Montenegro", + "MF": "Saint Martin (French part)", + "MG": "Madagascar", + "MH": "Marshall Islands", + "MK": "North Macedonia", + "ML": "Mali", + "MM": "Myanmar", + "MN": "Mongolia", + "MO": "Macao", + "MP": "Northern Mariana Islands", + "MQ": "Martinique", + "MR": "Mauritania", + "MS": "Montserrat", + "MT": "Malta", + "MU": "Mauritius", + "MV": "Maldives", + "MW": "Malawi", + "MX": "Mexico", + "MY": "Malaysia", + "MZ": "Mozambique", + "NA": "Namibia", + "NC": "New Caledonia", + "NE": "Niger", + "NF": "Norfolk Island", + "NG": "Nigeria", + "NI": "Nicaragua", + "NL": "Netherlands", + "NO": "Norway", + "NP": "Nepal", + "NR": "Nauru", + "NU": "Niue", + "NZ": "New Zealand", + "OM": "Oman", + "PA": "Panama", + "PE": "Peru", + "PF": "French Polynesia", + "PG": "Papua New Guinea", + "PH": "Philippines", + "PK": "Pakistan", + "PL": "Poland", + "PM": "Saint Pierre and Miquelon", + "PN": "Pitcairn", + "PR": "Puerto Rico", + "PS": "Palestine", + "PT": "Portugal", + "PW": "Palau", + "PY": "Paraguay", + "QA": "Qatar", + "RE": "Réunion", + "RO": "Romania", + "RS": "Serbia", + "RU": "Russia", + "RW": "Rwanda", + "SA": "Saudi Arabia", + "SB": "Solomon Islands", + "SC": "Seychelles", + "SD": "Sudan", + "SE": "Sweden", + "SG": "Singapore", + "SH": "Saint Helena, Ascension and Tristan da Cunha", + "SI": "Slovenia", + "SJ": "Svalbard and Jan Mayen", + "SK": "Slovakia", + "SL": "Sierra Leone", + "SM": "San Marino", + "SN": "Senegal", + "SO": "Somalia", + "SR": "Suriname", + "SS": "South Sudan", + "ST": "Sao Tome and Principe", + "SV": "El Salvador", + "SX": "Sint Maarten (Dutch part)", + "SY": "Syria", + "SZ": "Eswatini", + "TC": "Turks and Caicos Islands", + "TD": "Chad", + "TF": "TAAF", + "TG": "Togo", + "TH": "Thailand", + "TJ": "Tajikistan", + "TK": "Tokelau", + "TL": "Timor-Leste", + "TM": "Turkmenistan", + "TN": "Tunisia", + "TO": "Tonga", + "TR": "Turkey", + "TT": "Trinidad and Tobago", + "TV": "Tuvalu", + "TW": "Taiwan", + "TZ": "Tanzania", + "UA": "Ukraine", + "UG": "Uganda", + "UM": "United States Minor Outlying Islands", + "US": "United States", + "UY": "Uruguay", + "UZ": "Uzbekistan", + "VA": "Holy See", + "VC": "Saint Vincent and the Grenadines", + "VE": "Venezuela", + "VG": "British Virgin Islands", + "VI": "U.S. Virgin Islands", + "VN": "Viet Nam", + "VU": "Vanuatu", + "WF": "Wallis and Futuna", + "WS": "Samoa", + "YE": "Yemen", + "YT": "Mayotte", + "XK": "Kosovo", + "ZA": "South Africa", + "ZM": "Zambia", + "ZW": "Zimbabwe", + "EU": "European Union" + }, + "languages": { + "en-US": "English - US", + "fr": "French", + "nl": "Dutch", + "pt_BR": "Portuguese - Brazil" + } + }, + "KnownHosts": { + "label": "Host", + "add": "Add new host", + "toast": "Host successfully {mode, select, created {created} deleted {deleted} other {edited}}." + }, + "InitializeContainer": { + "title": "DID YOU KNOW", + "subtitle": "<1>Cockatrice is run by volunteers<1>that love card games!" + }, + "LoginContainer": { + "header": { + "title": "Login", + "subtitle": "A cross-platform virtual tabletop for multiplayer card games." + }, + "footer": { + "registerPrompt": "Not registered yet?", + "registerAction": "Create an account", + "credit": "Cockatrice is an open source project", + "version": "Version" + }, + "content": { + "subtitle1": "Play multiplayer card games online.", + "subtitle2": "Cross-platform virtual tabletop for multiplayer card games. Forever free." + }, + "toasts": { + "passwordResetSuccessToast": "Password Reset Successfully", + "accountActivationSuccess": "Account Activated Successfully" + } + }, + "UnsupportedContainer": { + "title": "Unsupported Browser", + "subtitle1": "Please update your browser and/or check your permissions.", + "subtitle2": "Note: Private browsing causes some browsers to disable certain permissions or features." + }, + "AccountActivationDialog": { + "title": "Account Activation", + "subtitle1": "Your account has not been activated yet.", + "subtitle2": "You need to provide the activation token received in the activation email." + }, + "KnownHostDialog": { + "title": "{mode, select, edit {Edit} other {Add}} Known Host", + "subtitle": "Adding a new host allows you to connect to different servers. Enter the details below to your host list." + }, + "RegistrationDialog": { + "title": "Create New Account" + }, + "RequestPasswordResetDialog": { + "title": "Request Password Reset" + }, + "ResetPasswordDialog": { + "title": "Reset Password" + }, + "AccountActivationForm": { + "error": { + "failed": "Account activation failed" + }, + "label": { + "activate": "Activate Account" + } + }, + "KnownHostForm": { + "help": "Need help adding a new host?", + "label": { + "add": "Add Host", + "find": "Find Host" + } + }, + "LoginForm": { + "label": { + "autoConnect": "Auto Connect", + "forgot": "Forgot Password", + "login": "Login", + "savePassword": "Save Password", + "savedPassword": "Saved Password" + } + }, + "RegisterForm": { + "label": { + "register": "Register" + }, + "toast": { + "registerSuccess": "Registration Successful!" + } + }, + "RequestPasswordResetForm": { + "error": "Request password reset failed", + "mfaEnabled": "Server has multi-factor authentication enabled", + "request": "Request Reset Token", + "skipRequest": "I already have a reset token" + }, + "ResetPasswordForm": { + "error": "Password reset failed", + "label": { + "reset": "Reset Password" + } + } +} \ No newline at end of file diff --git a/webclient/public/logo192.png b/webclient/public/logo192.png new file mode 100644 index 000000000..fa313abf5 Binary files /dev/null and b/webclient/public/logo192.png differ diff --git a/webclient/public/logo512.png b/webclient/public/logo512.png new file mode 100644 index 000000000..bd5d4b5e2 Binary files /dev/null and b/webclient/public/logo512.png differ diff --git a/webclient/public/manifest.json b/webclient/public/manifest.json new file mode 100644 index 000000000..080d6c77a --- /dev/null +++ b/webclient/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/webclient/public/pb/.gitignore b/webclient/public/pb/.gitignore new file mode 100644 index 000000000..571442132 --- /dev/null +++ b/webclient/public/pb/.gitignore @@ -0,0 +1,4 @@ +# Ignore all files +* +# Except gitignore +!.gitignore \ No newline at end of file diff --git a/webclient/public/reset.css b/webclient/public/reset.css new file mode 100644 index 000000000..af944401f --- /dev/null +++ b/webclient/public/reset.css @@ -0,0 +1,48 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} \ No newline at end of file diff --git a/webclient/public/robots.txt b/webclient/public/robots.txt new file mode 100644 index 000000000..01b0f9a10 --- /dev/null +++ b/webclient/public/robots.txt @@ -0,0 +1,2 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * diff --git a/webclient/src/api/AdminService.tsx b/webclient/src/api/AdminService.tsx new file mode 100644 index 000000000..623ad546a --- /dev/null +++ b/webclient/src/api/AdminService.tsx @@ -0,0 +1,19 @@ +import { AdminCommands } from 'websocket'; + +export class AdminService { + static adjustMod(userName: string, shouldBeMod?: boolean, shouldBeJudge?: boolean): void { + AdminCommands.adjustMod(userName, shouldBeMod, shouldBeJudge); + } + + static reloadConfig(): void { + AdminCommands.reloadConfig(); + } + + static shutdownServer(reason: string, minutes: number): void { + AdminCommands.shutdownServer(reason, minutes); + } + + static updateServerMessage(): void { + AdminCommands.updateServerMessage(); + } +} diff --git a/webclient/src/api/AuthenticationService.tsx b/webclient/src/api/AuthenticationService.tsx new file mode 100644 index 000000000..7b3a46988 --- /dev/null +++ b/webclient/src/api/AuthenticationService.tsx @@ -0,0 +1,55 @@ +import { StatusEnum, User, WebSocketConnectReason, WebSocketConnectOptions } from 'types'; +import { SessionCommands, webClient } from 'websocket'; +import { ProtoController } from 'websocket/services/ProtoController'; + +export class AuthenticationService { + static login(options: WebSocketConnectOptions): void { + SessionCommands.connect(options, WebSocketConnectReason.LOGIN); + } + + static testConnection(options: WebSocketConnectOptions): void { + SessionCommands.connect(options, WebSocketConnectReason.TEST_CONNECTION); + } + + static register(options: WebSocketConnectOptions): void { + SessionCommands.connect(options, WebSocketConnectReason.REGISTER); + } + + static activateAccount(options: WebSocketConnectOptions): void { + SessionCommands.connect(options, WebSocketConnectReason.ACTIVATE_ACCOUNT); + } + + static resetPasswordRequest(options: WebSocketConnectOptions): void { + SessionCommands.connect(options, WebSocketConnectReason.PASSWORD_RESET_REQUEST); + } + + static resetPasswordChallenge(options: WebSocketConnectOptions): void { + SessionCommands.connect(options, WebSocketConnectReason.PASSWORD_RESET_CHALLENGE); + } + + static resetPassword(options: WebSocketConnectOptions): void { + SessionCommands.connect(options, WebSocketConnectReason.PASSWORD_RESET); + } + + static disconnect(): void { + SessionCommands.disconnect(); + } + + static isConnected(state: number): boolean { + return state === StatusEnum.LOGGED_IN; + } + + static isModerator(user: User): boolean { + const moderatorLevel = ProtoController.root.ServerInfo_User.UserLevelFlag.IsModerator; + // @TODO tell cockatrice not to do this so shittily + return (user.userLevel & moderatorLevel) === moderatorLevel; + } + + static isAdmin() { + + } + + static connectionAttemptMade() { + return webClient.connectionAttemptMade; + } +} diff --git a/webclient/src/api/ModeratorService.tsx b/webclient/src/api/ModeratorService.tsx new file mode 100644 index 000000000..6c22ee55e --- /dev/null +++ b/webclient/src/api/ModeratorService.tsx @@ -0,0 +1,29 @@ +import { ModeratorCommands } from 'websocket'; +import { LogFilters } from 'types'; + +export class ModeratorService { + static banFromServer(minutes: number, userName?: string, address?: string, reason?: string, + visibleReason?: string, clientid?: string, removeMessages?: number): void { + ModeratorCommands.banFromServer(minutes, userName, address, reason, visibleReason, clientid, removeMessages); + } + + static getBanHistory(userName: string): void { + ModeratorCommands.getBanHistory(userName); + } + + static getWarnHistory(userName: string): void { + ModeratorCommands.getWarnHistory(userName); + } + + static getWarnList(modName: string, userName: string, userClientid: string): void { + ModeratorCommands.getWarnList(modName, userName, userClientid); + } + + static viewLogHistory(filters: LogFilters): void { + ModeratorCommands.viewLogHistory(filters); + } + + static warnUser(userName: string, reason: string, clientid?: string, removeMessages?: number): void { + ModeratorCommands.warnUser(userName, reason, clientid, removeMessages); + } +} diff --git a/webclient/src/api/RoomsService.tsx b/webclient/src/api/RoomsService.tsx new file mode 100644 index 000000000..bfb63d92a --- /dev/null +++ b/webclient/src/api/RoomsService.tsx @@ -0,0 +1,15 @@ +import { RoomCommands, SessionCommands } from 'websocket'; + +export class RoomsService { + static joinRoom(roomId: number): void { + SessionCommands.joinRoom(roomId); + } + + static leaveRoom(roomId: number): void { + RoomCommands.leaveRoom(roomId); + } + + static roomSay(roomId: number, message: string): void { + RoomCommands.roomSay(roomId, message); + } +} diff --git a/webclient/src/api/SessionService.tsx b/webclient/src/api/SessionService.tsx new file mode 100644 index 000000000..2787f098d --- /dev/null +++ b/webclient/src/api/SessionService.tsx @@ -0,0 +1,43 @@ +import { SessionCommands } from 'websocket'; + +export class SessionService { + static addToBuddyList(userName: string) { + SessionCommands.addToBuddyList(userName); + } + + static removeFromBuddyList(userName: string) { + SessionCommands.removeFromBuddyList(userName); + } + + static addToIgnoreList(userName: string) { + SessionCommands.addToIgnoreList(userName); + } + + static removeFromIgnoreList(userName: string) { + SessionCommands.removeFromIgnoreList(userName); + } + + static changeAccountPassword(oldPassword: string, newPassword: string, hashedNewPassword?: string): void { + SessionCommands.accountPassword(oldPassword, newPassword, hashedNewPassword); + } + + static changeAccountDetails(passwordCheck: string, realName?: string, email?: string, country?: string): void { + SessionCommands.accountEdit(passwordCheck, realName, email, country); + } + + static changeAccountImage(image: Uint8Array): void { + SessionCommands.accountImage(image); + } + + static sendDirectMessage(userName: string, message: string): void { + SessionCommands.message(userName, message); + } + + static getUserInfo(userName: string): void { + SessionCommands.getUserInfo(userName); + } + + static getUserGames(userName: string): void { + SessionCommands.getGamesOfUser(userName); + } +} diff --git a/webclient/src/api/index.ts b/webclient/src/api/index.ts new file mode 100644 index 000000000..c4f67092e --- /dev/null +++ b/webclient/src/api/index.ts @@ -0,0 +1,5 @@ +export { AdminService } from './AdminService'; +export { AuthenticationService } from './AuthenticationService'; +export { ModeratorService } from './ModeratorService'; +export { RoomsService } from './RoomsService'; +export { SessionService } from './SessionService'; diff --git a/webclient/src/common.i18n.json b/webclient/src/common.i18n.json new file mode 100644 index 000000000..64a102c38 --- /dev/null +++ b/webclient/src/common.i18n.json @@ -0,0 +1,286 @@ +{ + "Common": { + "language": "English", + "disconnect": "Disconnect", + "label": { + "confirmPassword": "Confirm Password", + "confirmSure": "Are you sure?", + "country": "Country", + "delete": "Delete", + "email": "Email", + "hostName": "Host Name", + "hostAddress": "Host Address", + "password": "Password", + "passwordAgain": "Password Again", + "port": "Port", + "realName": "Real Name", + "saveChanges": "Save Changes", + "token": "Token", + "username": "Username" + }, + "validation": { + "minChars": "Minimum of {count} {count, plural, one {character} other {characters}} required", + "passwordsMustMatch": "Passwords don't match", + "required": "Required" + }, + "countries": { + "AD": "Andorra", + "AE": "United Arab Emirates", + "AF": "Afghanistan", + "AG": "Antigua and Barbuda", + "AI": "Anguilla", + "AL": "Albania", + "AM": "Armenia", + "AO": "Angola", + "AQ": "Antarctica", + "AR": "Argentina", + "AS": "American Samoa", + "AT": "Austria", + "AU": "Australia", + "AW": "Aruba", + "AX": "Åland Islands", + "AZ": "Azerbaijan", + "BA": "Bosnia and Herzegovina", + "BB": "Barbados", + "BD": "Bangladesh", + "BE": "Belgium", + "BF": "Burkina Faso", + "BG": "Bulgaria", + "BH": "Bahrain", + "BI": "Burundi", + "BJ": "Benin", + "BL": "Saint Barthélemy", + "BM": "Bermuda", + "BN": "Brunei Darussalam", + "BO": "Bolivia", + "BQ": "Bonaire, Sint Eustatius and Saba", + "BR": "Brazil", + "BS": "Bahamas", + "BT": "Bhutan", + "BV": "Bouvet Island", + "BW": "Botswana", + "BY": "Belarus", + "BZ": "Belize", + "CA": "Canada", + "CC": "Cocos (Keeling) Islands", + "CD": "DR Congo", + "CF": "Central African Republic", + "CG": "Republic of the Congo", + "CH": "Switzerland", + "CI": "Ivory Coast", + "CK": "Cook Islands", + "CL": "Chile", + "CM": "Cameroon", + "CN": "China", + "CO": "Colombia", + "CR": "Costa Rica", + "CU": "Cuba", + "CV": "Cape Verde", + "CW": "Curaçao", + "CX": "Christmas Island", + "CY": "Cyprus", + "CZ": "Czechia", + "DE": "Germany", + "DJ": "Djibouti", + "DK": "Denmark", + "DM": "Dominica", + "DO": "Dominican Republic", + "DZ": "Algeria", + "EC": "Ecuador", + "EE": "Estonia", + "EG": "Egypt", + "EH": "Western Sahara", + "ER": "Eritrea", + "ES": "Spain", + "ET": "Ethiopia", + "FI": "Finland", + "FJ": "Fiji", + "FK": "Falkland Islands", + "FM": "Micronesia", + "FO": "Faroe Islands", + "FR": "France", + "GA": "Gabon", + "GB": "United Kingdom", + "GD": "Grenada", + "GE": "Georgia", + "GF": "French Guiana", + "GG": "Guernsey", + "GH": "Ghana", + "GI": "Gibraltar", + "GL": "Greenland", + "GM": "Gambia", + "GN": "Guinea", + "GP": "Guadeloupe", + "GQ": "Equatorial Guinea", + "GR": "Greece", + "GS": "South Georgia and the South Sandwich Islands", + "GT": "Guatemala", + "GU": "Guam", + "GW": "Guinea-Bissau", + "GY": "Guyana", + "HK": "Hong Kong", + "HM": "Heard Island and McDonald Islands", + "HN": "Honduras", + "HR": "Croatia", + "HT": "Haiti", + "HU": "Hungary", + "ID": "Indonesia", + "IE": "Ireland", + "IL": "Israel", + "IM": "Isle of Man", + "IN": "India", + "IO": "British Indian Ocean Territory", + "IQ": "Iraq", + "IR": "Iran", + "IS": "Iceland", + "IT": "Italy", + "JE": "Jersey", + "JM": "Jamaica", + "JO": "Jordan", + "JP": "Japan", + "KE": "Kenya", + "KG": "Kyrgyzstan", + "KH": "Cambodia", + "KI": "Kiribati", + "KM": "Comoros", + "KN": "Saint Kitts and Nevis", + "KP": "North Korea", + "KR": "South Korea", + "KW": "Kuwait", + "KY": "Cayman Islands", + "KZ": "Kazakhstan", + "LA": "Laos", + "LB": "Lebanon", + "LC": "Saint Lucia", + "LI": "Liechtenstein", + "LK": "Sri Lanka", + "LR": "Liberia", + "LS": "Lesotho", + "LT": "Lithuania", + "LU": "Luxembourg", + "LV": "Latvia", + "LY": "Libya", + "MA": "Morocco", + "MC": "Monaco", + "MD": "Moldova", + "ME": "Montenegro", + "MF": "Saint Martin (French part)", + "MG": "Madagascar", + "MH": "Marshall Islands", + "MK": "North Macedonia", + "ML": "Mali", + "MM": "Myanmar", + "MN": "Mongolia", + "MO": "Macao", + "MP": "Northern Mariana Islands", + "MQ": "Martinique", + "MR": "Mauritania", + "MS": "Montserrat", + "MT": "Malta", + "MU": "Mauritius", + "MV": "Maldives", + "MW": "Malawi", + "MX": "Mexico", + "MY": "Malaysia", + "MZ": "Mozambique", + "NA": "Namibia", + "NC": "New Caledonia", + "NE": "Niger", + "NF": "Norfolk Island", + "NG": "Nigeria", + "NI": "Nicaragua", + "NL": "Netherlands", + "NO": "Norway", + "NP": "Nepal", + "NR": "Nauru", + "NU": "Niue", + "NZ": "New Zealand", + "OM": "Oman", + "PA": "Panama", + "PE": "Peru", + "PF": "French Polynesia", + "PG": "Papua New Guinea", + "PH": "Philippines", + "PK": "Pakistan", + "PL": "Poland", + "PM": "Saint Pierre and Miquelon", + "PN": "Pitcairn", + "PR": "Puerto Rico", + "PS": "Palestine", + "PT": "Portugal", + "PW": "Palau", + "PY": "Paraguay", + "QA": "Qatar", + "RE": "Réunion", + "RO": "Romania", + "RS": "Serbia", + "RU": "Russia", + "RW": "Rwanda", + "SA": "Saudi Arabia", + "SB": "Solomon Islands", + "SC": "Seychelles", + "SD": "Sudan", + "SE": "Sweden", + "SG": "Singapore", + "SH": "Saint Helena, Ascension and Tristan da Cunha", + "SI": "Slovenia", + "SJ": "Svalbard and Jan Mayen", + "SK": "Slovakia", + "SL": "Sierra Leone", + "SM": "San Marino", + "SN": "Senegal", + "SO": "Somalia", + "SR": "Suriname", + "SS": "South Sudan", + "ST": "Sao Tome and Principe", + "SV": "El Salvador", + "SX": "Sint Maarten (Dutch part)", + "SY": "Syria", + "SZ": "Eswatini", + "TC": "Turks and Caicos Islands", + "TD": "Chad", + "TF": "TAAF", + "TG": "Togo", + "TH": "Thailand", + "TJ": "Tajikistan", + "TK": "Tokelau", + "TL": "Timor-Leste", + "TM": "Turkmenistan", + "TN": "Tunisia", + "TO": "Tonga", + "TR": "Turkey", + "TT": "Trinidad and Tobago", + "TV": "Tuvalu", + "TW": "Taiwan", + "TZ": "Tanzania", + "UA": "Ukraine", + "UG": "Uganda", + "UM": "United States Minor Outlying Islands", + "US": "United States", + "UY": "Uruguay", + "UZ": "Uzbekistan", + "VA": "Holy See", + "VC": "Saint Vincent and the Grenadines", + "VE": "Venezuela", + "VG": "British Virgin Islands", + "VI": "U.S. Virgin Islands", + "VN": "Viet Nam", + "VU": "Vanuatu", + "WF": "Wallis and Futuna", + "WS": "Samoa", + "YE": "Yemen", + "YT": "Mayotte", + "XK": "Kosovo", + "ZA": "South Africa", + "ZM": "Zambia", + "ZW": "Zimbabwe", + "EU": "European Union" + }, + "languages": { + "en-US": "English - US", + "fr": "French", + "nl": "Dutch", + "pt_BR": "Portuguese - Brazil" + } + } +} diff --git a/webclient/src/components/Card/Card.css b/webclient/src/components/Card/Card.css new file mode 100644 index 000000000..976618634 --- /dev/null +++ b/webclient/src/components/Card/Card.css @@ -0,0 +1,4 @@ +.card { + width: 100%; + height: 100%; +} diff --git a/webclient/src/components/Card/Card.tsx b/webclient/src/components/Card/Card.tsx new file mode 100644 index 000000000..f89622c3b --- /dev/null +++ b/webclient/src/components/Card/Card.tsx @@ -0,0 +1,20 @@ +// eslint-disable-next-line +import React, { useMemo, useState } from 'react'; + +import { CardDTO } from 'services'; + +import './Card.css'; + +interface CardProps { + card: CardDTO; +} + +const Card = ({ card }: CardProps) => { + const src = `https://api.scryfall.com/cards/${card?.identifiers?.scryfallId}?format=image`; + + return card && ( + {card?.name} + ); +} + +export default Card; diff --git a/webclient/src/components/CardDetails/CardDetails.css b/webclient/src/components/CardDetails/CardDetails.css new file mode 100644 index 000000000..7009049d2 --- /dev/null +++ b/webclient/src/components/CardDetails/CardDetails.css @@ -0,0 +1,45 @@ +.cardDetails { + padding: 10px; + width: calc(400px * .716); + font-size: 10px; +} + +.cardDetails-card { + height: 400px; + margin: 0 auto; +} + +.cardDetails-attribute { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.cardDetails-attributes { + margin: 10px 0; +} + +.cardDetails-attribute__label { + text-transform: uppercase; + font-size: 10px; + margin-right: 10px; +} + +.cardDetails-attribute__value { + text-align: right; +} + +.cardDetails-text { + font-size: 12px; + padding: 5px; + background: rgba(0, 0, 0, .15); + white-space: pre-line; +} + +.cardDetails-text__flavor { + font-style: italic; +} + +.cardDetails-text__current:not(:empty) + .cardDetails-text__flavor { + margin-top: 10px; +} diff --git a/webclient/src/components/CardDetails/CardDetails.tsx b/webclient/src/components/CardDetails/CardDetails.tsx new file mode 100644 index 000000000..84196fda6 --- /dev/null +++ b/webclient/src/components/CardDetails/CardDetails.tsx @@ -0,0 +1,130 @@ +// eslint-disable-next-line +import React, { useMemo, useState } from 'react'; + +import { CardDTO } from 'services'; + +import Card from '../Card/Card'; + +import './CardDetails.css'; + +interface CardProps { + card: CardDTO; +} + +// @TODO: add missing fields (loyalty, hand, etc) + +const CardDetails = ({ card }: CardProps) => { + return ( +
+
+ +
+ + { + card && ( +
+
+
+ Name: + {card.name} +
+ + { + (!card.power && !card.toughness) ? null : ( +
+ P/T: + {card.power || 0}/{card.toughness || 0} +
+ ) + } + + { + !card.manaCost ? null : ( +
+ Cost: + {card.manaCost.replace(/\{|\}/g, '')} +
+ ) + } + + { + !card.convertedManaCost ? null : ( +
+ CMC: + {card.convertedManaCost} +
+ ) + } + + { + !card.colorIdentity?.length ? null : ( +
+ Identity: + {card.colorIdentity.join('')} +
+ ) + } + + { + !card.colors?.length ? null : ( +
+ Color(s): + {card.colors.join('')} +
+ ) + } + + { + !card.types?.length ? null : ( +
+ Main Type: + {card.types.join(', ')} +
+ ) + } + + { + !card.type ? null : ( +
+ Type: + {card.type} +
+ ) + } + + { + !card.side ? null : ( +
+ Side: + {card.side} +
+ ) + } + + { + !card.layout ? null : ( +
+ Layout: + {card.layout} +
+ ) + } +
+ +
+
+ {card.text?.trim()} +
+ +
+ {card.flavorText?.trim()} +
+
+
+ ) + } +
+ ); +} + +export default CardDetails; diff --git a/webclient/src/components/CheckboxField/CheckboxField.css b/webclient/src/components/CheckboxField/CheckboxField.css new file mode 100644 index 000000000..e69de29bb diff --git a/webclient/src/components/CheckboxField/CheckboxField.tsx b/webclient/src/components/CheckboxField/CheckboxField.tsx new file mode 100644 index 000000000..562687489 --- /dev/null +++ b/webclient/src/components/CheckboxField/CheckboxField.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import Checkbox from '@mui/material/Checkbox'; +import FormControlLabel from '@mui/material/FormControlLabel'; + +const CheckboxField = (props) => { + const { input: { value, onChange }, label, ...args } = props; + + // @TODO this isnt unchecking properly + return ( + onChange(checked)} + color="primary" + /> + } + /> + ); +}; + +export default CheckboxField; diff --git a/webclient/src/components/CountryDropdown/CountryDropdown.css b/webclient/src/components/CountryDropdown/CountryDropdown.css new file mode 100644 index 000000000..9b1679516 --- /dev/null +++ b/webclient/src/components/CountryDropdown/CountryDropdown.css @@ -0,0 +1,13 @@ +.CountryDropdown { + width: 100%; +} + +.CountryDropdown-item { + display: flex; + align-items: center; +} + +.CountryDropdown-item__image { + width: 1.5em; + margin-right: 1em; +} diff --git a/webclient/src/components/CountryDropdown/CountryDropdown.tsx b/webclient/src/components/CountryDropdown/CountryDropdown.tsx new file mode 100644 index 000000000..09b1cf71e --- /dev/null +++ b/webclient/src/components/CountryDropdown/CountryDropdown.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from 'react'; +import { Select, MenuItem } from '@mui/material'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import { useTranslation } from 'react-i18next'; + +import { useLocaleSort } from 'hooks'; +import { Images } from 'images/Images'; +import { countryCodes } from 'types'; + + +import './CountryDropdown.css'; + +const CountryDropdown = ({ input: { onChange } }) => { + const [value, setValue] = useState(''); + const { t } = useTranslation(); + + useEffect(() => onChange(value), [value]); + + const translateCountry = country => t(`Common.countries.${country}`); + const sortedCountries = useLocaleSort(countryCodes, translateCountry); + + return ( + + Country + + + ) +}; + +export default CountryDropdown; diff --git a/webclient/src/components/Guard/AuthGuard.tsx b/webclient/src/components/Guard/AuthGuard.tsx new file mode 100644 index 000000000..eea42dc7b --- /dev/null +++ b/webclient/src/components/Guard/AuthGuard.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { Navigate } from 'react-router-dom'; + +import { ServerSelectors } from 'store'; +import { RouteEnum } from 'types'; + +import { AuthenticationService } from 'api'; + +const AuthGuard = ({ state }: AuthGuardProps) => { + return !AuthenticationService.isConnected(state) + ? + :
; +}; + +interface AuthGuardProps { + state: number; +} + +const mapStateToProps = state => ({ + state: ServerSelectors.getState(state), +}); + +export default connect(mapStateToProps)(AuthGuard); diff --git a/webclient/src/components/Guard/ModGuard.tsx b/webclient/src/components/Guard/ModGuard.tsx new file mode 100644 index 000000000..c8bc6d663 --- /dev/null +++ b/webclient/src/components/Guard/ModGuard.tsx @@ -0,0 +1,27 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { Navigate } from 'react-router-dom'; + +import { ServerSelectors } from 'store'; +import { User } from 'types'; + +import { AuthenticationService } from 'api'; +import { RouteEnum } from 'types'; + +class ModGuard extends Component { + render() { + return !AuthenticationService.isModerator(this.props.user) + ? + : ''; + } +}; + +interface ModGuardProps { + user: User; +} + +const mapStateToProps = state => ({ + user: ServerSelectors.getUser(state), +}); + +export default connect(mapStateToProps)(ModGuard); diff --git a/webclient/src/components/InputAction/InputAction.css b/webclient/src/components/InputAction/InputAction.css new file mode 100644 index 000000000..25e784a3a --- /dev/null +++ b/webclient/src/components/InputAction/InputAction.css @@ -0,0 +1,19 @@ +.input-action { + display: flex; + width: 100%; + align-items: center; +} + +.input-action, +.input-action__item, +.input-action__submit { + padding: 5px; +} + +.input-action__item { + width: 100%; + height: 100%; +} +.input-action__item > div { + margin: 0; +} \ No newline at end of file diff --git a/webclient/src/components/InputAction/InputAction.tsx b/webclient/src/components/InputAction/InputAction.tsx new file mode 100644 index 000000000..459b89904 --- /dev/null +++ b/webclient/src/components/InputAction/InputAction.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Field } from 'react-final-form' +import Button from '@mui/material/Button'; + +import { InputField } from 'components'; + +import './InputAction.css'; + +const InputAction = ({ action, label, name, validate, disabled }) => ( +
+
+ +
+
+ +
+
+); + +InputAction.defaultProps = { + disabled: false, + validate: () => false, +} + +export default InputAction; diff --git a/webclient/src/components/InputField/InputField.css b/webclient/src/components/InputField/InputField.css new file mode 100644 index 000000000..819f7112a --- /dev/null +++ b/webclient/src/components/InputField/InputField.css @@ -0,0 +1,20 @@ +.InputField { + position: relative; +} + +.InputField-validation { + position: absolute; + top: 0; + right: 0; + transform: translateY(-50%); + font-weight: bold; +} + +.InputField-error { + display: flex; + align-items: center; +} + +.InputField-error svg { + margin-left: 4px; +} diff --git a/webclient/src/components/InputField/InputField.tsx b/webclient/src/components/InputField/InputField.tsx new file mode 100644 index 000000000..3299ba383 --- /dev/null +++ b/webclient/src/components/InputField/InputField.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { styled } from '@mui/material/styles'; +import TextField from '@mui/material/TextField'; +import ErrorOutlinedIcon from '@mui/icons-material/ErrorOutlined'; + +import './InputField.css'; + +const PREFIX = 'InputField'; + +const classes = { + root: `${PREFIX}-root` +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { + '& .InputField-error': { + color: theme.palette.error.main + }, + + '& .InputField-warning': { + color: theme.palette.warning.main + }, + }, +})); + +const InputField = ({ input, meta, ...args }) => { + const { touched, error, warning } = meta; + + return ( + + { touched && ( +
+ { + (error && +
+ {error} + +
+ ) || + + (warning &&
{warning}
) + } +
+ ) } + + +
+ ); +}; + +export default InputField; diff --git a/webclient/src/components/KnownHosts/KnownHosts.css b/webclient/src/components/KnownHosts/KnownHosts.css new file mode 100644 index 000000000..32a3a9fcc --- /dev/null +++ b/webclient/src/components/KnownHosts/KnownHosts.css @@ -0,0 +1,70 @@ +.KnownHosts { +} + +.KnownHosts-form { + width: 100%; + position: relative; +} + +.KnownHosts-item { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; +} + +.KnownHosts-item__wrapper { + display: flex; + align-items: center; +} + +.KnownHosts-item__label { + position: relative; +} + +.KnownHosts-item__label svg { + display: none; + position: absolute; + left: -.1em; + top: 50%; + transform: translate(-100%, -50%); + font-size: .9em; +} + +.KnownHosts-item__status { + display: none; +} + +.KnownHosts-item__status svg { + margin-left: -5px; + margin-right: 5px; +} + +.KnownHosts-validation { + position: absolute; + top: 0; + right: 0; + transform: translateY(-100%); + font-weight: bold; +} + +.KnownHosts-error { + display: flex; + align-items: center; +} + +.KnownHosts-error svg { + margin-left: 4px; +} + +.KnownHosts .MuiSelect-select .KnownHosts-item__status { + display: flex; +} + +.Mui-selected .KnownHosts-item__label svg { + display: block; +} + +.MuiSelect-select .KnownHosts-item__edit { + display: none; +} diff --git a/webclient/src/components/KnownHosts/KnownHosts.i18n.json b/webclient/src/components/KnownHosts/KnownHosts.i18n.json new file mode 100644 index 000000000..3de8fd887 --- /dev/null +++ b/webclient/src/components/KnownHosts/KnownHosts.i18n.json @@ -0,0 +1,7 @@ +{ + "KnownHosts": { + "label": "Host", + "add": "Add new host", + "toast": "Host successfully {mode, select, created {created} deleted {deleted} other {edited}}." + } +} diff --git a/webclient/src/components/KnownHosts/KnownHosts.tsx b/webclient/src/components/KnownHosts/KnownHosts.tsx new file mode 100644 index 000000000..f65a98c38 --- /dev/null +++ b/webclient/src/components/KnownHosts/KnownHosts.tsx @@ -0,0 +1,285 @@ +import { useCallback, useEffect, useState } from 'react'; +import { styled } from '@mui/material/styles'; +import { useTranslation } from 'react-i18next'; +import { Select, MenuItem } from '@mui/material'; +import Button from '@mui/material/Button'; +import FormControl from '@mui/material/FormControl'; +import IconButton from '@mui/material/IconButton'; +import WifiTetheringIcon from '@mui/icons-material/WifiTethering'; +import PortableWifiOffIcon from '@mui/icons-material/PortableWifiOff'; +import InputLabel from '@mui/material/InputLabel'; +import Check from '@mui/icons-material/Check'; +import AddIcon from '@mui/icons-material/Add'; +import EditRoundedIcon from '@mui/icons-material/Edit'; +import ErrorOutlinedIcon from '@mui/icons-material/ErrorOutlined'; + +import { AuthenticationService } from 'api'; +import { KnownHostDialog } from 'dialogs'; +import { useReduxEffect } from 'hooks'; +import { HostDTO } from 'services'; +import { ServerTypes } from 'store'; +import { DefaultHosts, Host, getHostPort } from 'types'; +import Toast from 'components/Toast/Toast'; + +import './KnownHosts.css'; + +enum TestConnection { + TESTING = 'testing', + FAILED = 'failed', + SUCCESS = 'success', +} + +const PREFIX = 'KnownHosts'; + +const classes = { + root: `${PREFIX}-root` +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { + '& .KnownHosts-error': { + color: theme.palette.error.main + }, + + '& .KnownHosts-warning': { + color: theme.palette.warning.main + }, + + '& .KnownHosts-item': { + [`& .${TestConnection.TESTING}`]: { + color: theme.palette.warning.main + }, + [`& .${TestConnection.FAILED}`]: { + color: theme.palette.error.main + }, + [`& .${TestConnection.SUCCESS}`]: { + color: theme.palette.success.main + } + } + } +})); + +const KnownHosts = (props) => { + const { input: { onChange }, meta, disabled } = props; + const { touched, error, warning } = meta; + + const { t } = useTranslation(); + + const [hostsState, setHostsState] = useState({ + hosts: [], + selectedHost: {} as any, + }); + + const [dialogState, setDialogState] = useState({ + open: false, + edit: null, + }); + + const [testingConnection, setTestingConnection] = useState(null); + + const [showCreateToast, setShowCreateToast] = useState(false); + const [showDeleteToast, setShowDeleteToast] = useState(false); + const [showEditToast, setShowEditToast] = useState(false); + + const loadKnownHosts = useCallback(async () => { + const hosts = await HostDTO.getAll(); + + if (!hosts?.length) { + // @TODO: find a better pattern to seeding default data in indexedDB + await HostDTO.bulkAdd(DefaultHosts); + loadKnownHosts(); + } else { + const selectedHost = hosts.find(({ lastSelected }) => lastSelected) || hosts[0]; + setHostsState(s => ({ ...s, hosts, selectedHost })); + } + }, []); + + useEffect(() => { + loadKnownHosts(); + }, [loadKnownHosts]); + + useEffect(() => { + const { hosts, selectedHost } = hostsState; + + if (selectedHost?.id) { + updateLastSelectedHost(selectedHost.id).then(() => { + onChange(selectedHost); + }); + } + }, [hostsState, onChange]); + + useReduxEffect(() => { + setTestingConnection(TestConnection.SUCCESS); + }, ServerTypes.TEST_CONNECTION_SUCCESSFUL, []); + + useReduxEffect(() => { + setTestingConnection(TestConnection.FAILED); + }, ServerTypes.TEST_CONNECTION_FAILED, []); + + const selectHost = (selectedHost) => { + setHostsState(s => ({ ...s, selectedHost })); + }; + + const openAddKnownHostDialog = () => { + setDialogState(s => ({ ...s, open: true, edit: null })); + }; + + const openEditKnownHostDialog = (host: HostDTO) => { + setDialogState(s => ({ ...s, open: true, edit: host })); + }; + + const closeKnownHostDialog = () => { + setDialogState(s => ({ ...s, open: false })); + } + + const handleDialogRemove = async ({ id }) => { + setHostsState(s => ({ + ...s, + hosts: s.hosts.filter(host => host.id !== id), + selectedHost: s.selectedHost.id === id ? s.hosts[0] : s.selectedHost, + })); + + closeKnownHostDialog(); + HostDTO.delete(id); + setShowDeleteToast(true) + }; + + const handleDialogSubmit = async ({ id, name, host, port }) => { + if (id) { + const hostDTO = await HostDTO.get(id); + hostDTO.name = name; + hostDTO.host = host; + hostDTO.port = port; + await hostDTO.save(); + + setHostsState(s => ({ + ...s, + hosts: s.hosts.map(h => h.id === id ? hostDTO : h), + selectedHost: hostDTO + })); + setShowEditToast(true) + } else { + const newHost: Host = { name, host, port, editable: true }; + newHost.id = await HostDTO.add(newHost) as number; + + setHostsState(s => ({ + ...s, + hosts: [...s.hosts, newHost], + selectedHost: newHost, + })); + setShowCreateToast(true) + } + + closeKnownHostDialog(); + }; + + const updateLastSelectedHost = (hostId): Promise => { + testConnection(); + + return HostDTO.getAll().then(hosts => + hosts.map(async host => { + if (host.id === hostId) { + host.lastSelected = true; + return await host.save(); + } + + if (host.lastSelected) { + host.lastSelected = false; + return await host.save(); + } + + return host; + }) + ); + }; + + const testConnection = () => { + setTestingConnection(TestConnection.TESTING); + + const options = { ...getHostPort(hostsState.selectedHost) }; + AuthenticationService.testConnection(options); + } + + return ( + + + { touched && ( +
+ { + (error && +
+ {error} + +
+ ) || + + (warning &&
{warning}
) + } +
+ ) } + + { t('KnownHosts.label') } + +
+ + + setShowCreateToast(false)}>{ t('KnownHosts.toast', { mode: 'created' }) } + setShowDeleteToast(false)}>{ t('KnownHosts.toast', { mode: 'deleted' }) } + setShowEditToast(false)}>{ t('KnownHosts.toast', { mode: 'edited' }) } +
+ ); +}; + +export default KnownHosts; diff --git a/webclient/src/components/LanguageDropdown/LanguageDropdown.css b/webclient/src/components/LanguageDropdown/LanguageDropdown.css new file mode 100644 index 000000000..c4db3d0d0 --- /dev/null +++ b/webclient/src/components/LanguageDropdown/LanguageDropdown.css @@ -0,0 +1,19 @@ +.LanguageDropdown { +} + +.LanguageDropdown-item { + display: flex; + align-items: center; +} + +.LanguageDropdown-item__image { + width: 1.5em; +} + +.MuiSelect-select .LanguageDropdown-item__label { + display: none; +} + +.MuiList-root .LanguageDropdown-item__image { + margin-right: 1em; +} diff --git a/webclient/src/components/LanguageDropdown/LanguageDropdown.tsx b/webclient/src/components/LanguageDropdown/LanguageDropdown.tsx new file mode 100644 index 000000000..8f63bd549 --- /dev/null +++ b/webclient/src/components/LanguageDropdown/LanguageDropdown.tsx @@ -0,0 +1,57 @@ +// eslint-disable-next-line +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Select, MenuItem } from '@mui/material'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; + +import { Images } from 'images/Images'; +import { Language, LanguageCountry, LanguageNative } from 'types'; + +import './LanguageDropdown.css'; + +const LanguageDropdown = () => { + const { t, i18n } = useTranslation(); + const [language, setLanguage] = useState(i18n.resolvedLanguage); + + useEffect(() => { + if (language !== i18n.resolvedLanguage) { + i18n.changeLanguage(language); + } + }, [language]); + + return ( + + + + ) +}; + +export default LanguageDropdown; diff --git a/webclient/src/components/Message/CardCallout.css b/webclient/src/components/Message/CardCallout.css new file mode 100644 index 000000000..0011b238a --- /dev/null +++ b/webclient/src/components/Message/CardCallout.css @@ -0,0 +1,4 @@ +.callout { + font-weight: bold; + color: green; +} diff --git a/webclient/src/components/Message/CardCallout.tsx b/webclient/src/components/Message/CardCallout.tsx new file mode 100644 index 000000000..d34316541 --- /dev/null +++ b/webclient/src/components/Message/CardCallout.tsx @@ -0,0 +1,94 @@ +// eslint-disable-next-line +import React, { useMemo, useState } from 'react'; +import { styled } from '@mui/material/styles'; +import Popover from '@mui/material/Popover'; + +import { CardDTO, TokenDTO } from 'services'; + +import CardDetails from '../CardDetails/CardDetails'; +import TokenDetails from '../TokenDetails/TokenDetails'; + +import './CardCallout.css'; + +const PREFIX = 'CardCallout'; + +const classes = { + popover: `${PREFIX}-popover`, + popoverContent: `${PREFIX}-popoverContent` +}; + +const Root = styled('span')(({ theme }) => ({ + [`& .${classes.popover}`]: { + pointerEvents: 'none', + }, + + [`& .${classes.popoverContent}`]: { + pointerEvents: 'none', + } +})); + +const CardCallout = ({ name }) => { + const [card, setCard] = useState(null); + const [token, setToken] = useState(null); + const [anchorEl, setAnchorEl] = useState(null); + + useMemo(async () => { + const card = await CardDTO.get(name); + if (card) { + return setCard(card) + } + + const token = await TokenDTO.get(name); + if (token) { + return setToken(token); + } + }, [name]); + + const handlePopoverOpen = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handlePopoverClose = () => { + setAnchorEl(null); + }; + + const open = Boolean(anchorEl); + + return ( + + {card?.name || token?.name?.value || name} + + { + (card || token) && ( + +
+ { card && () } + { token && () } +
+
+ ) + } +
+ ); +}; + +export default CardCallout; diff --git a/webclient/src/components/Message/Message.css b/webclient/src/components/Message/Message.css new file mode 100644 index 000000000..cbb0df2a9 --- /dev/null +++ b/webclient/src/components/Message/Message.css @@ -0,0 +1,3 @@ +.link { + color: blue; +} diff --git a/webclient/src/components/Message/Message.tsx b/webclient/src/components/Message/Message.tsx new file mode 100644 index 000000000..ec0b05507 --- /dev/null +++ b/webclient/src/components/Message/Message.tsx @@ -0,0 +1,105 @@ +// eslint-disable-next-line +import React, { useEffect, useMemo, useState } from 'react'; + +import { NavLink, generatePath } from 'react-router-dom'; + +import { + RouteEnum, + URL_REGEX, + MESSAGE_SENDER_REGEX, + MENTION_REGEX, + CARD_CALLOUT_REGEX, + CALLOUT_BOUNDARY_REGEX, +} from 'types'; + +import CardCallout from './CardCallout'; +import './Message.css'; + +const Message = ({ message: { message, messageType, timeOf, timeReceived } }) => ( +
+
+ +
+
+); + +const ParsedMessage = ({ message }) => { + const [messageChunks, setMessageChunks] = useState(null); + const [name, setName] = useState(null); + + useMemo(() => { + const name = message.match(MESSAGE_SENDER_REGEX); + + if (name) { + setName(name[1]); + } + + setMessageChunks(parseMessage(message)); + }, [message]); + + return ( +
+ { name && (:) } + { messageChunks } +
+ ); +}; + +const PlayerLink = ({ name, label = name }) => ( + + {label} + +); + +function parseMessage(message) { + return message.replace(MESSAGE_SENDER_REGEX, '') + .split(CARD_CALLOUT_REGEX) + .filter(chunk => !!chunk) + .map(parseChunks); +} + +function parseChunks(chunk, index) { + if (chunk.match(CARD_CALLOUT_REGEX)) { + const name = chunk.replace(CALLOUT_BOUNDARY_REGEX, '').trim(); + return (); + } + + if (chunk.match(URL_REGEX)) { + return parseUrlChunk(chunk); + } + + if (chunk.match(MENTION_REGEX)) { + return parseMentionChunk(chunk); + } + + return chunk; +} + +function parseUrlChunk(chunk) { + return chunk.split(URL_REGEX) + .filter(urlChunk => !!urlChunk) + .map((urlChunk, index) => { + if (urlChunk.match(URL_REGEX)) { + return ({urlChunk}); + } + + return urlChunk; + }); +} + +function parseMentionChunk(chunk) { + return chunk.split(MENTION_REGEX) + .filter(mentionChunk => !!mentionChunk) + .map((mentionChunk, index) => { + const mention = mentionChunk.match(MENTION_REGEX); + + if (mention) { + const name = mention[0].substr(1); + return (); + } + + return mentionChunk; + }); +} + +export default Message; diff --git a/webclient/src/components/ScrollToBottomOnChanges/ScrollToBottomOnChanges.tsx b/webclient/src/components/ScrollToBottomOnChanges/ScrollToBottomOnChanges.tsx new file mode 100644 index 000000000..939a7d753 --- /dev/null +++ b/webclient/src/components/ScrollToBottomOnChanges/ScrollToBottomOnChanges.tsx @@ -0,0 +1,25 @@ +import React, { useEffect, useRef } from 'react'; + +const ScrollToBottomOnChanges = ({ content, changes }) => { + const messagesEndRef = useRef(null); + + // @TODO (2) + const scrollToBottom = () => { + messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }) + } + + useEffect(scrollToBottom, [changes]); + + const styling = { + height: '100%' + }; + + return ( +
+ {content} +
+
+ ) +} + +export default ScrollToBottomOnChanges; diff --git a/webclient/src/components/SelectField/SelectField.css b/webclient/src/components/SelectField/SelectField.css new file mode 100644 index 000000000..c59e851ae --- /dev/null +++ b/webclient/src/components/SelectField/SelectField.css @@ -0,0 +1,4 @@ +.select-field label { + background: white; + padding: 0 5px; +} \ No newline at end of file diff --git a/webclient/src/components/SelectField/SelectField.tsx b/webclient/src/components/SelectField/SelectField.tsx new file mode 100644 index 000000000..fdbef0e9c --- /dev/null +++ b/webclient/src/components/SelectField/SelectField.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; + +import './SelectField.css'; + +const SelectField = ({ input, label, options, value }) => { + const id = label + '-select-field'; + const labelId = id + '-label'; + + return ( + + {label} + + + ); +}; + +export default SelectField; diff --git a/webclient/src/components/ThreePaneLayout/ThreePaneLayout.css b/webclient/src/components/ThreePaneLayout/ThreePaneLayout.css new file mode 100644 index 000000000..f439dee5c --- /dev/null +++ b/webclient/src/components/ThreePaneLayout/ThreePaneLayout.css @@ -0,0 +1,37 @@ +.three-pane-layout, +.three-pane-layout .grid { + width: 100%; + height: 100%; + margin: 0; +} + +.three-pane-layout .grid-main, +.three-pane-layout .grid-side { + height: 100%; +} + +.three-pane-layout .grid-main { + display: flex; + flex-direction: column; +} + +.three-pane-layout .grid-main__top { + max-height: 50%; + width: 100%; + padding-bottom: 20px; + flex-shrink: 0; +} + +.three-pane-layout .grid-main__bottom { + height: 100%; + width: 100%; + flex-shrink: 1; + overflow: hidden; +} + +.three-pane-layout .grid-main__top.fixedHeight, +.three-pane-layout .grid-main__bottom.fixedHeight { + height: 50%; + overflow: visible; + padding: 0 0 16px; +} diff --git a/webclient/src/components/ThreePaneLayout/ThreePaneLayout.tsx b/webclient/src/components/ThreePaneLayout/ThreePaneLayout.tsx new file mode 100644 index 000000000..7919f9d35 --- /dev/null +++ b/webclient/src/components/ThreePaneLayout/ThreePaneLayout.tsx @@ -0,0 +1,49 @@ +import { Component, CElement } from 'react'; +import { connect } from 'react-redux'; +import Grid from '@mui/material/Grid'; +import Hidden from '@mui/material/Hidden'; + +import './ThreePaneLayout.css'; + +// @DEPRECATED +// This component sucks balls, dont use it. It will be removed sooner than later. +class ThreePaneLayout extends Component { + render() { + return ( +
+ + + + {this.props.top} + + + {this.props.bottom} + + + + + {this.props.side} + + + +
+ ); + } +} + +interface ThreePaneLayoutProps { + top: CElement, + bottom: CElement, + side?: CElement, + fixedHeight?: boolean, +} + +const mapStateToProps = state => ({}); + +export default connect(mapStateToProps)(ThreePaneLayout); diff --git a/webclient/src/components/Toast/Toast.tsx b/webclient/src/components/Toast/Toast.tsx new file mode 100644 index 000000000..4ef8a3cad --- /dev/null +++ b/webclient/src/components/Toast/Toast.tsx @@ -0,0 +1,65 @@ +import * as React from 'react' +import ReactDOM from 'react-dom' + +import Alert, { AlertProps } from '@mui/material/Alert'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import Slide, { SlideProps } from '@mui/material/Slide'; +import Snackbar from '@mui/material/Snackbar'; + +const iconMapping = { + success: +} + +function Toast(props) { + const { open, onClose, severity, autoHideDuration, children } = props + + const rootElemRef = React.useRef(document.createElement('div')); + + React.useEffect(() => { + document.body.appendChild(rootElemRef.current) + return () => { + rootElemRef.current.remove(); + } + }, [rootElemRef]) + + const handleClose = (event?: React.SyntheticEvent, reason?: string) => { + if (reason === 'clickaway') { + return; + } + onClose(event); + }; + + const node = ( + + + {children} + + + ) + if (!rootElemRef.current) { + return null + } + + return ReactDOM.createPortal( + node, + rootElemRef.current + ); +} + +Toast.defaultProps = { + severity: 'success', + // 10s wait before automatically dismissing the Toast. + autoHideDuration: 10000, +} + +function TransitionLeft(props) { + return ; +} + +export default Toast diff --git a/webclient/src/components/Toast/ToastContext.tsx b/webclient/src/components/Toast/ToastContext.tsx new file mode 100644 index 000000000..44753d87f --- /dev/null +++ b/webclient/src/components/Toast/ToastContext.tsx @@ -0,0 +1,71 @@ +import { createContext, FC, PropsWithChildren, ReactChild, ReactNode, useContext, useEffect, useReducer, ContextType, Context } from 'react' + +import { ACTIONS, initialState, reducer } from './reducer'; +import Toast from './Toast' + +interface ToastEntry { + isOpen: boolean, + children: ReactChild, +} + +interface ToastState { + toasts: Map, + addToast: (key, children) => void, + openToast: (key) => void, + closeToast: (key) => void, + removeToast: (key) => void, +} + +const ToastContext: Context = createContext({ + toasts: new Map(), + addToast: (key, children) => {}, + openToast: (key) => {}, + closeToast: (key) => {}, + removeToast: (key) => {}, +}); + +export const ToastProvider: FC = (props) => { + const { children } = props + const [state, dispatch] = useReducer(reducer, initialState) + const providerState = { + toasts: state.toasts, + addToast: (key, children) => dispatch({ type: ACTIONS.ADD_TOAST, payload: { key, children } }), + openToast: key => dispatch({ type: ACTIONS.OPEN_TOAST, payload: { key } }), + closeToast: key => dispatch({ type: ACTIONS.CLOSE_TOAST, payload: { key } }), + removeToast: key => dispatch({ type: ACTIONS.REMOVE_TOAST, payload: { key } }), + } + return ( + + {children} +
+ {Array.from(state.toasts).map(([key, value]) => { + const { isOpen, children } = value; + return ( + dispatch({ type: ACTIONS.CLOSE_TOAST, payload: { key } })}> + {children} + + ) + })} +
+
+ ) +} + +export interface ToastHookOptions { + key: string, + children: ReactNode +} + +export function useToast({ key, children }) { + const { addToast, openToast, closeToast, removeToast } = useContext(ToastContext) + + useEffect(() => { + addToast(key, children) + }, []) + + return { + openToast: () => openToast(key), + closeToast: () => closeToast(key), + removeToast: () => removeToast(key), + } +} diff --git a/webclient/src/components/Toast/index.ts b/webclient/src/components/Toast/index.ts new file mode 100644 index 000000000..40e7c7368 --- /dev/null +++ b/webclient/src/components/Toast/index.ts @@ -0,0 +1,8 @@ +import { useToast, ToastProvider } from './ToastContext'; +import Toast from './Toast'; + +export { + Toast as default, + useToast, + ToastProvider, +} diff --git a/webclient/src/components/Toast/reducer.ts b/webclient/src/components/Toast/reducer.ts new file mode 100644 index 000000000..3f600db52 --- /dev/null +++ b/webclient/src/components/Toast/reducer.ts @@ -0,0 +1,61 @@ +export const ACTIONS = { + ADD_TOAST: 'ADD_TOAST', + OPEN_TOAST: 'OPEN_TOAST', + CLOSE_TOAST: 'CLOSE_TOAST', + REMOVE_TOAST: 'REMOVE_TOAST', +} + +export const initialState = { + toasts: {} +} + +export function reducer(state, { type, payload }) { + const { key, children } = payload; + + switch (type) { + case ACTIONS.ADD_TOAST: { + return { + ...state, + toasts: { + ...state.toasts, + [key]: { + isOpen: false, + children, + }, + }, + }; + } + case ACTIONS.OPEN_TOAST: { + return { + ...state, + toasts: { + ...state.toasts, + [key]: { + ...state.toasts[key], + isOpen: true, + }, + }, + }; + } + case ACTIONS.CLOSE_TOAST: { + return { + ...state, + toasts: { + ...state.toasts, + [key]: { + ...state.toasts[key], + isOpen: false, + }, + }, + }; + } + case ACTIONS.REMOVE_TOAST: { + const newState = { ...state }; + delete newState.toasts[key]; + + return newState; + } + default: + throw Error('Please pick an available action') + } +} diff --git a/webclient/src/components/Token/Token.css b/webclient/src/components/Token/Token.css new file mode 100644 index 000000000..08f18b872 --- /dev/null +++ b/webclient/src/components/Token/Token.css @@ -0,0 +1,4 @@ +.token { + width: 100%; + height: 100%; +} diff --git a/webclient/src/components/Token/Token.tsx b/webclient/src/components/Token/Token.tsx new file mode 100644 index 000000000..29b39ecc5 --- /dev/null +++ b/webclient/src/components/Token/Token.tsx @@ -0,0 +1,19 @@ +// eslint-disable-next-line +import React, { useMemo, useState } from 'react'; + +import { TokenDTO } from 'services'; + +import './Token.css'; + +interface TokenProps { + token: TokenDTO; +} + +const Token = ({ token }: TokenProps) => { + const set = Array.isArray(token?.set) ? token?.set[0] : token?.set; + return token && ( + {token?.name?.value} + ); +} + +export default Token; diff --git a/webclient/src/components/TokenDetails/TokenDetails.css b/webclient/src/components/TokenDetails/TokenDetails.css new file mode 100644 index 000000000..3c3267839 --- /dev/null +++ b/webclient/src/components/TokenDetails/TokenDetails.css @@ -0,0 +1,46 @@ +.tokenDetails { + padding: 10px; + width: calc(400px * .716); + font-size: 10px; +} + +.tokenDetails-token { + height: 400px; + margin: 0 auto; +} + +.tokenDetails-attribute { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.tokenDetails-attributes { + margin-top: 10px; +} + +.tokenDetails-attribute__label { + text-transform: uppercase; + font-size: 10px; + margin-right: 10px; +} + +.tokenDetails-attribute__value { + text-align: right; +} + +.tokenDetails-text { + font-size: 12px; + margin-top: 10px; + padding: 5px; + background: rgba(0, 0, 0, .15); + white-space: pre-line; +} + +.tokenDetails-text__flavor { + font-style: italic; +} + +.tokenDetails-text__current:not(:empty) + .tokenDetails-text__flavor { + margin-top: 10px; +} diff --git a/webclient/src/components/TokenDetails/TokenDetails.tsx b/webclient/src/components/TokenDetails/TokenDetails.tsx new file mode 100644 index 000000000..f3d8aed96 --- /dev/null +++ b/webclient/src/components/TokenDetails/TokenDetails.tsx @@ -0,0 +1,86 @@ +// eslint-disable-next-line +import React, { useMemo, useState } from 'react'; + +import { TokenDTO } from 'services'; + +import Token from '../Token/Token'; + +import './TokenDetails.css'; + +interface TokenProps { + token: TokenDTO; +} + +const TokenDetails = ({ token }: TokenProps) => { + const props = token?.prop?.value; + + return ( +
+
+ +
+ + { + token && ( +
+
+
+ Name: + {token.name?.value} +
+ + { + (!props.pt?.value) ? null : ( +
+ P/T: + {props.pt.value} +
+ ) + } + + { + !props.colors?.value ? null : ( +
+ Color(s): + {props.colors.value} +
+ ) + } + + { + !props.maintype?.value ? null : ( +
+ Main Type: + {props.maintype.value} +
+ ) + } + + { + !props.type?.value ? null : ( +
+ Type: + {props.type.value} +
+ ) + } +
+ + { + !token.text?.value ? null : ( +
+
+ {token.text.value} +
+
+ ) + } +
+ ) + } + +
+ ); +} + +export default TokenDetails; diff --git a/webclient/src/components/UserDisplay/UserDisplay.css b/webclient/src/components/UserDisplay/UserDisplay.css new file mode 100644 index 000000000..0697c7f68 --- /dev/null +++ b/webclient/src/components/UserDisplay/UserDisplay.css @@ -0,0 +1,16 @@ +.user-display, +.user-display__link { + height: 100%; + width: 100%; +} + +.user-display__details { + height: 100%; + display: flex; + align-items: center; +} + +.user-display__country { + width: 1.1em; + margin-right: 0.4em; +} diff --git a/webclient/src/components/UserDisplay/UserDisplay.tsx b/webclient/src/components/UserDisplay/UserDisplay.tsx new file mode 100644 index 000000000..9c45a0f51 --- /dev/null +++ b/webclient/src/components/UserDisplay/UserDisplay.tsx @@ -0,0 +1,150 @@ +// eslint-disable-next-line +import React, { Component } from "react"; +import { connect } from 'react-redux'; +import { NavLink, generatePath } from 'react-router-dom'; + +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; + +import { Images } from 'images/Images'; +import { SessionService } from 'api'; +import { ServerSelectors } from 'store'; +import { RouteEnum, User } from 'types'; + +import './UserDisplay.css'; + + +class UserDisplay extends Component { + constructor(props) { + super(props); + + this.handleClick = this.handleClick.bind(this); + this.handleClose = this.handleClose.bind(this); + this.navigateToUserProfile = this.navigateToUserProfile.bind(this); + this.addToBuddyList = this.addToBuddyList.bind(this); + this.removeFromBuddyList = this.removeFromBuddyList.bind(this); + this.addToIgnoreList = this.addToIgnoreList.bind(this); + this.removeFromIgnoreList = this.removeFromIgnoreList.bind(this); + + this.isABuddy = this.isABuddy.bind(this); + this.isIgnored = this.isIgnored.bind(this); + + this.state = { + position: null + }; + } + + handleClick(event) { + event.preventDefault(); + + this.setState({ + position: { + x: event.clientX + 2, + y: event.clientY + 4, + } + }); + } + + handleClose() { + this.setState({ + position: null + }); + } + + navigateToUserProfile() { + this.handleClose(); + } + + addToBuddyList() { + SessionService.addToBuddyList(this.props.user.name); + this.handleClose(); + } + + removeFromBuddyList() { + SessionService.removeFromBuddyList(this.props.user.name); + this.handleClose(); + } + + addToIgnoreList() { + SessionService.addToIgnoreList(this.props.user.name); + this.handleClose(); + } + + removeFromIgnoreList() { + SessionService.removeFromIgnoreList(this.props.user.name); + this.handleClose(); + } + + isABuddy() { + return this.props.buddyList.filter(user => user.name === this.props.user.name).length; + } + + isIgnored() { + return this.props.ignoreList.filter(user => user.name === this.props.user.name).length; + } + + render() { + const { user } = this.props; + const { position } = this.state; + const { name, country } = user; + + const isABuddy = this.isABuddy(); + const isIgnored = this.isIgnored(); + + // console.log('user', name, !!isABuddy, !!isIgnored); + + return ( +
+ +
+ {country} +
{name}
+
+
+
+ + + Chat + + { + !isABuddy + ? (Add to Buddy List) + : (Remove From Buddy List) + } + { + !isIgnored + ? (Add to Ignore List) + : (Remove From Ignore List) + } + +
+
+ ); + } +} + +interface UserDisplayProps { + user: User; + buddyList: User[]; + ignoreList: User[]; +} + +interface UserDisplayState { + position: any; +} + +const mapStateToProps = (state) => ({ + buddyList: ServerSelectors.getBuddyList(state), + ignoreList: ServerSelectors.getIgnoreList(state) +}); + +export default connect(mapStateToProps)(UserDisplay); diff --git a/webclient/src/components/VirtualList/VirtualList.css b/webclient/src/components/VirtualList/VirtualList.css new file mode 100644 index 000000000..330cd5a90 --- /dev/null +++ b/webclient/src/components/VirtualList/VirtualList.css @@ -0,0 +1,3 @@ +.virtual-list { + height: 100%; +} \ No newline at end of file diff --git a/webclient/src/components/VirtualList/VirtualList.tsx b/webclient/src/components/VirtualList/VirtualList.tsx new file mode 100644 index 000000000..e40ef712c --- /dev/null +++ b/webclient/src/components/VirtualList/VirtualList.tsx @@ -0,0 +1,35 @@ +// eslint-disable-next-line +import React from "react"; + +import { FixedSizeList as List } from 'react-window'; +import AutoSizer from 'react-virtualized-auto-sizer'; + +import './VirtualList.css'; + +const VirtualList = ({ items, itemKey, className = {}, size = 30 }) => ( +
+ + {({ height, width }) => ( + + {Row} + + )} + +
+); + +const Row = ({ data, index, style }) => ( +
+ {data[index]} +
+); + +export default VirtualList; diff --git a/webclient/src/components/index.ts b/webclient/src/components/index.ts new file mode 100644 index 000000000..2d67b3003 --- /dev/null +++ b/webclient/src/components/index.ts @@ -0,0 +1,19 @@ +// Common components +export { default as Card } from './Card/Card'; +export { default as CardDetails } from './CardDetails/CardDetails'; +export { default as CountryDropdown } from './CountryDropdown/CountryDropdown'; +export { default as InputField } from './InputField/InputField'; +export { default as InputAction } from './InputAction/InputAction'; +export { default as KnownHosts } from './KnownHosts/KnownHosts'; +export { default as LanguageDropdown } from './LanguageDropdown/LanguageDropdown'; +export { default as Message } from './Message/Message'; +export { default as VirtualList } from './VirtualList/VirtualList'; +export { default as UserDisplay } from './UserDisplay/UserDisplay'; +export { default as ThreePaneLayout } from './ThreePaneLayout/ThreePaneLayout'; +export { default as CheckboxField } from './CheckboxField/CheckboxField'; +export { default as SelectField } from './SelectField/SelectField'; +export { default as ScrollToBottomOnChanges } from './ScrollToBottomOnChanges/ScrollToBottomOnChanges'; + +// Guards +export { default as AuthGuard } from './Guard/AuthGuard'; +export { default as ModGuard } from './Guard/ModGuard'; diff --git a/webclient/src/containers/Account/Account.css b/webclient/src/containers/Account/Account.css new file mode 100644 index 000000000..6b5c995a9 --- /dev/null +++ b/webclient/src/containers/Account/Account.css @@ -0,0 +1,53 @@ +.account { + display: flex; + justify-content: space-between; + height: 100%; + padding: 5px; +} + +.account-column { + display: flex; + flex-direction: column; + width: 33%; +} + + +.account-list { + display: flex; + flex-direction: column; + height: 100%; + padding: 20px; +} + +.account-details { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; +} + + +.account-details__actions { + display: flex; + align-items: stretch; + justify-content: space-around; + width: 100%; +} + +.account-details p { + margin-bottom: 10px; +} + +.account-details button { + margin-top: 10px; + font-size: 10px; +} + +.account-details > img { + width: 100%; + margin-bottom: 20px; +} + +.account-details__lang { + margin-top: 20px; +} diff --git a/webclient/src/containers/Account/Account.tsx b/webclient/src/containers/Account/Account.tsx new file mode 100644 index 000000000..0c4e001a0 --- /dev/null +++ b/webclient/src/containers/Account/Account.tsx @@ -0,0 +1,120 @@ +// eslint-disable-next-line +import React, { Component } from "react"; +import { useTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; + +import Button from '@mui/material/Button'; +import ListItem from '@mui/material/ListItem'; +import Paper from '@mui/material/Paper'; + +import { UserDisplay, VirtualList, AuthGuard, LanguageDropdown } from 'components'; +import { AuthenticationService, SessionService } from 'api'; +import { ServerSelectors } from 'store'; +import { User } from 'types'; +import Layout from 'containers/Layout/Layout'; + +import AddToBuddies from './AddToBuddies'; +import AddToIgnore from './AddToIgnore'; + +import './Account.css'; + +const Account = (props: AccountProps) => { + const { buddyList, ignoreList, serverName, serverVersion, user } = props; + const { country, realName, name, userLevel, accountageSecs, avatarBmp } = user || {}; + let url = URL.createObjectURL(new Blob([avatarBmp], { 'type': 'image/png' })); + + const { t } = useTranslation(); + + const handleAddToBuddies = ({ userName }) => { + SessionService.addToBuddyList(userName); + }; + + const handleAddToIgnore = ({ userName }) => { + SessionService.addToIgnoreList(userName); + }; + + return ( + + +
+ +
+ Buddies Online: ?/{buddyList.length} +
+ buddyList[index].name } + items={ buddyList.map(user => ( + + + + )) } + /> +
+ +
+
+
+
+ +
+ Ignored Users Online: ?/{ignoreList.length} +
+ ignoreList[index].name } + items={ ignoreList.map(user => ( + + + + )) } + /> +
+ +
+
+
+
+ + {name} +

{name}

+

Location: ({country?.toUpperCase()})

+

User Level: {userLevel}

+

Account Age: {accountageSecs}

+

Real Name: {realName}

+
+ + + +
+ +
+ +

Server Name: {serverName}

+

Server Version: {serverVersion}

+ + +
+ +
+
+
+
+ ) +} + +interface AccountProps { + buddyList: User[]; + ignoreList: User[]; + serverName: string; + serverVersion: string; + user: User; +} + +const mapStateToProps = state => ({ + buddyList: ServerSelectors.getBuddyList(state), + ignoreList: ServerSelectors.getIgnoreList(state), + serverName: ServerSelectors.getName(state), + serverVersion: ServerSelectors.getVersion(state), + user: ServerSelectors.getUser(state), +}); + +export default connect(mapStateToProps)(Account); diff --git a/webclient/src/containers/Account/AddToBuddies.tsx b/webclient/src/containers/Account/AddToBuddies.tsx new file mode 100644 index 000000000..3aa5489ad --- /dev/null +++ b/webclient/src/containers/Account/AddToBuddies.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { Form } from 'react-final-form' + +import { InputAction } from 'components'; + +const AddToBuddies = ({ onSubmit }) => ( +
onSubmit(values)}> + {({ handleSubmit }) => ( + + + + )} + +); + +export default AddToBuddies; diff --git a/webclient/src/containers/Account/AddToIgnore.tsx b/webclient/src/containers/Account/AddToIgnore.tsx new file mode 100644 index 000000000..270036946 --- /dev/null +++ b/webclient/src/containers/Account/AddToIgnore.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { Form } from 'react-final-form' + +import { InputAction } from 'components'; + +const AddToIgnore = ({ onSubmit }) => ( +
onSubmit(values)}> + {({ handleSubmit }) => ( + + + + )} + +); + +export default AddToIgnore; diff --git a/webclient/src/containers/App/AppShell.css b/webclient/src/containers/App/AppShell.css new file mode 100644 index 000000000..4492a6bcf --- /dev/null +++ b/webclient/src/containers/App/AppShell.css @@ -0,0 +1,10 @@ +.AppShell, +.AppShell-routes { + height: 100%; +} + +.AppShell { + display: flex; + flex-direction: column; + min-width: 768px; +} \ No newline at end of file diff --git a/webclient/src/containers/App/AppShell.tsx b/webclient/src/containers/App/AppShell.tsx new file mode 100644 index 000000000..c28d20067 --- /dev/null +++ b/webclient/src/containers/App/AppShell.tsx @@ -0,0 +1,42 @@ +import { Component, Suspense } from 'react'; +import { Provider } from 'react-redux'; +import { MemoryRouter as Router } from 'react-router-dom'; +import CssBaseline from '@mui/material/CssBaseline'; +import { store } from 'store'; +import Routes from './AppShellRoutes'; +import FeatureDetection from './FeatureDetection'; + +import './AppShell.css'; + +import { ToastProvider } from 'components/Toast' + +class AppShell extends Component { + componentDidMount() { + // @TODO (1) + window.onbeforeunload = () => true; + } + + handleContextMenu(event) { + event.preventDefault(); + } + + render() { + return ( + + + + +
+ + + + +
+
+
+
+ ); + } +} + +export default AppShell; diff --git a/webclient/src/containers/App/AppShellRoutes.tsx b/webclient/src/containers/App/AppShellRoutes.tsx new file mode 100644 index 000000000..c80f74c30 --- /dev/null +++ b/webclient/src/containers/App/AppShellRoutes.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Navigate, Route, Routes } from 'react-router-dom'; + +import { RouteEnum } from 'types'; +import { + Account, + Decks, + Game, + Player, + Room, + Server, + Login, + Logs, + Initialize, + Unsupported +} from 'containers'; + +const AppShellRoutes = () => ( +
+ + } /> + + } /> + } /> + } /> + } /> + } /> + {} />} + } /> + } /> + } /> + +
+); + +export default AppShellRoutes; diff --git a/webclient/src/containers/App/FeatureDetection.tsx b/webclient/src/containers/App/FeatureDetection.tsx new file mode 100644 index 000000000..ed9e1f241 --- /dev/null +++ b/webclient/src/containers/App/FeatureDetection.tsx @@ -0,0 +1,26 @@ +import { useEffect, useState } from 'react'; +import { Navigate } from 'react-router-dom'; +import { dexieService } from 'services'; +import { RouteEnum } from 'types'; + +const FeatureDetection = () => { + const [unsupported, setUnsupported] = useState(false); + + useEffect(() => { + const features: Promise[] = [ + detectIndexedDB(), + ]; + + Promise.all(features).catch((e) => setUnsupported(true)); + }, []); + + return unsupported + ? + : <>; + + function detectIndexedDB() { + return dexieService.testConnection(); + } +}; + +export default FeatureDetection; diff --git a/webclient/src/containers/Decks/Decks.css b/webclient/src/containers/Decks/Decks.css new file mode 100644 index 000000000..e69de29bb diff --git a/webclient/src/containers/Decks/Decks.tsx b/webclient/src/containers/Decks/Decks.tsx new file mode 100644 index 000000000..190196dc9 --- /dev/null +++ b/webclient/src/containers/Decks/Decks.tsx @@ -0,0 +1,20 @@ +// eslint-disable-next-line +import React, { Component } from "react"; + +import { AuthGuard } from 'components/index'; +import Layout from 'containers/Layout/Layout'; + +import './Decks.css'; + +class Decks extends Component { + render() { + return ( + + + "Decks" + + ) + } +} + +export default Decks; diff --git a/webclient/src/containers/Game/Game.css b/webclient/src/containers/Game/Game.css new file mode 100644 index 000000000..e69de29bb diff --git a/webclient/src/containers/Game/Game.tsx b/webclient/src/containers/Game/Game.tsx new file mode 100644 index 000000000..694ffb46d --- /dev/null +++ b/webclient/src/containers/Game/Game.tsx @@ -0,0 +1,20 @@ +// eslint-disable-next-line +import React, { Component } from "react"; + +import { AuthGuard } from 'components'; +import Layout from 'containers/Layout/Layout'; + +import './Game.css'; + +class Game extends Component { + render() { + return ( + + + "Game" + + ) + } +} + +export default Game; diff --git a/webclient/src/containers/Initialize/Initialize.css b/webclient/src/containers/Initialize/Initialize.css new file mode 100644 index 000000000..d52eafd5f --- /dev/null +++ b/webclient/src/containers/Initialize/Initialize.css @@ -0,0 +1,88 @@ +.Initialize { + position: relative; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + text-align: center; +} + +.Initialize img { + width: 60px; +} + +h6.subtitle { + margin: 20px 0 10px; +} + +.Initialize-graphics { + position: absolute; + height: 100%; + width: 100%; + overflow: hidden; +} + +.Initialize-graphics__square { + position: absolute; + border: 2px solid; + opacity: .05; +} + +.Initialize-graphics__bar { + position: absolute; + opacity: .05; + border-radius: 8px; +} + +.Initialize-graphics__square.topLeft { + transform: rotate(27deg); + top: 38px; + left: 64px; + height: 134px; + width: 100px; + border-radius: 8px; +} + +.Initialize-graphics__square.topRight { + transform: rotate(10deg); + top: 74px; + right: 62px; + height: 50px; + width: 66px; + border-radius: 20px; +} + +.Initialize-graphics__square.bottomLeft { + transform: rotate(120deg); + bottom: 61px; + left: 66px; + height: 50px; + width: 66px; + border-radius: 20px; +} + +.Initialize-graphics__square.bottomRight { + transform: rotate(-24deg); + bottom: 54px; + right: 0; + height: 88px; + width: 66px; + border-radius: 8px; +} + +.Initialize-graphics__bar.bottomBar { + transform: rotate(30deg); + bottom: -4px; + left: -29px; + height: 50px; + width: 222px; +} + +.Initialize-graphics__bar.topBar { + transform: rotate(-330deg); + top: 10px; + right: -49px; + height: 50px; + width: 222px; +} diff --git a/webclient/src/containers/Initialize/Initialize.i18n.json b/webclient/src/containers/Initialize/Initialize.i18n.json new file mode 100644 index 000000000..77ca5e8cd --- /dev/null +++ b/webclient/src/containers/Initialize/Initialize.i18n.json @@ -0,0 +1,6 @@ +{ + "InitializeContainer": { + "title": "DID YOU KNOW", + "subtitle": "<1>Cockatrice is run by volunteers<1>that love card games!" + } +} diff --git a/webclient/src/containers/Initialize/Initialize.tsx b/webclient/src/containers/Initialize/Initialize.tsx new file mode 100644 index 000000000..bc777f065 --- /dev/null +++ b/webclient/src/containers/Initialize/Initialize.tsx @@ -0,0 +1,71 @@ +import { useState } from 'react'; +import { styled } from '@mui/material/styles'; +import { useTranslation, Trans } from 'react-i18next'; +import { connect } from 'react-redux'; +import { Navigate } from 'react-router-dom'; +import Typography from '@mui/material/Typography'; + +import { Images } from 'images'; +import { ServerSelectors } from 'store'; +import { RouteEnum } from 'types'; +import Layout from 'containers/Layout/Layout'; + +import './Initialize.css'; + +const PREFIX = 'Initialize'; + +const classes = { + root: `${PREFIX}-root` +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { + '& .Initialize-graphics': { + color: theme.palette.primary.contrastText, + }, + + '& .Initialize-graphics__bar': { + backgroundColor: theme.palette.primary.contrastText, + }, + } +})); + +const Initialize = ({ initialized }: InitializeProps) => { + const { t } = useTranslation(); + + return initialized + ? + : ( + + +
+ logo + { t('InitializeContainer.title') } + + + + +
+ +
+
+
+
+
+
+
+
+ + + ); +} + +interface InitializeProps { + initialized: boolean; +} + +const mapStateToProps = state => ({ + initialized: ServerSelectors.getInitialized(state), +}); + +export default connect(mapStateToProps)(Initialize); diff --git a/webclient/src/containers/Layout/Layout.css b/webclient/src/containers/Layout/Layout.css new file mode 100644 index 000000000..a98cba47c --- /dev/null +++ b/webclient/src/containers/Layout/Layout.css @@ -0,0 +1,31 @@ +.layout { + height: 100%; + max-height: 100%; + width: 100%; + max-width: 100%; + display: flex; + flex-flow: row nowrap; + overflow: hidden; +} + +.layout--no-height-limit { + height: initial; + max-height: initial; +} + +.bottom-bar__container { + background: #555; + height: 50px; + width: 100%; +} + +.page__body { + flex: 1; + max-height: calc(100% - 50px); +} + +.page { + display: flex; + flex-flow: column; + width: 100%; +} diff --git a/webclient/src/containers/Layout/Layout.tsx b/webclient/src/containers/Layout/Layout.tsx new file mode 100644 index 000000000..6d04172ba --- /dev/null +++ b/webclient/src/containers/Layout/Layout.tsx @@ -0,0 +1,39 @@ +import LeftNav from './LeftNav'; + +import './Layout.css' + +function Layout(props:LayoutProps) { + const { children, className, showNav = true, noHeightLimit = false } = props; + const containerClasses = ['layout'] + if (noHeightLimit === true) { + containerClasses.push('layout--no-height-limit') + } + + return ( +
+ {showNav && } +
+
+ {children} +
+ {showNav && } +
+
+ ) +} + +function BottomBar(props) { + return ( +
+
+ ) +} + +interface LayoutProps { + showNav?: boolean; + children: any; + className?: string; + noHeightLimit?: boolean +} + +export default Layout; diff --git a/webclient/src/containers/Layout/LeftNav.css b/webclient/src/containers/Layout/LeftNav.css new file mode 100644 index 000000000..85c851d2e --- /dev/null +++ b/webclient/src/containers/Layout/LeftNav.css @@ -0,0 +1,128 @@ +.LeftNav__container { + background: #7033DB; + width: 100px; + min-width: 100px; + height: 100%; +} + +.LeftNav__logo { + display: flex; + align-items: center; + justify-content: center; + padding: 16px 0; +} + +.LeftNav__logo a { + line-height: 1; +} + +.LeftNav__logo img { + height: 32px; +} + +.LeftNav-content { + color: white; +} + +.LeftNav-serverDetails { + font-size: 12px; +} + +.LeftNav-server__indicator { + display: inline-block; + height: 12px; + width: 12px; + background: red; + border: 1px solid; + border-radius: 50%; + margin-left: 10px; +} + +.LeftNav-nav { +} + +.LeftNav-nav__links { + display: flex; + flex-flow: column; + align-items: center; + gap: 16px; +} + +.LeftNav-nav__link { + position: relative; + height: 100%; +} + +.LeftNav-nav__link:hover { + background: rgba(0, 0, 0, .125); +} + +.LeftNav-nav__link:hover .LeftNav-nav__link-menu { + display: block; +} + +.LeftNav-nav__link-btn { + display: flex; + height: 100%; + width: 100%; + align-items: center; + padding: 5px 20px; + font-weight: bold; +} + +.LeftNav-nav__link-btn__icon { + margin-left: 5px; +} + +.LeftNav-nav__link-menu { + display: none; + position: absolute; + bottom: 0; + transform: translateY(100%); + min-width: 150px; + background: #3f51b5; + box-shadow: 1px 1px 2px 0px black; + z-index: 1; +} + +.LeftNav-nav__link-menu__item { + padding: 0 !important; +} + +.LeftNav-nav__link-menu__btn { + padding: 6px 16px; + width: 100%; + color: white; + display: flex; + justify-content: space-between; +} + +.LeftNav-nav__actions { + display: flex; + justify-content: center; +} + +.LeftNav-nav__action { + +} + +.LeftNav-nav__action button { + color: white; +} + +.temp-subnav__rooms { + display: flex; + align-items: center; + font-size: 10px; + padding: 5px; +} + +.temp-chip { + margin-left: 5px; + text-decoration: none; +} + + +.temp-chip > div { + cursor: inherit; +} diff --git a/webclient/src/containers/Layout/LeftNav.tsx b/webclient/src/containers/Layout/LeftNav.tsx new file mode 100644 index 000000000..a81eb6ce2 --- /dev/null +++ b/webclient/src/containers/Layout/LeftNav.tsx @@ -0,0 +1,195 @@ +import React, { useState, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { NavLink, useNavigate, generatePath } from 'react-router-dom'; +import IconButton from '@mui/material/IconButton'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import CloseIcon from '@mui/icons-material/Close'; +import MailOutlineRoundedIcon from '@mui/icons-material/MailOutline'; +import MenuRoundedIcon from '@mui/icons-material/MenuRounded'; +import * as _ from 'lodash'; + +import { AuthenticationService, RoomsService } from 'api'; +import { CardImportDialog } from 'dialogs'; +import { Images } from 'images'; +import { RoomsSelectors, ServerSelectors } from 'store'; +import { Room, RouteEnum, User } from 'types'; + +import './LeftNav.css'; + +const LeftNav = ({ joinedRooms, serverState, user }: LeftNavProps) => { + const navigate = useNavigate(); + const [state, setState] = useState({ + anchorEl: null, + showCardImportDialog: false, + options: [], + }); + + useEffect(() => { + let options: string[] = [ + 'Account', + 'Replays', + ]; + + if (user && AuthenticationService.isModerator(user)) { + options = [ + ...options, + 'Administration', + 'Logs' + ]; + } + + setState(s => ({ ...s, options })); + }, [user]); + + const handleMenuOpen = (event) => { + setState(s => ({ ...s, anchorEl: event.target })); + } + + const handleMenuItemClick = (option: string) => { + const route = RouteEnum[option.toUpperCase()]; + navigate(generatePath(route)); + } + + const handleMenuClose = () => { + setState(s => ({ ...s, anchorEl: null })); + } + + const leaveRoom = (event, roomId) => { + event.preventDefault(); + RoomsService.leaveRoom(roomId); + }; + + const openImportCardWizard = () => { + setState(s => ({ ...s, showCardImportDialog: true })); + handleMenuClose(); + } + + const closeImportCardWizard = () => { + setState(s => ({ ...s, showCardImportDialog: false })); + } + + return ( +
+
+
+ + logo + + { AuthenticationService.isConnected(serverState) && ( + + ) } +
+ { AuthenticationService.isConnected(serverState) && ( +
+ +
+ ) } +
+ + +
+ ); +} + +interface LeftNavProps { + serverState: number; + server: string; + user: User; + joinedRooms: Room[]; + showNav?: boolean; +} + +interface LeftNavState { + anchorEl: Element; + showCardImportDialog: boolean; + options: string[]; +} + +const mapStateToProps = state => ({ + serverState: ServerSelectors.getState(state), + server: ServerSelectors.getName(state), + user: ServerSelectors.getUser(state), + joinedRooms: RoomsSelectors.getJoinedRooms(state), +}); + +export default connect(mapStateToProps)(LeftNav); diff --git a/webclient/src/containers/Layout/logo.png b/webclient/src/containers/Layout/logo.png new file mode 100644 index 000000000..7ce83bd20 Binary files /dev/null and b/webclient/src/containers/Layout/logo.png differ diff --git a/webclient/src/containers/Login/Login.css b/webclient/src/containers/Login/Login.css new file mode 100644 index 000000000..7166187ad --- /dev/null +++ b/webclient/src/containers/Login/Login.css @@ -0,0 +1,202 @@ +.login { + height: 100%; + padding: 50px; +} + +.login__wrapper { + display: flex; + flex-direction: column; + align-items: center; +} + +.login-content { + width: 100%; + max-width: 500px; + display: flex; + border-radius: 8px; + overflow: hidden; +} + +.login-content__header { + font-family: 'Teko', sans-serif; + font-size: 34px; + font-weight: bold; + display: flex; + align-items: center; + margin-bottom: 20px; +} + +.login-content__header img { + height: 60px; + margin-right: 15px; +} + +.login-content__form { + width: 100%; + padding: 50px 50px 33px; +} + +.login-content__form h1 { + margin-bottom: 20px; +} + +.login-form { + margin-top: 30px; +} + +.login-content__description { + display: none; + position: relative; + justify-content: center; + align-items: center; + text-align: center; + font-size: 24px; + overflow: hidden; +} + +.login-content__description-wrapper { + position: relative; + width: 70%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.login-content__description-cards { + width: 100%; + position: relative; + display: flex; + justify-content: space-between; +} + +.login-content__description-cards__card { + position: relative; + width: 34%; + padding-bottom: 46%; + border-radius: 8px; + box-shadow: 0 5px 10px 2px rgba(0,0,0,0.20); + font-weight: bold; + font-size: 16px; +} + +.login-content__description-cards__card-wrapper { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.login-content__description-cards__card img { + width: 70%; + border-radius: 50%; + margin: 21% 0 9%; +} + +.login-content__description-cards__card.leftCard { + transform: rotate(-12deg); +} + +.login-content__description-cards__card.rightCard { + transform: rotate(12deg); +} + +.login-content__description-cards__card.topCard { + width: 44%; + padding-bottom: 59%; + position: absolute; + top: 45%; + left: 50%; + transform: translate(-50%, -50%); +} + +.login-content__description-subtitle1 { + margin: 40px 0 20px; + font-size: 28px; + font-weight: bold; + +} +.login-content__description-subtitle2 { + font-size: 14px; +} +.login-content__description-square { + position: absolute; + border: 1px solid; + opacity: .1; +} +.login-content__description-bar { + position: absolute; + opacity: .1; + border-radius: 8px; +} + + +.login-content__description-square.topLeft { + transform: rotate(27deg); + top: 38px; + left: 64px; + height: 134px; + width: 100px; + border-radius: 8px; +} + +.login-content__description-square.topRight { + transform: rotate(10deg); + top: 74px; + right: 62px; + height: 50px; + width: 66px; + border-radius: 20px; +} + +.login-content__description-square.bottomLeft { + transform: rotate(120deg); + bottom: 61px; + left: 66px; + height: 50px; + width: 66px; + border-radius: 20px; +} + +.login-content__description-square.bottomRight { + transform: rotate(-24deg); + bottom: 54px; + right: 0; + height: 88px; + width: 66px; + border-radius: 8px; +} +.login-content__description-bar.bottomBar { + transform: rotate(30deg); + bottom: -4px; + left: -29px; + height: 50px; + width: 222px; +} +.login-content__description-bar.topBar { + transform: rotate(-330deg); + top: 10px; + right: -49px; + height: 50px; + width: 222px; +} +.login-footer { + margin-top: 30px; +} + +.login-footer__register { + margin-bottom: 10px; + font-weight: bold; +} + +.login-footer__language { + margin-top: 20px; +} + +.login-content__connectionStatus { + text-align: center; + margin: 20px 0; + padding: 20px; + font-weight: bold; +} diff --git a/webclient/src/containers/Login/Login.i18n.json b/webclient/src/containers/Login/Login.i18n.json new file mode 100644 index 000000000..13d248bb1 --- /dev/null +++ b/webclient/src/containers/Login/Login.i18n.json @@ -0,0 +1,22 @@ +{ + "LoginContainer": { + "header": { + "title": "Login", + "subtitle": "A cross-platform virtual tabletop for multiplayer card games." + }, + "footer": { + "registerPrompt": "Not registered yet?", + "registerAction": "Create an account", + "credit": "Cockatrice is an open source project", + "version": "Version" + }, + "content": { + "subtitle1": "Play multiplayer card games online.", + "subtitle2": "Cross-platform virtual tabletop for multiplayer card games. Forever free." + }, + "toasts": { + "passwordResetSuccessToast": "Password Reset Successfully", + "accountActivationSuccess": "Account Activated Successfully" + } + } +} diff --git a/webclient/src/containers/Login/Login.tsx b/webclient/src/containers/Login/Login.tsx new file mode 100644 index 000000000..fe008375c --- /dev/null +++ b/webclient/src/containers/Login/Login.tsx @@ -0,0 +1,360 @@ +import { useState, useCallback } from 'react'; +import { styled } from '@mui/material/styles'; +import { useTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; +import { Navigate } from 'react-router-dom'; +import Button from '@mui/material/Button'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; + +import { AuthenticationService } from 'api'; +import { RegistrationDialog, RequestPasswordResetDialog, ResetPasswordDialog, AccountActivationDialog } from 'dialogs'; +import { LanguageDropdown } from 'components'; +import { LoginForm } from 'forms'; +import { useReduxEffect, useFireOnce } from 'hooks'; +import { Images } from 'images'; +import { HostDTO, serverProps } from 'services'; +import { RouteEnum, WebSocketConnectOptions, getHostPort } from 'types'; +import { ServerSelectors, ServerTypes } from 'store'; +import Layout from 'containers/Layout/Layout'; + +import './Login.css'; +import { useToast } from 'components/Toast'; + +const PREFIX = 'Login'; + +const classes = { + root: `${PREFIX}-root` +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { + '& .login-content__header': { + color: theme.palette.success.light + }, + + '& .login-content__description': { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + }, + + '& .login-content__description-bar': { + backgroundColor: theme.palette.primary.dark, + }, + + '& .login-content__description-cards__card': { + backgroundColor: theme.palette.background.paper, + color: theme.palette.primary.main, + }, + + [theme.breakpoints.up('lg')]: { + '& .login-content': { + maxWidth: '1000px', + }, + + '& .login-content__form': { + width: '50%', + }, + + '& .login-content__description': { + width: '50%', + display: 'flex', + }, + }, + } +})); + +const Login = ({ state, description, connectOptions }: LoginProps) => { + const { t } = useTranslation(); + + const isConnected = AuthenticationService.isConnected(state); + + const [rememberLogin, setRememberLogin] = useState(null); + const [dialogState, setDialogState] = useState({ + passwordResetRequestDialog: false, + resetPasswordDialog: false, + registrationDialog: false, + activationDialog: false, + }); + const [userToResetPassword, setUserToResetPassword] = useState(null); + + const passwordResetToast = useToast({ key: 'password-reset-success', children: t('LoginContainer.toasts.passwordResetSuccess') }); + const accountActivatedToast = useToast({ + key: 'account-activation-success', + children: t('LoginContainer.toasts.accountActivationSuccess') + }); + + useReduxEffect(() => { + closeRequestPasswordResetDialog(); + openResetPasswordDialog(); + }, ServerTypes.RESET_PASSWORD_REQUESTED, []); + + useReduxEffect(() => { + passwordResetToast.openToast() + closeResetPasswordDialog(); + }, ServerTypes.RESET_PASSWORD_SUCCESS, []); + + useReduxEffect(() => { + accountActivatedToast.openToast() + closeActivateAccountDialog(); + }, ServerTypes.ACCOUNT_ACTIVATION_SUCCESS, []); + + useReduxEffect(() => { + closeRegistrationDialog(); + openActivateAccountDialog(); + }, ServerTypes.ACCOUNT_AWAITING_ACTIVATION, []); + + useReduxEffect(() => { + resetSubmitButton(); + }, [ServerTypes.CONNECTION_FAILED, ServerTypes.LOGIN_FAILED], []); + + useReduxEffect(({ options: { hashedPassword } }) => { + updateHost(hashedPassword, rememberLogin); + }, ServerTypes.LOGIN_SUCCESSFUL, [rememberLogin]); + + const showDescription = () => { + return !isConnected && description?.length; + }; + + const onSubmitLogin = useCallback((loginForm) => { + setRememberLogin(loginForm); + const { userName, password, selectedHost, remember } = loginForm; + + const options: WebSocketConnectOptions = { + ...getHostPort(selectedHost), + userName, + password + }; + + if (remember && !password) { + options.hashedPassword = selectedHost.hashedPassword; + } + + AuthenticationService.login(options as WebSocketConnectOptions); + }, []); + + const [submitButtonDisabled, resetSubmitButton, handleLogin] = useFireOnce(onSubmitLogin); + + const updateHost = (hashedPassword, { selectedHost, remember, userName }) => { + HostDTO.get(selectedHost.id).then(hostDTO => { + hostDTO.remember = remember; + hostDTO.userName = remember ? userName : null; + hostDTO.hashedPassword = remember ? hashedPassword : null; + + hostDTO.save(); + }); + }; + + const handleRegistrationDialogSubmit = (registerForm) => { + setRememberLogin(registerForm); + const { userName, password, email, country, realName, selectedHost } = registerForm; + + AuthenticationService.register({ + ...getHostPort(selectedHost), + userName, + password, + email, + country, + realName, + }); + }; + + const handleAccountActivationDialogSubmit = ({ token }) => { + AuthenticationService.activateAccount({ + ...connectOptions, + token, + }); + }; + + const handleRequestPasswordResetDialogSubmit = (form) => { + const { userName, email, selectedHost } = form; + const { host, port } = getHostPort(selectedHost); + + if (email) { + AuthenticationService.resetPasswordChallenge({ userName, email, host, port }); + } else { + setUserToResetPassword(userName); + AuthenticationService.resetPasswordRequest({ userName, host, port }); + } + }; + + const handleResetPasswordDialogSubmit = ({ userName, token, newPassword, selectedHost }) => { + const { host, port } = getHostPort(selectedHost); + + AuthenticationService.resetPassword({ userName, token, newPassword, host, port }); + }; + + const skipTokenRequest = (userName) => { + setUserToResetPassword(userName); + + setDialogState(s => ({ ...s, + passwordResetRequestDialog: false, + resetPasswordDialog: true, + })); + }; + + const closeRequestPasswordResetDialog = () => { + setDialogState(s => ({ ...s, passwordResetRequestDialog: false })); + } + + const openRequestPasswordResetDialog = () => { + setDialogState(s => ({ ...s, passwordResetRequestDialog: true })); + } + + const closeResetPasswordDialog = () => { + setDialogState(s => ({ ...s, resetPasswordDialog: false })); + } + + const openResetPasswordDialog = () => { + setDialogState(s => ({ ...s, resetPasswordDialog: true })); + } + + const closeRegistrationDialog = () => { + setDialogState(s => ({ ...s, registrationDialog: false })); + } + + const openRegistrationDialog = () => { + setDialogState(s => ({ ...s, registrationDialog: true })); + } + + const closeActivateAccountDialog = () => { + setDialogState(s => ({ ...s, activationDialog: false })); + }; + + const openActivateAccountDialog = () => { + setDialogState(s => ({ ...s, activationDialog: true })); + }; + + return ( + + + { isConnected && } + +
+ +
+
+ logo + COCKATRICE +
+ { t('LoginContainer.header.title') } + { t('LoginContainer.header.subtitle') } +
+ +
+ + { + showDescription() && ( + + {description} + + ) + } + +
+
+ { t('LoginContainer.footer.registerPrompt') } + +
+ + { t('LoginContainer.footer.credit') } - { new Date().getUTCFullYear() } + + + { + serverProps.REACT_APP_VERSION && ( + + { t('LoginContainer.footer.version') }: { serverProps.REACT_APP_VERSION } + + ) + } + +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Stock Player + 1mrlee +
+
+
+
+ Stock Player + CyberX +
+
+
+
+ Stock Player + Gamer69 +
+
+
+ { /**/} +

{ t('LoginContainer.content.subtitle1') }

+

{ t('LoginContainer.content.subtitle2') }

+
+
+ +
+ + + + + + + + + + + ); +} + +interface LoginProps { + state: number; + description: string; + connectOptions: WebSocketConnectOptions; +} + +const mapStateToProps = state => ({ + state: ServerSelectors.getState(state), + description: ServerSelectors.getDescription(state), + connectOptions: ServerSelectors.getConnectOptions(state), +}); + +export default connect(mapStateToProps)(Login); diff --git a/webclient/src/containers/Logs/LogResults.css b/webclient/src/containers/Logs/LogResults.css new file mode 100644 index 000000000..3a6cbd185 --- /dev/null +++ b/webclient/src/containers/Logs/LogResults.css @@ -0,0 +1,3 @@ +.log-results { + margin-bottom: 20px; +} \ No newline at end of file diff --git a/webclient/src/containers/Logs/LogResults.tsx b/webclient/src/containers/Logs/LogResults.tsx new file mode 100644 index 000000000..06abdbce6 --- /dev/null +++ b/webclient/src/containers/Logs/LogResults.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import * as _ from 'lodash'; + +import AppBar from '@mui/material/AppBar'; +import Box from '@mui/material/Box'; +import Paper from '@mui/material/Paper'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import Typography from '@mui/material/Typography'; + +import './LogResults.css'; + +const LogResults = (props) => { + const { logs } = props; + + const hasRoomLogs = logs.room && logs.room.length; + const hasGameLogs = logs.game && logs.game.length; + const hasChatLogs = logs.chat && logs.chat.length; + + const [value, setValue] = React.useState(0); + + const handleChange = (event, newValue) => { + setValue(newValue); + }; + + const headerCells = [ + { + label: 'Time' + }, + { + label: 'Sender Name' + }, + { + label: 'Sender IP' + }, + { + label: 'Message' + }, + { + label: 'Target ID' + }, + { + label: 'Target Name' + } + ]; + + return ( +
+ + + + + + + + + + + + + + + + +
+ ) +}; + +const a11yProps = index => { + return { + id: `simple-tab-${index}`, + 'aria-controls': `simple-tabpanel-${index}`, + }; +}; + +const TabPanel = ({ children, value, index, ...other }) => { + return ( + + ); +}; + +const Results = ({ headerCells, logs }) => ( + + + + + { _.map(headerCells, ({ label }) => ( + {label} + ))} + + + + { _.map(logs, ({ time, senderName, senderIp, message, targetId, targetName }, index) => ( + + {time} + {senderName} + {senderIp} + {message} + {targetId} + {targetName} + + ))} + +
+
+); + +export default LogResults; diff --git a/webclient/src/containers/Logs/Logs.css b/webclient/src/containers/Logs/Logs.css new file mode 100644 index 000000000..37ac8eb03 --- /dev/null +++ b/webclient/src/containers/Logs/Logs.css @@ -0,0 +1,14 @@ +.moderator-logs { + height: 100%; + display: flex; + padding: 20px; +} + +.moderator-logs__form { + width: 40%; + margin-right: 20px; +} + +.moderator-logs__results { + width: 100%; +} \ No newline at end of file diff --git a/webclient/src/containers/Logs/Logs.tsx b/webclient/src/containers/Logs/Logs.tsx new file mode 100644 index 000000000..a34a7b008 --- /dev/null +++ b/webclient/src/containers/Logs/Logs.tsx @@ -0,0 +1,109 @@ +// eslint-disable-next-line +import React, { Component } from "react"; +import { connect } from 'react-redux'; +import * as _ from 'lodash'; + +import { ModeratorService } from 'api'; +import { AuthGuard, ModGuard } from 'components'; +import { SearchForm } from 'forms'; +import { ServerDispatch, ServerSelectors, ServerStateLogs } from 'store'; +import { LogFilters } from 'types'; + +import LogResults from './LogResults'; +import './Logs.css'; + +class Logs extends Component { + MAXIMUM_RESULTS = 1000; + + constructor(props) { + super(props); + + this.onSubmit = this.onSubmit.bind(this); + } + + componentWillUnmount() { + ServerDispatch.clearLogs(); + } + + onSubmit(fields: LogFilters) { + const trimmedFields: any = this.trimFields(fields); + + const { userName, ipAddress, gameName, gameId, message, logLocation } = trimmedFields; + + const required = _.filter({ + userName, ipAddress, gameName, gameId, message + }, field => field); + + if (logLocation) { + trimmedFields.logLocation = this.flattenLogLocations(logLocation); + } + + trimmedFields.maximumResults = this.MAXIMUM_RESULTS; + + if (_.size(required)) { + ModeratorService.viewLogHistory(trimmedFields); + } else { + // @TODO use yet-to-be-implemented banner/alert + } + } + + private trimFields(fields) { + return _.reduce(fields, (obj, field, key) => { + if (typeof field === 'string') { + const trimmed = _.trim(field); + + if (!!trimmed) { + obj[key] = trimmed; + } + } else { + obj[key] = field; + } + + return obj; + }, {}); + } + + private flattenLogLocations(logLocations) { + return _.reduce(logLocations, (arr, loc, key) => { + arr.push(key); + return arr; + }, []) + } + + render() { + return ( +
+ + + +
+ +
+ +
+ +
+
+ ) + } +} + +interface LogsTypes { + logs: ServerStateLogs +} + +const mapStateToProps = state => ({ + logs: ServerSelectors.getLogs(state) +}); + +export default connect(mapStateToProps)(Logs); + + + + + + + + + + diff --git a/webclient/src/containers/Player/Player.tsx b/webclient/src/containers/Player/Player.tsx new file mode 100644 index 000000000..74feff8bd --- /dev/null +++ b/webclient/src/containers/Player/Player.tsx @@ -0,0 +1,18 @@ +// eslint-disable-next-line +import React, { Component } from "react"; +import Layout from 'containers/Layout/Layout'; + +import { AuthGuard } from 'components'; + +class Player extends Component { + render() { + return ( + + + "Player" + + ) + } +} + +export default Player; diff --git a/webclient/src/containers/Room/Games.css b/webclient/src/containers/Room/Games.css new file mode 100644 index 000000000..623ab47f5 --- /dev/null +++ b/webclient/src/containers/Room/Games.css @@ -0,0 +1,30 @@ +.games { +} + +.games-header, +.game { + display: flex; + padding: 10px; + border-bottom: 1px solid black; +} + +.games-header__cell { + max-width: 200px; +} + +.games-header__label, +.game__detail { + width: 10%; + flex-grow: 0; +} + +.games-header__label.description, +.game__detail.description { + width: 20%; + flex-grow: 1; +} + +.games-header__label.creator, +.game__detail.creator { + width: 20%; +} diff --git a/webclient/src/containers/Room/Games.tsx b/webclient/src/containers/Room/Games.tsx new file mode 100644 index 000000000..67a9239d3 --- /dev/null +++ b/webclient/src/containers/Room/Games.tsx @@ -0,0 +1,143 @@ +// eslint-disable-next-line +import React, { Component } from "react"; +import { connect } from 'react-redux'; +import * as _ from 'lodash'; + +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import TableSortLabel from '@mui/material/TableSortLabel'; +import Tooltip from '@mui/material/Tooltip'; + +// import { RoomsService } from "AppShell/common/services"; + +import { SortUtil, RoomsDispatch, RoomsSelectors } from 'store'; +import { UserDisplay } from 'components'; + +import './Games.css'; + +// @TODO run interval to update timeSinceCreated +class Games extends Component { + private headerCells = [ + { + label: 'Age', + field: 'startTime' + }, + { + label: 'Description', + field: 'description' + }, + { + label: 'Creator', + field: 'creatorInfo.name' + }, + { + label: 'Type', + field: 'gameType' + }, + { + label: 'Restrictions', + // field: "?" + }, + { + label: 'Players', + // field: ["maxPlayers", "playerCount"] + }, + { + label: 'Spectators', + field: 'spectatorsCount' + }, + ]; + + handleSort(sortByField) { + const { room: { roomId }, sortBy } = this.props; + const { field, order } = SortUtil.toggleSortBy(sortByField, sortBy); + RoomsDispatch.sortGames(roomId, field, order); + } + + private isUnavailableGame({ started, maxPlayers, playerCount }) { + return !started && playerCount < maxPlayers; + } + + private isPasswordProtectedGame({ withPassword }) { + return !withPassword; + } + + private isBuddiesOnlyGame({ onlyBuddies }) { + return !onlyBuddies; + } + + render() { + const { room, sortBy } = this.props; + + const games = room.gameList.filter(game => ( + this.isUnavailableGame(game) && + this.isPasswordProtectedGame(game) && + this.isBuddiesOnlyGame(game) + )); + + return ( +
+ + + + { _.map(this.headerCells, ({ label, field }) => { + const active = field === sortBy.field; + const order = sortBy.order.toLowerCase(); + const sortDirection = active ? order : false; + + return ( + + {!field ? label : ( + this.handleSort(field)} + > + {label} + + )} + + ); + })} + + + + { _.map(games, ({ description, gameId, gameType, creatorInfo, maxPlayers, playerCount, spectatorsCount, startTime }) => ( + + {startTime} + + +
+ {description} +
+
+
+ + + + {gameType} + ? + {`${playerCount}/${maxPlayers}`} + {spectatorsCount} +
+ ))} +
+
+
+ ); + } +} + +interface GamesProps { + room: any; + sortBy: any; +} + +const mapStateToProps = state => ({ + sortBy: RoomsSelectors.getSortGamesBy(state) +}); + +export default connect(mapStateToProps)(Games); diff --git a/webclient/src/containers/Room/Messages.css b/webclient/src/containers/Room/Messages.css new file mode 100644 index 000000000..731002cb8 --- /dev/null +++ b/webclient/src/containers/Room/Messages.css @@ -0,0 +1,17 @@ +.messages { + height: 100%; + width: 100%; + padding: 10px; + font-size: 12px; + line-height: 1.3; +} + +.message-wrapper { + padding: 5px 0; + margin: 2px 0; + border-bottom: 1px dashed rgba(0, 0, 0, 0.25); +} + +.message-wrapper:last-of-type { + border: 0; +} diff --git a/webclient/src/containers/Room/Messages.tsx b/webclient/src/containers/Room/Messages.tsx new file mode 100644 index 000000000..24b576759 --- /dev/null +++ b/webclient/src/containers/Room/Messages.tsx @@ -0,0 +1,20 @@ +// eslint-disable-next-line +import React from "react"; + +import { Message } from 'components'; + +import './Messages.css'; + +const Messages = ({ messages }) => ( +
+ { + messages && messages.map((message, index) => ( +
+ +
+ )) + } +
+); + +export default Messages; diff --git a/webclient/src/containers/Room/OpenGames.css b/webclient/src/containers/Room/OpenGames.css new file mode 100644 index 000000000..623ab47f5 --- /dev/null +++ b/webclient/src/containers/Room/OpenGames.css @@ -0,0 +1,30 @@ +.games { +} + +.games-header, +.game { + display: flex; + padding: 10px; + border-bottom: 1px solid black; +} + +.games-header__cell { + max-width: 200px; +} + +.games-header__label, +.game__detail { + width: 10%; + flex-grow: 0; +} + +.games-header__label.description, +.game__detail.description { + width: 20%; + flex-grow: 1; +} + +.games-header__label.creator, +.game__detail.creator { + width: 20%; +} diff --git a/webclient/src/containers/Room/OpenGames.tsx b/webclient/src/containers/Room/OpenGames.tsx new file mode 100644 index 000000000..49d4d2503 --- /dev/null +++ b/webclient/src/containers/Room/OpenGames.tsx @@ -0,0 +1,143 @@ +// eslint-disable-next-line +import React, { Component } from "react"; +import { connect } from 'react-redux'; +import * as _ from 'lodash'; + +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import TableSortLabel from '@mui/material/TableSortLabel'; +import Tooltip from '@mui/material/Tooltip'; + +// import { RoomsService } from "AppShell/common/services"; + +import { SortUtil, RoomsDispatch, RoomsSelectors } from 'store'; +import { UserDisplay } from 'components'; + +import './OpenGames.css'; + +// @TODO run interval to update timeSinceCreated +class OpenGames extends Component { + private headerCells = [ + { + label: 'Age', + field: 'startTime' + }, + { + label: 'Description', + field: 'description' + }, + { + label: 'Creator', + field: 'creatorInfo.name' + }, + { + label: 'Type', + field: 'gameType' + }, + { + label: 'Restrictions', + // field: "?" + }, + { + label: 'Players', + // field: ["maxPlayers", "playerCount"] + }, + { + label: 'Spectators', + field: 'spectatorsCount' + }, + ]; + + handleSort(sortByField) { + const { room: { roomId }, sortBy } = this.props; + const { field, order } = SortUtil.toggleSortBy(sortByField, sortBy); + RoomsDispatch.sortGames(roomId, field, order); + } + + private isUnavailableGame({ started, maxPlayers, playerCount }) { + return !started && playerCount < maxPlayers; + } + + private isPasswordProtectedGame({ withPassword }) { + return !withPassword; + } + + private isBuddiesOnlyGame({ onlyBuddies }) { + return !onlyBuddies; + } + + render() { + const { room, sortBy } = this.props; + + const games = room.gameList.filter(game => ( + this.isUnavailableGame(game) && + this.isPasswordProtectedGame(game) && + this.isBuddiesOnlyGame(game) + )); + + return ( +
+ + + + { _.map(this.headerCells, ({ label, field }) => { + const active = field === sortBy.field; + const order = sortBy.order.toLowerCase(); + const sortDirection = active ? order : false; + + return ( + + {!field ? label : ( + this.handleSort(field)} + > + {label} + + )} + + ); + })} + + + + { _.map(games, ({ description, gameId, gameType, creatorInfo, maxPlayers, playerCount, spectatorsCount, startTime }) => ( + + {startTime} + + +
+ {description} +
+
+
+ + + + {gameType} + ? + {`${playerCount}/${maxPlayers}`} + {spectatorsCount} +
+ ))} +
+
+
+ ); + } +} + +interface OpenGamesProps { + room: any; + sortBy: any; +} + +const mapStateToProps = state => ({ + sortBy: RoomsSelectors.getSortGamesBy(state) +}); + +export default connect(mapStateToProps)(OpenGames); diff --git a/webclient/src/containers/Room/Room.css b/webclient/src/containers/Room/Room.css new file mode 100644 index 000000000..4b6aaec93 --- /dev/null +++ b/webclient/src/containers/Room/Room.css @@ -0,0 +1,45 @@ +.room-view, +.room-view__main, +.room-view__games, +.room-view__messages, +.room-view__messages-content, +.room-view__side { + height: 100%; +} + +.room-view, +.room-view__messages, +.room-view__side { + display: flex; + flex-direction: column; +} + +.room-view__main { + overflow: hidden; +} + +.room-view__messages-sayMessage { + width: 100%; + margin: 10px auto 2px; +} + +.room-view__side-label { + position: sticky; + top: 0; + padding: 10px; + background: white; + z-index: 1; +} + +.room-view__side-list, +.room-view__side-list .room-view__side-list__item { + height: 100%; +} + +.room-view__side-list .room-view__side-list__item { + padding: 0; +} + +.room-view__side-list .room-view__side-list__item .user-display__details { + padding: 0 10px; +} \ No newline at end of file diff --git a/webclient/src/containers/Room/Room.tsx b/webclient/src/containers/Room/Room.tsx new file mode 100644 index 000000000..c73930d7f --- /dev/null +++ b/webclient/src/containers/Room/Room.tsx @@ -0,0 +1,104 @@ +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { useNavigate, useParams, generatePath } from 'react-router-dom'; + +import ListItem from '@mui/material/ListItem'; +import Paper from '@mui/material/Paper'; + +import { RoomsService } from 'api'; +import { ScrollToBottomOnChanges, ThreePaneLayout, UserDisplay, VirtualList, AuthGuard } from 'components'; +import { RoomsStateMessages, RoomsStateRooms, JoinedRooms, RoomsSelectors, RoomsTypes } from 'store'; +import { RouteEnum } from 'types'; +import Layout from 'containers/Layout/Layout'; + +import OpenGames from './OpenGames'; +import Messages from './Messages'; +import SayMessage from './SayMessage'; + +import './Room.css'; + +// @TODO (3) +const Room = (props) => { + const { joined, rooms, messages } = props; + const navigate = useNavigate(); + const params = useParams(); + + const roomId = parseInt(params.roomId, 0); + const room = rooms[roomId]; + const roomMessages = messages[roomId]; + const users = room.userList; + + useEffect(() => { + if (!joined.find(({ roomId: id }) => id === roomId)) { + navigate(generatePath(RouteEnum.SERVER)); + } + }, [joined]); + + const handleRoomSay = ({ message }) => { + if (message) { + RoomsService.roomSay(roomId, message); + } + } + + return ( + + + +
+ + + + )} + + bottom={( +
+ + + )} /> + + + + +
+ )} + + side={( + +
+ Users in this room: {users.length} +
+ users[index].name } + items={ users.map(user => ( + + + + )) } + /> +
+ )} + /> +
+
+ ); +} + +interface RoomProps { + messages: RoomsStateMessages; + rooms: RoomsStateRooms; + joined: JoinedRooms; +} + +const mapStateToProps = state => ({ + messages: RoomsSelectors.getMessages(state), + rooms: RoomsSelectors.getRooms(state), + joined: RoomsSelectors.getJoinedRooms(state), +}); + +export default connect(mapStateToProps)(Room); diff --git a/webclient/src/containers/Room/SayMessage.tsx b/webclient/src/containers/Room/SayMessage.tsx new file mode 100644 index 000000000..4dd62dfb4 --- /dev/null +++ b/webclient/src/containers/Room/SayMessage.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Form } from 'react-final-form' + +import { InputAction } from 'components'; + +const SayMessage = ({ onSubmit }) => ( +
+ {({ handleSubmit, form }) => ( + { + handleSubmit(e) + form.restart() + }}> + + + )} + +); + +export default SayMessage; diff --git a/webclient/src/containers/Server/Rooms.css b/webclient/src/containers/Server/Rooms.css new file mode 100644 index 000000000..bfcdc82cf --- /dev/null +++ b/webclient/src/containers/Server/Rooms.css @@ -0,0 +1,26 @@ +.rooms { +} + +.rooms-header, +.room { + display: flex; + padding: 10px; + border-bottom: 1px solid black; +} + +.rooms-header__label, +.room__detail { + width: 10%; + flex-grow: 0; +} + +.rooms-header__label.name, +.room__detail.name { + width: 20%; +} + +.rooms-header__label.description, +.room__detail.description { + width: 30%; + flex-grow: 1; +} \ No newline at end of file diff --git a/webclient/src/containers/Server/Rooms.tsx b/webclient/src/containers/Server/Rooms.tsx new file mode 100644 index 000000000..517f24380 --- /dev/null +++ b/webclient/src/containers/Server/Rooms.tsx @@ -0,0 +1,64 @@ +// eslint-disable-next-line +import React from "react"; +import { generatePath, useNavigate } from 'react-router-dom'; +import * as _ from 'lodash'; + +import Button from '@mui/material/Button'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; + + +import { RoomsService } from 'api'; +import { RouteEnum } from 'types'; + +import './Rooms.css'; + +const Rooms = ({ rooms, joinedRooms }) => { + const navigate = useNavigate(); + + function onClick(roomId) { + if (_.find(joinedRooms, room => room.roomId === roomId)) { + navigate(generatePath(RouteEnum.ROOM, { roomId })); + } else { + RoomsService.joinRoom(roomId); + } + } + + return ( +
+ + + + Name + Description + Permissions + Players + Games + + + + + { _.map(rooms, ({ description, gameCount, name, permissionlevel, playerCount, roomId }) => ( + + {name} + {description} + {permissionlevel} + {playerCount} + {gameCount} + + + + + ))} + +
+
+ ); +}; + +export default Rooms; diff --git a/webclient/src/containers/Server/Server.css b/webclient/src/containers/Server/Server.css new file mode 100644 index 000000000..8d4e2e8dd --- /dev/null +++ b/webclient/src/containers/Server/Server.css @@ -0,0 +1,34 @@ +.server, +.server-rooms, +.server-rooms__side { + height: 100%; +} + +.server { + display: flex; + flex-direction: column; + align-items: center; +} + + +.serverRoomWrapper { + height: 100%; +} + +.serverMessage { + height: 100%; + padding: 20px; + margin-bottom: 2px; +} + +.server-rooms { + width: 100%; +} + +.server-rooms__side-label { + position: sticky; + top: 0; + padding: 10px; + background: white; + z-index: 1; +} diff --git a/webclient/src/containers/Server/Server.tsx b/webclient/src/containers/Server/Server.tsx new file mode 100644 index 000000000..7ed5e2464 --- /dev/null +++ b/webclient/src/containers/Server/Server.tsx @@ -0,0 +1,77 @@ +// eslint-disable-next-line +import React, { Component } from "react"; +import { connect } from 'react-redux'; +import { generatePath, useNavigate } from 'react-router-dom'; + +import ListItem from '@mui/material/ListItem'; +import Paper from '@mui/material/Paper'; + +import { AuthGuard, ThreePaneLayout, UserDisplay, VirtualList } from 'components'; +import { useReduxEffect } from 'hooks'; +import { RoomsSelectors, RoomsTypes, ServerSelectors } from 'store'; +import { Room, RouteEnum, User } from 'types'; +import Rooms from './Rooms'; +import Layout from 'containers/Layout/Layout'; + +import './Server.css'; + +const Server = ({ message, rooms, joinedRooms, users }: ServerProps) => { + const navigate = useNavigate(); + + useReduxEffect((action: any) => { + const roomId = action.roomInfo.roomId.toString(); + navigate(generatePath(RouteEnum.ROOM, { roomId })); + }, RoomsTypes.JOIN_ROOM, []); + + return ( + + + + + + + )} + + bottom={( + +
+ + )} + + side={( + +
+ Users connected to server: {users.length} +
+ users[index].name } + items={ users.map(user => ( + + + + )) } + /> +
+ )} + /> + + ); +} + +interface ServerProps { + message: string; + rooms: Room[]; + joinedRooms: Room[]; + users: User[]; +} + +const mapStateToProps = state => ({ + message: ServerSelectors.getMessage(state), + rooms: RoomsSelectors.getRooms(state), + joinedRooms: RoomsSelectors.getJoinedRooms(state), + users: ServerSelectors.getUsers(state) +}); + +export default connect(mapStateToProps)(Server); diff --git a/webclient/src/containers/Unsupported/Unsupported.css b/webclient/src/containers/Unsupported/Unsupported.css new file mode 100644 index 000000000..46752e746 --- /dev/null +++ b/webclient/src/containers/Unsupported/Unsupported.css @@ -0,0 +1,18 @@ +.Unsupported { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.Unsupported-paper { + width: 600px; + max-width: 100%; + padding: 40px; + text-align: center; +} + +.Unsupported-paper__header { + margin-bottom: 40px; +} diff --git a/webclient/src/containers/Unsupported/Unsupported.i18n.json b/webclient/src/containers/Unsupported/Unsupported.i18n.json new file mode 100644 index 000000000..225a13a67 --- /dev/null +++ b/webclient/src/containers/Unsupported/Unsupported.i18n.json @@ -0,0 +1,7 @@ +{ + "UnsupportedContainer": { + "title": "Unsupported Browser", + "subtitle1": "Please update your browser and/or check your permissions.", + "subtitle2": "Note: Private browsing causes some browsers to disable certain permissions or features." + } +} diff --git a/webclient/src/containers/Unsupported/Unsupported.tsx b/webclient/src/containers/Unsupported/Unsupported.tsx new file mode 100644 index 000000000..b666da12e --- /dev/null +++ b/webclient/src/containers/Unsupported/Unsupported.tsx @@ -0,0 +1,30 @@ +import { connect } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import Layout from 'containers/Layout/Layout'; + +import './Unsupported.css'; + +const Unsupported = () => { + const { t } = useTranslation(); + + return ( + + +
+ { t('UnsupportedContainer.title') } + { t('UnsupportedContainer.subtitle1') } +
+ + { t('UnsupportedContainer.subtitle2') } +
+
+ ); +}; + +const mapStateToProps = state => ({ + +}); + +export default connect(mapStateToProps)(Unsupported); diff --git a/webclient/src/containers/index.ts b/webclient/src/containers/index.ts new file mode 100644 index 000000000..f43de1c8e --- /dev/null +++ b/webclient/src/containers/index.ts @@ -0,0 +1,11 @@ +export { default as AppShell } from './App/AppShell'; +export { default as Account } from './Account/Account'; +export { default as Game } from './Game/Game'; +export { default as Decks } from './Decks/Decks'; +export { default as Room } from './Room/Room'; +export { default as Player } from './Player/Player'; +export { default as Server } from './Server/Server'; +export { default as Logs } from './Logs/Logs'; +export { default as Login } from './Login/Login'; +export { default as Initialize } from './Initialize/Initialize'; +export { default as Unsupported } from './Unsupported/Unsupported'; diff --git a/webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.css b/webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.css new file mode 100644 index 000000000..5175ab845 --- /dev/null +++ b/webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.css @@ -0,0 +1,13 @@ +.dialog-title { + display: flex; + justify-content: space-between; + align-items: center; +} + +.MuiDialogTitle-root.dialog-title { + padding-bottom: 0; +} + +.content { + margin-bottom: 20px; +} \ No newline at end of file diff --git a/webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.i18n.json b/webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.i18n.json new file mode 100644 index 000000000..9cfd876f5 --- /dev/null +++ b/webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.i18n.json @@ -0,0 +1,7 @@ +{ + "AccountActivationDialog": { + "title": "Account Activation", + "subtitle1": "Your account has not been activated yet.", + "subtitle2": "You need to provide the activation token received in the activation email." + } +} diff --git a/webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.tsx b/webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.tsx new file mode 100644 index 000000000..cb5239619 --- /dev/null +++ b/webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import IconButton from '@mui/material/IconButton'; +import CloseIcon from '@mui/icons-material/Close'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; + +import { AccountActivationForm } from 'forms'; + +import './AccountActivationDialog.css'; + +const AccountActivationDialog = ({ classes, handleClose, isOpen, onSubmit }: any) => { + const { t } = useTranslation(); + + const handleOnClose = () => { + handleClose(); + } + + return ( + + + { t('AccountActivationDialog.title') } + + {handleOnClose ? ( + + + + ) : null} + + +
+ { t('AccountActivationDialog.subtitle1') } + { t('AccountActivationDialog.subtitle2') } +
+ + +
+
+ ); +}; + +export default AccountActivationDialog; diff --git a/webclient/src/dialogs/CardImportDialog/CardImportDialog.css b/webclient/src/dialogs/CardImportDialog/CardImportDialog.css new file mode 100644 index 000000000..b089ed200 --- /dev/null +++ b/webclient/src/dialogs/CardImportDialog/CardImportDialog.css @@ -0,0 +1,5 @@ +.dialog-title { + display: flex; + justify-content: space-between; + align-items: center; +} diff --git a/webclient/src/dialogs/CardImportDialog/CardImportDialog.tsx b/webclient/src/dialogs/CardImportDialog/CardImportDialog.tsx new file mode 100644 index 000000000..8011d317a --- /dev/null +++ b/webclient/src/dialogs/CardImportDialog/CardImportDialog.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import IconButton from '@mui/material/IconButton'; +import CloseIcon from '@mui/icons-material/Close'; +import Typography from '@mui/material/Typography'; + +import { CardImportForm } from 'forms'; + +import './CardImportDialog.css'; + +const CardImportDialog = ({ classes, handleClose, isOpen }: any) => { + const handleOnClose = () => { + handleClose(); + } + + return ( + + + Import Cards + + {handleOnClose ? ( + + + + ) : null} + + + + + + ); +}; + +export default CardImportDialog; diff --git a/webclient/src/dialogs/KnownHostDialog/KnownHostDialog.css b/webclient/src/dialogs/KnownHostDialog/KnownHostDialog.css new file mode 100644 index 000000000..e8a350f0d --- /dev/null +++ b/webclient/src/dialogs/KnownHostDialog/KnownHostDialog.css @@ -0,0 +1,26 @@ +.KnownHostDialog { + +} + +.KnownHostDialog .MuiDialog-paper { + width: 100%; + max-width: 420px; +} + +.dialog-title__wrapper { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid; + padding-bottom: 10px; +} + +.dialog-title__label { + display: flex; + align-items: center; +} + +.dialog-content__subtitle.MuiTypography-root { + margin-bottom: 20px; +} diff --git a/webclient/src/dialogs/KnownHostDialog/KnownHostDialog.i18n.json b/webclient/src/dialogs/KnownHostDialog/KnownHostDialog.i18n.json new file mode 100644 index 000000000..4ae521050 --- /dev/null +++ b/webclient/src/dialogs/KnownHostDialog/KnownHostDialog.i18n.json @@ -0,0 +1,6 @@ +{ + "KnownHostDialog": { + "title": "{mode, select, edit {Edit} other {Add}} Known Host", + "subtitle": "Adding a new host allows you to connect to different servers. Enter the details below to your host list." + } +} diff --git a/webclient/src/dialogs/KnownHostDialog/KnownHostDialog.tsx b/webclient/src/dialogs/KnownHostDialog/KnownHostDialog.tsx new file mode 100644 index 000000000..5bde19d92 --- /dev/null +++ b/webclient/src/dialogs/KnownHostDialog/KnownHostDialog.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { styled } from '@mui/material/styles'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import IconButton from '@mui/material/IconButton'; +import AddIcon from '@mui/icons-material/Add'; +import CloseIcon from '@mui/icons-material/Close'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; + +import { KnownHostForm } from 'forms'; + +import './KnownHostDialog.css'; + +const PREFIX = 'KnownHostDialog'; + +const classes = { + root: `${PREFIX}-root` +}; + +const StyledDialog = styled(Dialog)(({ theme }) => ({ + [`&.${classes.root}`]: { + '& .dialog-title__wrapper': { + borderColor: theme.palette.grey[300] + } + } +})); + +const KnownHostDialog = ({ handleClose, onRemove, onSubmit, isOpen, host }: any) => { + const { t } = useTranslation(); + + const mode = host ? 'edit' : 'add'; + + const handleOnClose = () => { + if (handleClose) { + handleClose(); + } + }; + + return ( + + +
+ { t('KnownHostDialog.title', { mode }) } + + {handleClose ? ( + + + + ) : null} +
+
+ + + { t('KnownHostDialog.subtitle') } + + + +
+ ); +}; + +export default KnownHostDialog; diff --git a/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.css b/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.css new file mode 100644 index 000000000..73822f190 --- /dev/null +++ b/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.css @@ -0,0 +1,10 @@ +.dialog-title { + display: flex; + justify-content: space-between; + align-items: center; +} + +.dialog-content { + width: 700px; + max-width: 100%; +} diff --git a/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.i18n.json b/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.i18n.json new file mode 100644 index 000000000..bc0aa646d --- /dev/null +++ b/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.i18n.json @@ -0,0 +1,5 @@ +{ + "RegistrationDialog": { + "title": "Create New Account" + } +} diff --git a/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.tsx b/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.tsx new file mode 100644 index 000000000..2388dddb3 --- /dev/null +++ b/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import IconButton from '@mui/material/IconButton'; +import CloseIcon from '@mui/icons-material/Close'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; + +import { RegisterForm } from 'forms'; + +import './RegistrationDialog.css'; + +const RegistrationDialog = ({ classes, handleClose, isOpen, onSubmit }: any) => { + const { t } = useTranslation(); + + const handleOnClose = () => { + handleClose(); + } + + return ( + + + { t('RegistrationDialog.title') } + + {handleOnClose ? ( + + + + ) : null} + + + + + + ); +}; + +export default RegistrationDialog; diff --git a/webclient/src/dialogs/RequestPasswordResetDialog/RequestPasswordResetDialog.css b/webclient/src/dialogs/RequestPasswordResetDialog/RequestPasswordResetDialog.css new file mode 100644 index 000000000..731927c13 --- /dev/null +++ b/webclient/src/dialogs/RequestPasswordResetDialog/RequestPasswordResetDialog.css @@ -0,0 +1,5 @@ +.dialog-title { + display: flex; + justify-content: space-between; + align-items: center; +} diff --git a/webclient/src/dialogs/RequestPasswordResetDialog/RequestPasswordResetDialog.i18n.json b/webclient/src/dialogs/RequestPasswordResetDialog/RequestPasswordResetDialog.i18n.json new file mode 100644 index 000000000..c287b7a82 --- /dev/null +++ b/webclient/src/dialogs/RequestPasswordResetDialog/RequestPasswordResetDialog.i18n.json @@ -0,0 +1,5 @@ +{ + "RequestPasswordResetDialog": { + "title": "Request Password Reset" + } +} diff --git a/webclient/src/dialogs/RequestPasswordResetDialog/RequestPasswordResetDialog.tsx b/webclient/src/dialogs/RequestPasswordResetDialog/RequestPasswordResetDialog.tsx new file mode 100644 index 000000000..be2032a08 --- /dev/null +++ b/webclient/src/dialogs/RequestPasswordResetDialog/RequestPasswordResetDialog.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import IconButton from '@mui/material/IconButton'; +import CloseIcon from '@mui/icons-material/Close'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; + +import { RequestPasswordResetForm } from 'forms'; + +import './RequestPasswordResetDialog.css'; + +const RequestPasswordResetDialog = ({ classes, handleClose, isOpen, onSubmit, skipTokenRequest }: any) => { + const { t } = useTranslation(); + + const handleOnClose = () => { + handleClose(); + } + + return ( + + + { t('RequestPasswordResetDialog.title') } + + {handleOnClose ? ( + + + + ) : null} + + + + + + ); +}; + +export default RequestPasswordResetDialog; diff --git a/webclient/src/dialogs/ResetPasswordDialog/ResetPasswordDialog.css b/webclient/src/dialogs/ResetPasswordDialog/ResetPasswordDialog.css new file mode 100644 index 000000000..731927c13 --- /dev/null +++ b/webclient/src/dialogs/ResetPasswordDialog/ResetPasswordDialog.css @@ -0,0 +1,5 @@ +.dialog-title { + display: flex; + justify-content: space-between; + align-items: center; +} diff --git a/webclient/src/dialogs/ResetPasswordDialog/ResetPasswordDialog.i18n.json b/webclient/src/dialogs/ResetPasswordDialog/ResetPasswordDialog.i18n.json new file mode 100644 index 000000000..8047dcae0 --- /dev/null +++ b/webclient/src/dialogs/ResetPasswordDialog/ResetPasswordDialog.i18n.json @@ -0,0 +1,5 @@ +{ + "ResetPasswordDialog": { + "title": "Reset Password" + } +} diff --git a/webclient/src/dialogs/ResetPasswordDialog/ResetPasswordDialog.tsx b/webclient/src/dialogs/ResetPasswordDialog/ResetPasswordDialog.tsx new file mode 100644 index 000000000..877c8729f --- /dev/null +++ b/webclient/src/dialogs/ResetPasswordDialog/ResetPasswordDialog.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import IconButton from '@mui/material/IconButton'; +import CloseIcon from '@mui/icons-material/Close'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; + +import { ResetPasswordForm } from 'forms'; + +import './ResetPasswordDialog.css'; + +const ResetPasswordDialog = ({ classes, handleClose, isOpen, onSubmit, userName }: any) => { + const { t } = useTranslation(); + + const handleOnClose = () => { + handleClose(); + } + + return ( + + + {t('ResetPasswordDialog.title')} + + {handleOnClose ? ( + + + + ) : null} + + + + + + ); +}; + +export default ResetPasswordDialog; diff --git a/webclient/src/dialogs/index.ts b/webclient/src/dialogs/index.ts new file mode 100644 index 000000000..5c5ace1c5 --- /dev/null +++ b/webclient/src/dialogs/index.ts @@ -0,0 +1,6 @@ +export { default as AccountActivationDialog } from './AccountActivationDialog/AccountActivationDialog'; +export { default as CardImportDialog } from './CardImportDialog/CardImportDialog'; +export { default as KnownHostDialog } from './KnownHostDialog/KnownHostDialog'; +export { default as RegistrationDialog } from './RegistrationDialog/RegistrationDialog'; +export { default as RequestPasswordResetDialog } from './RequestPasswordResetDialog/RequestPasswordResetDialog'; +export { default as ResetPasswordDialog } from './ResetPasswordDialog/ResetPasswordDialog'; diff --git a/webclient/src/forms/AccountActivationForm/AccountActivationForm.css b/webclient/src/forms/AccountActivationForm/AccountActivationForm.css new file mode 100644 index 000000000..ffb4ecc77 --- /dev/null +++ b/webclient/src/forms/AccountActivationForm/AccountActivationForm.css @@ -0,0 +1,12 @@ +.AccountActivationForm { + width: 100%; + padding-bottom: 15px; +} + +.AccountActivationForm-item { + margin-bottom: 20px; +} + +.AccountActivationForm-submit { + width: 100%; +} diff --git a/webclient/src/forms/AccountActivationForm/AccountActivationForm.i18n.json b/webclient/src/forms/AccountActivationForm/AccountActivationForm.i18n.json new file mode 100644 index 000000000..0b510fb83 --- /dev/null +++ b/webclient/src/forms/AccountActivationForm/AccountActivationForm.i18n.json @@ -0,0 +1,10 @@ +{ + "AccountActivationForm": { + "error": { + "failed": "Account activation failed" + }, + "label": { + "activate": "Activate Account" + } + } +} diff --git a/webclient/src/forms/AccountActivationForm/AccountActivationForm.tsx b/webclient/src/forms/AccountActivationForm/AccountActivationForm.tsx new file mode 100644 index 000000000..61b20f9a5 --- /dev/null +++ b/webclient/src/forms/AccountActivationForm/AccountActivationForm.tsx @@ -0,0 +1,69 @@ +// eslint-disable-next-line +import React, { useState } from "react"; +import { connect } from 'react-redux'; +import { Form, Field } from 'react-final-form'; +import { OnChange } from 'react-final-form-listeners'; +import { useTranslation } from 'react-i18next'; + +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; + +import { InputField, KnownHosts } from 'components'; +import { FormKey } from 'types'; + +import './AccountActivationForm.css'; +import { useReduxEffect } from 'hooks'; +import { ServerTypes } from 'store'; + +const AccountActivationForm = ({ onSubmit }) => { + const [errorMessage, setErrorMessage] = useState(false); + const { t } = useTranslation(); + + useReduxEffect(() => { + setErrorMessage(true); + }, ServerTypes.ACCOUNT_ACTIVATION_FAILED, []); + + const handleOnSubmit = ({ token, ...values }) => { + setErrorMessage(false); + + token = token?.trim(); + + onSubmit({ token, ...values }); + } + + const validate = values => { + const errors: any = {}; + + if (!values.token) { + errors.token = t('Common.validation.required'); + } + + return errors; + }; + + return ( +
+ {({ handleSubmit, form }) => { + return ( + +
+ +
+ + {errorMessage && ( +
+ { t('AccountActivationForm.error.failed') } +
+ )} + + +
+ ); + }} + + ); +}; + +export default AccountActivationForm; diff --git a/webclient/src/forms/CardImportForm/CardImportForm.css b/webclient/src/forms/CardImportForm/CardImportForm.css new file mode 100644 index 000000000..56ac3e16e --- /dev/null +++ b/webclient/src/forms/CardImportForm/CardImportForm.css @@ -0,0 +1,35 @@ +.cardImportForm { + width: 550px; +} + +.cardImportForm-content.done { + font-size: 32px; + height: 150px; + display: flex; + align-items: center; + justify-content: center; +} + +.cardImportForm-actions { + display: flex; + justify-content: flex-end; + margin-top: 20px; +} + +.cardImportForm-error { + color: red; +} + +.card-import-list { + height: 300px; + line-height: 1; + border: 1px solid lightgrey; + padding: 10px; +} + +.loading { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} diff --git a/webclient/src/forms/CardImportForm/CardImportForm.tsx b/webclient/src/forms/CardImportForm/CardImportForm.tsx new file mode 100644 index 000000000..b9745f31e --- /dev/null +++ b/webclient/src/forms/CardImportForm/CardImportForm.tsx @@ -0,0 +1,227 @@ +// eslint-disable-next-line +import React, { useEffect, useState } from 'react'; +import { connect } from 'react-redux'; +import { Form, Field, reduxForm } from 'redux-form' + +import Button from '@mui/material/Button'; +import Stepper from '@mui/material/Stepper'; +import Step from '@mui/material/Step'; +import StepLabel from '@mui/material/StepLabel'; +import CircularProgress from '@mui/material/CircularProgress'; + +import { InputField, VirtualList } from 'components'; +import { cardImporterService, CardDTO, SetDTO, TokenDTO } from 'services'; +import { FormKey } from 'types'; + +import './CardImportForm.css'; + +const CardImportForm = (props) => { + const { handleSubmit, onSubmit: onClose } = props; + + const [loading, setLoading] = useState(false); + const [activeStep, setActiveStep] = useState(0); + const [importedCards, setImportedCards] = useState([]); + const [importedSets, setImportedSets] = useState([]); + const [error, setError] = useState(null); + + useEffect(() => { + if (loading) { + setError(null); + } + }, [loading]) + + const steps = ['Imports sets', 'Save sets', 'Import tokens', 'Finished']; + + const handleNext = () => { + setActiveStep((prevActiveStep) => prevActiveStep + 1); + }; + + const handleBack = () => { + setActiveStep((prevActiveStep) => prevActiveStep - 1); + }; + + const handleCardDownload = ({ cardDownloadUrl }) => { + setLoading(true); + + cardImporterService.importCards(cardDownloadUrl) + .then(({ cards, sets }) => { + setImportedCards(cards); + setImportedSets(sets); + + handleNext(); + }) + .catch(({ message }) => setError(message)) + .finally(() => setLoading(false)); + }; + + const handleCardSave = async () => { + setLoading(true); + + try { + await CardDTO.bulkAdd(importedCards); + await SetDTO.bulkAdd(importedSets); + + handleNext(); + } catch (e) { + console.error(e); + setError('Failed to save cards'); + } + + setLoading(false); + }; + + const handleTokenDownload = ({ tokenDownloadUrl }) => { + setLoading(true); + + cardImporterService.importTokens(tokenDownloadUrl) + .then(async tokens => { + await TokenDTO.bulkAdd(tokens); + handleNext(); + }) + .catch(({ message }) => setError(message)) + .finally(() => setLoading(false)); + }; + + const getStepContent = (stepIndex) => { + switch (stepIndex) { + case 0: return ( +
+
+ +
+ +
+ +
+ +
+ +
+
+ ); + + case 1: return ( +
+
+ +
+ +
+ + +
+ +
+ +
+
+ ); + + case 2: return ( +
+
+ +
+ +
+ + +
+ +
+ +
+
+ ); + + case 3: return ( +
+
Finished!
+ +
+ + +
+
+ ); + } + }; + + return ( +
+ + {steps.map((label) => ( + + {label} + + ))} + + +
+ { getStepContent(activeStep) } +
+ + { loading && ( +
+ +
+ ) } +
+ ); +}; + +const BackButton = ({ click, disabled }) => ( + +); + +const ErrorMessage = ({ error }) => { + return error && ( +
{error}
+ ); +}; + +const CardsImported = ({ cards, sets }) => { + const items = [ + ( +
+ Import finished: {cards.length} cards. +
+ ), + + (
), + + ...sets.map(set => ( +
{set.name}: {set.cards.length} cards imported
+ )) + ]; + + return ( +
+ index } + items={items} + size={15} + /> +
+ ); +}; + +const propsMap = { + form: FormKey.CARD_IMPORT, + onClose: Function +}; + +const mapStateToProps = () => ({ + initialValues: { + cardDownloadUrl: 'https://www.mtgjson.com/api/v5/AllPrintings.json', + tokenDownloadUrl: 'https://raw.githubusercontent.com/Cockatrice/Magic-Token/master/tokens.xml' + }, +}); + +export default connect(mapStateToProps)(reduxForm(propsMap)(CardImportForm)); diff --git a/webclient/src/forms/KnownHostForm/KnownHostForm.css b/webclient/src/forms/KnownHostForm/KnownHostForm.css new file mode 100644 index 000000000..76e1722e0 --- /dev/null +++ b/webclient/src/forms/KnownHostForm/KnownHostForm.css @@ -0,0 +1,21 @@ +.KnownHostForm { + width: 100%; +} + +.KnownHostForm-item { + display: flex; + flex-direction: column; + margin-bottom: 20px; +} + +.KnownHostForm-submit { + width: 100%; +} + +.KnownHostForm-actions { + display: flex; + justify-content: space-between; + align-items: center; + margin: 5px 0 10px; + color: red; +} diff --git a/webclient/src/forms/KnownHostForm/KnownHostForm.i18n.json b/webclient/src/forms/KnownHostForm/KnownHostForm.i18n.json new file mode 100644 index 000000000..fe614a126 --- /dev/null +++ b/webclient/src/forms/KnownHostForm/KnownHostForm.i18n.json @@ -0,0 +1,9 @@ +{ + "KnownHostForm": { + "help": "Need help adding a new host?", + "label": { + "add": "Add Host", + "find": "Find Host" + } + } +} diff --git a/webclient/src/forms/KnownHostForm/KnownHostForm.tsx b/webclient/src/forms/KnownHostForm/KnownHostForm.tsx new file mode 100644 index 000000000..1241336f3 --- /dev/null +++ b/webclient/src/forms/KnownHostForm/KnownHostForm.tsx @@ -0,0 +1,94 @@ +// eslint-disable-next-line +import React, { useState } from "react"; +import { connect } from 'react-redux'; +import { Form, Field } from 'react-final-form' +import { useTranslation } from 'react-i18next'; + +import Button from '@mui/material/Button'; +import AnchorLink from '@mui/material/Link'; + +import { InputField } from 'components'; + +import './KnownHostForm.css'; + +const KnownHostForm = ({ host, onRemove, onSubmit }) => { + const [confirmDelete, setConfirmDelete] = useState(false); + const { t } = useTranslation(); + + const validate = values => { + const errors: any = {}; + + if (!values.name) { + errors.name = t('Common.validation.required'); + } + + if (!values.host) { + errors.host = t('Common.validation.required'); + } + + if (!values.port) { + errors.port = t('Common.validation.required'); + } + + if (Object.keys(errors).length) { + return errors; + } + }; + + const handleOnSubmit = ({ name, host, ...values }) => { + name = name?.trim(); + host = host?.trim(); + + onSubmit({ name, host, ...values }); + } + + return ( +
+ {({ handleSubmit }) => ( + +
+ +
+
+ +
+
+ +
+ + + +
+
+ { host && ( + + ) } +
+ + { t('KnownHostForm.label.find') } + +
+
+ ) } + + ); +}; + +const mapStateToProps = () => ({ + +}); + +export default connect(mapStateToProps)(KnownHostForm); diff --git a/webclient/src/forms/LoginForm/LoginForm.css b/webclient/src/forms/LoginForm/LoginForm.css new file mode 100644 index 000000000..4b176476f --- /dev/null +++ b/webclient/src/forms/LoginForm/LoginForm.css @@ -0,0 +1,20 @@ +.loginForm { + width: 100%; +} + +.loginForm-item { + margin-bottom: 20px; +} + +.loginForm-actions { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: -20px; + margin-bottom: 20px; + font-weight: bold; +} + +.loginForm-submit { + width: 100%; +} diff --git a/webclient/src/forms/LoginForm/LoginForm.i18n.json b/webclient/src/forms/LoginForm/LoginForm.i18n.json new file mode 100644 index 000000000..260b88b27 --- /dev/null +++ b/webclient/src/forms/LoginForm/LoginForm.i18n.json @@ -0,0 +1,11 @@ +{ + "LoginForm": { + "label": { + "autoConnect": "Auto Connect", + "forgot": "Forgot Password", + "login": "Login", + "savePassword": "Save Password", + "savedPassword": "Saved Password" + } + } +} diff --git a/webclient/src/forms/LoginForm/LoginForm.tsx b/webclient/src/forms/LoginForm/LoginForm.tsx new file mode 100644 index 000000000..1e8f104fe --- /dev/null +++ b/webclient/src/forms/LoginForm/LoginForm.tsx @@ -0,0 +1,171 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { Form, Field } from 'react-final-form'; +import { OnChange } from 'react-final-form-listeners'; +import { useTranslation } from 'react-i18next'; + +import Button from '@mui/material/Button'; + +import { AuthenticationService } from 'api'; +import { CheckboxField, InputField, KnownHosts } from 'components'; +import { useAutoConnect } from 'hooks'; +import { HostDTO, SettingDTO } from 'services'; +import { APP_USER } from 'types'; + +import './LoginForm.css'; + +const LoginForm = ({ onSubmit, disableSubmitButton, onResetPassword }: LoginFormProps) => { + const { t } = useTranslation(); + const PASSWORD_LABEL = t('Common.label.password'); + const STORED_PASSWORD_LABEL = `* ${t('LoginForm.label.savedPassword')} *`; + + const [host, setHost] = useState(null); + const [useStoredPasswordLabel, setUseStoredPasswordLabel] = useState(false); + const [autoConnect, setAutoConnect] = useAutoConnect(); + + const validate = values => { + const errors: any = {}; + + if (!values.userName) { + errors.userName = t('Common.validation.required'); + } + if (!values.selectedHost) { + errors.selectedHost = t('Common.validation.required'); + } + + return errors; + } + + const useStoredPassword = (remember, password) => remember && host?.hashedPassword && !password; + const togglePasswordLabel = (useStoredLabel) => { + setUseStoredPasswordLabel(useStoredLabel); + }; + + const handleOnSubmit = ({ userName, ...values }) => { + userName = userName?.trim(); + console.log(userName, values); + + onSubmit({ userName, ...values }); + } + + return ( +
+ {({ handleSubmit, form }) => { + const { values } = form.getState(); + + useEffect(() => { + SettingDTO.get(APP_USER).then((userSetting: SettingDTO) => { + if (userSetting?.autoConnect && !AuthenticationService.connectionAttemptMade()) { + HostDTO.getAll().then(hosts => { + let lastSelectedHost = hosts.find(({ lastSelected }) => lastSelected); + + if (lastSelectedHost?.remember && lastSelectedHost?.hashedPassword) { + togglePasswordLabel(true); + + form.change('selectedHost', lastSelectedHost); + form.change('userName', lastSelectedHost.userName); + form.change('remember', true); + form.submit(); + } + }); + } + }); + }, []); + + useEffect(() => { + if (!host) { + return; + } + + form.change('userName', host.userName); + form.change('password', ''); + + onRememberChange(host.remember); + onAutoConnectChange(host.remember && autoConnect); + togglePasswordLabel(useStoredPassword(host.remember, values.password)); + }, [host]); + + const onUserNameChange = (userName) => { + const fieldChanged = host?.userName?.toLowerCase() !== values.userName?.toLowerCase(); + if (useStoredPassword(values.remember, values.password) && fieldChanged) { + setHost(({ hashedPassword, ...s }) => ({ ...s, userName })); + } + } + + const onRememberChange = (checked) => { + form.change('remember', checked); + + if (!checked && values.autoConnect) { + onAutoConnectChange(false); + } + + togglePasswordLabel(useStoredPassword(checked, values.password)); + } + + const onAutoConnectChange = (checked) => { + setAutoConnect(checked); + + form.change('autoConnect', checked); + + if (checked && !values.remember) { + form.change('remember', true); + } + } + + return ( + +
+
+ + {onUserNameChange} +
+
+ setUseStoredPasswordLabel(false)} + onBlur={() => togglePasswordLabel(useStoredPassword(values.remember, values.password))} + name='password' + type='password' + component={InputField} + autoComplete='new-password' + /> +
+
+ + {onRememberChange} + + +
+
+ + {setHost} +
+
+ + {onAutoConnectChange} +
+
+ +
+ ) + }} + + ); +}; + +interface LoginFormProps { + onSubmit: any; + disableSubmitButton: boolean, + onResetPassword: any; +} + +export default LoginForm; diff --git a/webclient/src/forms/RegisterForm/RegisterForm.css b/webclient/src/forms/RegisterForm/RegisterForm.css new file mode 100644 index 000000000..324aa7e63 --- /dev/null +++ b/webclient/src/forms/RegisterForm/RegisterForm.css @@ -0,0 +1,18 @@ +.RegisterForm { + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.RegisterForm-column { + width: 48%; +} + +.RegisterForm-item { + margin-bottom: 20px; +} + +.RegisterForm-submit { + width: 100%; +} diff --git a/webclient/src/forms/RegisterForm/RegisterForm.i18n.json b/webclient/src/forms/RegisterForm/RegisterForm.i18n.json new file mode 100644 index 000000000..50e802ea5 --- /dev/null +++ b/webclient/src/forms/RegisterForm/RegisterForm.i18n.json @@ -0,0 +1,10 @@ +{ + "RegisterForm": { + "label": { + "register": "Register" + }, + "toast": { + "registerSuccess": "Registration Successful!" + } + } +} diff --git a/webclient/src/forms/RegisterForm/RegisterForm.tsx b/webclient/src/forms/RegisterForm/RegisterForm.tsx new file mode 100644 index 000000000..f5f4d9174 --- /dev/null +++ b/webclient/src/forms/RegisterForm/RegisterForm.tsx @@ -0,0 +1,177 @@ +import { useState } from 'react'; +import { Form, Field } from 'react-final-form'; +import { OnChange } from 'react-final-form-listeners'; +import setFieldTouched from 'final-form-set-field-touched'; +import { useTranslation } from 'react-i18next'; + +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; + +import { CountryDropdown, InputField, KnownHosts } from 'components'; +import { useReduxEffect } from 'hooks'; +import { ServerTypes } from 'store'; + +import './RegisterForm.css'; +import { useToast } from 'components/Toast'; + +const RegisterForm = ({ onSubmit }: RegisterFormProps) => { + const { t } = useTranslation(); + const [emailRequired, setEmailRequired] = useState(false); + const [error, setError] = useState(null); + const [emailError, setEmailError] = useState(null); + const [passwordError, setPasswordError] = useState(null); + const [userNameError, setUserNameError] = useState(null); + const { openToast } = useToast({ key: 'registration-success', children: t('RegisterForm.toast.registerSuccess') }) + + const onHostChange = (host) => setEmailRequired(false); + const onEmailChange = () => emailError && setEmailError(null); + const onPasswordChange = () => passwordError && setPasswordError(null); + const onUserNameChange = () => userNameError && setUserNameError(null); + + useReduxEffect(() => { + setEmailRequired(true); + }, ServerTypes.REGISTRATION_REQUIRES_EMAIL); + + useReduxEffect(({ error }) => { + setError(error); + }, ServerTypes.REGISTRATION_FAILED); + + useReduxEffect(() => { + openToast() + }, ServerTypes.REGISTRATION_SUCCES); + + useReduxEffect(({ error }) => { + setEmailError(error); + }, ServerTypes.REGISTRATION_EMAIL_ERROR); + + useReduxEffect(({ error }) => { + setPasswordError(error); + }, ServerTypes.REGISTRATION_PASSWORD_ERROR); + + useReduxEffect(({ error }) => { + setUserNameError(error); + }, ServerTypes.REGISTRATION_USERNAME_ERROR); + + const handleOnSubmit = ({ userName, email, realName, ...values }) => { + setError(null); + + userName = userName?.trim(); + email = email?.trim(); + realName = realName?.trim(); + + onSubmit({ userName, email, realName, ...values }); + } + + const validate = values => { + const errors: any = {}; + + if (!values.userName) { + errors.userName = t('Common.validation.required'); + } else if (userNameError) { + errors.userName = userNameError; + } + + if (!values.password) { + errors.password = t('Common.validation.required'); + } else if (values.password.length < 8) { + errors.password = t('Common.validation.minChars', { count: 8 }); + } else if (passwordError) { + errors.password = passwordError; + } + + if (!values.passwordConfirm) { + errors.passwordConfirm = t('Common.validation.required'); + } else if (values.password !== values.passwordConfirm) { + errors.passwordConfirm = t('Common.validation.passwordsMustMatch'); + } + + if (!values.selectedHost) { + errors.selectedHost = t('Common.validation.required'); + } + + if (emailRequired && !values.email) { + errors.email = t('Common.validation.required'); + } else if (emailError) { + errors.email = emailError; + } + + return errors; + } + + return ( +
+ {({ handleSubmit, form, ...args }) => { + const { values } = form.getState(); + + if (emailRequired) { + // Allow form render to complete + setTimeout(() => form.mutators.setFieldTouched('email', true)) + } + + return ( + <> + +
+
+ + {onUserNameChange} +
+
+ + {onPasswordChange} +
+
+ +
+
+ + {onHostChange} +
+
+
+
+ +
+
+ + {onEmailChange} +
+
+ +
+ +
+
+ + { error && ( +
+ {error} +
+ )} + + ); + }} + + + ); +}; + +interface RegisterFormProps { + onSubmit: any; +} + +export default RegisterForm; diff --git a/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.css b/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.css new file mode 100644 index 000000000..83ed7420f --- /dev/null +++ b/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.css @@ -0,0 +1,25 @@ +.RequestPasswordResetForm { + width: 100%; + padding-bottom: 15px; +} + +.RequestPasswordResetForm-item { + margin-bottom: 20px; +} + +.RequestPasswordResetForm-actions { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: -20px; + margin-bottom: 20px; + font-weight: bold; +} + +.RequestPasswordResetForm-submit { + width: 100%; +} + +.selectedHost { + margin-top: 40px; +} diff --git a/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.i18n.json b/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.i18n.json new file mode 100644 index 000000000..c53f6d097 --- /dev/null +++ b/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.i18n.json @@ -0,0 +1,8 @@ +{ + "RequestPasswordResetForm": { + "error": "Request password reset failed", + "mfaEnabled": "Server has multi-factor authentication enabled", + "request": "Request Reset Token", + "skipRequest": "I already have a reset token" + } +} diff --git a/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.tsx b/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.tsx new file mode 100644 index 000000000..01d4a2959 --- /dev/null +++ b/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.tsx @@ -0,0 +1,104 @@ +// eslint-disable-next-line +import React, { useState } from "react"; +import { connect } from 'react-redux'; +import { Form, Field } from 'react-final-form'; +import { OnChange } from 'react-final-form-listeners'; +import { useTranslation } from 'react-i18next'; + +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; + +import { InputField, KnownHosts } from 'components'; +import { FormKey } from 'types'; + +import './RequestPasswordResetForm.css'; +import { useReduxEffect } from 'hooks'; +import { ServerTypes } from 'store'; + +const RequestPasswordResetForm = ({ onSubmit, skipTokenRequest }) => { + const [errorMessage, setErrorMessage] = useState(false); + const [isMFA, setIsMFA] = useState(false); + const { t } = useTranslation(); + + useReduxEffect(() => { + setErrorMessage(true); + }, ServerTypes.RESET_PASSWORD_FAILED, []); + + useReduxEffect(() => { + setIsMFA(true); + }, ServerTypes.RESET_PASSWORD_CHALLENGE, []); + + const handleOnSubmit = ({ userName, email, ...values }) => { + setErrorMessage(false); + + userName = userName?.trim(); + email = email?.trim(); + + onSubmit({ userName, email, ...values }); + } + + const validate = values => { + const errors: any = {}; + + if (!values.userName) { + errors.userName = t('Common.validation.required'); + } + if (isMFA && !values.email) { + errors.email = t('Common.validation.required'); + } + if (!values.selectedHost) { + errors.selectedHost = t('Common.validation.required'); + } + + return errors; + }; + + return ( +
+ {({ handleSubmit, form }) => { + const onHostChange: any = ({ userName }) => { + form.change('userName', userName); + setIsMFA(false); + } + + return ( + +
+
+ +
+ {isMFA ? ( +
+ +
{ t('RequestPasswordResetForm.mfaEnabled') }
+
+ ) : null} +
+ + {onHostChange} +
+ + {errorMessage && ( +
+ { t('RequestPasswordResetForm.error') } +
+ )} +
+ + + +
+ +
+
+ ); + }} + + ); +}; + +export default RequestPasswordResetForm; diff --git a/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.css b/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.css new file mode 100644 index 000000000..ad82b7efd --- /dev/null +++ b/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.css @@ -0,0 +1,21 @@ +.ResetPasswordForm { + width: 100%; + padding-bottom: 15px; +} + +.ResetPasswordForm-item { + margin-bottom: 20px; +} + +.ResetPasswordForm-actions { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: -20px; + margin-bottom: 20px; + font-weight: bold; +} + +.ResetPasswordForm-submit { + width: 100%; +} diff --git a/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.i18n.json b/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.i18n.json new file mode 100644 index 000000000..2835134c0 --- /dev/null +++ b/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.i18n.json @@ -0,0 +1,8 @@ +{ + "ResetPasswordForm": { + "error": "Password reset failed", + "label": { + "reset": "Reset Password" + } + } +} diff --git a/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.tsx b/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.tsx new file mode 100644 index 000000000..8b56f4b69 --- /dev/null +++ b/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.tsx @@ -0,0 +1,115 @@ +// eslint-disable-next-line +import React, { useEffect, useState } from 'react'; +import { connect } from 'react-redux'; +import { Form, Field } from 'react-final-form' +import { OnChange } from 'react-final-form-listeners'; +import { useTranslation } from 'react-i18next'; + +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; + +import { InputField, KnownHosts } from 'components'; +import { FormKey } from 'types'; + +import './ResetPasswordForm.css'; +import { useReduxEffect } from '../../hooks'; +import { ServerTypes } from '../../store'; + +const ResetPasswordForm = ({ onSubmit, userName }) => { + const [errorMessage, setErrorMessage] = useState(false); + const { t } = useTranslation(); + + useReduxEffect(() => { + setErrorMessage(true); + }, ServerTypes.RESET_PASSWORD_FAILED, []); + + const validate = values => { + const errors: any = {}; + + if (!values.userName) { + errors.userName = t('Common.validation.required'); + } + if (!values.token) { + errors.token = t('Common.validation.required'); + } + + if (!values.newPassword) { + errors.newPassword = t('Common.validation.required'); + } else if (values.newPassword.length < 8) { + errors.newPassword = t('Common.validation.minChars', { count: 8 }); + } + + if (!values.passwordAgain) { + errors.passwordAgain = t('Common.validation.required'); + } else if (values.newPassword !== values.passwordAgain) { + errors.passwordAgain = t('Common.validation.passwordsMustMatch'); + } + if (!values.selectedHost) { + errors.selectedHost = t('Common.validation.required'); + } + + return errors; + }; + + const handleOnSubmit = ({ userName, token, ...values }) => { + userName = userName?.trim(); + token = token?.trim(); + + onSubmit({ userName, token, ...values }); + } + + return ( +
+ {({ handleSubmit, form }) => ( + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + {errorMessage && ( +
+ { t('ResetPasswordForm.error') } +
+ )} +
+ +
+ )} + + ); +}; + +export default ResetPasswordForm; diff --git a/webclient/src/forms/SearchForm/SearchForm.css b/webclient/src/forms/SearchForm/SearchForm.css new file mode 100644 index 000000000..9e3cd6cb8 --- /dev/null +++ b/webclient/src/forms/SearchForm/SearchForm.css @@ -0,0 +1,35 @@ +.log-search { + margin-bottom: 20px; +} + +hr.MuiDivider-root { + margin: 20px 0; +} + +.log-search__form { + width: 100%; + padding: 20px; +} + +.log-search__form-item { + display: flex; +} + +.log-search__form-item.log-location { + display: flex; + justify-content: space-around; +} + +.log-search__form-item.log-location .checkbox-field { + display: flex; + flex-direction: column; +} + +.log-search__form-item.log-location .checkbox-field__box { + order: 1; +} + +.log-search__form-submit.MuiButton-root { + display: block; + margin: 0 auto; +} diff --git a/webclient/src/forms/SearchForm/SearchForm.tsx b/webclient/src/forms/SearchForm/SearchForm.tsx new file mode 100644 index 000000000..793543086 --- /dev/null +++ b/webclient/src/forms/SearchForm/SearchForm.tsx @@ -0,0 +1,63 @@ +// eslint-disable-next-line +import React, { Component } from "react"; +import { connect } from 'react-redux'; +import { Form, Field, reduxForm } from 'redux-form' + +import Button from '@mui/material/Button'; +import Divider from '@mui/material/Divider'; +import Paper from '@mui/material/Paper'; + +import { InputField, CheckboxField } from 'components'; +import { FormKey } from 'types'; + +import './SearchForm.css'; + +const SearchForm = ({ handleSubmit }) => ( + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ + + +
+ +
+ Date Range: Coming Soon +
+ +
+ Maximum Results: 1000 +
+ + + +
+); + +const propsMap = { + form: FormKey.SEARCH_LOGS, +}; + +const mapStateToProps = () => ({ + +}); + +export default connect(mapStateToProps)(reduxForm(propsMap)(SearchForm)); diff --git a/webclient/src/forms/index.ts b/webclient/src/forms/index.ts new file mode 100644 index 000000000..b922a9f40 --- /dev/null +++ b/webclient/src/forms/index.ts @@ -0,0 +1,8 @@ +export { default as AccountActivationForm } from './AccountActivationForm/AccountActivationForm'; +export { default as CardImportForm } from './CardImportForm/CardImportForm'; +export { default as LoginForm } from './LoginForm/LoginForm'; +export { default as KnownHostForm } from './KnownHostForm/KnownHostForm'; +export { default as RegisterForm } from './RegisterForm/RegisterForm'; +export { default as SearchForm } from './SearchForm/SearchForm'; +export { default as RequestPasswordResetForm } from './RequestPasswordResetForm/RequestPasswordResetForm'; +export { default as ResetPasswordForm } from './ResetPasswordForm/ResetPasswordForm'; diff --git a/webclient/src/hooks/index.ts b/webclient/src/hooks/index.ts new file mode 100644 index 000000000..b5e9cca55 --- /dev/null +++ b/webclient/src/hooks/index.ts @@ -0,0 +1,5 @@ +export * from './useAutoConnect'; +export * from './useFireOnce'; +export * from './useDebounce'; +export * from './useLocaleSort'; +export * from './useReduxEffect'; diff --git a/webclient/src/hooks/useAutoConnect.ts b/webclient/src/hooks/useAutoConnect.ts new file mode 100644 index 000000000..5258f58a7 --- /dev/null +++ b/webclient/src/hooks/useAutoConnect.ts @@ -0,0 +1,38 @@ +import { useEffect, useState } from 'react'; +import { debounce, DebouncedFunc } from 'lodash'; + +import { SettingDTO } from 'services'; +import { APP_USER } from 'types'; + +type OnChange = () => void; + +export function useAutoConnect() { + const [setting, setSetting] = useState(undefined); + const [autoConnect, setAutoConnect] = useState(undefined); + + useEffect(() => { + SettingDTO.get(APP_USER).then((setting: SettingDTO) => { + if (!setting) { + setting = new SettingDTO(APP_USER); + setting.save(); + } + + setSetting(setting); + }); + }, []); + + useEffect(() => { + if (setting) { + setAutoConnect(setting.autoConnect); + } + }, [setting]); + + useEffect(() => { + if (setting) { + setting.autoConnect = autoConnect; + setting.save(); + } + }, [setting, autoConnect]); + + return [autoConnect, setAutoConnect]; +} diff --git a/webclient/src/hooks/useDebounce.ts b/webclient/src/hooks/useDebounce.ts new file mode 100644 index 000000000..65aee9982 --- /dev/null +++ b/webclient/src/hooks/useDebounce.ts @@ -0,0 +1,13 @@ +import { useCallback } from 'react'; +import { debounce, DebouncedFunc } from 'lodash'; + +type UseDebounceType = (...args: any) => any; +const DEBOUNCE_DELAY = 250; + +export function useDebounce( + fn: T, + deps: any[] = [], + timeout: number = DEBOUNCE_DELAY +): DebouncedFunc { + return useCallback(debounce(fn, timeout), deps); +} diff --git a/webclient/src/hooks/useFireOnce/index.ts b/webclient/src/hooks/useFireOnce/index.ts new file mode 100644 index 000000000..442da884f --- /dev/null +++ b/webclient/src/hooks/useFireOnce/index.ts @@ -0,0 +1 @@ +export * from './useFireOnce' diff --git a/webclient/src/hooks/useFireOnce/useFireOnce.spec.tsx b/webclient/src/hooks/useFireOnce/useFireOnce.spec.tsx new file mode 100644 index 000000000..2f6ed34e0 --- /dev/null +++ b/webclient/src/hooks/useFireOnce/useFireOnce.spec.tsx @@ -0,0 +1,103 @@ +import { + render, + fireEvent, + getByRole, + waitFor, + act +} from '@testing-library/react'; +import { useFireOnce } from './useFireOnce'; + +describe('useFireOnce hook', () => { + test('it only fires once when button is clicked twice', async () => { + // Mock a promise with a delay + const onClickWithPromise = jest.fn((e) => { + e.preventDefault() + return new Promise((resolve) => { + setTimeout(() => { + resolve(true); + }, 100); + }); + }); + + function Button(props) { + const { children, onClick } = props + const [buttonIsDisabled, setButtonIsDisabled, handleClickOnce] = useFireOnce(onClick) + return + } + + // render the button + const { getByRole } = render( + + ); + + //Grab the button from the DOM and confirm it initialized in an enabled state + const button = getByRole('button', { name: 'Click Me!' }); + expect(button).toBeEnabled(); + + // Simulate two click events in a row + fireEvent.click(button); + fireEvent.click(button); + + // Confirm that it's disabled + await waitFor(() => { + expect(button).toBeDisabled(); + }); + + // Confirm it became enabled after the timeout and that the click event was only fired once + await waitFor( + () => { + expect(onClickWithPromise).toHaveBeenCalledTimes(1); + }, + { timeout: 100 } + ); + }); + + test('it only fires once when form is submitted twice', async () => { + // Mock a promise with a delay + const onClickWithPromise = jest.fn((e) => { + e.preventDefault() + return new Promise((resolve) => { + setTimeout(() => { + resolve(true); + }, 100); + }); + }); + + function Form(props) { + const { onSubmit } = props + const [buttonIsDisabled, setButtonIsDisabled, handleSubmitOnce] = useFireOnce(onSubmit) + return ( +
+ + +
+ ) + } + + // render the form + const { getByRole } = render( +
+ ); + + //Grab the button from the DOM and confirm it initialized in an enabled state + const button = getByRole('button', { name: 'Click Me!' }); + expect(button).toBeEnabled(); + + // Simulate two click events in a row + fireEvent.click(button); + fireEvent.click(button); + + // Confirm that it's disabled + await waitFor(() => { + expect(button).toBeDisabled(); + }); + + // Confirm it became enabled after the timeout and that the click event was only fired once + await waitFor( + () => { + expect(onClickWithPromise).toHaveBeenCalledTimes(1); + }, + { timeout: 100 } + ); + }); +}); diff --git a/webclient/src/hooks/useFireOnce/useFireOnce.ts b/webclient/src/hooks/useFireOnce/useFireOnce.ts new file mode 100644 index 000000000..0b042a907 --- /dev/null +++ b/webclient/src/hooks/useFireOnce/useFireOnce.ts @@ -0,0 +1,17 @@ +import { useCallback, useState } from 'react'; +import { useReduxEffect } from 'hooks'; +import { ServerTypes } from 'store'; + +type UseFireOnceType = (...args: any) => any; + +export function useFireOnce(fn: T): [boolean, any, any] { + const [actionIsInFlight, setActionIsInFlight] = useState(false) + const handleFireOnce = useCallback((args) => { + setActionIsInFlight(true); + fn(args); + }, []) + function resetInFlightStatus() { + setActionIsInFlight(false); + } + return [actionIsInFlight, resetInFlightStatus, handleFireOnce] +} diff --git a/webclient/src/hooks/useLocaleSort.ts b/webclient/src/hooks/useLocaleSort.ts new file mode 100644 index 000000000..219292ed7 --- /dev/null +++ b/webclient/src/hooks/useLocaleSort.ts @@ -0,0 +1,18 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +export function useLocaleSort(arr: string[], valueGetter: (value: string) => string) { + const [state] = useState(arr); + const [sorted, setSorted] = useState([]); + + const { i18n } = useTranslation(); + + useEffect(() => { + const collator = new Intl.Collator(i18n.language); + const sorter = (a, b) => collator.compare(valueGetter(a), valueGetter(b)); + + setSorted(state.sort(sorter)); + }, [state, i18n.language]); + + return sorted; +} diff --git a/webclient/src/hooks/useReduxEffect.tsx b/webclient/src/hooks/useReduxEffect.tsx new file mode 100644 index 000000000..83273a6d3 --- /dev/null +++ b/webclient/src/hooks/useReduxEffect.tsx @@ -0,0 +1,47 @@ +/** +File is adapted from https://github.com/Qeepsake/use-redux-effect under MIT License + * @author Aspect Apps Limited + * @description + */ + +import { useRef, useEffect, DependencyList } from 'react' +import { useStore } from 'react-redux' +import { AnyAction } from 'redux' +import { castArray } from 'lodash' + +export type ReduxEffect = (action: AnyAction) => void + +/** + * Subscribes to redux store events + * + * @param effect + * @param type + * @param deps + */ +export function useReduxEffect( + effect: ReduxEffect, + type: string | string[], + deps: DependencyList = [], +): void { + const currentValue = useRef(null); + const store = useStore(); + + const handleChange = (): void => { + const state: any = store.getState(); + const action = state.action; + const previousValue = currentValue.current; + currentValue.current = action.count; + + if ( + previousValue !== action.count && + castArray(type).includes(action.type) + ) { + effect(action); + } + } + + useEffect(() => { + const unsubscribe = store.subscribe(handleChange); + return (): void => unsubscribe(); + }, deps) +} diff --git a/webclient/src/i18n-backend.ts b/webclient/src/i18n-backend.ts new file mode 100644 index 000000000..d270b3503 --- /dev/null +++ b/webclient/src/i18n-backend.ts @@ -0,0 +1,21 @@ +import { ModuleType } from 'i18next'; + +import { Language } from 'types'; + +class I18nBackend { + static type: ModuleType = 'backend'; + static BASE_URL = `${process.env.PUBLIC_URL}/locales`; + + read(language, namespace, callback) { + if (!Language[language]) { + callback(true, null); + return; + } + + fetch(`${I18nBackend.BASE_URL}/${Language[language]}/${namespace}.json`) + .then(resp => resp.json().then(json => callback(null, json))) + .catch(error => callback(error, null)); + } +} + +export default I18nBackend; diff --git a/webclient/src/i18n-default.json b/webclient/src/i18n-default.json new file mode 100644 index 000000000..3c63e0abb --- /dev/null +++ b/webclient/src/i18n-default.json @@ -0,0 +1,382 @@ +{ + "Common": { + "language": "English", + "disconnect": "Disconnect", + "label": { + "confirmPassword": "Confirm Password", + "confirmSure": "Are you sure?", + "country": "Country", + "delete": "Delete", + "email": "Email", + "hostName": "Host Name", + "hostAddress": "Host Address", + "password": "Password", + "passwordAgain": "Password Again", + "port": "Port", + "realName": "Real Name", + "saveChanges": "Save Changes", + "token": "Token", + "username": "Username" + }, + "validation": { + "minChars": "Minimum of {count} {count, plural, one {character} other {characters}} required", + "passwordsMustMatch": "Passwords don't match", + "required": "Required" + }, + "countries": { + "AD": "Andorra", + "AE": "United Arab Emirates", + "AF": "Afghanistan", + "AG": "Antigua and Barbuda", + "AI": "Anguilla", + "AL": "Albania", + "AM": "Armenia", + "AO": "Angola", + "AQ": "Antarctica", + "AR": "Argentina", + "AS": "American Samoa", + "AT": "Austria", + "AU": "Australia", + "AW": "Aruba", + "AX": "Åland Islands", + "AZ": "Azerbaijan", + "BA": "Bosnia and Herzegovina", + "BB": "Barbados", + "BD": "Bangladesh", + "BE": "Belgium", + "BF": "Burkina Faso", + "BG": "Bulgaria", + "BH": "Bahrain", + "BI": "Burundi", + "BJ": "Benin", + "BL": "Saint Barthélemy", + "BM": "Bermuda", + "BN": "Brunei Darussalam", + "BO": "Bolivia", + "BQ": "Bonaire, Sint Eustatius and Saba", + "BR": "Brazil", + "BS": "Bahamas", + "BT": "Bhutan", + "BV": "Bouvet Island", + "BW": "Botswana", + "BY": "Belarus", + "BZ": "Belize", + "CA": "Canada", + "CC": "Cocos (Keeling) Islands", + "CD": "DR Congo", + "CF": "Central African Republic", + "CG": "Republic of the Congo", + "CH": "Switzerland", + "CI": "Ivory Coast", + "CK": "Cook Islands", + "CL": "Chile", + "CM": "Cameroon", + "CN": "China", + "CO": "Colombia", + "CR": "Costa Rica", + "CU": "Cuba", + "CV": "Cape Verde", + "CW": "Curaçao", + "CX": "Christmas Island", + "CY": "Cyprus", + "CZ": "Czechia", + "DE": "Germany", + "DJ": "Djibouti", + "DK": "Denmark", + "DM": "Dominica", + "DO": "Dominican Republic", + "DZ": "Algeria", + "EC": "Ecuador", + "EE": "Estonia", + "EG": "Egypt", + "EH": "Western Sahara", + "ER": "Eritrea", + "ES": "Spain", + "ET": "Ethiopia", + "FI": "Finland", + "FJ": "Fiji", + "FK": "Falkland Islands", + "FM": "Micronesia", + "FO": "Faroe Islands", + "FR": "France", + "GA": "Gabon", + "GB": "United Kingdom", + "GD": "Grenada", + "GE": "Georgia", + "GF": "French Guiana", + "GG": "Guernsey", + "GH": "Ghana", + "GI": "Gibraltar", + "GL": "Greenland", + "GM": "Gambia", + "GN": "Guinea", + "GP": "Guadeloupe", + "GQ": "Equatorial Guinea", + "GR": "Greece", + "GS": "South Georgia and the South Sandwich Islands", + "GT": "Guatemala", + "GU": "Guam", + "GW": "Guinea-Bissau", + "GY": "Guyana", + "HK": "Hong Kong", + "HM": "Heard Island and McDonald Islands", + "HN": "Honduras", + "HR": "Croatia", + "HT": "Haiti", + "HU": "Hungary", + "ID": "Indonesia", + "IE": "Ireland", + "IL": "Israel", + "IM": "Isle of Man", + "IN": "India", + "IO": "British Indian Ocean Territory", + "IQ": "Iraq", + "IR": "Iran", + "IS": "Iceland", + "IT": "Italy", + "JE": "Jersey", + "JM": "Jamaica", + "JO": "Jordan", + "JP": "Japan", + "KE": "Kenya", + "KG": "Kyrgyzstan", + "KH": "Cambodia", + "KI": "Kiribati", + "KM": "Comoros", + "KN": "Saint Kitts and Nevis", + "KP": "North Korea", + "KR": "South Korea", + "KW": "Kuwait", + "KY": "Cayman Islands", + "KZ": "Kazakhstan", + "LA": "Laos", + "LB": "Lebanon", + "LC": "Saint Lucia", + "LI": "Liechtenstein", + "LK": "Sri Lanka", + "LR": "Liberia", + "LS": "Lesotho", + "LT": "Lithuania", + "LU": "Luxembourg", + "LV": "Latvia", + "LY": "Libya", + "MA": "Morocco", + "MC": "Monaco", + "MD": "Moldova", + "ME": "Montenegro", + "MF": "Saint Martin (French part)", + "MG": "Madagascar", + "MH": "Marshall Islands", + "MK": "North Macedonia", + "ML": "Mali", + "MM": "Myanmar", + "MN": "Mongolia", + "MO": "Macao", + "MP": "Northern Mariana Islands", + "MQ": "Martinique", + "MR": "Mauritania", + "MS": "Montserrat", + "MT": "Malta", + "MU": "Mauritius", + "MV": "Maldives", + "MW": "Malawi", + "MX": "Mexico", + "MY": "Malaysia", + "MZ": "Mozambique", + "NA": "Namibia", + "NC": "New Caledonia", + "NE": "Niger", + "NF": "Norfolk Island", + "NG": "Nigeria", + "NI": "Nicaragua", + "NL": "Netherlands", + "NO": "Norway", + "NP": "Nepal", + "NR": "Nauru", + "NU": "Niue", + "NZ": "New Zealand", + "OM": "Oman", + "PA": "Panama", + "PE": "Peru", + "PF": "French Polynesia", + "PG": "Papua New Guinea", + "PH": "Philippines", + "PK": "Pakistan", + "PL": "Poland", + "PM": "Saint Pierre and Miquelon", + "PN": "Pitcairn", + "PR": "Puerto Rico", + "PS": "Palestine", + "PT": "Portugal", + "PW": "Palau", + "PY": "Paraguay", + "QA": "Qatar", + "RE": "Réunion", + "RO": "Romania", + "RS": "Serbia", + "RU": "Russia", + "RW": "Rwanda", + "SA": "Saudi Arabia", + "SB": "Solomon Islands", + "SC": "Seychelles", + "SD": "Sudan", + "SE": "Sweden", + "SG": "Singapore", + "SH": "Saint Helena, Ascension and Tristan da Cunha", + "SI": "Slovenia", + "SJ": "Svalbard and Jan Mayen", + "SK": "Slovakia", + "SL": "Sierra Leone", + "SM": "San Marino", + "SN": "Senegal", + "SO": "Somalia", + "SR": "Suriname", + "SS": "South Sudan", + "ST": "Sao Tome and Principe", + "SV": "El Salvador", + "SX": "Sint Maarten (Dutch part)", + "SY": "Syria", + "SZ": "Eswatini", + "TC": "Turks and Caicos Islands", + "TD": "Chad", + "TF": "TAAF", + "TG": "Togo", + "TH": "Thailand", + "TJ": "Tajikistan", + "TK": "Tokelau", + "TL": "Timor-Leste", + "TM": "Turkmenistan", + "TN": "Tunisia", + "TO": "Tonga", + "TR": "Turkey", + "TT": "Trinidad and Tobago", + "TV": "Tuvalu", + "TW": "Taiwan", + "TZ": "Tanzania", + "UA": "Ukraine", + "UG": "Uganda", + "UM": "United States Minor Outlying Islands", + "US": "United States", + "UY": "Uruguay", + "UZ": "Uzbekistan", + "VA": "Holy See", + "VC": "Saint Vincent and the Grenadines", + "VE": "Venezuela", + "VG": "British Virgin Islands", + "VI": "U.S. Virgin Islands", + "VN": "Viet Nam", + "VU": "Vanuatu", + "WF": "Wallis and Futuna", + "WS": "Samoa", + "YE": "Yemen", + "YT": "Mayotte", + "XK": "Kosovo", + "ZA": "South Africa", + "ZM": "Zambia", + "ZW": "Zimbabwe", + "EU": "European Union" + }, + "languages": { + "en-US": "English - US", + "fr": "French", + "nl": "Dutch", + "pt_BR": "Portuguese - Brazil" + } + }, + "KnownHosts": { + "label": "Host", + "add": "Add new host", + "toast": "Host successfully {mode, select, created {created} deleted {deleted} other {edited}}." + }, + "InitializeContainer": { + "title": "DID YOU KNOW", + "subtitle": "<1>Cockatrice is run by volunteers<1>that love card games!" + }, + "LoginContainer": { + "header": { + "title": "Login", + "subtitle": "A cross-platform virtual tabletop for multiplayer card games." + }, + "footer": { + "registerPrompt": "Not registered yet?", + "registerAction": "Create an account", + "credit": "Cockatrice is an open source project", + "version": "Version" + }, + "content": { + "subtitle1": "Play multiplayer card games online.", + "subtitle2": "Cross-platform virtual tabletop for multiplayer card games. Forever free." + }, + "toasts": { + "passwordResetSuccessToast": "Password Reset Successfully", + "accountActivationSuccess": "Account Activated Successfully" + } + }, + "UnsupportedContainer": { + "title": "Unsupported Browser", + "subtitle1": "Please update your browser and/or check your permissions.", + "subtitle2": "Note: Private browsing causes some browsers to disable certain permissions or features." + }, + "AccountActivationDialog": { + "title": "Account Activation", + "subtitle1": "Your account has not been activated yet.", + "subtitle2": "You need to provide the activation token received in the activation email." + }, + "KnownHostDialog": { + "title": "{mode, select, edit {Edit} other {Add}} Known Host", + "subtitle": "Adding a new host allows you to connect to different servers. Enter the details below to your host list." + }, + "RegistrationDialog": { + "title": "Create New Account" + }, + "RequestPasswordResetDialog": { + "title": "Request Password Reset" + }, + "ResetPasswordDialog": { + "title": "Reset Password" + }, + "AccountActivationForm": { + "error": { + "failed": "Account activation failed" + }, + "label": { + "activate": "Activate Account" + } + }, + "KnownHostForm": { + "help": "Need help adding a new host?", + "label": { + "add": "Add Host", + "find": "Find Host" + } + }, + "LoginForm": { + "label": { + "autoConnect": "Auto Connect", + "forgot": "Forgot Password", + "login": "Login", + "savePassword": "Save Password", + "savedPassword": "Saved Password" + } + }, + "RegisterForm": { + "label": { + "register": "Register" + }, + "toast": { + "registerSuccess": "Registration Successful!" + } + }, + "RequestPasswordResetForm": { + "error": "Request password reset failed", + "mfaEnabled": "Server has multi-factor authentication enabled", + "request": "Request Reset Token", + "skipRequest": "I already have a reset token" + }, + "ResetPasswordForm": { + "error": "Password reset failed", + "label": { + "reset": "Reset Password" + } + } +} \ No newline at end of file diff --git a/webclient/src/i18n.ts b/webclient/src/i18n.ts new file mode 100644 index 000000000..ef1885965 --- /dev/null +++ b/webclient/src/i18n.ts @@ -0,0 +1,32 @@ +import i18n from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import ICU from 'i18next-icu'; +import { initReactI18next } from 'react-i18next'; + +import { Language } from 'types'; + +import I18nBackend from './i18n-backend'; + +// Bundle default translation with application +import translation from './i18n-default.json'; + +i18n + .use(ICU) + .use(I18nBackend) + .use(LanguageDetector) + .use(initReactI18next) + // for all options read: https://www.i18next.com/overview/configuration-options + .init({ + fallbackLng: Language['en-US'], + resources: { + [Language['en-US']]: { translation }, + }, + partialBundledLanguages: true, + + interpolation: { + // not needed for react as it escapes by default + escapeValue: false, + } + }); + +export default i18n; diff --git a/webclient/src/images/Images.ts b/webclient/src/images/Images.ts new file mode 100644 index 000000000..9d08eb3ec --- /dev/null +++ b/webclient/src/images/Images.ts @@ -0,0 +1,9 @@ +import { Countries } from './countries/_Countries'; +import { Faces } from './faces/_Faces'; +import Logo from './logo.png'; + +export class Images { + static Countries = Countries; + static Faces = Faces; + static Logo = Logo; +} diff --git a/webclient/src/images/countries/.gitignore b/webclient/src/images/countries/.gitignore new file mode 100644 index 000000000..1337edaa7 --- /dev/null +++ b/webclient/src/images/countries/.gitignore @@ -0,0 +1,6 @@ +# Ignore all files +* +# Except gitignore +!.gitignore + +!_Countries.ts diff --git a/webclient/src/images/countries/_Countries.ts b/webclient/src/images/countries/_Countries.ts new file mode 100644 index 000000000..a4867db46 --- /dev/null +++ b/webclient/src/images/countries/_Countries.ts @@ -0,0 +1,507 @@ +// Remove !file-loader! once the following is no longer an issue +// https://github.com/facebook/create-react-app/issues/11770 +import ad from '!file-loader!./ad.svg'; +import ae from '!file-loader!./ae.svg'; +import af from '!file-loader!./af.svg'; +import ag from '!file-loader!./ag.svg'; +import ai from '!file-loader!./ai.svg'; +import al from '!file-loader!./al.svg'; +import am from '!file-loader!./am.svg'; +import ao from '!file-loader!./ao.svg'; +import aq from '!file-loader!./aq.svg'; +import ar from '!file-loader!./ar.svg'; +import as from '!file-loader!./as.svg'; +import at from '!file-loader!./at.svg'; +import au from '!file-loader!./au.svg'; +import aw from '!file-loader!./aw.svg'; +import ax from '!file-loader!./ax.svg'; +import az from '!file-loader!./az.svg'; +import ba from '!file-loader!./ba.svg'; +import bb from '!file-loader!./bb.svg'; +import bd from '!file-loader!./bd.svg'; +import be from '!file-loader!./be.svg'; +import bf from '!file-loader!./bf.svg'; +import bg from '!file-loader!./bg.svg'; +import bh from '!file-loader!./bh.svg'; +import bi from '!file-loader!./bi.svg'; +import bj from '!file-loader!./bj.svg'; +import bl from '!file-loader!./bl.svg'; +import bm from '!file-loader!./bm.svg'; +import bn from '!file-loader!./bn.svg'; +import bo from '!file-loader!./bo.svg'; +import bq from '!file-loader!./bq.svg'; +import br from '!file-loader!./br.svg'; +import bs from '!file-loader!./bs.svg'; +import bt from '!file-loader!./bt.svg'; +import bv from '!file-loader!./bv.svg'; +import bw from '!file-loader!./bw.svg'; +import by from '!file-loader!./by.svg'; +import bz from '!file-loader!./bz.svg'; +import ca from '!file-loader!./ca.svg'; +import cc from '!file-loader!./cc.svg'; +import cd from '!file-loader!./cd.svg'; +import cf from '!file-loader!./cf.svg'; +import cg from '!file-loader!./cg.svg'; +import ch from '!file-loader!./ch.svg'; +import ci from '!file-loader!./ci.svg'; +import ck from '!file-loader!./ck.svg'; +import cl from '!file-loader!./cl.svg'; +import cm from '!file-loader!./cm.svg'; +import cn from '!file-loader!./cn.svg'; +import co from '!file-loader!./co.svg'; +import cr from '!file-loader!./cr.svg'; +import cu from '!file-loader!./cu.svg'; +import cv from '!file-loader!./cv.svg'; +import cw from '!file-loader!./cw.svg'; +import cx from '!file-loader!./cx.svg'; +import cy from '!file-loader!./cy.svg'; +import cz from '!file-loader!./cz.svg'; +import de from '!file-loader!./de.svg'; +import dj from '!file-loader!./dj.svg'; +import dk from '!file-loader!./dk.svg'; +import dm from '!file-loader!./dm.svg'; +import _do from '!file-loader!./do.svg'; +import dz from '!file-loader!./dz.svg'; +import ec from '!file-loader!./ec.svg'; +import ee from '!file-loader!./ee.svg'; +import eg from '!file-loader!./eg.svg'; +import eh from '!file-loader!./eh.svg'; +import er from '!file-loader!./er.svg'; +import es from '!file-loader!./es.svg'; +import et from '!file-loader!./et.svg'; +import eu from '!file-loader!./eu.svg'; +import fi from '!file-loader!./fi.svg'; +import fj from '!file-loader!./fj.svg'; +import fk from '!file-loader!./fk.svg'; +import fm from '!file-loader!./fm.svg'; +import fo from '!file-loader!./fo.svg'; +import fr from '!file-loader!./fr.svg'; +import ga from '!file-loader!./ga.svg'; +import gb from '!file-loader!./gb.svg'; +import gd from '!file-loader!./gd.svg'; +import ge from '!file-loader!./ge.svg'; +import gf from '!file-loader!./gf.svg'; +import gg from '!file-loader!./gg.svg'; +import gh from '!file-loader!./gh.svg'; +import gi from '!file-loader!./gi.svg'; +import gl from '!file-loader!./gl.svg'; +import gm from '!file-loader!./gm.svg'; +import gn from '!file-loader!./gn.svg'; +import gp from '!file-loader!./gp.svg'; +import gq from '!file-loader!./gq.svg'; +import gr from '!file-loader!./gr.svg'; +import gs from '!file-loader!./gs.svg'; +import gt from '!file-loader!./gt.svg'; +import gu from '!file-loader!./gu.svg'; +import gw from '!file-loader!./gw.svg'; +import gy from '!file-loader!./gy.svg'; +import hk from '!file-loader!./hk.svg'; +import hm from '!file-loader!./hm.svg'; +import hn from '!file-loader!./hn.svg'; +import hr from '!file-loader!./hr.svg'; +import ht from '!file-loader!./ht.svg'; +import hu from '!file-loader!./hu.svg'; +import id from '!file-loader!./id.svg'; +import ie from '!file-loader!./ie.svg'; +import il from '!file-loader!./il.svg'; +import im from '!file-loader!./im.svg'; +import _in from '!file-loader!./in.svg'; +import io from '!file-loader!./io.svg'; +import iq from '!file-loader!./iq.svg'; +import ir from '!file-loader!./ir.svg'; +import is from '!file-loader!./is.svg'; +import it from '!file-loader!./it.svg'; +import je from '!file-loader!./je.svg'; +import jm from '!file-loader!./jm.svg'; +import jo from '!file-loader!./jo.svg'; +import jp from '!file-loader!./jp.svg'; +import ke from '!file-loader!./ke.svg'; +import kg from '!file-loader!./kg.svg'; +import kh from '!file-loader!./kh.svg'; +import ki from '!file-loader!./ki.svg'; +import km from '!file-loader!./km.svg'; +import kn from '!file-loader!./kn.svg'; +import kp from '!file-loader!./kp.svg'; +import kr from '!file-loader!./kr.svg'; +import kw from '!file-loader!./kw.svg'; +import ky from '!file-loader!./ky.svg'; +import kz from '!file-loader!./kz.svg'; +import la from '!file-loader!./la.svg'; +import lb from '!file-loader!./lb.svg'; +import lc from '!file-loader!./lc.svg'; +import li from '!file-loader!./li.svg'; +import lk from '!file-loader!./lk.svg'; +import lr from '!file-loader!./lr.svg'; +import ls from '!file-loader!./ls.svg'; +import lt from '!file-loader!./lt.svg'; +import lu from '!file-loader!./lu.svg'; +import lv from '!file-loader!./lv.svg'; +import ly from '!file-loader!./ly.svg'; +import ma from '!file-loader!./ma.svg'; +import mc from '!file-loader!./mc.svg'; +import md from '!file-loader!./md.svg'; +import me from '!file-loader!./me.svg'; +import mf from '!file-loader!./mf.svg'; +import mg from '!file-loader!./mg.svg'; +import mh from '!file-loader!./mh.svg'; +import mk from '!file-loader!./mk.svg'; +import ml from '!file-loader!./ml.svg'; +import mm from '!file-loader!./mm.svg'; +import mn from '!file-loader!./mn.svg'; +import mo from '!file-loader!./mo.svg'; +import mp from '!file-loader!./mp.svg'; +import mq from '!file-loader!./mq.svg'; +import mr from '!file-loader!./mr.svg'; +import ms from '!file-loader!./ms.svg'; +import mt from '!file-loader!./mt.svg'; +import mu from '!file-loader!./mu.svg'; +import mv from '!file-loader!./mv.svg'; +import mw from '!file-loader!./mw.svg'; +import mx from '!file-loader!./mx.svg'; +import my from '!file-loader!./my.svg'; +import mz from '!file-loader!./mz.svg'; +import na from '!file-loader!./na.svg'; +import nc from '!file-loader!./nc.svg'; +import ne from '!file-loader!./ne.svg'; +import nf from '!file-loader!./nf.svg'; +import ng from '!file-loader!./ng.svg'; +import ni from '!file-loader!./ni.svg'; +import nl from '!file-loader!./nl.svg'; +import no from '!file-loader!./no.svg'; +import np from '!file-loader!./np.svg'; +import nr from '!file-loader!./nr.svg'; +import nu from '!file-loader!./nu.svg'; +import nz from '!file-loader!./nz.svg'; +import om from '!file-loader!./om.svg'; +import pa from '!file-loader!./pa.svg'; +import pe from '!file-loader!./pe.svg'; +import pf from '!file-loader!./pf.svg'; +import pg from '!file-loader!./pg.svg'; +import ph from '!file-loader!./ph.svg'; +import pk from '!file-loader!./pk.svg'; +import pl from '!file-loader!./pl.svg'; +import pm from '!file-loader!./pm.svg'; +import pn from '!file-loader!./pn.svg'; +import pr from '!file-loader!./pr.svg'; +import ps from '!file-loader!./ps.svg'; +import pt from '!file-loader!./pt.svg'; +import pw from '!file-loader!./pw.svg'; +import py from '!file-loader!./py.svg'; +import qa from '!file-loader!./qa.svg'; +import re from '!file-loader!./re.svg'; +import ro from '!file-loader!./ro.svg'; +import rs from '!file-loader!./rs.svg'; +import ru from '!file-loader!./ru.svg'; +import rw from '!file-loader!./rw.svg'; +import sa from '!file-loader!./sa.svg'; +import sb from '!file-loader!./sb.svg'; +import sc from '!file-loader!./sc.svg'; +import sd from '!file-loader!./sd.svg'; +import se from '!file-loader!./se.svg'; +import sg from '!file-loader!./sg.svg'; +import sh from '!file-loader!./sh.svg'; +import si from '!file-loader!./si.svg'; +import sj from '!file-loader!./sj.svg'; +import sk from '!file-loader!./sk.svg'; +import sl from '!file-loader!./sl.svg'; +import sm from '!file-loader!./sm.svg'; +import sn from '!file-loader!./sn.svg'; +import so from '!file-loader!./so.svg'; +import sr from '!file-loader!./sr.svg'; +import ss from '!file-loader!./ss.svg'; +import st from '!file-loader!./st.svg'; +import sv from '!file-loader!./sv.svg'; +import sx from '!file-loader!./sx.svg'; +import sy from '!file-loader!./sy.svg'; +import sz from '!file-loader!./sz.svg'; +import tc from '!file-loader!./tc.svg'; +import td from '!file-loader!./td.svg'; +import tf from '!file-loader!./tf.svg'; +import tg from '!file-loader!./tg.svg'; +import th from '!file-loader!./th.svg'; +import tj from '!file-loader!./tj.svg'; +import tk from '!file-loader!./tk.svg'; +import tl from '!file-loader!./tl.svg'; +import tm from '!file-loader!./tm.svg'; +import tn from '!file-loader!./tn.svg'; +import to from '!file-loader!./to.svg'; +import tr from '!file-loader!./tr.svg'; +import tt from '!file-loader!./tt.svg'; +import tv from '!file-loader!./tv.svg'; +import tw from '!file-loader!./tw.svg'; +import tz from '!file-loader!./tz.svg'; +import ua from '!file-loader!./ua.svg'; +import ug from '!file-loader!./ug.svg'; +import um from '!file-loader!./um.svg'; +import us from '!file-loader!./us.svg'; +import uy from '!file-loader!./uy.svg'; +import uz from '!file-loader!./uz.svg'; +import va from '!file-loader!./va.svg'; +import vc from '!file-loader!./vc.svg'; +import ve from '!file-loader!./ve.svg'; +import vg from '!file-loader!./vg.svg'; +import vi from '!file-loader!./vi.svg'; +import vn from '!file-loader!./vn.svg'; +import vu from '!file-loader!./vu.svg'; +import wf from '!file-loader!./wf.svg'; +import ws from '!file-loader!./ws.svg'; +import xk from '!file-loader!./xk.svg'; +import ye from '!file-loader!./ye.svg'; +import yt from '!file-loader!./yt.svg'; +import za from '!file-loader!./za.svg'; +import zm from '!file-loader!./zm.svg'; +import zw from '!file-loader!./zw.svg'; + +export const Countries = { + ad, + ae, + af, + ag, + ai, + al, + am, + ao, + aq, + ar, + as, + at, + au, + aw, + ax, + az, + ba, + bb, + bd, + be, + bf, + bg, + bh, + bi, + bj, + bl, + bm, + bn, + bo, + bq, + br, + bs, + bt, + bv, + bw, + by, + bz, + ca, + cc, + cd, + cf, + cg, + ch, + ci, + ck, + cl, + cm, + cn, + co, + cr, + cu, + cv, + cw, + cx, + cy, + cz, + de, + dj, + dk, + dm, + do: _do, + dz, + ec, + ee, + eg, + eh, + er, + es, + et, + eu, + fi, + fj, + fk, + fm, + fo, + fr, + ga, + gb, + gd, + ge, + gf, + gg, + gh, + gi, + gl, + gm, + gn, + gp, + gq, + gr, + gs, + gt, + gu, + gw, + gy, + hk, + hm, + hn, + hr, + ht, + hu, + id, + ie, + il, + im, + in: _in, + io, + iq, + ir, + is, + it, + je, + jm, + jo, + jp, + ke, + kg, + kh, + ki, + km, + kn, + kp, + kr, + kw, + ky, + kz, + la, + lb, + lc, + li, + lk, + lr, + ls, + lt, + lu, + lv, + ly, + ma, + mc, + md, + me, + mf, + mg, + mh, + mk, + ml, + mm, + mn, + mo, + mp, + mq, + mr, + ms, + mt, + mu, + mv, + mw, + mx, + my, + mz, + na, + nc, + ne, + nf, + ng, + ni, + nl, + no, + np, + nr, + nu, + nz, + om, + pa, + pe, + pf, + pg, + ph, + pk, + pl, + pm, + pn, + pr, + ps, + pt, + pw, + py, + qa, + re, + ro, + rs, + ru, + rw, + sa, + sb, + sc, + sd, + se, + sg, + sh, + si, + sj, + sk, + sl, + sm, + sn, + so, + sr, + ss, + st, + sv, + sx, + sy, + sz, + tc, + td, + tf, + tg, + th, + tj, + tk, + tl, + tm, + tn, + to, + tr, + tt, + tv, + tw, + tz, + ua, + ug, + um, + us, + uy, + uz, + va, + vc, + ve, + vg, + vi, + vn, + vu, + wf, + ws, + xk, + ye, + yt, + za, + zm, + zw, +}; diff --git a/webclient/src/images/faces/_Faces.ts b/webclient/src/images/faces/_Faces.ts new file mode 100644 index 000000000..73a8c361b --- /dev/null +++ b/webclient/src/images/faces/_Faces.ts @@ -0,0 +1,9 @@ +import face1 from './face1.jpg'; +import face2 from './face2.jpg'; +import face3 from './face3.jpg'; + +export const Faces = { + face1, + face2, + face3, +} diff --git a/webclient/src/images/faces/face1.jpg b/webclient/src/images/faces/face1.jpg new file mode 100644 index 000000000..931c5822d Binary files /dev/null and b/webclient/src/images/faces/face1.jpg differ diff --git a/webclient/src/images/faces/face2.jpg b/webclient/src/images/faces/face2.jpg new file mode 100644 index 000000000..8b5bea967 Binary files /dev/null and b/webclient/src/images/faces/face2.jpg differ diff --git a/webclient/src/images/faces/face3.jpg b/webclient/src/images/faces/face3.jpg new file mode 100644 index 000000000..ace651dc0 Binary files /dev/null and b/webclient/src/images/faces/face3.jpg differ diff --git a/webclient/src/images/index.ts b/webclient/src/images/index.ts new file mode 100644 index 000000000..e96072916 --- /dev/null +++ b/webclient/src/images/index.ts @@ -0,0 +1 @@ +export * from './Images'; diff --git a/webclient/src/images/logo.png b/webclient/src/images/logo.png new file mode 100644 index 000000000..7ce83bd20 Binary files /dev/null and b/webclient/src/images/logo.png differ diff --git a/webclient/src/index.css b/webclient/src/index.css new file mode 100644 index 000000000..c5f9b9230 --- /dev/null +++ b/webclient/src/index.css @@ -0,0 +1,70 @@ +@import url('https://fonts.googleapis.com/css2?family=Teko&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&display=swap'); + +:root { + +} + +* { + box-sizing: border-box; +} + +html, +body, +#root { + height: 100%; +} + +body { + margin: 0; + font-family: + -apple-system, + BlinkMacSystemFont, + "Open Sans", + "Segoe UI", + "Roboto", + "Oxygen", + "Ubuntu", + "Cantarell", + "Fira Sans", + "Droid Sans", + "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} + +strong, +b { + font-weight: bold; +} + +a { + color: inherit; + text-decoration: none; +} + +.overflow-scroll { + overflow-y: scroll; /* has to be scroll, not auto */ + -webkit-overflow-scrolling: touch; +} + +.plain-link { + color: inherit; + text-decoration: none; +} + +.single-line-ellipsis { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.disabled-link { + pointer-events: none; +} diff --git a/webclient/src/index.tsx b/webclient/src/index.tsx new file mode 100644 index 000000000..97b5bb014 --- /dev/null +++ b/webclient/src/index.tsx @@ -0,0 +1,25 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { Theme, StyledEngineProvider } from '@mui/material'; +import { ThemeProvider } from '@mui/material/styles'; + +import { AppShell } from './containers'; +import { materialTheme } from './material-theme'; + +import './i18n'; +import './index.css'; + +const AppWithMaterialTheme = () => ( + + + + + + + +); + +const container = document.getElementById('root'); +const root = createRoot(container!); + +root.render(); diff --git a/webclient/src/material-theme.ts b/webclient/src/material-theme.ts new file mode 100644 index 000000000..beee1146e --- /dev/null +++ b/webclient/src/material-theme.ts @@ -0,0 +1,239 @@ +import { PaletteMode } from '@mui/material'; +import { createTheme } from '@mui/material/styles'; + +const mode: PaletteMode = 'light'; + +const palette = { + mode, + + background: { + default: 'rgb(35, 35, 35)', + paper: '#FFFFFF', + }, + primary: { + main: '#7033DB', + light: 'rgba(112, 51, 219, .3)', + dark: '#401C7F', + contrastText: '#FFFFFF', + }, + grey: { + 50: '#fafafa', + 100: '#f5f5f5', + 200: '#eeeeee', + 300: '#e0e0e0', + 400: '#bdbdbd', + 500: '#9e9e9e', + 600: '#757575', + 700: '#616161', + 800: '#424242', + 900: '#212121', + A100: '#d5d5d5', + A200: '#aaaaaa', + A400: '#303030', + A700: '#616161', + }, + // secondary: { + // main: '', + // light: '', + // dark: '', + // contrastText: '', + // }, + // error: { + // main: '', + // light: '', + // dark: '', + // contrastText: '', + // }, + // warning: { + // main: '', + // light: '', + // dark: '', + // contrastText: '', + // }, + // info: { + // main: '', + // light: '', + // dark: '', + // contrastText: '', + // }, + success: { + main: '#6CDF39', + light: '#6CDF39', + // dark: '', + // contrastText: '', + }, +}; + +export const materialTheme = createTheme({ + palette, + + components: { + MuiCssBaseline: { + styleOverrides: { + '@global': { + '@font-face': [], + }, + }, + }, + MuiButton: { + styleOverrides: { + root: { + fontWeight: 'bold', + textTransform: 'none', + + '&.rounded': { + // 'border-radius': '50px', + }, + + '&.tall': { + 'height': '40px', + }, + }, + }, + }, + + MuiCheckbox: { + styleOverrides: { + root: { + '& .MuiSvgIcon-root': { + width: '.75em', + height: '.75em', + }, + }, + }, + }, + + MuiFormControlLabel: { + styleOverrides: { + label: { + fontSize: 12, + fontWeight: 'bold', + color: palette.primary.main, + }, + }, + }, + + MuiLink: { + styleOverrides: { + root: { + fontWeight: 'bold', + }, + }, + }, + + MuiList: { + styleOverrides: { + root: { + padding: '8px', + + '&.MuiList-padding': { + paddingBottom: '4px', + }, + + '& .MuiButton-root': { + width: '100%', + }, + + '& > .MuiButtonBase-root': { + padding: '8px 16px', + marginBottom: '4px', + borderRadius: 0, + justifyContent: 'space-between', + }, + + '& .MuiButtonBase-root.Mui-selected, & .MuiButtonBase-root.Mui-selected:focus': { + background: 'none', + fontWeight: 'bold', + }, + + ['& .MuiButtonBase-root:hover, & .MuiButtonBase-root.Mui-selected:hover']: { + background: palette.primary.light + }, + }, + }, + }, + + MuiListItem: { + styleOverrides: { + root: { + }, + } + }, + + MuiInputBase: { + styleOverrides: { + formControl: { + '& .MuiSelect-root svg': { + display: 'none', + }, + }, + }, + }, + + MuiOutlinedInput: { + styleOverrides: { + root: { + '&.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderWidth: '1px', + }, + + '.rounded &': { + // 'border-radius': '50px', + }, + + '.tall &': { + height: '40px', + }, + }, + }, + }, + }, + + typography: { + fontSize: 12, + fontFamily: 'Open Sans, sans-serif', + + h1: { + fontSize: 28, + fontWeight: 'bold', + }, + h2: { + fontSize: 24, + fontWeight: 'bold', + }, + // h3: {}, + // h4: {}, + // h5: {}, + // h6: {}, + subtitle1: { + fontSize: 14, + fontWeight: 'bold', + lineHeight: 1.4, + color: palette.grey[500], + }, + subtitle2: { + lineHeight: 1.4, + color: palette.grey[500], + }, + body1: { + fontSize: '.75rem', + lineHeight: 1.4, + }, + // body2: {}, + // button: {}, + // caption: {}, + // overline: {}, + }, + + spacing: 8, + + breakpoints: { + values: { + xs: 0, + sm: 640, + md: 768, + lg: 1024, + xl: 1280, + }, + }, +}); diff --git a/webclient/src/react-app-env.d.ts b/webclient/src/react-app-env.d.ts new file mode 100644 index 000000000..6431bc5fc --- /dev/null +++ b/webclient/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/webclient/src/services/CardImporterService.ts b/webclient/src/services/CardImporterService.ts new file mode 100644 index 000000000..691e86f0e --- /dev/null +++ b/webclient/src/services/CardImporterService.ts @@ -0,0 +1,104 @@ +// Fetch and parse card sets + +class CardImporterService { + importCards(url): Promise { + const error = 'Card import must be in valid MTG JSON format'; + + return fetch(url) + .then(response => { + if (response.headers.get('Content-Type') !== 'application/json') { + throw new Error(error); + } + + return response.json(); + }) + .then((json) => { + try { + const sortedSets = Object.keys(json.data) + .map(key => json.data[key]) + .sort((a, b) => new Date(a.releaseDate).getTime() - new Date(b.releaseDate).getTime()); + + const sets = sortedSets.map(({ cards, tokens, ...set }) => ({ + ...set, + cards: cards.map(({ name }) => name), + tokens: tokens.map(({ name }) => name), + })); + + const unsortedCards = sortedSets.reduce((acc, set) => { + set.cards.forEach(card => acc[card.name] = card); + return acc; + }, {}); + + const cards = Object.keys(unsortedCards) + .sort((a, b) => a.localeCompare(b)) + .map(key => unsortedCards[key]); + + return { cards, sets }; + } catch (e) { + throw new Error(error); + } + }); + } + + importTokens(url): Promise { + const error = 'Token import must be in valid MTG XML format'; + + return fetch(url) + .then(response => { + if (!response.ok) { + throw new Error('Failed to fetch'); + } + + return response.text() + }) + .then((xmlString) => { + try { + const parser = new DOMParser(); + const dom = parser.parseFromString(xmlString, 'application/xml'); + + const tokens = Array.from(dom.querySelectorAll('card')).map( + (tokenElement) => this.parseXmlAttributes(tokenElement) + ); + + return tokens; + } catch (e) { + throw new Error(error); + } + }) + } + + private parseXmlAttributes(dom: Element) { + return Array.from(dom.children).reduce((attributes, child) => { + const value = child.children.length ? this.parseXmlAttributes(child) : child.innerHTML; + + let parsedAttributes = { value }; + + if (child.attributes.length) { + const childAttributes = Array.from(child.attributes).reduce((acc, { name, value }) => { + acc[name] = value; + return acc; + }, {}); + + parsedAttributes = { + ...parsedAttributes, + ...childAttributes, + }; + } + + // @TODO: clean this up and normalize what i'm returning + if (attributes[child.tagName]) { + if (Array.isArray(attributes[child.tagName])) { + attributes[child.tagName].push(parsedAttributes) + } else { + attributes[child.tagName] = [parsedAttributes]; + } + } else { + attributes[child.tagName] = parsedAttributes; + } + + return attributes; + }, {}); + } +} + +export const cardImporterService = new CardImporterService(); diff --git a/webclient/src/services/ServerProps.ts b/webclient/src/services/ServerProps.ts new file mode 100644 index 000000000..0d8c3d652 --- /dev/null +++ b/webclient/src/services/ServerProps.ts @@ -0,0 +1,9 @@ +import props from '../server-props.json'; + +class ServerProps { + get REACT_APP_VERSION() { + return props?.REACT_APP_VERSION; + } +} + +export const serverProps = new ServerProps(); diff --git a/webclient/src/services/dexie/DexieDTOs/CardDTO.ts b/webclient/src/services/dexie/DexieDTOs/CardDTO.ts new file mode 100644 index 000000000..a060aedb1 --- /dev/null +++ b/webclient/src/services/dexie/DexieDTOs/CardDTO.ts @@ -0,0 +1,19 @@ +import { Card } from 'types'; + +import { dexieService } from '../DexieService'; + +export class CardDTO extends Card { + save() { + return dexieService.cards.put(this); + } + + static get(name) { + return dexieService.cards.where('name').equalsIgnoreCase(name).first(); + } + + static bulkAdd(cards: CardDTO[]): Promise { + return dexieService.cards.bulkPut(cards); + } +}; + +dexieService.cards.mapToClass(CardDTO); diff --git a/webclient/src/services/dexie/DexieDTOs/HostDTO.ts b/webclient/src/services/dexie/DexieDTOs/HostDTO.ts new file mode 100644 index 000000000..17f600626 --- /dev/null +++ b/webclient/src/services/dexie/DexieDTOs/HostDTO.ts @@ -0,0 +1,32 @@ +import { IndexableType } from 'dexie'; +import { Host } from 'types'; + +import { dexieService } from '../DexieService'; + +export class HostDTO extends Host { + save() { + return dexieService.hosts.put(this); + } + + static add(host: Host): Promise { + return dexieService.hosts.add(host); + } + + static get(id: number): Promise { + return dexieService.hosts.where('id').equals(id).first(); + } + + static getAll(): Promise { + return dexieService.hosts.toArray(); + } + + static bulkAdd(hosts: Host[]): Promise { + return dexieService.hosts.bulkAdd(hosts); + } + + static delete(id: string): Promise { + return dexieService.hosts.delete(id); + } +}; + +dexieService.hosts.mapToClass(HostDTO); diff --git a/webclient/src/services/dexie/DexieDTOs/SetDTO.ts b/webclient/src/services/dexie/DexieDTOs/SetDTO.ts new file mode 100644 index 000000000..6ae2dd20a --- /dev/null +++ b/webclient/src/services/dexie/DexieDTOs/SetDTO.ts @@ -0,0 +1,19 @@ +import { Set } from 'types'; + +import { dexieService } from '../DexieService'; + +export class SetDTO extends Set { + save() { + return dexieService.sets.put(this); + } + + static get(name) { + return dexieService.sets.where('name').equalsIgnoreCase(name).first(); + } + + static bulkAdd(sets: SetDTO[]): Promise { + return dexieService.sets.bulkPut(sets); + } +}; + +dexieService.sets.mapToClass(SetDTO); diff --git a/webclient/src/services/dexie/DexieDTOs/SettingDTO.ts b/webclient/src/services/dexie/DexieDTOs/SettingDTO.ts new file mode 100644 index 000000000..bbfa1680d --- /dev/null +++ b/webclient/src/services/dexie/DexieDTOs/SettingDTO.ts @@ -0,0 +1,22 @@ +import { Setting } from 'types'; + +import { dexieService } from '../DexieService'; + +export class SettingDTO extends Setting { + constructor(user) { + super(); + + this.user = user; + this.autoConnect = false; + } + + save() { + return dexieService.settings.put(this); + } + + static get(user) { + return dexieService.settings.where('user').equalsIgnoreCase(user).first(); + } +}; + +dexieService.settings.mapToClass(SettingDTO); diff --git a/webclient/src/services/dexie/DexieDTOs/TokenDTO.ts b/webclient/src/services/dexie/DexieDTOs/TokenDTO.ts new file mode 100644 index 000000000..35d537709 --- /dev/null +++ b/webclient/src/services/dexie/DexieDTOs/TokenDTO.ts @@ -0,0 +1,19 @@ +import { Token } from 'types'; + +import { dexieService } from '../DexieService'; + +export class TokenDTO extends Token { + save() { + return dexieService.tokens.put(this); + } + + static get(name) { + return dexieService.tokens.where('name.value').equalsIgnoreCase(name).first(); + } + + static bulkAdd(tokens: TokenDTO[]): Promise { + return dexieService.tokens.bulkPut(tokens); + } +}; + +dexieService.tokens.mapToClass(TokenDTO); diff --git a/webclient/src/services/dexie/DexieDTOs/index.ts b/webclient/src/services/dexie/DexieDTOs/index.ts new file mode 100644 index 000000000..9121f1f4b --- /dev/null +++ b/webclient/src/services/dexie/DexieDTOs/index.ts @@ -0,0 +1,5 @@ +export * from './CardDTO'; +export * from './SetDTO'; +export * from './SettingDTO'; +export * from './TokenDTO'; +export * from './HostDTO'; diff --git a/webclient/src/services/dexie/DexieSchemas/v1.schema.ts b/webclient/src/services/dexie/DexieSchemas/v1.schema.ts new file mode 100644 index 000000000..18e7f35c4 --- /dev/null +++ b/webclient/src/services/dexie/DexieSchemas/v1.schema.ts @@ -0,0 +1,17 @@ +export enum Stores { + SETTINGS = 'settings', + CARDS = 'cards', + SETS = 'sets', + TOKENS = 'tokens', + HOSTS = 'hosts', +} + +export const schemaV1 = (db) => { + db.version(1).stores({ + [Stores.CARDS]: 'name', + [Stores.SETS]: 'code', + [Stores.SETTINGS]: 'user', + [Stores.TOKENS]: 'name.value', + [Stores.HOSTS]: '++id', + }); +} diff --git a/webclient/src/services/dexie/DexieService.ts b/webclient/src/services/dexie/DexieService.ts new file mode 100644 index 000000000..9dcd3b941 --- /dev/null +++ b/webclient/src/services/dexie/DexieService.ts @@ -0,0 +1,37 @@ +import Dexie from 'dexie'; + +import { Stores, schemaV1 } from './DexieSchemas/v1.schema'; + +class DexieService { + private db: Dexie = new Dexie('Webatrice'); + + constructor() { + schemaV1(this.db); + } + + get settings() { + return this.db.table(Stores.SETTINGS); + } + + get cards() { + return this.db.table(Stores.CARDS); + } + + get sets() { + return this.db.table(Stores.SETS); + } + + get tokens() { + return this.db.table(Stores.TOKENS); + } + + get hosts() { + return this.db.table(Stores.HOSTS); + } + + testConnection() { + return this.db.open(); + } +} + +export const dexieService = new DexieService(); diff --git a/webclient/src/services/dexie/index.ts b/webclient/src/services/dexie/index.ts new file mode 100644 index 000000000..593f19397 --- /dev/null +++ b/webclient/src/services/dexie/index.ts @@ -0,0 +1,2 @@ +export * from './DexieDTOs'; +export * from './DexieService'; diff --git a/webclient/src/services/index.ts b/webclient/src/services/index.ts new file mode 100644 index 000000000..ebebeb65e --- /dev/null +++ b/webclient/src/services/index.ts @@ -0,0 +1,3 @@ +export * from './CardImporterService'; +export * from './ServerProps'; +export * from './dexie'; diff --git a/webclient/src/setupTests.ts b/webclient/src/setupTests.ts new file mode 100644 index 000000000..c4e40e2ec --- /dev/null +++ b/webclient/src/setupTests.ts @@ -0,0 +1,10 @@ +import protobuf from 'protobufjs'; + +// ensure jest-dom is always available during testing to cut down on boilerplate +import '@testing-library/jest-dom'; + +class MockProtobufRoot { + load() {} +} + +(protobuf as any).Root = MockProtobufRoot; diff --git a/webclient/src/store/actions/actionReducer.ts b/webclient/src/store/actions/actionReducer.ts new file mode 100644 index 000000000..a01dba984 --- /dev/null +++ b/webclient/src/store/actions/actionReducer.ts @@ -0,0 +1,43 @@ +/** + * @author Luke Brandon Farrell + * @description Application reducer. + */ + +import { AnyAction } from 'redux' + + interface InitialState { + type: string | null + payload: any + meta: any + error: boolean + count: number + } + +/** + * Initial data. + */ +const initialState: InitialState = { + type: null, + payload: null, + meta: null, + error: false, + count: 0, +} + +/** + * Calculates the application state. + * + * @param state + * @param action + * @return {*} + */ +export const actionReducer = ( + state = initialState, + action: AnyAction, +): InitialState => { + return { + ...state, + ...action, + count: state.count + 1, + } +} diff --git a/webclient/src/store/actions/index.ts b/webclient/src/store/actions/index.ts new file mode 100644 index 000000000..4c5f9ef48 --- /dev/null +++ b/webclient/src/store/actions/index.ts @@ -0,0 +1 @@ +export { actionReducer } from './actionReducer'; diff --git a/webclient/src/store/common/SortUtil.ts b/webclient/src/store/common/SortUtil.ts new file mode 100644 index 000000000..56910259d --- /dev/null +++ b/webclient/src/store/common/SortUtil.ts @@ -0,0 +1,153 @@ +import { SortBy, SortDirection, User } from 'types'; + +export default class SortUtil { + static sortByField(arr: any[], sortBy: SortBy): void { + if (arr.length) { + const field = SortUtil.resolveFieldChain(arr[0], sortBy.field); + const fieldType = typeof field; + + if (fieldType === 'string') { + SortUtil.sortByString(arr, sortBy); + return; + } + + if (fieldType === 'number') { + SortUtil.sortByNumber(arr, sortBy); + return; + } + + throw new Error('SortField must resolve to either a string or number'); + } + } + + static sortByFields(arr: any[], sorts: SortBy[]) { + if (arr.length) { + arr.sort((a, b) => { + for (let i = 0; i < sorts.length; i++) { + const sortBy = sorts[i]; + const field = SortUtil.resolveFieldChain(arr[0], sortBy.field); + + const fieldType = typeof field; + + if (fieldType === 'string') { + const result = SortUtil.stringComparator(a, b, sortBy); + + if (result) { + return result; + } + } + + if (fieldType === 'number') { + const result = SortUtil.numberComparator(a, b, sortBy); + + if (result) { + return result; + } + } + + throw new Error('SortField must resolve to either a string or number'); + } + + return 0; + }) + } + } + + static sortUsersByField(users: User[], sortBy: SortBy) { + if (users.length) { + users.sort((a, b) => SortUtil.userComparator(a, b, sortBy)) + } + } + + static toggleSortBy(field: string, sortBy: SortBy) { + const sameField = field === sortBy.field; + const isASC = sortBy.order === SortDirection.ASC; + + return { + field, + order: sameField && isASC ? SortDirection.DESC : SortDirection.ASC + } + } + + private static sortByNumber(arr: any[], sortBy: SortBy): void { + arr.sort((a, b) => SortUtil.numberComparator(a, b, sortBy)); + } + + private static sortByString(arr: any[], sortBy: SortBy): void { + arr.sort((a, b) => SortUtil.stringComparator(a, b, sortBy)); + } + + private static userComparator(a, b, sortBy, sortByUserLevel = true) { + if (sortByUserLevel) { + const adminSortBy = { + field: 'userLevel', + order: SortDirection.DESC + }; + + const adminSorted = SortUtil.numberComparator(a, b, adminSortBy); + + if (adminSorted) { + return adminSorted; + } + } + + const sorted = SortUtil.stringComparator(a, b, sortBy); + + if (sorted) { + return sorted; + } + + return 0; + } + + private static numberComparator(a, b, { field, order }: SortBy) { + const aResolved = SortUtil.resolveFieldChain(a, field); + const bResolved = SortUtil.resolveFieldChain(b, field); + + if (order === SortDirection.ASC) { + return aResolved - bResolved; + } else { + return bResolved - aResolved; + } + } + + private static stringComparator(a, b, { field, order }: SortBy) { + const aResolved = SortUtil.resolveFieldChain(a, field); + const bResolved = SortUtil.resolveFieldChain(b, field); + + // Force empty strings to sort to bottom + if (!aResolved && !bResolved) { + return 0; + } + if (!aResolved) { + return 1; + } + if (!bResolved) { + return -1; + } + + if (order === SortDirection.ASC) { + return aResolved.localeCompare(bResolved); + } else { + return bResolved.localeCompare(aResolved); + } + } + + private static resolveFieldChain(obj: object, field: string) { + const links = field.split('.'); + + if (links.length > 1) { + return links.reduce((obj, link) => { + const parsed = parseInt(link, 10); + + if (parsed.toLocaleString() === 'NaN') { + return obj[link]; + } else { + return obj[parsed]; + } + }, obj) || null; + } else { + return obj[field]; + } + } +} diff --git a/webclient/src/store/common/index.ts b/webclient/src/store/common/index.ts new file mode 100644 index 000000000..340676b6d --- /dev/null +++ b/webclient/src/store/common/index.ts @@ -0,0 +1 @@ +export { default as SortUtil } from './SortUtil'; diff --git a/webclient/src/store/index.ts b/webclient/src/store/index.ts new file mode 100644 index 000000000..b43f290c5 --- /dev/null +++ b/webclient/src/store/index.ts @@ -0,0 +1,22 @@ +export { store } from './store'; + +// Common +export { SortUtil } from './common'; + +// Server + +export { + Types as ServerTypes, + Selectors as ServerSelectors, + Dispatch as ServerDispatch } from './server'; + +export * from 'store/server/server.interfaces'; + +export { + Types as RoomsTypes, + Selectors as RoomsSelectors, + Dispatch as RoomsDispatch } from 'store/rooms'; + +export * from 'store/rooms/rooms.interfaces'; + + diff --git a/webclient/src/store/rooms/index.ts b/webclient/src/store/rooms/index.ts new file mode 100644 index 000000000..3ebe3905a --- /dev/null +++ b/webclient/src/store/rooms/index.ts @@ -0,0 +1,5 @@ +export * from './rooms.actions'; +export * from './rooms.dispatch'; +export * from './rooms.reducer'; +export * from './rooms.selectors'; +export * from './rooms.types'; diff --git a/webclient/src/store/rooms/rooms.actions.tsx b/webclient/src/store/rooms/rooms.actions.tsx new file mode 100644 index 000000000..98108fe8b --- /dev/null +++ b/webclient/src/store/rooms/rooms.actions.tsx @@ -0,0 +1,71 @@ +import { Types } from './rooms.types'; + +export const Actions = { + clearStore: () => ({ + type: Types.CLEAR_STORE + }), + + updateRooms: rooms => ({ + type: Types.UPDATE_ROOMS, + rooms + }), + + joinRoom: roomInfo => ({ + type: Types.JOIN_ROOM, + roomInfo + }), + + leaveRoom: roomId => ({ + type: Types.LEAVE_ROOM, + roomId + }), + + addMessage: (roomId, message) => ({ + type: Types.ADD_MESSAGE, + roomId, + message + }), + + updateGames: (roomId, games) => ({ + type: Types.UPDATE_GAMES, + roomId, + games + }), + + userJoined: (roomId, user) => ({ + type: Types.USER_JOINED, + roomId, + user + }), + + userLeft: (roomId, name) => ({ + type: Types.USER_LEFT, + roomId, + name + }), + + sortGames: (roomId, field, order) => ({ + type: Types.SORT_GAMES, + roomId, + field, + order + }), + + removeMessages: (roomId, name, amount) => ({ + type: Types.REMOVE_MESSAGES, + roomId, + name, + amount + }), + + gameCreated: (roomId) => ({ + type: Types.GAME_CREATED, + roomId + }), + + joinedGame: (roomId, gameId) => ({ + type: Types.JOINED_GAME, + roomId, + gameId + }), +} diff --git a/webclient/src/store/rooms/rooms.dispatch.tsx b/webclient/src/store/rooms/rooms.dispatch.tsx new file mode 100644 index 000000000..c89b5ebc4 --- /dev/null +++ b/webclient/src/store/rooms/rooms.dispatch.tsx @@ -0,0 +1,58 @@ +import { reset } from 'redux-form'; +import { Actions } from './rooms.actions'; +import { store } from 'store'; + +export const Dispatch = { + clearStore: () => { + store.dispatch(Actions.clearStore()); + }, + + updateRooms: rooms => { + store.dispatch(Actions.updateRooms(rooms)); + }, + + joinRoom: roomInfo => { + store.dispatch(Actions.joinRoom(roomInfo)); + + }, + + leaveRoom: roomId => { + store.dispatch(Actions.leaveRoom(roomId)); + }, + + addMessage: (roomId, message) => { + if (message.name) { + store.dispatch(reset('sayMessage')); + } + + store.dispatch(Actions.addMessage(roomId, message)); + }, + + updateGames: (roomId, games) => { + store.dispatch(Actions.updateGames(roomId, games)); + }, + + userJoined: (roomId, user) => { + store.dispatch(Actions.userJoined(roomId, user)); + }, + + userLeft: (roomId, name) => { + store.dispatch(Actions.userLeft(roomId, name)); + }, + + sortGames: (roomId, field, order) => { + store.dispatch(Actions.sortGames(roomId, field, order)); + }, + + removeMessages: (roomId, name, amount) => { + store.dispatch(Actions.removeMessages(roomId, name, amount)); + }, + + gameCreated: (roomId) => { + store.dispatch(Actions.gameCreated(roomId)); + }, + + joinedGame: (roomId, gameId) => { + store.dispatch(Actions.joinedGame(roomId, gameId)); + } +} diff --git a/webclient/src/store/rooms/rooms.interfaces.tsx b/webclient/src/store/rooms/rooms.interfaces.tsx new file mode 100644 index 000000000..c7b90ac84 --- /dev/null +++ b/webclient/src/store/rooms/rooms.interfaces.tsx @@ -0,0 +1,50 @@ +import { GameSortField, Room, Game, SortBy, UserSortField } from 'types'; + +export interface RoomsState { + rooms: RoomsStateRooms; + games: RoomsStateGames; + joinedRoomIds: JoinedRooms; + joinedGameIds: JoinedGames; + messages: RoomsStateMessages; + sortGamesBy: RoomsStateSortGamesBy; + sortUsersBy: RoomsStateSortUsersBy; +} + +export interface RoomsStateRooms { + [roomId: number]: Room; +} + +export interface RoomsStateGames { + [roomId: number]: { + [gameId: number]: Game; + }; +} + +export interface JoinedRooms { + [roomId: number]: boolean; +} + +export interface JoinedGames { + [roomId: number]: { + [gameId: number]: boolean; + }; +} + +export interface RoomsStateMessages { + [roomId: number]: Message[]; +} + +export interface RoomsStateSortGamesBy extends SortBy { + field: GameSortField +} + +export interface RoomsStateSortUsersBy extends SortBy { + field: UserSortField +} + +export interface Message { + message: string; + messageType: number; + timeReceived: number; + timeOf?: number; +} diff --git a/webclient/src/store/rooms/rooms.reducer.tsx b/webclient/src/store/rooms/rooms.reducer.tsx new file mode 100644 index 000000000..5898c7150 --- /dev/null +++ b/webclient/src/store/rooms/rooms.reducer.tsx @@ -0,0 +1,326 @@ +import * as _ from 'lodash'; + +import { GameSortField, UserSortField, SortDirection } from 'types'; + +import { SortUtil } from '../common'; + +import { RoomsState } from './rooms.interfaces' +import { MAX_ROOM_MESSAGES, Types } from './rooms.types'; + +const initialState: RoomsState = { + rooms: {}, + games: {}, + joinedRoomIds: {}, + joinedGameIds: {}, + messages: {}, + sortGamesBy: { + field: GameSortField.START_TIME, + order: SortDirection.DESC + }, + sortUsersBy: { + field: UserSortField.NAME, + order: SortDirection.ASC + } +}; + +export const roomsReducer = (state = initialState, action: any) => { + switch (action.type) { + case Types.CLEAR_STORE: { + return { + ...initialState + }; + } + + case Types.UPDATE_ROOMS: { + const rooms = { + ...state.rooms + }; + + // Server does not send everything on updates + _.each(action.rooms, (room, order) => { + const { roomId } = room; + const existing = rooms[roomId] || {}; + + const update = { ...room }; + delete update.gameList; + delete update.gametypeList; + delete update.userList; + + rooms[roomId] = { + ...existing, + ...update, + order + }; + }); + + return { ...state, rooms }; + } + + case Types.JOIN_ROOM: { + const { roomInfo } = action; + const { joinedRoomIds, rooms, sortGamesBy, sortUsersBy } = state; + + const { roomId } = roomInfo; + + const gameList = [ + ...roomInfo.gameList + ]; + + const userList = [ + ...roomInfo.userList + ]; + + SortUtil.sortByField(gameList, sortGamesBy); + SortUtil.sortUsersByField(userList, sortUsersBy); + + return { + ...state, + + rooms: { + ...rooms, + [roomId]: { + ...roomInfo, + gameList, + userList + } + }, + + joinedRoomIds: { + ...joinedRoomIds, + [roomId]: true + }, + } + } + + case Types.LEAVE_ROOM: { + const { roomId } = action; + const { joinedRoomIds, messages } = state; + + const _joined = { + ...joinedRoomIds + }; + + const _messages = { + ...messages + }; + + delete _joined[roomId]; + delete _messages[roomId]; + + return { + ...state, + + joinedRoomIds: _joined, + messages: _messages, + } + } + + case Types.ADD_MESSAGE: { + const { roomId, message } = action; + const { messages } = state; + + let roomMessages = [...(messages[roomId] || [])]; + + if (roomMessages.length === MAX_ROOM_MESSAGES) { + roomMessages.shift(); + } + + message.timeReceived = new Date().getTime(); + roomMessages.push(message); + + return { + ...state, + messages: { + ...messages, + + [roomId]: [ + ...roomMessages + ] + } + } + } + // @TODO improve this reducer, likely by improving the store model + + case Types.UPDATE_GAMES: { + const { roomId, games } = action; + const { rooms, sortGamesBy } = state; + const room = rooms[roomId]; + + if (!room) { + return { ...state }; + } + + // Create map of games with update objects + const toUpdate = games.reduce((map, game) => { + map[game.gameId] = game; + return map; + }, {}); + + const gameUpdates = room.gameList + // filter out closed games and remove from update map + .filter(game => { + const gameUpdate = toUpdate[game.gameId]; + const closedGame = gameUpdate && gameUpdate.closed; + + if (closedGame) { + delete toUpdate[game.gameId]; + } + + return !closedGame; + }) + .map(game => { + const gameUpdate = toUpdate[game.gameId]; + + if (gameUpdate) { + delete toUpdate[game.gameId]; + + return { + ...game, + ...gameUpdate + }; + } + + return game; + }); + + // Push new games to end of list + if (_.size(toUpdate)) { + _.each(toUpdate, game => gameUpdates.push(game)); + } + + const gameList = [...gameUpdates]; + + SortUtil.sortByField(gameList, sortGamesBy); + + return { + ...state, + rooms: { + ...rooms, + [roomId]: { + ...room, + gameList + } + } + } + } + + case Types.USER_JOINED: { + const { roomId, user } = action; + const { rooms, sortUsersBy } = state; + + const room = { ...rooms[roomId] }; + + const userList = [ + ...room.userList, + user + ]; + + SortUtil.sortUsersByField(userList, sortUsersBy); + + return { + ...state, + rooms: { + ...rooms, + [roomId]: { + ...room, + userList + } + } + }; + } + + case Types.USER_LEFT: { + const { roomId, name } = action; + const { rooms } = state; + + const room = { ...rooms[roomId] }; + const userList = room.userList.filter(user => user.name !== name); + + return { + ...state, + rooms: { + ...rooms, + [roomId]: { + ...room, + userList + } + } + }; + } + + case Types.SORT_GAMES: { + const { field, order, roomId } = action; + const { rooms } = state; + + const gameList = [...rooms[roomId].gameList]; + + const sortGamesBy = { + field, order + }; + + SortUtil.sortByField(gameList, sortGamesBy); + + return { + ...state, + + rooms: { + ...rooms, + [roomId]: { + ...rooms[roomId], + gameList + } + }, + + sortGamesBy + } + } + + case Types.REMOVE_MESSAGES: { + const { name, amount, roomId } = action; + const { messages } = state; + let amountRemoved = 0; + + return { + ...state, + messages: { + ...messages, + [roomId]: messages[roomId] + .reverse() + .filter(({ message }) => { + if (amount === amountRemoved) { + return true; + } + + const keep = message.indexOf(`${name}:`) !== 0; + + if (!keep) { + amountRemoved++; + } + + return keep; + }) + .reverse() + } + } + } + + case Types.JOINED_GAME: { + const { gameId, roomId } = action; + const { joinedGameIds } = state; + + return { + ...state, + joinedGameIds: { + ...joinedGameIds, + [roomId]: { + ...joinedGameIds[roomId], + [gameId]: true, + } + } + } + } + + default: + return state; + } +} diff --git a/webclient/src/store/rooms/rooms.selectors.tsx b/webclient/src/store/rooms/rooms.selectors.tsx new file mode 100644 index 000000000..e1e7ec818 --- /dev/null +++ b/webclient/src/store/rooms/rooms.selectors.tsx @@ -0,0 +1,34 @@ +import * as _ from 'lodash'; +import { RoomsState } from './rooms.interfaces'; + +interface State { + rooms: RoomsState +} + +export const Selectors = { + getRooms: ({ rooms }: State) => rooms.rooms, + getGames: ({ rooms }: State) => rooms.games, + getRoom: ({ rooms }: State, id: number) => + _.find(rooms.rooms, ({ roomId }) => roomId === id), + getJoinedRoomIds: ({ rooms }: State) => rooms.joinedRoomIds, + getJoinedGameIds: ({ rooms }: State) => rooms.joinedGameIds, + getMessages: ({ rooms }: State) => rooms.messages, + getSortGamesBy: ({ rooms: { sortGamesBy } }: State) => sortGamesBy, + getSortUsersBy: ({ rooms: { sortUsersBy } }: State) => sortUsersBy, + + getJoinedRooms: (state: State) => { + const joined = Selectors.getJoinedRoomIds(state); + return _.filter(Selectors.getRooms(state), room => joined[room.roomId]); + }, + + getJoinedGames: (state: State, roomId: number) => { + const joined = Selectors.getJoinedGameIds(state)[roomId]; + return _.filter(Selectors.getGames(state)[roomId], game => joined[game.gameId]); + }, + + getRoomMessages: (state: State, roomId: number) => Selectors.getMessages(state)[roomId], + getRoomGames: (state: State, roomId: number) => Selectors.getRooms(state)[roomId].gameList, + getRoomUsers: (state: State, roomId: number) => Selectors.getRooms(state)[roomId].userList + +} + diff --git a/webclient/src/store/rooms/rooms.types.tsx b/webclient/src/store/rooms/rooms.types.tsx new file mode 100644 index 000000000..0b07eadd1 --- /dev/null +++ b/webclient/src/store/rooms/rooms.types.tsx @@ -0,0 +1,16 @@ +export const Types = { + CLEAR_STORE: '[Rooms] Clear Store', + UPDATE_ROOMS: '[Rooms] Update Rooms', + JOIN_ROOM: '[Rooms] Join Room', + LEAVE_ROOM: '[Rooms] Leave Room', + ADD_MESSAGE: '[Rooms] Add Message', + UPDATE_GAMES: '[Rooms] Update Games', + USER_JOINED: '[Rooms] User Joined', + USER_LEFT: '[Rooms] User Left', + SORT_GAMES: '[Rooms] Sort Games', + REMOVE_MESSAGES: '[Rooms] Remove Messages', + GAME_CREATED: '[Rooms] Game Created', + JOINED_GAME: '[Rooms] Joined Game', +}; + +export const MAX_ROOM_MESSAGES = 1000; diff --git a/webclient/src/store/rootReducer.ts b/webclient/src/store/rootReducer.ts new file mode 100644 index 000000000..0f39d7e37 --- /dev/null +++ b/webclient/src/store/rootReducer.ts @@ -0,0 +1,14 @@ +import { combineReducers } from 'redux'; + +import { roomsReducer } from './rooms'; +import { serverReducer } from './server'; +import { reducer as formReducer } from 'redux-form' +import { actionReducer } from './actions' + +export default combineReducers({ + rooms: roomsReducer, + server: serverReducer, + + form: formReducer, + action: actionReducer +}); diff --git a/webclient/src/store/server/index.ts b/webclient/src/store/server/index.ts new file mode 100644 index 000000000..f2a9c5dca --- /dev/null +++ b/webclient/src/store/server/index.ts @@ -0,0 +1,5 @@ +export { Actions } from './server.actions'; +export { Dispatch } from './server.dispatch'; +export * from './server.reducer'; +export { Selectors } from './server.selectors'; +export * from './server.types'; diff --git a/webclient/src/store/server/server.actions.ts b/webclient/src/store/server/server.actions.ts new file mode 100644 index 000000000..8af5adb75 --- /dev/null +++ b/webclient/src/store/server/server.actions.ts @@ -0,0 +1,242 @@ +import { DeckList, DeckStorageTreeItem, ReplayMatch, WebSocketConnectOptions } from 'types'; +import { Types } from './server.types'; + +export const Actions = { + initialized: () => ({ + type: Types.INITIALIZED + }), + clearStore: () => ({ + type: Types.CLEAR_STORE + }), + loginSuccessful: (options: WebSocketConnectOptions) => ({ + type: Types.LOGIN_SUCCESSFUL, + options + }), + loginFailed: () => ({ + type: Types.LOGIN_FAILED, + }), + connectionClosed: reason => ({ + type: Types.CONNECTION_CLOSED, + reason + }), + connectionFailed: () => ({ + type: Types.CONNECTION_FAILED, + }), + testConnectionSuccessful: () => ({ + type: Types.TEST_CONNECTION_SUCCESSFUL, + }), + testConnectionFailed: () => ({ + type: Types.TEST_CONNECTION_FAILED, + }), + serverMessage: message => ({ + type: Types.SERVER_MESSAGE, + message + }), + updateBuddyList: buddyList => ({ + type: Types.UPDATE_BUDDY_LIST, + buddyList + }), + addToBuddyList: user => ({ + type: Types.ADD_TO_BUDDY_LIST, + user + }), + removeFromBuddyList: userName => ({ + type: Types.REMOVE_FROM_BUDDY_LIST, + userName + }), + updateIgnoreList: ignoreList => ({ + type: Types.UPDATE_IGNORE_LIST, + ignoreList + }), + addToIgnoreList: user => ({ + type: Types.ADD_TO_IGNORE_LIST, + user + }), + removeFromIgnoreList: userName => ({ + type: Types.REMOVE_FROM_IGNORE_LIST, + userName + }), + updateInfo: info => ({ + type: Types.UPDATE_INFO, + info + }), + updateStatus: status => ({ + type: Types.UPDATE_STATUS, + status + }), + updateUser: user => ({ + type: Types.UPDATE_USER, + user + }), + updateUsers: users => ({ + type: Types.UPDATE_USERS, + users + }), + userJoined: user => ({ + type: Types.USER_JOINED, + user + }), + userLeft: name => ({ + type: Types.USER_LEFT, + name + }), + viewLogs: logs => ({ + type: Types.VIEW_LOGS, + logs + }), + clearLogs: () => ({ + type: Types.CLEAR_LOGS, + }), + registrationRequiresEmail: () => ({ + type: Types.REGISTRATION_REQUIRES_EMAIL, + }), + registrationSuccess: () => ({ + type: Types.REGISTRATION_SUCCES, + }), + registrationFailed: (error) => ({ + type: Types.REGISTRATION_FAILED, + error + }), + registrationEmailError: (error) => ({ + type: Types.REGISTRATION_EMAIL_ERROR, + error + }), + registrationPasswordError: (error) => ({ + type: Types.REGISTRATION_PASSWORD_ERROR, + error + }), + registrationUserNameError: (error) => ({ + type: Types.REGISTRATION_USERNAME_ERROR, + error + }), + accountAwaitingActivation: (options: WebSocketConnectOptions) => ({ + type: Types.ACCOUNT_AWAITING_ACTIVATION, + options + }), + accountActivationSuccess: () => ({ + type: Types.ACCOUNT_ACTIVATION_SUCCESS, + }), + accountActivationFailed: () => ({ + type: Types.ACCOUNT_ACTIVATION_FAILED, + }), + resetPassword: () => ({ + type: Types.RESET_PASSWORD_REQUESTED, + }), + resetPasswordFailed: () => ({ + type: Types.RESET_PASSWORD_FAILED, + }), + resetPasswordChallenge: () => ({ + type: Types.RESET_PASSWORD_CHALLENGE, + }), + resetPasswordSuccess: () => ({ + type: Types.RESET_PASSWORD_SUCCESS, + }), + adjustMod: (userName, shouldBeMod, shouldBeJudge) => ({ + type: Types.ADJUST_MOD, + userName, + shouldBeMod, + shouldBeJudge, + }), + reloadConfig: () => ({ + type: Types.RELOAD_CONFIG, + }), + shutdownServer: () => ({ + type: Types.SHUTDOWN_SERVER, + }), + updateServerMessage: () => ({ + type: Types.UPDATE_SERVER_MESSAGE, + }), + accountPasswordChange: () => ({ + type: Types.ACCOUNT_PASSWORD_CHANGE, + }), + accountEditChanged: (user) => ({ + type: Types.ACCOUNT_EDIT_CHANGED, + user, + }), + accountImageChanged: (user) => ({ + type: Types.ACCOUNT_IMAGE_CHANGED, + user, + }), + directMessageSent: (userName, message) => ({ + type: Types.DIRECT_MESSAGE_SENT, + userName, + message, + }), + getUserInfo: (userInfo) => ({ + type: Types.GET_USER_INFO, + userInfo, + }), + notifyUser: (notification) => ({ + type: Types.NOTIFY_USER, + notification, + }), + serverShutdown: (data) => ({ + type: Types.SERVER_SHUTDOWN, + data, + }), + userMessage: (messageData) => ({ + type: Types.USER_MESSAGE, + messageData, + }), + addToList: (list, userName) => ({ + type: Types.ADD_TO_LIST, + list, + userName, + }), + removeFromList: (list, userName) => ({ + type: Types.REMOVE_FROM_LIST, + list, + userName, + }), + banFromServer: (userName) => ({ + type: Types.BAN_FROM_SERVER, + userName, + }), + banHistory: (userName, banHistory) => ({ + type: Types.BAN_HISTORY, + userName, + banHistory, + }), + warnHistory: (userName, warnHistory) => ({ + type: Types.WARN_HISTORY, + userName, + warnHistory, + }), + warnListOptions: (warnList) => ({ + type: Types.WARN_LIST_OPTIONS, + warnList, + }), + warnUser: (userName) => ({ + type: Types.WARN_USER, + userName, + }), + grantReplayAccess: (replayId: number, moderatorName: string) => ({ + type: Types.GRANT_REPLAY_ACCESS, + replayId, + moderatorName, + }), + forceActivateUser: (usernameToActivate: string, moderatorName: string) => ({ + type: Types.FORCE_ACTIVATE_USER, + usernameToActivate, + moderatorName, + }), + getAdminNotes: (userName: string, notes: string) => ({ + type: Types.GET_ADMIN_NOTES, + userName, + notes, + }), + updateAdminNotes: (userName: string, notes: string) => ({ + type: Types.UPDATE_ADMIN_NOTES, + userName, + notes, + }), + replayList: (matchList: ReplayMatch[]) => ({ type: Types.REPLAY_LIST, matchList }), + replayAdded: (matchInfo: ReplayMatch) => ({ type: Types.REPLAY_ADDED, matchInfo }), + replayModifyMatch: (gameId: number, doNotHide: boolean) => ({ type: Types.REPLAY_MODIFY_MATCH, gameId, doNotHide }), + replayDeleteMatch: (gameId: number) => ({ type: Types.REPLAY_DELETE_MATCH, gameId }), + backendDecks: (deckList: DeckList) => ({ type: Types.BACKEND_DECKS, deckList }), + deckNewDir: (path: string, dirName: string) => ({ type: Types.DECK_NEW_DIR, path, dirName }), + deckDelDir: (path: string) => ({ type: Types.DECK_DEL_DIR, path }), + deckUpload: (path: string, treeItem: DeckStorageTreeItem) => ({ type: Types.DECK_UPLOAD, path, treeItem }), + deckDelete: (deckId: number) => ({ type: Types.DECK_DELETE, deckId }), +} diff --git a/webclient/src/store/server/server.dispatch.ts b/webclient/src/store/server/server.dispatch.ts new file mode 100644 index 000000000..3d3b6d360 --- /dev/null +++ b/webclient/src/store/server/server.dispatch.ts @@ -0,0 +1,219 @@ +import { reset } from 'redux-form'; +import { Actions } from './server.actions'; +import { store } from 'store'; +import { DeckList, DeckStorageTreeItem, ReplayMatch, WebSocketConnectOptions } from 'types'; + +export const Dispatch = { + initialized: () => { + store.dispatch(Actions.initialized()); + }, + clearStore: () => { + store.dispatch(Actions.clearStore()); + }, + loginSuccessful: options => { + store.dispatch(Actions.loginSuccessful(options)); + }, + loginFailed: () => { + store.dispatch(Actions.loginFailed()); + }, + connectionClosed: reason => { + store.dispatch(Actions.connectionClosed(reason)); + }, + connectionFailed: () => { + store.dispatch(Actions.connectionFailed()); + }, + testConnectionSuccessful: () => { + store.dispatch(Actions.testConnectionSuccessful()); + }, + testConnectionFailed: () => { + store.dispatch(Actions.testConnectionFailed()); + }, + updateBuddyList: buddyList => { + store.dispatch(Actions.updateBuddyList(buddyList)); + }, + addToBuddyList: user => { + store.dispatch(reset('addToBuddies')); + store.dispatch(Actions.addToBuddyList(user)); + }, + removeFromBuddyList: userName => { + store.dispatch(Actions.removeFromBuddyList(userName)); + }, + updateIgnoreList: ignoreList => { + store.dispatch(Actions.updateIgnoreList(ignoreList)); + }, + addToIgnoreList: user => { + store.dispatch(reset('addToIgnore')); + store.dispatch(Actions.addToIgnoreList(user)); + }, + removeFromIgnoreList: userName => { + store.dispatch(Actions.removeFromIgnoreList(userName)); + }, + updateInfo: (name, version) => { + store.dispatch(Actions.updateInfo({ + name, + version + })); + }, + updateStatus: (state, description) => { + store.dispatch(Actions.updateStatus({ + state, + description + })); + }, + updateUser: user => { + store.dispatch(Actions.updateUser(user)); + }, + updateUsers: users => { + store.dispatch(Actions.updateUsers(users)); + }, + userJoined: user => { + store.dispatch(Actions.userJoined(user)); + }, + userLeft: name => { + store.dispatch(Actions.userLeft(name)); + }, + viewLogs: name => { + store.dispatch(Actions.viewLogs(name)); + }, + clearLogs: () => { + store.dispatch(Actions.clearLogs()); + }, + serverMessage: message => { + store.dispatch(Actions.serverMessage(message)); + }, + registrationRequiresEmail: () => { + store.dispatch(Actions.registrationRequiresEmail()); + }, + registrationSuccess: () => { + store.dispatch(Actions.registrationSuccess()) + }, + registrationFailed: (error) => { + store.dispatch(Actions.registrationFailed(error)); + }, + registrationEmailError: (error) => { + store.dispatch(Actions.registrationEmailError(error)); + }, + registrationPasswordError: (error) => { + store.dispatch(Actions.registrationPasswordError(error)); + }, + registrationUserNameError: (error) => { + store.dispatch(Actions.registrationUserNameError(error)); + }, + accountAwaitingActivation: (options: WebSocketConnectOptions) => { + store.dispatch(Actions.accountAwaitingActivation(options)); + }, + accountActivationSuccess: () => { + store.dispatch(Actions.accountActivationSuccess()); + }, + accountActivationFailed: () => { + store.dispatch(Actions.accountActivationFailed()); + }, + resetPassword: () => { + store.dispatch(Actions.resetPassword()); + }, + resetPasswordFailed: () => { + store.dispatch(Actions.resetPasswordFailed()); + }, + resetPasswordChallenge: () => { + store.dispatch(Actions.resetPasswordChallenge()); + }, + resetPasswordSuccess: () => { + store.dispatch(Actions.resetPasswordSuccess()); + }, + adjustMod: (userName, shouldBeMod, shouldBeJudge) => { + store.dispatch(Actions.adjustMod(userName, shouldBeMod, shouldBeJudge)); + }, + reloadConfig: () => { + store.dispatch(Actions.reloadConfig()); + }, + shutdownServer: () => { + store.dispatch(Actions.shutdownServer()); + }, + updateServerMessage: () => { + store.dispatch(Actions.updateServerMessage()); + }, + accountPasswordChange: () => { + store.dispatch(Actions.accountPasswordChange()); + }, + accountEditChanged: (user) => { + store.dispatch(Actions.accountEditChanged(user)); + }, + accountImageChanged: (user) => { + store.dispatch(Actions.accountImageChanged(user)); + }, + directMessageSent: (userName, message) => { + store.dispatch(Actions.directMessageSent(userName, message)); + }, + getUserInfo: (userInfo) => { + store.dispatch(Actions.getUserInfo(userInfo)); + }, + notifyUser: (notification) => { + store.dispatch(Actions.notifyUser(notification)) + }, + serverShutdown: (data) => { + store.dispatch(Actions.serverShutdown(data)) + }, + userMessage: (messageData) => { + store.dispatch(Actions.userMessage(messageData)) + }, + addToList: (list, userName) => { + store.dispatch(Actions.addToList(list, userName)) + }, + removeFromList: (list, userName) => { + store.dispatch(Actions.removeFromList(list, userName)) + }, + banFromServer: (userName) => { + store.dispatch(Actions.banFromServer(userName)); + }, + banHistory: (userName, banHistory) => { + store.dispatch(Actions.banHistory(userName, banHistory)) + }, + warnHistory: (userName, warnHistory) => { + store.dispatch(Actions.warnHistory(userName, warnHistory)) + }, + warnListOptions: (warnList) => { + store.dispatch(Actions.warnListOptions(warnList)) + }, + warnUser: (userName) => { + store.dispatch(Actions.warnUser(userName)) + }, + grantReplayAccess: (replayId: number, moderatorName: string) => { + store.dispatch(Actions.grantReplayAccess(replayId, moderatorName)); + }, + forceActivateUser: (usernameToActivate: string, moderatorName: string) => { + store.dispatch(Actions.forceActivateUser(usernameToActivate, moderatorName)); + }, + getAdminNotes: (userName: string, notes: string) => { + store.dispatch(Actions.getAdminNotes(userName, notes)); + }, + updateAdminNotes: (userName: string, notes: string) => { + store.dispatch(Actions.updateAdminNotes(userName, notes)); + }, + replayList: (matchList: ReplayMatch[]) => { + store.dispatch(Actions.replayList(matchList)); + }, + replayAdded: (matchInfo: ReplayMatch) => { + store.dispatch(Actions.replayAdded(matchInfo)); + }, + replayModifyMatch: (gameId: number, doNotHide: boolean) => { + store.dispatch(Actions.replayModifyMatch(gameId, doNotHide)); + }, + replayDeleteMatch: (gameId: number) => { + store.dispatch(Actions.replayDeleteMatch(gameId)); + }, + backendDecks: (deckList: DeckList) => { + store.dispatch(Actions.backendDecks(deckList)); + }, + deckNewDir: (path: string, dirName: string) => { + store.dispatch(Actions.deckNewDir(path, dirName)); + }, + deckDelDir: (path: string) => { + store.dispatch(Actions.deckDelDir(path)); + }, + deckUpload: (path: string, treeItem: DeckStorageTreeItem) => { + store.dispatch(Actions.deckUpload(path, treeItem)); + }, + deckDelete: (deckId: number) => { + store.dispatch(Actions.deckDelete(deckId)); + }, +} diff --git a/webclient/src/store/server/server.interfaces.ts b/webclient/src/store/server/server.interfaces.ts new file mode 100644 index 000000000..97e9d99da --- /dev/null +++ b/webclient/src/store/server/server.interfaces.ts @@ -0,0 +1,96 @@ +import { + WarnHistoryItem, BanHistoryItem, DeckList, LogItem, ReplayMatch, SortBy, User, UserSortField, WebSocketConnectOptions, WarnListItem +} from 'types'; +import { NotifyUserData, ServerShutdownData, UserMessageData } from 'websocket/events/session/interfaces'; + +export interface ServerConnectParams { + host: string; + port: string; + userName: string; + password: string; +} + +export interface ServerRegisterParams { + host: string; + port: string; + userName: string; + password: string; + email: string; + country: string; + realName: string; +} + +export interface RequestPasswordSaltParams { + userName: string; +} + +export interface ForgotPasswordParams { + userName: string; +} + +export interface ForgotPasswordChallengeParams extends ForgotPasswordParams { + email: string; +} + +export interface ForgotPasswordResetParams extends ForgotPasswordParams { + token: string; + newPassword: string; +} + +export interface AccountActivationParams extends ServerRegisterParams { + token: string; +} + +export interface ServerState { + initialized: boolean; + buddyList: User[]; + ignoreList: User[]; + info: ServerStateInfo; + status: ServerStateStatus; + logs: ServerStateLogs; + user: User; + users: User[]; + sortUsersBy: ServerStateSortUsersBy; + connectOptions: WebSocketConnectOptions; + messages: { + [userName: string]: UserMessageData[]; + } + userInfo: { + [userName: string]: User; + } + notifications: NotifyUserData[]; + serverShutdown: ServerShutdownData; + banUser: string; + banHistory: { + [userName: string]: BanHistoryItem[]; + }; + warnHistory: { + [userName: string]: WarnHistoryItem[]; + }; + warnListOptions: WarnListItem[]; + warnUser: string; + adminNotes: { [userName: string]: string }; + replays: ReplayMatch[]; + backendDecks: DeckList | null; +} + +export interface ServerStateStatus { + description: string; + state: number; +} + +export interface ServerStateInfo { + message: string; + name: string; + version: string; +} + +export interface ServerStateLogs { + room: LogItem[]; + game: LogItem[]; + chat: LogItem[]; +} + +export interface ServerStateSortUsersBy extends SortBy { + field: UserSortField +} diff --git a/webclient/src/store/server/server.reducer.ts b/webclient/src/store/server/server.reducer.ts new file mode 100644 index 000000000..a63418eeb --- /dev/null +++ b/webclient/src/store/server/server.reducer.ts @@ -0,0 +1,481 @@ +import { DeckStorageFolder, DeckStorageTreeItem, SortDirection, StatusEnum, UserLevelFlag, UserSortField } from 'types'; + +import { SortUtil } from '../common'; + +import { ServerState } from './server.interfaces' +import { Types } from './server.types'; + +function splitPath(path: string): string[] { + return path ? path.split('/') : []; +} + +function insertAtPath(folder: DeckStorageFolder, pathSegments: string[], item: DeckStorageTreeItem): DeckStorageFolder { + if (pathSegments.length === 0 || (pathSegments.length === 1 && pathSegments[0] === '')) { + return { items: [...folder.items, item] }; + } + const [head, ...tail] = pathSegments; + const match = folder.items.find(child => child.name === head && child.folder); + if (match) { + return { + items: folder.items.map(child => + child === match + ? { ...child, folder: insertAtPath(child.folder!, tail, item) } + : child + ), + }; + } + const created: DeckStorageTreeItem = { id: 0, name: head, file: null, folder: insertAtPath({ items: [] }, tail, item) }; + return { items: [...folder.items, created] }; +} + +function removeById(folder: DeckStorageFolder, id: number): DeckStorageFolder { + return { + items: folder.items + .filter(item => item.id !== id) + .map(item => + item.folder ? { ...item, folder: removeById(item.folder, id) } : item + ), + }; +} + +function removeByPath(folder: DeckStorageFolder, pathSegments: string[]): DeckStorageFolder { + if (pathSegments.length === 0 || (pathSegments.length === 1 && pathSegments[0] === '')) { + return folder; + } + const [head, ...tail] = pathSegments; + if (tail.length === 0) { + return { items: folder.items.filter(item => !(item.name === head && item.folder !== null)) }; + } + return { + items: folder.items.map(item => + item.name === head && item.folder + ? { ...item, folder: removeByPath(item.folder, tail) } + : item + ), + }; +} + +const initialState: ServerState = { + initialized: false, + buddyList: [], + ignoreList: [], + + status: { + state: StatusEnum.DISCONNECTED, + description: null + }, + info: { + message: null, + name: null, + version: null + }, + logs: { + room: [], + game: [], + chat: [] + }, + user: null, + users: [], + sortUsersBy: { + field: UserSortField.NAME, + order: SortDirection.ASC + }, + connectOptions: {}, + messages: {}, + userInfo: {}, + notifications: [], + serverShutdown: null, + banUser: '', + banHistory: {}, + warnHistory: {}, + warnListOptions: [], + warnUser: '', + adminNotes: {}, + replays: [], + backendDecks: null, +}; + +export const serverReducer = (state = initialState, action: any) => { + switch (action.type) { + case Types.INITIALIZED: { + return { + ...initialState, + initialized: true + } + } + case Types.ACCOUNT_AWAITING_ACTIVATION: { + return { + ...state, + connectOptions: { + ...action.options + } + } + } + case Types.ACCOUNT_ACTIVATION_FAILED: + case Types.ACCOUNT_ACTIVATION_SUCCESS: { + return { + ...state, + connectOptions: {} + } + } + case Types.CLEAR_STORE: { + return { + ...initialState, + status: { + ...state.status + } + } + } + case Types.SERVER_MESSAGE: { + const { message } = action; + const { info } = state; + + return { + ...state, + info: { ...info, message } + } + } + case Types.UPDATE_BUDDY_LIST: { + const { buddyList } = action; + const { sortUsersBy } = state; + + SortUtil.sortUsersByField(buddyList, sortUsersBy); + + return { + ...state, + buddyList: [ + ...buddyList + ] + }; + } + case Types.ADD_TO_BUDDY_LIST: { + const { user } = action; + const { sortUsersBy } = state; + + const buddyList = [...state.buddyList]; + + buddyList.push(user); + SortUtil.sortUsersByField(buddyList, sortUsersBy); + + return { + ...state, + buddyList + }; + } + case Types.REMOVE_FROM_BUDDY_LIST: { + const { userName } = action; + const buddyList = state.buddyList.filter(user => user.name !== userName); + + return { + ...state, + buddyList + }; + } + case Types.UPDATE_IGNORE_LIST: { + const { ignoreList } = action; + const { sortUsersBy } = state; + + SortUtil.sortUsersByField(ignoreList, sortUsersBy); + + return { + ...state, + ignoreList: [ + ...ignoreList + ] + }; + } + case Types.ADD_TO_IGNORE_LIST: { + const { user } = action; + const { sortUsersBy } = state; + + const ignoreList = [...state.ignoreList]; + + ignoreList.push(user); + SortUtil.sortUsersByField(ignoreList, sortUsersBy); + + return { + ...state, + ignoreList + }; + } + case Types.REMOVE_FROM_IGNORE_LIST: { + const { userName } = action; + const ignoreList = state.ignoreList.filter(user => user.name !== userName); + + return { + ...state, + ignoreList + }; + } + case Types.UPDATE_INFO: { + const { name, version } = action.info; + const { info } = state; + + return { + ...state, + info: { ...info, name, version } + } + } + case Types.UPDATE_STATUS: { + const { status } = action; + + return { + ...state, + status: { ...status } + } + } + case Types.UPDATE_USER: + case Types.ACCOUNT_EDIT_CHANGED: + case Types.ACCOUNT_IMAGE_CHANGED: { + const { user } = action; + + return { + ...state, + user: { + ...state.user, + ...user + } + } + } + case Types.UPDATE_USERS: { + const users = [...action.users]; + const { sortUsersBy } = state; + + + SortUtil.sortUsersByField(users, sortUsersBy); + + return { + ...state, + users + }; + } + case Types.USER_JOINED: { + const { sortUsersBy } = state; + + const users = [ + ...state.users, + { ...action.user } + ]; + + SortUtil.sortUsersByField(users, sortUsersBy); + + return { + ...state, + users + }; + } + case Types.USER_LEFT: { + const { name } = action; + const users = state.users.filter(user => user.name !== name); + + return { + ...state, + users + }; + } + case Types.VIEW_LOGS: { + const { logs } = action; + + return { + ...state, + logs: { + ...logs + } + }; + } + case Types.CLEAR_LOGS: { + return { + ...state, + logs: { + ...initialState.logs + } + } + } + case Types.USER_MESSAGE: { + const { senderName, receiverName } = action.messageData; + const userName = state.user.name === senderName ? receiverName : senderName; + + return { + ...state, + messages: { + ...state.messages, + [userName]: [ + ...(state.messages[userName] ?? []), + action.messageData, + ], + } + }; + } + case Types.GET_USER_INFO: { + const { userInfo } = action; + + return { + ...state, + userInfo: { + ...state.userInfo, + [userInfo.name]: userInfo, + } + }; + } + case Types.NOTIFY_USER: { + const { notification } = action; + + return { + ...state, + notifications: [ + ...state.notifications, + notification + ] + }; + } + case Types.SERVER_SHUTDOWN: { + const { data } = action; + + return { + ...state, + serverShutdown: data, + }; + } + case Types.BAN_FROM_SERVER: { + const { userName } = action; + + return { + ...state, + banUser: userName, + }; + } + case Types.BAN_HISTORY: { + const { userName, banHistory } = action; + + return { + ...state, + banHistory: { + ...state.banHistory, + [userName]: banHistory, + } + }; + } + case Types.WARN_HISTORY: { + const { userName, warnHistory } = action; + + return { + ...state, + warnHistory: { + ...state.warnHistory, + [userName]: warnHistory, + } + }; + } + case Types.WARN_LIST_OPTIONS: { + const { warnList } = action; + + return { + ...state, + warnListOptions: warnList, + }; + } + case Types.WARN_USER: { + const { userName } = action; + return { + ...state, + warnUser: userName, + }; + } + case Types.GET_ADMIN_NOTES: + case Types.UPDATE_ADMIN_NOTES: { + const { userName, notes } = action; + return { + ...state, + adminNotes: { + ...state.adminNotes, + [userName]: notes, + } + }; + } + case Types.ADJUST_MOD: { + const { userName, shouldBeMod, shouldBeJudge } = action; + + return { + ...state, + users: state.users.map((user) => { + if (user.name !== userName) { + return user; + } + const judgeFlag = shouldBeJudge ? UserLevelFlag.IsJudge : UserLevelFlag.IsNothing; + const modFlag = shouldBeMod ? UserLevelFlag.IsModerator : UserLevelFlag.IsNothing; + return { + ...user, + userLevel: user.userLevel & (judgeFlag | modFlag) + } + }) + }; + } + case Types.REPLAY_LIST: { + return { ...state, replays: [...action.matchList] }; + } + case Types.REPLAY_ADDED: { + return { ...state, replays: [...state.replays, action.matchInfo] }; + } + case Types.REPLAY_MODIFY_MATCH: { + return { + ...state, + replays: state.replays.map(r => + r.gameId === action.gameId ? { ...r, doNotHide: action.doNotHide } : r + ), + }; + } + case Types.REPLAY_DELETE_MATCH: { + return { ...state, replays: state.replays.filter(r => r.gameId !== action.gameId) }; + } + case Types.BACKEND_DECKS: { + return { ...state, backendDecks: action.deckList }; + } + case Types.DECK_UPLOAD: { + if (!state.backendDecks) { + return state; + } + return { + ...state, + backendDecks: { + root: insertAtPath(state.backendDecks.root, splitPath(action.path), action.treeItem), + }, + }; + } + case Types.DECK_DELETE: { + if (!state.backendDecks) { + return state; + } + return { + ...state, + backendDecks: { + root: removeById(state.backendDecks.root, action.deckId), + }, + }; + } + case Types.DECK_NEW_DIR: { + if (!state.backendDecks) { + return state; + } + const newFolder: DeckStorageTreeItem = { id: 0, name: action.dirName, file: null, folder: { items: [] } }; + return { + ...state, + backendDecks: { + root: insertAtPath(state.backendDecks.root, splitPath(action.path), newFolder), + }, + }; + } + case Types.DECK_DEL_DIR: { + if (!state.backendDecks) { + return state; + } + return { + ...state, + backendDecks: { + root: removeByPath(state.backendDecks.root, splitPath(action.path)), + }, + }; + } + default: + return state; + } +} diff --git a/webclient/src/store/server/server.selectors.ts b/webclient/src/store/server/server.selectors.ts new file mode 100644 index 000000000..fa9f82297 --- /dev/null +++ b/webclient/src/store/server/server.selectors.ts @@ -0,0 +1,22 @@ +import { ServerState } from './server.interfaces'; + +interface State { + server: ServerState +} + +export const Selectors = { + getInitialized: ({ server }: State) => server.initialized, + getConnectOptions: ({ server }: State) => server.connectOptions, + getMessage: ({ server }: State) => server.info.message, + getName: ({ server }: State) => server.info.name, + getVersion: ({ server }: State) => server.info.version, + getDescription: ({ server }: State) => server.status.description, + getState: ({ server }: State) => server.status.state, + getUser: ({ server }: State) => server.user, + getUsers: ({ server }: State) => server.users, + getLogs: ({ server }: State) => server.logs, + getBuddyList: ({ server }: State) => server.buddyList, + getIgnoreList: ({ server }: State) => server.ignoreList, + getReplays: ({ server }: State) => server.replays, + getBackendDecks: ({ server }: State) => server.backendDecks, +} diff --git a/webclient/src/store/server/server.types.ts b/webclient/src/store/server/server.types.ts new file mode 100644 index 000000000..fb4249011 --- /dev/null +++ b/webclient/src/store/server/server.types.ts @@ -0,0 +1,72 @@ +export const Types = { + INITIALIZED: '[Server] Initialized', + CLEAR_STORE: '[Server] Clear Store', + LOGIN_SUCCESSFUL: '[Server] Login Successful', + LOGIN_FAILED: '[Server] Login Failed', + CONNECTION_CLOSED: '[Server] Connection Closed', + CONNECTION_FAILED: '[Server] Connection Failed', + TEST_CONNECTION_SUCCESSFUL: '[Server] Test Connection Successful', + TEST_CONNECTION_FAILED: '[Server] Test Connection Failed', + SERVER_MESSAGE: '[Server] Server Message', + UPDATE_BUDDY_LIST: '[Server] Update Buddy List', + ADD_TO_BUDDY_LIST: '[Server] Add to Buddy List', + REMOVE_FROM_BUDDY_LIST: '[Server] Remove from Buddy List', + UPDATE_IGNORE_LIST: '[Server] Update Ignore List', + ADD_TO_IGNORE_LIST: '[Server] Add to Ignore List', + REMOVE_FROM_IGNORE_LIST: '[Server] Remove from Ignore List', + UPDATE_INFO: '[Server] Update Info', + UPDATE_STATUS: '[Server] Update Status', + UPDATE_USER: '[Server] Update User', + UPDATE_USERS: '[Server] Update Users', + USER_JOINED: '[Server] User Joined', + USER_LEFT: '[Server] User Left', + VIEW_LOGS: '[Server] View Logs', + CLEAR_LOGS: '[Server] Clear Logs', + REGISTRATION_REQUIRES_EMAIL: '[Server] Registration Requires Email', + REGISTRATION_SUCCES: '[Server] Registration Success', + REGISTRATION_FAILED: '[Server] Registration Failed', + REGISTRATION_EMAIL_ERROR: '[Server] Registration Email Error', + REGISTRATION_PASSWORD_ERROR: '[Server] Registration Password Error', + REGISTRATION_USERNAME_ERROR: '[Server] Registration Username Error', + ACCOUNT_AWAITING_ACTIVATION: '[Server] Account Awaiting Activation', + ACCOUNT_ACTIVATION_SUCCESS: '[Server] Account Activation Success', + ACCOUNT_ACTIVATION_FAILED: '[Server] Account Activation Failed', + RESET_PASSWORD_REQUESTED: '[Server] Reset Password Requested', + RESET_PASSWORD_FAILED: '[Server] Reset Password Failed', + RESET_PASSWORD_CHALLENGE: '[Server] Reset Password Challenge', + RESET_PASSWORD_SUCCESS: '[Server] Reset Password Success', + ADJUST_MOD: '[Server] Adjust Mod', + RELOAD_CONFIG: '[Server] Reload Config', + SHUTDOWN_SERVER: '[Server] Shutdown Server', + UPDATE_SERVER_MESSAGE: '[Server] Update Server Message', + ACCOUNT_PASSWORD_CHANGE: '[Server] Account Password Change', + ACCOUNT_EDIT_CHANGED: '[Server] Account Edit Changed', + ACCOUNT_IMAGE_CHANGED: '[Server] Account Image Changed', + DIRECT_MESSAGE_SENT: '[Server] Direct Message Sent', + GET_USER_INFO: '[Server] Get User Info', + NOTIFY_USER: '[Server] Notify User', + SERVER_SHUTDOWN: '[Server] Server Shutdown', + USER_MESSAGE: '[Server] User Message', + ADD_TO_LIST: '[Server] Add To List', + REMOVE_FROM_LIST: '[Server] Remove From List', + BAN_FROM_SERVER: '[Server] Ban From Server', + BAN_HISTORY: '[Server] Ban History', + WARN_HISTORY: '[Server] Warn History', + WARN_LIST_OPTIONS: '[Server] Warn List Options', + WARN_USER: '[Server] Warn User', + GRANT_REPLAY_ACCESS: '[Server] Grant Replay Access', + FORCE_ACTIVATE_USER: '[Server] Force Activate User', + GET_ADMIN_NOTES: '[Server] Get Admin Notes', + UPDATE_ADMIN_NOTES: '[Server] Update Admin Notes', + // Replay + REPLAY_LIST: '[Server] Replay List', + REPLAY_ADDED: '[Server] Replay Added', + REPLAY_MODIFY_MATCH: '[Server] Replay Modify Match', + REPLAY_DELETE_MATCH: '[Server] Replay Delete Match', + // Deck Storage + BACKEND_DECKS: '[Server] Backend Decks', + DECK_NEW_DIR: '[Server] Deck New Dir', + DECK_DEL_DIR: '[Server] Deck Del Dir', + DECK_UPLOAD: '[Server] Deck Upload', + DECK_DELETE: '[Server] Deck Delete', +}; diff --git a/webclient/src/store/store.ts b/webclient/src/store/store.ts new file mode 100644 index 000000000..e6ef5df3a --- /dev/null +++ b/webclient/src/store/store.ts @@ -0,0 +1,9 @@ +import { createStore, applyMiddleware } from 'redux'; +import thunk from 'redux-thunk'; +import rootReducer from './rootReducer'; + +const initialState = {}; + +const middleware: any = [thunk]; + +export const store = createStore(rootReducer, initialState, applyMiddleware(...middleware)); diff --git a/webclient/src/types/cards.ts b/webclient/src/types/cards.ts new file mode 100644 index 000000000..b28bae203 --- /dev/null +++ b/webclient/src/types/cards.ts @@ -0,0 +1,93 @@ +export class Card { + artist: string; + availability: string[]; + borderColor: string; + colorIdentity: string[]; + colors: string[]; + convertedManaCost: number; + edhrecRank: number; + flavorText: string; + identifiers: { + cardKingdomId: string; + mcmId: string; + mcmMetaId: string; + mtgjsonV4Id: string; + multiverseId: string; + scryfallId: string; + scryfallIllustrationId: string; + scryfallOracleId: string; + tcgplayerProductId: string; + }; + isOnlineOnly: boolean; + layout: string; + legalities: { + brawl: string; + commander: string; + duel: string; + future: string; + gladiator: string; + historic: string; + legacy: string; + modern: string; + pauper: string; + penny: string; + pioneer: string; + premodern: string; + standard: string; + vintage: string; + }; + manaCost: string; + name: string; + originalText: string; + originalType: string; + power: string; + printings: string[]; + rarity: string; + rulings: { + date: string; + text: string; + }[]; + side: string; + setCode: string; + subtypes: string[]; + supertypes: string[]; + text: string; + toughness: string; + type: string; + types: string[]; + uuid: string; + variations: string[]; +} + +export class Set { + baseSetSize: number; + block: string; + cards: string[]; + code: string; + isOnlineOnly: boolean; + name: string; + releaseDate: string; + totalSetSize: number; + type: string; +} + +export class Token { + name: { value: string }; + prop: { + value: { + cmc: { value: string; }; + colors: { value: string; }; + maintype: { value: string; }; + pt: { value: string; }; + type: { value: string; }; + }; + }; + related: { value: string; }[]; + 'reverse-related': { value: string; }[]; + set: { + value: string; + picURL: string; + }[]; + tablerow: { value: string; }; + text: { value: string; }; +} diff --git a/webclient/src/types/constants.spec.ts b/webclient/src/types/constants.spec.ts new file mode 100644 index 000000000..f95a16eaa --- /dev/null +++ b/webclient/src/types/constants.spec.ts @@ -0,0 +1,84 @@ +import { + URL_REGEX, + MESSAGE_SENDER_REGEX, + MENTION_REGEX, + CARD_CALLOUT_REGEX, + CALLOUT_BOUNDARY_REGEX, +} from './constants'; + +describe('RegEx', () => { + describe('URL_REGEX', () => { + it('should match and capture whole url in main capture group', () => { + const test = [ + 'http://example.com', + 'https://example.com', + 'https://www.example.com', + ]; + + test.forEach(str => { + const match = str.match(URL_REGEX); + + expect(match).toBeDefined(); + expect(match[0]).toBe(str); + }); + }); + + it('should not match bad urls', () => { + const test = [ + 'htt://example.com', + 'https:/example.com', + 'https//www.example.com', + 'www.example.com', + 'example.com', + ]; + + test.forEach(str => + expect(str.match(URL_REGEX)).toBe(null) + ); + }); + }); + + describe('MESSAGE_SENDER_REGEX', () => { + it('should match and capture sender name in second capture group', () => { + const sender = 'sender'; + const match = `${sender}: message`.match(MESSAGE_SENDER_REGEX); + + expect(match).toBeDefined(); + expect(match[1]).toBe(sender); + }); + + it('should not match if spaces before :', () => { + const test = [ + ' sender: message', + 'sender : message', + ' sender : message', + ]; + + test.forEach(str => + expect(str.match(URL_REGEX)).toBe(null) + ); + }); + }); + + describe('MENTION_REGEX', () => { + it('should match and capture user mentions in second capture group', () => { + expect('@mention'.match(MENTION_REGEX)[0]).toBe('@mention'); + expect('@mention '.match(MENTION_REGEX)[0]).toBe('@mention'); + expect(' @mention'.match(MENTION_REGEX)[0]).toBe(' @mention'); + expect(' @mention '.match(MENTION_REGEX)[0]).toBe(' @mention'); + expect('leading @mention'.match(MENTION_REGEX)[0]).toBe(' @mention'); + expect('leading @mention trailing'.match(MENTION_REGEX)[0]).toBe(' @mention'); + expect('@mention trailing'.match(MENTION_REGEX)[0]).toBe('@mention'); + }); + + it('should not match preceded by character', () => { + const test = [ + 'leading@mention', + ]; + + test.forEach(str => + expect(str.match(MENTION_REGEX)).toBe(null) + ); + }); + }); +}); diff --git a/webclient/src/types/constants.ts b/webclient/src/types/constants.ts new file mode 100644 index 000000000..7341a22b1 --- /dev/null +++ b/webclient/src/types/constants.ts @@ -0,0 +1,6 @@ +// eslint-disable-next-line +export const URL_REGEX = /(https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b(?:[-a-zA-Z0-9@:%_\+.~#?&//=]*))/g; +export const MESSAGE_SENDER_REGEX = /(^[^:\s]+):/; +export const MENTION_REGEX = /(^|\s)(@\w+)/g; +export const CARD_CALLOUT_REGEX = /(\[\[[^\]]+\]\])/g; +export const CALLOUT_BOUNDARY_REGEX = /(\[\[|\]\])/g; diff --git a/webclient/src/types/countries.ts b/webclient/src/types/countries.ts new file mode 100644 index 000000000..0289b63ad --- /dev/null +++ b/webclient/src/types/countries.ts @@ -0,0 +1,253 @@ +export const countryCodes = [ + 'AF', + 'AX', + 'AL', + 'DZ', + 'AS', + 'AD', + 'AO', + 'AI', + 'AQ', + 'AG', + 'AR', + 'AM', + 'AW', + 'AU', + 'AT', + 'AZ', + 'BH', + 'BS', + 'BD', + 'BB', + 'BY', + 'BE', + 'BZ', + 'BJ', + 'BM', + 'BT', + 'BO', + 'BQ', + 'BA', + 'BW', + 'BV', + 'BR', + 'IO', + 'BN', + 'BG', + 'BF', + 'BI', + 'KH', + 'CM', + 'CA', + 'CV', + 'KY', + 'CF', + 'TD', + 'CL', + 'CN', + 'CX', + 'CC', + 'CO', + 'KM', + 'CG', + 'CD', + 'CK', + 'CR', + 'CI', + 'HR', + 'CU', + 'CW', + 'CY', + 'CZ', + 'DK', + 'DJ', + 'DM', + 'DO', + 'EC', + 'EG', + 'SV', + 'GQ', + 'ER', + 'EE', + 'ET', + 'EU', + 'FK', + 'FO', + 'FJ', + 'FI', + 'FR', + 'GF', + 'PF', + 'TF', + 'GA', + 'GM', + 'GE', + 'DE', + 'GH', + 'GI', + 'GR', + 'GL', + 'GD', + 'GP', + 'GU', + 'GT', + 'GG', + 'GN', + 'GW', + 'GY', + 'HT', + 'HM', + 'VA', + 'HN', + 'HK', + 'HU', + 'IS', + 'IN', + 'ID', + 'IR', + 'IQ', + 'IE', + 'IM', + 'IL', + 'IT', + 'JM', + 'JP', + 'JE', + 'JO', + 'KZ', + 'KE', + 'KI', + 'KP', + 'KR', + 'KW', + 'KG', + 'LA', + 'LV', + 'LB', + 'LS', + 'LR', + 'LY', + 'LI', + 'LT', + 'LU', + 'MO', + 'MK', + 'MG', + 'MW', + 'MY', + 'MV', + 'ML', + 'MT', + 'MH', + 'MQ', + 'MR', + 'MU', + 'YT', + 'MX', + 'FM', + 'MD', + 'MC', + 'MN', + 'ME', + 'MS', + 'MA', + 'MZ', + 'MM', + 'NA', + 'NR', + 'NP', + 'NL', + 'NC', + 'NZ', + 'NI', + 'NE', + 'NG', + 'NU', + 'NF', + 'MP', + 'NO', + 'OM', + 'PK', + 'PW', + 'PS', + 'PA', + 'PG', + 'PY', + 'PE', + 'PH', + 'PN', + 'PL', + 'PT', + 'PR', + 'QA', + 'RE', + 'RO', + 'RU', + 'RW', + 'BL', + 'SH', + 'KN', + 'LC', + 'MF', + 'PM', + 'VC', + 'WS', + 'SM', + 'ST', + 'SA', + 'SN', + 'RS', + 'SC', + 'SL', + 'SG', + 'SX', + 'SK', + 'SI', + 'SB', + 'SO', + 'ZA', + 'GS', + 'SS', + 'ES', + 'LK', + 'SD', + 'SR', + 'SJ', + 'SZ', + 'SE', + 'CH', + 'SY', + 'TW', + 'TJ', + 'TZ', + 'TH', + 'TL', + 'TG', + 'TK', + 'TO', + 'TT', + 'TN', + 'TR', + 'TM', + 'TC', + 'TV', + 'UG', + 'UA', + 'AE', + 'GB', + 'US', + 'UM', + 'UY', + 'UZ', + 'VU', + 'VE', + 'VN', + 'VG', + 'VI', + 'WF', + 'EH', + 'YE', + 'XK', + 'ZM', + 'ZW', +]; diff --git a/webclient/src/types/deckList.ts b/webclient/src/types/deckList.ts new file mode 100644 index 000000000..9d7d792a6 --- /dev/null +++ b/webclient/src/types/deckList.ts @@ -0,0 +1,18 @@ +export interface DeckList { + root: DeckStorageFolder; +} + +export interface DeckStorageFolder { + items: DeckStorageTreeItem[]; +} + +export interface DeckStorageFile { + creationTime: number; +} + +export interface DeckStorageTreeItem { + id: number; + name: string; + file: DeckStorageFile | null; + folder: DeckStorageFolder | null; +} diff --git a/webclient/src/types/forms.ts b/webclient/src/types/forms.ts new file mode 100644 index 000000000..421bc2615 --- /dev/null +++ b/webclient/src/types/forms.ts @@ -0,0 +1,11 @@ +export enum FormKey { + ADD_TO_BUDDIES = 'ADD_TO_BUDDIES', + ADD_TO_IGNORE = 'ADD_TO_IGNORE', + CARD_IMPORT = 'CARD_IMPORT', + CONNECT = 'CONNECT', + LOGIN = 'LOGIN', + RESET_PASSWORD_REQUEST = 'RESET_PASSWORD_REQUEST', + RESET_PASSWORD = 'RESET_PASSWORD', + REGISTER = 'REGISTER', + SEARCH_LOGS = 'SEARCH_LOGS', +} diff --git a/webclient/src/types/game.ts b/webclient/src/types/game.ts new file mode 100644 index 000000000..b9fcc1dc2 --- /dev/null +++ b/webclient/src/types/game.ts @@ -0,0 +1,42 @@ +export interface Game { + description: string; + gameId: number; + gameType: string; + gameTypes: string[]; + roomId: number; + started: boolean; +} + +export enum GameSortField { + START_TIME = 'startTime' +} + +export interface GameConfig { + description: string; + password: string; + maxPlayer: number; + onlyBuddies: boolean; + onlyRegistered: boolean; + spectatorsAllowed: boolean; + spectatorsNeedPassword: boolean; + spectatorsCanTalk: boolean; + spectatorsSeeEverything: boolean; + gameTypeIds: number[]; + joinAsJudge: boolean; + joinAsSpectator: boolean; +} + +export interface JoinGameParams { + gameId: number; + password: string; + spectator: boolean; + overrideRestrictions: boolean; + joinAsJudge: boolean; +} + +export enum LeaveGameReason { + OTHER = 1, + USER_KICKED = 2, + USER_LEFT = 3, + USER_DISCONNECTED = 4 +} diff --git a/webclient/src/types/index.ts b/webclient/src/types/index.ts new file mode 100644 index 000000000..ded9962e5 --- /dev/null +++ b/webclient/src/types/index.ts @@ -0,0 +1,19 @@ +export * from './cards'; +export * from './constants'; +export * from './countries'; +export * from './game'; +export * from './room'; +export * from './server'; +export * from './sort'; +export * from './user'; +export * from './routes'; +export * from './sort'; +export * from './forms'; +export * from './message'; +export * from './settings'; +export * from './languages'; +export * from './logs'; +export * from './session'; +export * from './deckList'; +export * from './moderator'; +export * from './replay'; diff --git a/webclient/src/types/languages.ts b/webclient/src/types/languages.ts new file mode 100644 index 000000000..17699fd0e --- /dev/null +++ b/webclient/src/types/languages.ts @@ -0,0 +1,20 @@ +export enum Language { + 'en-US' = 'en-US', + 'fr' = 'fr', + 'nl' = 'nl', + 'pt_BR' = 'pt_BR', +} + +export enum LanguageCountry { + 'en-US' = 'us', + 'fr' = 'fr', + 'nl' = 'nl', + 'pt_BR' = 'br' +} + +export enum LanguageNative { + 'en-US' = 'English - US', + 'fr' = 'Français', + 'nl' = 'Nederlands', + 'pt_BR' = 'Portugues do Brasil', +} diff --git a/webclient/src/types/logs.ts b/webclient/src/types/logs.ts new file mode 100644 index 000000000..3cf34b486 --- /dev/null +++ b/webclient/src/types/logs.ts @@ -0,0 +1,10 @@ +export interface LogFilters { + userName?: string; + ipAddress?: string; + gameName?: string; + gameId?: string; + message?: string; + logLocation?: string[]; + dateRange: number; + maximumResults?: number; +} diff --git a/webclient/src/types/message.ts b/webclient/src/types/message.ts new file mode 100644 index 000000000..d7f256011 --- /dev/null +++ b/webclient/src/types/message.ts @@ -0,0 +1,7 @@ +export interface Message { + name: string; + message: string; + messageType: number; + timeOf: number; + timeReceived: number; +} diff --git a/webclient/src/types/moderator.ts b/webclient/src/types/moderator.ts new file mode 100644 index 000000000..5991ec6c1 --- /dev/null +++ b/webclient/src/types/moderator.ts @@ -0,0 +1,21 @@ +export interface BanHistoryItem { + adminId: string; + adminName: string; + banTime: string; + banLength: string; + banReason: string; + visibleReason: string; +} + +export interface WarnHistoryItem { + userName: string; + adminName: string; + reason: string; + timeOf: string; +} + +export interface WarnListItem { + warning: string; + userName: string; + userClientid: string; +} diff --git a/webclient/src/types/replay.ts b/webclient/src/types/replay.ts new file mode 100644 index 000000000..dfa78538a --- /dev/null +++ b/webclient/src/types/replay.ts @@ -0,0 +1,16 @@ +export interface Replay { + replayId: number; + replayName: string; + duration: number; +} + +export interface ReplayMatch { + replayList: Replay[]; + gameId: number; + roomName: string; + timeStarted: number; + length: number; + gameName: string; + playerNames: string[]; + doNotHide: boolean; +} diff --git a/webclient/src/types/room.ts b/webclient/src/types/room.ts new file mode 100644 index 000000000..e69b1a499 --- /dev/null +++ b/webclient/src/types/room.ts @@ -0,0 +1,23 @@ +import { User } from './user'; + +export interface Room { + autoJoin: boolean + description: string; + gameCount: number; + gameList: any[]; + gametypeList: any[]; + gametypeMap: GametypeMap; + name: string; + permissionlevel: RoomAccessLevel; + playerCount: number; + privilegelevel: RoomAccessLevel; + roomId: number; + userList: User[]; + order: number; +} + +export interface GametypeMap { [index: number]: string } + +export enum RoomAccessLevel { + 'none' +} diff --git a/webclient/src/types/routes.ts b/webclient/src/types/routes.ts new file mode 100644 index 000000000..d91e8a7cb --- /dev/null +++ b/webclient/src/types/routes.ts @@ -0,0 +1,15 @@ +export enum RouteEnum { + PLAYER = '/player/:name', + SERVER = '/server', + ROOM = '/room/:roomId', + LOGIN = '/login', + LOGS = '/logs', + GAME = '/game', + DECKS = '/decks', + DECK = '/deck', + ACCOUNT = '/account', + ADMINISTRATION = '/administration', + REPLAYS = '/replays', + INITIALIZE = '/initialize', + UNSUPPORTED = '/unsupported', +} diff --git a/webclient/src/types/server.ts b/webclient/src/types/server.ts new file mode 100644 index 000000000..305a5a810 --- /dev/null +++ b/webclient/src/types/server.ts @@ -0,0 +1,127 @@ +export interface ServerStatus { + status: StatusEnum; + description: string; +} + +export enum StatusEnum { + DISCONNECTED, + CONNECTING, + CONNECTED, + LOGGING_IN, + LOGGED_IN, + DISCONNECTING = 99 +} + +export interface WebSocketConnectOptions { + host?: string; + port?: string; + userName?: string; + password?: string; + hashedPassword?: string; + newPassword?: string; + token?: string; + email?: string; + realName?: string; + country?: string; + autojoinrooms?: boolean; + keepalive?: number; + clientid?: string; + reason?: WebSocketConnectReason; +} + +export enum WebSocketConnectReason { + LOGIN, + REGISTER, + ACTIVATE_ACCOUNT, + PASSWORD_RESET_REQUEST, + PASSWORD_RESET_CHALLENGE, + PASSWORD_RESET, + TEST_CONNECTION, +} + +export class Host { + id?: number; + name: string; + host: string; + port: string; + localHost?: string; + localPort?: string; + editable: boolean; + lastSelected?: boolean; + userName?: string; + hashedPassword?: string; + remember?: boolean; +} + +export const DefaultHosts: Host[] = [ + { + name: 'Chickatrice', + host: 'mtg.chickatrice.net', + port: '443', + localPort: '4748', + editable: false, + }, + { + name: 'Rooster', + host: 'server.cockatrice.us/servatrice', + port: '4748', + localHost: 'server.cockatrice.us', + editable: false, + }, + { + name: 'Rooster Beta', + host: 'beta.cockatrice.us/servatrice', + port: '4748', + localHost: 'beta.cockatrice.us', + editable: false, + }, + { + name: 'Tetrarch', + host: 'mtg.tetrarch.co/servatrice', + port: '443', + editable: false, + }, +]; + +export const getHostPort = (host: Host): { host: string, port: string } => { + const isLocal = window.location.hostname === 'localhost'; + + if (!host) { + return { + host: '', + port: '' + }; + } + + return { + host: !isLocal ? host.host : host.localHost || host.host, + port: !isLocal ? host.port : host.localPort || host.port, + } +}; + +export enum KnownHost { + ROOSTER = 'Rooster', + TETRARCH = 'Tetrarch', +} + +export const KnownHosts = { + [KnownHost.ROOSTER]: { port: 4748, host: 'server.cockatrice.us', }, + [KnownHost.TETRARCH]: { port: 443, host: 'mtg.tetrarch.co/servatrice' }, +} + +export interface LogItem { + message: string; + senderId: string; + senderIp: string; + senderName: string; + targetId: string; + targetName: string; + targetType: string; + time: string; +} + +export interface LogGroups { + room: LogItem[]; + game: LogItem[]; + chat: LogItem[]; +} diff --git a/webclient/src/types/session.ts b/webclient/src/types/session.ts new file mode 100644 index 000000000..1b49616c5 --- /dev/null +++ b/webclient/src/types/session.ts @@ -0,0 +1,7 @@ +export enum NotificationType { + UNKNOWN = 0, + PROMOTED = 1, + WARNING = 2, + IDLEWARNING = 3, + CUSTOM = 4, +}; diff --git a/webclient/src/types/settings.ts b/webclient/src/types/settings.ts new file mode 100644 index 000000000..c91394331 --- /dev/null +++ b/webclient/src/types/settings.ts @@ -0,0 +1,6 @@ +export class Setting { + user: string; + autoConnect?: boolean; +} + +export const APP_USER = '*app'; diff --git a/webclient/src/types/sort.ts b/webclient/src/types/sort.ts new file mode 100644 index 000000000..c3312732c --- /dev/null +++ b/webclient/src/types/sort.ts @@ -0,0 +1,9 @@ +export enum SortDirection { + ASC = 'ASC', + DESC = 'DESC' +} + +export interface SortBy { + field: string; + order: SortDirection; +} diff --git a/webclient/src/types/user.ts b/webclient/src/types/user.ts new file mode 100644 index 000000000..2d0383eb7 --- /dev/null +++ b/webclient/src/types/user.ts @@ -0,0 +1,28 @@ +export interface User { + accountageSecs: number; + name: string; + privlevel: UserPrivLevel; + userLevel: number; + realName?: string; + country?: string; + avatarBmp?: Uint8Array; +} + +export enum UserLevelFlag { + IsNothing = 0, + IsUser = 1, + IsRegistered = 2, + IsModerator = 4, + IsAdmin = 8, + IsJudge = 16, +} + +export enum UserPrivLevel { + NONE = 0, + VIP = 1, + DONOR = 2 +} + +export enum UserSortField { + NAME = 'name' +} diff --git a/webclient/src/websocket/WebClient.ts b/webclient/src/websocket/WebClient.ts new file mode 100644 index 000000000..1dae24d0c --- /dev/null +++ b/webclient/src/websocket/WebClient.ts @@ -0,0 +1,89 @@ +import { StatusEnum, WebSocketConnectOptions } from 'types'; + +import { ProtobufService } from './services/ProtobufService'; +import { WebSocketService } from './services/WebSocketService'; + +import { RoomPersistence, SessionPersistence } from './persistence'; + +export class WebClient { + public socket = new WebSocketService(this); + public protobuf = new ProtobufService(this); + + public protocolVersion = 14; + public clientConfig = { + clientid: 'webatrice', + clientver: 'webclient-1.0 (2019-10-31)', + clientfeatures: [ + 'client_id', + 'client_ver', + 'feature_set', + 'room_chat_history', + 'client_warnings', + /* unimplemented features */ + 'forgot_password', + 'idle_client', + 'mod_log_lookup', + 'user_ban_history', + // satisfy server reqs for POC + 'websocket', + '2.7.0_min_version', + '2.8.0_min_version' + ] + }; + + public clientOptions = { + autojoinrooms: true, + keepalive: 5000 + }; + + public options: WebSocketConnectOptions; + public status: StatusEnum; + + public connectionAttemptMade = false; + + constructor() { + this.socket.message$.subscribe((message: MessageEvent) => { + this.protobuf.handleMessageEvent(message); + }); + + if (process.env.NODE_ENV !== 'test') { + console.log(this); + } + } + + public connect(options: WebSocketConnectOptions) { + this.connectionAttemptMade = true; + this.options = options; + this.socket.connect(options); + } + + public testConnect(options: WebSocketConnectOptions) { + this.socket.testConnect(options); + } + + public disconnect() { + this.socket.disconnect(); + } + + public updateStatus(status: StatusEnum) { + this.status = status; + + if (status === StatusEnum.DISCONNECTED) { + this.protobuf.resetCommands(); + this.clearStores(); + } + } + + public keepAlive(pingReceived: Function) { + this.protobuf.sendKeepAliveCommand(pingReceived); + } + + private clearStores() { + RoomPersistence.clearStore(); + SessionPersistence.clearStore(); + } +} + +const webClient = new WebClient(); + +export default webClient; diff --git a/webclient/src/websocket/commands/admin/adjustMod.ts b/webclient/src/websocket/commands/admin/adjustMod.ts new file mode 100644 index 000000000..fd71fece1 --- /dev/null +++ b/webclient/src/websocket/commands/admin/adjustMod.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { AdminPersistence } from '../../persistence'; + +export function adjustMod(userName: string, shouldBeMod?: boolean, shouldBeJudge?: boolean): void { + BackendService.sendAdminCommand('Command_AdjustMod', { userName, shouldBeMod, shouldBeJudge }, { + onSuccess: () => { + AdminPersistence.adjustMod(userName, shouldBeMod, shouldBeJudge); + }, + }); +} diff --git a/webclient/src/websocket/commands/admin/index.ts b/webclient/src/websocket/commands/admin/index.ts new file mode 100644 index 000000000..5d4ae21bf --- /dev/null +++ b/webclient/src/websocket/commands/admin/index.ts @@ -0,0 +1,4 @@ +export * from './adjustMod'; +export * from './reloadConfig'; +export * from './shutdownServer'; +export * from './updateServerMessage'; diff --git a/webclient/src/websocket/commands/admin/reloadConfig.ts b/webclient/src/websocket/commands/admin/reloadConfig.ts new file mode 100644 index 000000000..979f3ec73 --- /dev/null +++ b/webclient/src/websocket/commands/admin/reloadConfig.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { AdminPersistence } from '../../persistence'; + +export function reloadConfig(): void { + BackendService.sendAdminCommand('Command_ReloadConfig', {}, { + onSuccess: () => { + AdminPersistence.reloadConfig(); + }, + }); +} diff --git a/webclient/src/websocket/commands/admin/shutdownServer.ts b/webclient/src/websocket/commands/admin/shutdownServer.ts new file mode 100644 index 000000000..e65c900db --- /dev/null +++ b/webclient/src/websocket/commands/admin/shutdownServer.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { AdminPersistence } from '../../persistence'; + +export function shutdownServer(reason: string, minutes: number): void { + BackendService.sendAdminCommand('Command_ShutdownServer', { reason, minutes }, { + onSuccess: () => { + AdminPersistence.shutdownServer(); + }, + }); +} diff --git a/webclient/src/websocket/commands/admin/updateServerMessage.ts b/webclient/src/websocket/commands/admin/updateServerMessage.ts new file mode 100644 index 000000000..e2b194514 --- /dev/null +++ b/webclient/src/websocket/commands/admin/updateServerMessage.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { AdminPersistence } from '../../persistence'; + +export function updateServerMessage(): void { + BackendService.sendAdminCommand('Command_UpdateServerMessage', {}, { + onSuccess: () => { + AdminPersistence.updateServerMessage(); + }, + }); +} diff --git a/webclient/src/websocket/commands/index.ts b/webclient/src/websocket/commands/index.ts new file mode 100644 index 000000000..2c68fcfb9 --- /dev/null +++ b/webclient/src/websocket/commands/index.ts @@ -0,0 +1,4 @@ +export * as AdminCommands from './admin'; +export * as ModeratorCommands from './moderator'; +export * as RoomCommands from './room'; +export * as SessionCommands from './session'; diff --git a/webclient/src/websocket/commands/moderator/banFromServer.ts b/webclient/src/websocket/commands/moderator/banFromServer.ts new file mode 100644 index 000000000..e45e34504 --- /dev/null +++ b/webclient/src/websocket/commands/moderator/banFromServer.ts @@ -0,0 +1,13 @@ +import { BackendService } from '../../services/BackendService'; +import { ModeratorPersistence } from '../../persistence'; + +export function banFromServer(minutes: number, userName?: string, address?: string, reason?: string, + visibleReason?: string, clientid?: string, removeMessages?: number): void { + BackendService.sendModeratorCommand('Command_BanFromServer', { + minutes, userName, address, reason, visibleReason, clientid, removeMessages + }, { + onSuccess: () => { + ModeratorPersistence.banFromServer(userName); + }, + }); +} diff --git a/webclient/src/websocket/commands/moderator/forceActivateUser.ts b/webclient/src/websocket/commands/moderator/forceActivateUser.ts new file mode 100644 index 000000000..d4138a015 --- /dev/null +++ b/webclient/src/websocket/commands/moderator/forceActivateUser.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { ModeratorPersistence } from '../../persistence'; + +export function forceActivateUser(usernameToActivate: string, moderatorName: string): void { + BackendService.sendModeratorCommand('Command_ForceActivateUser', { usernameToActivate, moderatorName }, { + onSuccess: () => { + ModeratorPersistence.forceActivateUser(usernameToActivate, moderatorName); + }, + }); +} diff --git a/webclient/src/websocket/commands/moderator/getAdminNotes.ts b/webclient/src/websocket/commands/moderator/getAdminNotes.ts new file mode 100644 index 000000000..d4f626aa2 --- /dev/null +++ b/webclient/src/websocket/commands/moderator/getAdminNotes.ts @@ -0,0 +1,11 @@ +import { BackendService } from '../../services/BackendService'; +import { ModeratorPersistence } from '../../persistence'; + +export function getAdminNotes(userName: string): void { + BackendService.sendModeratorCommand('Command_GetAdminNotes', { userName }, { + responseName: 'Response_GetAdminNotes', + onSuccess: (response) => { + ModeratorPersistence.getAdminNotes(userName, response.notes); + }, + }); +} diff --git a/webclient/src/websocket/commands/moderator/getBanHistory.ts b/webclient/src/websocket/commands/moderator/getBanHistory.ts new file mode 100644 index 000000000..dd4e90eda --- /dev/null +++ b/webclient/src/websocket/commands/moderator/getBanHistory.ts @@ -0,0 +1,11 @@ +import { BackendService } from '../../services/BackendService'; +import { ModeratorPersistence } from '../../persistence'; + +export function getBanHistory(userName: string): void { + BackendService.sendModeratorCommand('Command_GetBanHistory', { userName }, { + responseName: 'Response_BanHistory', + onSuccess: (response) => { + ModeratorPersistence.banHistory(userName, response.banList); + }, + }); +} diff --git a/webclient/src/websocket/commands/moderator/getWarnHistory.ts b/webclient/src/websocket/commands/moderator/getWarnHistory.ts new file mode 100644 index 000000000..c47e2c6e4 --- /dev/null +++ b/webclient/src/websocket/commands/moderator/getWarnHistory.ts @@ -0,0 +1,11 @@ +import { BackendService } from '../../services/BackendService'; +import { ModeratorPersistence } from '../../persistence'; + +export function getWarnHistory(userName: string): void { + BackendService.sendModeratorCommand('Command_GetWarnHistory', { userName }, { + responseName: 'Response_WarnHistory', + onSuccess: (response) => { + ModeratorPersistence.warnHistory(userName, response.warnList); + }, + }); +} diff --git a/webclient/src/websocket/commands/moderator/getWarnList.ts b/webclient/src/websocket/commands/moderator/getWarnList.ts new file mode 100644 index 000000000..412aee09e --- /dev/null +++ b/webclient/src/websocket/commands/moderator/getWarnList.ts @@ -0,0 +1,11 @@ +import { BackendService } from '../../services/BackendService'; +import { ModeratorPersistence } from '../../persistence'; + +export function getWarnList(modName: string, userName: string, userClientid: string): void { + BackendService.sendModeratorCommand('Command_GetWarnList', { modName, userName, userClientid }, { + responseName: 'Response_WarnList', + onSuccess: (response) => { + ModeratorPersistence.warnListOptions(response.warning); + }, + }); +} diff --git a/webclient/src/websocket/commands/moderator/grantReplayAccess.ts b/webclient/src/websocket/commands/moderator/grantReplayAccess.ts new file mode 100644 index 000000000..74d64d17a --- /dev/null +++ b/webclient/src/websocket/commands/moderator/grantReplayAccess.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { ModeratorPersistence } from '../../persistence'; + +export function grantReplayAccess(replayId: number, moderatorName: string): void { + BackendService.sendModeratorCommand('Command_GrantReplayAccess', { replayId, moderatorName }, { + onSuccess: () => { + ModeratorPersistence.grantReplayAccess(replayId, moderatorName); + }, + }); +} diff --git a/webclient/src/websocket/commands/moderator/index.ts b/webclient/src/websocket/commands/moderator/index.ts new file mode 100644 index 000000000..10bb0e1c6 --- /dev/null +++ b/webclient/src/websocket/commands/moderator/index.ts @@ -0,0 +1,10 @@ +export * from './banFromServer'; +export * from './forceActivateUser'; +export * from './getAdminNotes'; +export * from './getBanHistory'; +export * from './getWarnHistory'; +export * from './getWarnList'; +export * from './grantReplayAccess'; +export * from './updateAdminNotes'; +export * from './viewLogHistory'; +export * from './warnUser'; diff --git a/webclient/src/websocket/commands/moderator/updateAdminNotes.ts b/webclient/src/websocket/commands/moderator/updateAdminNotes.ts new file mode 100644 index 000000000..c7ac315c5 --- /dev/null +++ b/webclient/src/websocket/commands/moderator/updateAdminNotes.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { ModeratorPersistence } from '../../persistence'; + +export function updateAdminNotes(userName: string, notes: string): void { + BackendService.sendModeratorCommand('Command_UpdateAdminNotes', { userName, notes }, { + onSuccess: () => { + ModeratorPersistence.updateAdminNotes(userName, notes); + }, + }); +} diff --git a/webclient/src/websocket/commands/moderator/viewLogHistory.ts b/webclient/src/websocket/commands/moderator/viewLogHistory.ts new file mode 100644 index 000000000..19a930608 --- /dev/null +++ b/webclient/src/websocket/commands/moderator/viewLogHistory.ts @@ -0,0 +1,12 @@ +import { BackendService } from '../../services/BackendService'; +import { ModeratorPersistence } from '../../persistence'; +import { LogFilters } from 'types'; + +export function viewLogHistory(filters: LogFilters): void { + BackendService.sendModeratorCommand('Command_ViewLogHistory', filters, { + responseName: 'Response_ViewLogHistory', + onSuccess: (response) => { + ModeratorPersistence.viewLogs(response.logMessage); + }, + }); +} diff --git a/webclient/src/websocket/commands/moderator/warnUser.ts b/webclient/src/websocket/commands/moderator/warnUser.ts new file mode 100644 index 000000000..0e0271d4b --- /dev/null +++ b/webclient/src/websocket/commands/moderator/warnUser.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { ModeratorPersistence } from '../../persistence'; + +export function warnUser(userName: string, reason: string, clientid?: string, removeMessages?: number): void { + BackendService.sendModeratorCommand('Command_WarnUser', { userName, reason, clientid, removeMessages }, { + onSuccess: () => { + ModeratorPersistence.warnUser(userName); + }, + }); +} diff --git a/webclient/src/websocket/commands/room/createGame.ts b/webclient/src/websocket/commands/room/createGame.ts new file mode 100644 index 000000000..62565e0e6 --- /dev/null +++ b/webclient/src/websocket/commands/room/createGame.ts @@ -0,0 +1,11 @@ +import { BackendService } from '../../services/BackendService'; +import { RoomPersistence } from '../../persistence'; +import { GameConfig } from 'types'; + +export function createGame(roomId: number, gameConfig: GameConfig): void { + BackendService.sendRoomCommand(roomId, 'Command_CreateGame', gameConfig, { + onSuccess: () => { + RoomPersistence.gameCreated(roomId); + }, + }); +} diff --git a/webclient/src/websocket/commands/room/index.ts b/webclient/src/websocket/commands/room/index.ts new file mode 100644 index 000000000..18235618c --- /dev/null +++ b/webclient/src/websocket/commands/room/index.ts @@ -0,0 +1,4 @@ +export * from './createGame'; +export * from './joinGame'; +export * from './leaveRoom'; +export * from './roomSay'; diff --git a/webclient/src/websocket/commands/room/joinGame.ts b/webclient/src/websocket/commands/room/joinGame.ts new file mode 100644 index 000000000..ef4b1fff2 --- /dev/null +++ b/webclient/src/websocket/commands/room/joinGame.ts @@ -0,0 +1,11 @@ +import { BackendService } from '../../services/BackendService'; +import { RoomPersistence } from '../../persistence'; +import { JoinGameParams } from 'types'; + +export function joinGame(roomId: number, joinGameParams: JoinGameParams): void { + BackendService.sendRoomCommand(roomId, 'Command_JoinGame', joinGameParams, { + onSuccess: () => { + RoomPersistence.joinedGame(roomId, joinGameParams.gameId); + }, + }); +} diff --git a/webclient/src/websocket/commands/room/leaveRoom.ts b/webclient/src/websocket/commands/room/leaveRoom.ts new file mode 100644 index 000000000..7cd64a0e2 --- /dev/null +++ b/webclient/src/websocket/commands/room/leaveRoom.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { RoomPersistence } from '../../persistence'; + +export function leaveRoom(roomId: number): void { + BackendService.sendRoomCommand(roomId, 'Command_LeaveRoom', {}, { + onSuccess: () => { + RoomPersistence.leaveRoom(roomId); + }, + }); +} diff --git a/webclient/src/websocket/commands/room/roomSay.ts b/webclient/src/websocket/commands/room/roomSay.ts new file mode 100644 index 000000000..a429845be --- /dev/null +++ b/webclient/src/websocket/commands/room/roomSay.ts @@ -0,0 +1,11 @@ +import { BackendService } from '../../services/BackendService'; + +export function roomSay(roomId: number, message: string): void { + const trimmed = message.trim(); + + if (!trimmed) { + return; + } + + BackendService.sendRoomCommand(roomId, 'Command_RoomSay', { message: trimmed }, {}); +} diff --git a/webclient/src/websocket/commands/session/accountEdit.ts b/webclient/src/websocket/commands/session/accountEdit.ts new file mode 100644 index 000000000..31bf2d3f6 --- /dev/null +++ b/webclient/src/websocket/commands/session/accountEdit.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function accountEdit(passwordCheck: string, realName?: string, email?: string, country?: string): void { + BackendService.sendSessionCommand('Command_AccountEdit', { passwordCheck, realName, email, country }, { + onSuccess: () => { + SessionPersistence.accountEditChanged(realName, email, country); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/accountImage.ts b/webclient/src/websocket/commands/session/accountImage.ts new file mode 100644 index 000000000..cd0e24403 --- /dev/null +++ b/webclient/src/websocket/commands/session/accountImage.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function accountImage(image: Uint8Array): void { + BackendService.sendSessionCommand('Command_AccountImage', { image }, { + onSuccess: () => { + SessionPersistence.accountImageChanged(image); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/accountPassword.ts b/webclient/src/websocket/commands/session/accountPassword.ts new file mode 100644 index 000000000..81c7a993b --- /dev/null +++ b/webclient/src/websocket/commands/session/accountPassword.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function accountPassword(oldPassword: string, newPassword: string, hashedNewPassword: string): void { + BackendService.sendSessionCommand('Command_AccountPassword', { oldPassword, newPassword, hashedNewPassword }, { + onSuccess: () => { + SessionPersistence.accountPasswordChange(); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/activate.ts b/webclient/src/websocket/commands/session/activate.ts new file mode 100644 index 000000000..4cd0e8c4e --- /dev/null +++ b/webclient/src/websocket/commands/session/activate.ts @@ -0,0 +1,31 @@ +import { AccountActivationParams } from 'store'; +import { StatusEnum, WebSocketConnectOptions } from 'types'; + +import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; +import { ProtoController } from '../../services/ProtoController'; +import { SessionPersistence } from '../../persistence'; + +import { disconnect, login, updateStatus } from './'; + +export function activate(options: WebSocketConnectOptions, passwordSalt?: string): void { + const { userName, token } = options as unknown as AccountActivationParams; + + BackendService.sendSessionCommand('Command_Activate', { + ...webClient.clientConfig, + userName, + token, + }, { + onResponseCode: { + [ProtoController.root.Response.ResponseCode.RespActivationAccepted]: () => { + SessionPersistence.accountActivationSuccess(); + login(options, passwordSalt); + }, + }, + onError: () => { + updateStatus(StatusEnum.DISCONNECTED, 'Account Activation Failed'); + disconnect(); + SessionPersistence.accountActivationFailed(); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/addToList.ts b/webclient/src/websocket/commands/session/addToList.ts new file mode 100644 index 000000000..c5bc3c5f0 --- /dev/null +++ b/webclient/src/websocket/commands/session/addToList.ts @@ -0,0 +1,18 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function addToBuddyList(userName: string): void { + addToList('buddy', userName); +} + +export function addToIgnoreList(userName: string): void { + addToList('ignore', userName); +} + +export function addToList(list: string, userName: string): void { + BackendService.sendSessionCommand('Command_AddToList', { list, userName }, { + onSuccess: () => { + SessionPersistence.addToList(list, userName); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/connect.ts b/webclient/src/websocket/commands/session/connect.ts new file mode 100644 index 000000000..5b660fab1 --- /dev/null +++ b/webclient/src/websocket/commands/session/connect.ts @@ -0,0 +1,24 @@ +import { StatusEnum, WebSocketConnectOptions, WebSocketConnectReason } from 'types'; +import webClient from '../../WebClient'; +import { updateStatus } from './'; + +export function connect(options: WebSocketConnectOptions, reason: WebSocketConnectReason): void { + switch (reason) { + case WebSocketConnectReason.LOGIN: + case WebSocketConnectReason.REGISTER: + case WebSocketConnectReason.ACTIVATE_ACCOUNT: + case WebSocketConnectReason.PASSWORD_RESET_REQUEST: + case WebSocketConnectReason.PASSWORD_RESET_CHALLENGE: + case WebSocketConnectReason.PASSWORD_RESET: + updateStatus(StatusEnum.CONNECTING, 'Connecting...'); + break; + case WebSocketConnectReason.TEST_CONNECTION: + webClient.testConnect({ ...options }); + return; + default: + updateStatus(StatusEnum.DISCONNECTED, 'Unknown Connection Attempt: ' + reason); + return; + } + + webClient.connect({ ...options, reason }); +} diff --git a/webclient/src/websocket/commands/session/deckDel.ts b/webclient/src/websocket/commands/session/deckDel.ts new file mode 100644 index 000000000..752ce78d5 --- /dev/null +++ b/webclient/src/websocket/commands/session/deckDel.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function deckDel(deckId: number): void { + BackendService.sendSessionCommand('Command_DeckDel', { deckId }, { + onSuccess: () => { + SessionPersistence.deleteServerDeck(deckId); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/deckDelDir.ts b/webclient/src/websocket/commands/session/deckDelDir.ts new file mode 100644 index 000000000..df5bbc223 --- /dev/null +++ b/webclient/src/websocket/commands/session/deckDelDir.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function deckDelDir(path: string): void { + BackendService.sendSessionCommand('Command_DeckDelDir', { path }, { + onSuccess: () => { + SessionPersistence.deleteServerDeckDir(path); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/deckList.ts b/webclient/src/websocket/commands/session/deckList.ts new file mode 100644 index 000000000..3d5a3499a --- /dev/null +++ b/webclient/src/websocket/commands/session/deckList.ts @@ -0,0 +1,11 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function deckList(): void { + BackendService.sendSessionCommand('Command_DeckList', {}, { + responseName: 'Response_DeckList', + onSuccess: (response) => { + SessionPersistence.updateServerDecks(response); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/deckNewDir.ts b/webclient/src/websocket/commands/session/deckNewDir.ts new file mode 100644 index 000000000..85ab16afb --- /dev/null +++ b/webclient/src/websocket/commands/session/deckNewDir.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function deckNewDir(path: string, dirName: string): void { + BackendService.sendSessionCommand('Command_DeckNewDir', { path, dirName }, { + onSuccess: () => { + SessionPersistence.createServerDeckDir(path, dirName); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/deckUpload.ts b/webclient/src/websocket/commands/session/deckUpload.ts new file mode 100644 index 000000000..2679c4e8e --- /dev/null +++ b/webclient/src/websocket/commands/session/deckUpload.ts @@ -0,0 +1,11 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function deckUpload(path: string, deckId: number, deckList: string): void { + BackendService.sendSessionCommand('Command_DeckUpload', { path, deckId, deckList }, { + responseName: 'Response_DeckUpload', + onSuccess: (response) => { + SessionPersistence.uploadServerDeck(path, response.newFile); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/disconnect.ts b/webclient/src/websocket/commands/session/disconnect.ts new file mode 100644 index 000000000..9fe567677 --- /dev/null +++ b/webclient/src/websocket/commands/session/disconnect.ts @@ -0,0 +1,5 @@ +import webClient from '../../WebClient'; + +export function disconnect(): void { + webClient.disconnect(); +} diff --git a/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts b/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts new file mode 100644 index 000000000..05af1ccf9 --- /dev/null +++ b/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts @@ -0,0 +1,28 @@ +import { ForgotPasswordChallengeParams } from 'store'; +import { StatusEnum, WebSocketConnectOptions } from 'types'; + +import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; +import { disconnect, updateStatus } from './'; + +export function forgotPasswordChallenge(options: WebSocketConnectOptions): void { + const { userName, email } = options as unknown as ForgotPasswordChallengeParams; + + BackendService.sendSessionCommand('Command_ForgotPasswordChallenge', { + ...webClient.clientConfig, + userName, + email, + }, { + onSuccess: () => { + updateStatus(StatusEnum.DISCONNECTED, null); + SessionPersistence.resetPassword(); + disconnect(); + }, + onError: () => { + updateStatus(StatusEnum.DISCONNECTED, null); + SessionPersistence.resetPasswordFailed(); + disconnect(); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/forgotPasswordRequest.ts b/webclient/src/websocket/commands/session/forgotPasswordRequest.ts new file mode 100644 index 000000000..23d301450 --- /dev/null +++ b/webclient/src/websocket/commands/session/forgotPasswordRequest.ts @@ -0,0 +1,34 @@ +import { ForgotPasswordParams } from 'store'; +import { StatusEnum, WebSocketConnectOptions } from 'types'; + +import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +import { disconnect, updateStatus } from './'; + +export function forgotPasswordRequest(options: WebSocketConnectOptions): void { + const { userName } = options as unknown as ForgotPasswordParams; + + BackendService.sendSessionCommand('Command_ForgotPasswordRequest', { + ...webClient.clientConfig, + userName, + }, { + responseName: 'Response_ForgotPasswordRequest', + onSuccess: (resp) => { + if (resp?.challengeEmail) { + updateStatus(StatusEnum.DISCONNECTED, null); + SessionPersistence.resetPasswordChallenge(); + } else { + updateStatus(StatusEnum.DISCONNECTED, null); + SessionPersistence.resetPassword(); + } + disconnect(); + }, + onError: () => { + updateStatus(StatusEnum.DISCONNECTED, null); + SessionPersistence.resetPasswordFailed(); + disconnect(); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/forgotPasswordReset.ts b/webclient/src/websocket/commands/session/forgotPasswordReset.ts new file mode 100644 index 000000000..d9a775816 --- /dev/null +++ b/webclient/src/websocket/commands/session/forgotPasswordReset.ts @@ -0,0 +1,38 @@ +import { ForgotPasswordResetParams } from 'store'; +import { StatusEnum, WebSocketConnectOptions } from 'types'; + +import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; +import { hashPassword } from '../../utils'; + +import { disconnect, updateStatus } from '.'; + +export function forgotPasswordReset(options: WebSocketConnectOptions, passwordSalt?: string): void { + const { userName, token, newPassword } = options as unknown as ForgotPasswordResetParams; + + const params: any = { + ...webClient.clientConfig, + userName, + token, + }; + + if (passwordSalt) { + params.hashedNewPassword = hashPassword(passwordSalt, newPassword); + } else { + params.newPassword = newPassword; + } + + BackendService.sendSessionCommand('Command_ForgotPasswordReset', params, { + onSuccess: () => { + updateStatus(StatusEnum.DISCONNECTED, null); + SessionPersistence.resetPasswordSuccess(); + disconnect(); + }, + onError: () => { + updateStatus(StatusEnum.DISCONNECTED, null); + SessionPersistence.resetPasswordFailed(); + disconnect(); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/getGamesOfUser.ts b/webclient/src/websocket/commands/session/getGamesOfUser.ts new file mode 100644 index 000000000..8fb8aeb5b --- /dev/null +++ b/webclient/src/websocket/commands/session/getGamesOfUser.ts @@ -0,0 +1,11 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function getGamesOfUser(userName: string): void { + BackendService.sendSessionCommand('Command_GetGamesOfUser', { userName }, { + responseName: 'Response_GetGamesOfUser', + onSuccess: (response) => { + SessionPersistence.getGamesOfUser(userName, response); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/getUserInfo.ts b/webclient/src/websocket/commands/session/getUserInfo.ts new file mode 100644 index 000000000..5b0f178ae --- /dev/null +++ b/webclient/src/websocket/commands/session/getUserInfo.ts @@ -0,0 +1,11 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function getUserInfo(userName: string): void { + BackendService.sendSessionCommand('Command_GetUserInfo', { userName }, { + responseName: 'Response_GetUserInfo', + onSuccess: (response) => { + SessionPersistence.getUserInfo(response.userInfo); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/index.ts b/webclient/src/websocket/commands/session/index.ts new file mode 100644 index 000000000..74d0d062c --- /dev/null +++ b/webclient/src/websocket/commands/session/index.ts @@ -0,0 +1,30 @@ +export * from './accountEdit'; +export * from './accountImage'; +export * from './accountPassword'; +export * from './activate'; +export * from './addToList'; +export * from './connect'; +export * from './deckDel'; +export * from './deckDelDir'; +export * from './deckList'; +export * from './deckNewDir'; +export * from './deckUpload'; +export * from './disconnect'; +export * from './forgotPasswordChallenge'; +export * from './forgotPasswordRequest'; +export * from './forgotPasswordReset'; +export * from './getGamesOfUser'; +export * from './getUserInfo'; +export * from './joinRoom'; +export * from './listRooms'; +export * from './listUsers'; +export * from './login'; +export * from './message'; +export * from './ping'; +export * from './register'; +export * from './removeFromList'; +export * from './replayDeleteMatch'; +export * from './replayList'; +export * from './replayModifyMatch'; +export * from './requestPasswordSalt'; +export * from './updateStatus'; diff --git a/webclient/src/websocket/commands/session/joinRoom.ts b/webclient/src/websocket/commands/session/joinRoom.ts new file mode 100644 index 000000000..be79976a0 --- /dev/null +++ b/webclient/src/websocket/commands/session/joinRoom.ts @@ -0,0 +1,11 @@ +import { BackendService } from '../../services/BackendService'; +import { RoomPersistence } from '../../persistence'; + +export function joinRoom(roomId: number): void { + BackendService.sendSessionCommand('Command_JoinRoom', { roomId }, { + responseName: 'Response_JoinRoom', + onSuccess: (response) => { + RoomPersistence.joinRoom(response.roomInfo); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/listRooms.ts b/webclient/src/websocket/commands/session/listRooms.ts new file mode 100644 index 000000000..367dada9b --- /dev/null +++ b/webclient/src/websocket/commands/session/listRooms.ts @@ -0,0 +1,5 @@ +import { BackendService } from '../../services/BackendService'; + +export function listRooms(): void { + BackendService.sendSessionCommand('Command_ListRooms', {}, {}); +} diff --git a/webclient/src/websocket/commands/session/listUsers.ts b/webclient/src/websocket/commands/session/listUsers.ts new file mode 100644 index 000000000..9b95c1344 --- /dev/null +++ b/webclient/src/websocket/commands/session/listUsers.ts @@ -0,0 +1,11 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function listUsers(): void { + BackendService.sendSessionCommand('Command_ListUsers', {}, { + responseName: 'Response_ListUsers', + onSuccess: (response) => { + SessionPersistence.updateUsers(response.userList); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/login.ts b/webclient/src/websocket/commands/session/login.ts new file mode 100644 index 000000000..6f3ec5ef5 --- /dev/null +++ b/webclient/src/websocket/commands/session/login.ts @@ -0,0 +1,79 @@ +import { StatusEnum, WebSocketConnectOptions } from 'types'; +import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; +import { ProtoController } from '../../services/ProtoController'; +import { hashPassword } from '../../utils'; +import { SessionPersistence } from '../../persistence'; + +import { + disconnect, + listUsers, + listRooms, + updateStatus, +} from './'; + +export function login(options: WebSocketConnectOptions, passwordSalt?: string): void { + const { userName, password, hashedPassword } = options; + + const loginConfig: any = { + ...webClient.clientConfig, + clientid: 'webatrice', + userName, + }; + + if (passwordSalt) { + loginConfig.hashedPassword = hashedPassword || hashPassword(passwordSalt, password); + } else { + loginConfig.password = password; + } + + const { ResponseCode } = ProtoController.root.Response; + + const onLoginError = (message: string, extra?: () => void) => { + updateStatus(StatusEnum.DISCONNECTED, message); + extra?.(); + SessionPersistence.loginFailed(); + disconnect(); + }; + + BackendService.sendSessionCommand('Command_Login', loginConfig, { + responseName: 'Response_Login', + onSuccess: (resp) => { + const { buddyList, ignoreList, userInfo } = resp; + + SessionPersistence.updateBuddyList(buddyList); + SessionPersistence.updateIgnoreList(ignoreList); + SessionPersistence.updateUser(userInfo); + SessionPersistence.loginSuccessful(loginConfig); + + listUsers(); + listRooms(); + + updateStatus(StatusEnum.LOGGED_IN, 'Logged in.'); + }, + onResponseCode: { + [ResponseCode.RespClientUpdateRequired]: () => + onLoginError('Login failed: missing features'), + [ResponseCode.RespWrongPassword]: () => + onLoginError('Login failed: incorrect username or password'), + [ResponseCode.RespUsernameInvalid]: () => + onLoginError('Login failed: incorrect username or password'), + [ResponseCode.RespWouldOverwriteOldSession]: () => + onLoginError('Login failed: duplicated user session'), + [ResponseCode.RespUserIsBanned]: () => + onLoginError('Login failed: banned user'), + [ResponseCode.RespRegistrationRequired]: () => + onLoginError('Login failed: registration required'), + [ResponseCode.RespClientIdRequired]: () => + onLoginError('Login failed: missing client ID'), + [ResponseCode.RespContextError]: () => + onLoginError('Login failed: server error'), + [ResponseCode.RespAccountNotActivated]: () => + onLoginError('Login failed: account not activated', + () => SessionPersistence.accountAwaitingActivation(options) + ), + }, + onError: (responseCode) => + onLoginError(`Login failed: unknown error: ${responseCode}`), + }); +} diff --git a/webclient/src/websocket/commands/session/message.ts b/webclient/src/websocket/commands/session/message.ts new file mode 100644 index 000000000..075fc3c4b --- /dev/null +++ b/webclient/src/websocket/commands/session/message.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function message(userName: string, message: string): void { + BackendService.sendSessionCommand('Command_Message', { userName, message }, { + onSuccess: () => { + SessionPersistence.directMessageSent(userName, message); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/ping.ts b/webclient/src/websocket/commands/session/ping.ts new file mode 100644 index 000000000..fea2784a2 --- /dev/null +++ b/webclient/src/websocket/commands/session/ping.ts @@ -0,0 +1,7 @@ +import { BackendService } from '../../services/BackendService'; + +export function ping(pingReceived: Function): void { + BackendService.sendSessionCommand('Command_Ping', {}, { + onResponse: (raw) => pingReceived(raw), + }); +} diff --git a/webclient/src/websocket/commands/session/register.ts b/webclient/src/websocket/commands/session/register.ts new file mode 100644 index 000000000..a25b85868 --- /dev/null +++ b/webclient/src/websocket/commands/session/register.ts @@ -0,0 +1,77 @@ +import { ServerRegisterParams } from 'store'; +import { StatusEnum, WebSocketConnectOptions } from 'types'; + +import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; +import { ProtoController } from '../../services/ProtoController'; +import { SessionPersistence } from '../../persistence'; +import { hashPassword } from '../../utils'; + +import { login, disconnect, updateStatus } from './'; + +export function register(options: WebSocketConnectOptions, passwordSalt?: string): void { + const { userName, password, email, country, realName } = options as ServerRegisterParams; + + const params: any = { + ...webClient.clientConfig, + userName, + email, + country, + realName, + }; + + if (passwordSalt) { + params.hashedPassword = hashPassword(passwordSalt, password); + } else { + params.password = password; + } + + const { ResponseCode } = ProtoController.root.Response; + + const onRegistrationError = (action: () => void) => { + action(); + updateStatus(StatusEnum.DISCONNECTED, 'Registration failed'); + disconnect(); + }; + + BackendService.sendSessionCommand('Command_Register', params, { + onResponseCode: { + [ResponseCode.RespRegistrationAccepted]: () => { + login(options, passwordSalt); + SessionPersistence.registrationSuccess(); + }, + [ResponseCode.RespRegistrationAcceptedNeedsActivation]: () => { + updateStatus(StatusEnum.DISCONNECTED, 'Registration accepted, awaiting activation'); + SessionPersistence.accountAwaitingActivation(options); + disconnect(); + }, + [ResponseCode.RespUserAlreadyExists]: () => onRegistrationError( + () => SessionPersistence.registrationUserNameError('Username is taken') + ), + [ResponseCode.RespUsernameInvalid]: () => onRegistrationError( + () => SessionPersistence.registrationUserNameError('Invalid username') + ), + [ResponseCode.RespPasswordTooShort]: () => onRegistrationError( + () => SessionPersistence.registrationPasswordError('Your password was too short') + ), + [ResponseCode.RespEmailRequiredToRegister]: () => onRegistrationError( + () => SessionPersistence.registrationRequiresEmail() + ), + [ResponseCode.RespEmailBlackListed]: () => onRegistrationError( + () => SessionPersistence.registrationEmailError('This email provider has been blocked') + ), + [ResponseCode.RespTooManyRequests]: () => onRegistrationError( + () => SessionPersistence.registrationEmailError('Max accounts reached for this email') + ), + [ResponseCode.RespRegistrationDisabled]: () => onRegistrationError( + () => SessionPersistence.registrationFailed('Registration is currently disabled') + ), + [ResponseCode.RespUserIsBanned]: (raw) => onRegistrationError( + () => SessionPersistence.registrationFailed(raw.reasonStr, raw.endTime) + ), + }, + onError: () => onRegistrationError( + () => SessionPersistence.registrationFailed('Registration failed due to a server issue') + ), + }); +} diff --git a/webclient/src/websocket/commands/session/removeFromList.ts b/webclient/src/websocket/commands/session/removeFromList.ts new file mode 100644 index 000000000..aede49c49 --- /dev/null +++ b/webclient/src/websocket/commands/session/removeFromList.ts @@ -0,0 +1,18 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function removeFromBuddyList(userName: string): void { + removeFromList('buddy', userName); +} + +export function removeFromIgnoreList(userName: string): void { + removeFromList('ignore', userName); +} + +export function removeFromList(list: string, userName: string): void { + BackendService.sendSessionCommand('Command_RemoveFromList', { list, userName }, { + onSuccess: () => { + SessionPersistence.removeFromList(list, userName); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/replayDeleteMatch.ts b/webclient/src/websocket/commands/session/replayDeleteMatch.ts new file mode 100644 index 000000000..24ac48f1c --- /dev/null +++ b/webclient/src/websocket/commands/session/replayDeleteMatch.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function replayDeleteMatch(gameId: number): void { + BackendService.sendSessionCommand('Command_ReplayDeleteMatch', { gameId }, { + onSuccess: () => { + SessionPersistence.replayDeleteMatch(gameId); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/replayList.ts b/webclient/src/websocket/commands/session/replayList.ts new file mode 100644 index 000000000..f39eb279f --- /dev/null +++ b/webclient/src/websocket/commands/session/replayList.ts @@ -0,0 +1,11 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function replayList(): void { + BackendService.sendSessionCommand('Command_ReplayList', {}, { + responseName: 'Response_ReplayList', + onSuccess: (response) => { + SessionPersistence.replayList(response.matchList); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/replayModifyMatch.ts b/webclient/src/websocket/commands/session/replayModifyMatch.ts new file mode 100644 index 000000000..9825047f3 --- /dev/null +++ b/webclient/src/websocket/commands/session/replayModifyMatch.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function replayModifyMatch(gameId: number, doNotHide: boolean): void { + BackendService.sendSessionCommand('Command_ReplayModifyMatch', { gameId, doNotHide }, { + onSuccess: () => { + SessionPersistence.replayModifyMatch(gameId, doNotHide); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/requestPasswordSalt.ts b/webclient/src/websocket/commands/session/requestPasswordSalt.ts new file mode 100644 index 000000000..a3d1fc05c --- /dev/null +++ b/webclient/src/websocket/commands/session/requestPasswordSalt.ts @@ -0,0 +1,64 @@ +import { RequestPasswordSaltParams } from 'store'; +import { StatusEnum, WebSocketConnectOptions, WebSocketConnectReason } from 'types'; + +import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; +import { ProtoController } from '../../services/ProtoController'; +import { SessionPersistence } from '../../persistence'; + +import { + activate, + disconnect, + login, + forgotPasswordReset, + updateStatus +} from './'; + +export function requestPasswordSalt(options: WebSocketConnectOptions): void { + const { userName } = options as RequestPasswordSaltParams; + + const onFailure = () => { + switch (options.reason) { + case WebSocketConnectReason.ACTIVATE_ACCOUNT: + SessionPersistence.accountActivationFailed(); + break; + case WebSocketConnectReason.PASSWORD_RESET: + SessionPersistence.resetPasswordFailed(); + break; + default: + SessionPersistence.loginFailed(); + } + disconnect(); + }; + + BackendService.sendSessionCommand('Command_RequestPasswordSalt', { + ...webClient.clientConfig, + userName, + }, { + responseName: 'Response_PasswordSalt', + onSuccess: (resp) => { + const passwordSalt = resp?.passwordSalt; + + switch (options.reason) { + case WebSocketConnectReason.ACTIVATE_ACCOUNT: + activate(options, passwordSalt); + break; + case WebSocketConnectReason.PASSWORD_RESET: + forgotPasswordReset(options, passwordSalt); + break; + default: + login(options, passwordSalt); + } + }, + onResponseCode: { + [ProtoController.root.Response.ResponseCode.RespRegistrationRequired]: () => { + updateStatus(StatusEnum.DISCONNECTED, 'Login failed: registration required'); + onFailure(); + }, + }, + onError: () => { + updateStatus(StatusEnum.DISCONNECTED, 'Login failed: Unknown Reason'); + onFailure(); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/updateStatus.ts b/webclient/src/websocket/commands/session/updateStatus.ts new file mode 100644 index 000000000..eddd774f8 --- /dev/null +++ b/webclient/src/websocket/commands/session/updateStatus.ts @@ -0,0 +1,9 @@ +import { StatusEnum } from 'types'; +import webClient from '../../WebClient'; +import { SessionPersistence } from '../../persistence'; + +export function updateStatus(status: StatusEnum, description: string): void { + SessionPersistence.updateStatus(status, description); + + webClient.updateStatus(status); +} diff --git a/webclient/src/websocket/events/common/index.ts b/webclient/src/websocket/events/common/index.ts new file mode 100644 index 000000000..77a325c1e --- /dev/null +++ b/webclient/src/websocket/events/common/index.ts @@ -0,0 +1,6 @@ +import { ProtobufEvents } from '../../services/ProtobufService'; +import { playerPropertiesChanged } from './playerPropertiesChanged'; + +export const CommonEvents: ProtobufEvents = { + '.Event_PlayerPropertiesChanged.ext': playerPropertiesChanged, +} diff --git a/webclient/src/websocket/events/common/playerPropertiesChanged.ts b/webclient/src/websocket/events/common/playerPropertiesChanged.ts new file mode 100644 index 000000000..557e57ac9 --- /dev/null +++ b/webclient/src/websocket/events/common/playerPropertiesChanged.ts @@ -0,0 +1,6 @@ +import { PlayerGamePropertiesData } from '../session/interfaces'; +import { SessionPersistence } from '../../persistence'; + +export function playerPropertiesChanged(payload: PlayerGamePropertiesData): void { + SessionPersistence.playerPropertiesChanged(payload); +} diff --git a/webclient/src/websocket/events/game/index.ts b/webclient/src/websocket/events/game/index.ts new file mode 100644 index 000000000..a7b3277a5 --- /dev/null +++ b/webclient/src/websocket/events/game/index.ts @@ -0,0 +1,36 @@ +import { ProtobufEvents } from '../../services/ProtobufService'; +import { joinGame } from './joinGame'; +import { leaveGame } from './leaveGame'; + + +export const GameEvents: ProtobufEvents = { + '.Event_Join.ext': joinGame, + '.Event_Leave.ext': leaveGame, + '.Event_GameClosed.ext': () => console.log('Event_GameClosed.ext'), + '.Event_GameHostChanged.ext': () => console.log('Event_GameHostChanged.ext'), + '.Event_Kicked.ext': () => console.log('Event_Kicked.ext'), + '.Event_GameStateChanged.ext': () => console.log('Event_GameStateChanged.ext'), + // '.Event_PlayerPropertiesChanged.ext': () => console.log("Event_PlayerProperties.ext"), + '.Event_GameSay.ext': () => console.log('Event_GameSay.ext'), + '.Event_CreateArrow.ext': () => console.log('Event_CreateArrow.ext'), + '.Event_DeleteArrow.ext': () => console.log('Event_DeleteArrow.ext'), + '.Event_CreateCounter.ext': () => console.log('Event_CreateCounter.ext'), + '.Event_SetCounter.ext': () => console.log('Event_SetCounter.ext'), + '.Event_DelCounter.ext': () => console.log('Event_DelCounter.ext'), + '.Event_DrawCards.ext': () => console.log('Event_DrawCards.ext'), + '.Event_RevealCards.ext': () => console.log('Event_RevealCards.ext'), + '.Event_Shuffle.ext': () => console.log('Event_Shuffle.ext'), + '.Event_RollDie.ext': () => console.log('Event_Roll.ext'), + '.Event_MoveCard.ext': () => console.log('Event_MoveCard.ext'), + '.Event_FlipCard.ext': () => console.log('Event_FlipCard.ext'), + '.Event_DestroyCard.ext': () => console.log('Event_DestroyCard.ext'), + '.Event_AttachCard.ext': () => console.log('Event_AttachCard.ext'), + '.Event_CreateToken.ext': () => console.log('Event_CreateToken.ext'), + '.Event_SetCardAttribute.ext': () => console.log('Event_SetCardAttribute.ext'), + '.Event_SetCardCounter.ext': () => console.log('Event_SetCardCounter.ext'), + '.Event_SetActivePlayer.ext': () => console.log('Event_SetActivePlayer.ext'), + '.Event_SetActivePhase.ext': () => console.log('Event_SetActivePhase.ext'), + '.Event_DumpZone.ext': () => console.log('Event_DumpZone.ext'), + '.Event_ChangeZoneProperties.ext': () => console.log('Event_ChangeZoneProperties.ext'), + '.Event_ReverseTurn.ext': () => console.log('Event_ReverseTurn.ext'), +}; diff --git a/webclient/src/websocket/events/game/joinGame.ts b/webclient/src/websocket/events/game/joinGame.ts new file mode 100644 index 000000000..30cef87dc --- /dev/null +++ b/webclient/src/websocket/events/game/joinGame.ts @@ -0,0 +1,6 @@ +import { GamePersistence } from '../../persistence'; +import { PlayerGamePropertiesData } from '../session/interfaces'; + +export function joinGame(playerGamePropertiesData: PlayerGamePropertiesData): void { + GamePersistence.joinGame(playerGamePropertiesData); +} diff --git a/webclient/src/websocket/events/game/leaveGame.ts b/webclient/src/websocket/events/game/leaveGame.ts new file mode 100644 index 000000000..74a4dc6c5 --- /dev/null +++ b/webclient/src/websocket/events/game/leaveGame.ts @@ -0,0 +1,7 @@ +import { LeaveGameReason } from 'types'; +import { GamePersistence } from '../../persistence'; + + +export function leaveGame(reason: LeaveGameReason): void { + GamePersistence.leaveGame(reason); +} diff --git a/webclient/src/websocket/events/index.ts b/webclient/src/websocket/events/index.ts new file mode 100644 index 000000000..ed74f9b93 --- /dev/null +++ b/webclient/src/websocket/events/index.ts @@ -0,0 +1,4 @@ +export * from './common'; +export * from './room'; +export * from './session'; +export * from './game'; diff --git a/webclient/src/websocket/events/room/index.ts b/webclient/src/websocket/events/room/index.ts new file mode 100644 index 000000000..5b571d388 --- /dev/null +++ b/webclient/src/websocket/events/room/index.ts @@ -0,0 +1,15 @@ +import { ProtobufEvents } from '../../services/ProtobufService'; + +import { joinRoom } from './joinRoom'; +import { leaveRoom } from './leaveRoom'; +import { listGames } from './listGames'; +import { roomSay } from './roomSay'; +import { removeMessages } from './removeMessages'; + +export const RoomEvents: ProtobufEvents = { + '.Event_JoinRoom.ext': joinRoom, + '.Event_LeaveRoom.ext': leaveRoom, + '.Event_ListGames.ext': listGames, + '.Event_RemoveMessages.ext': removeMessages, + '.Event_RoomSay.ext': roomSay, +}; diff --git a/webclient/src/websocket/events/room/interfaces.ts b/webclient/src/websocket/events/room/interfaces.ts new file mode 100644 index 000000000..b3f922141 --- /dev/null +++ b/webclient/src/websocket/events/room/interfaces.ts @@ -0,0 +1,24 @@ +import { Game, User } from 'types'; + +export interface JoinRoomData { + userInfo: User; +} + +export interface LeaveRoomData { + name: string; +} + +export interface ListGamesData { + gameList: Game[]; +} + +export interface RemoveMessagesData { + name: string; + amount: number; +} + +export interface RoomEvent { + roomEvent: { + roomId: number; + } +} diff --git a/webclient/src/websocket/events/room/joinRoom.ts b/webclient/src/websocket/events/room/joinRoom.ts new file mode 100644 index 000000000..b1a5f6606 --- /dev/null +++ b/webclient/src/websocket/events/room/joinRoom.ts @@ -0,0 +1,6 @@ +import { RoomPersistence } from '../../persistence'; +import { JoinRoomData, RoomEvent } from './interfaces'; + +export function joinRoom({ userInfo }: JoinRoomData, { roomEvent: { roomId } }: RoomEvent): void { + RoomPersistence.userJoined(roomId, userInfo); +} diff --git a/webclient/src/websocket/events/room/leaveRoom.ts b/webclient/src/websocket/events/room/leaveRoom.ts new file mode 100644 index 000000000..6d45197fc --- /dev/null +++ b/webclient/src/websocket/events/room/leaveRoom.ts @@ -0,0 +1,6 @@ +import { RoomPersistence } from '../../persistence'; +import { LeaveRoomData, RoomEvent } from './interfaces'; + +export function leaveRoom({ name }: LeaveRoomData, { roomEvent: { roomId } }: RoomEvent): void { + RoomPersistence.userLeft(roomId, name); +} diff --git a/webclient/src/websocket/events/room/listGames.ts b/webclient/src/websocket/events/room/listGames.ts new file mode 100644 index 000000000..d460a5336 --- /dev/null +++ b/webclient/src/websocket/events/room/listGames.ts @@ -0,0 +1,6 @@ +import { RoomPersistence } from '../../persistence'; +import { ListGamesData, RoomEvent } from './interfaces'; + +export function listGames({ gameList }: ListGamesData, { roomEvent: { roomId } }: RoomEvent): void { + RoomPersistence.updateGames(roomId, gameList); +} diff --git a/webclient/src/websocket/events/room/removeMessages.ts b/webclient/src/websocket/events/room/removeMessages.ts new file mode 100644 index 000000000..4fe01cb6f --- /dev/null +++ b/webclient/src/websocket/events/room/removeMessages.ts @@ -0,0 +1,6 @@ +import { RoomPersistence } from '../../persistence'; +import { RemoveMessagesData, RoomEvent } from './interfaces'; + +export function removeMessages({ name, amount }: RemoveMessagesData, { roomEvent: { roomId } }: RoomEvent): void { + RoomPersistence.removeMessages(roomId, name, amount); +} diff --git a/webclient/src/websocket/events/room/roomSay.ts b/webclient/src/websocket/events/room/roomSay.ts new file mode 100644 index 000000000..5a96198ea --- /dev/null +++ b/webclient/src/websocket/events/room/roomSay.ts @@ -0,0 +1,8 @@ +import { Message } from 'types'; + +import { RoomPersistence } from '../../persistence'; +import { RoomEvent } from './interfaces'; + +export function roomSay(message: Message, { roomEvent: { roomId } }: RoomEvent): void { + RoomPersistence.addMessage(roomId, message); +} diff --git a/webclient/src/websocket/events/session/addToList.ts b/webclient/src/websocket/events/session/addToList.ts new file mode 100644 index 000000000..08b19b45d --- /dev/null +++ b/webclient/src/websocket/events/session/addToList.ts @@ -0,0 +1,18 @@ +import { SessionPersistence } from '../../persistence'; +import { AddToListData } from './interfaces'; + +export function addToList({ listName, userInfo }: AddToListData): void { + switch (listName) { + case 'buddy': { + SessionPersistence.addToBuddyList(userInfo); + break; + } + case 'ignore': { + SessionPersistence.addToIgnoreList(userInfo); + break; + } + default: { + console.log(`Attempted to add to unknown list: ${listName}`); + } + } +} diff --git a/webclient/src/websocket/events/session/connectionClosed.ts b/webclient/src/websocket/events/session/connectionClosed.ts new file mode 100644 index 000000000..227113059 --- /dev/null +++ b/webclient/src/websocket/events/session/connectionClosed.ts @@ -0,0 +1,44 @@ +import { StatusEnum } from 'types'; +import { ProtoController } from '../../services/ProtoController'; +import { updateStatus } from '../../commands/session'; +import { ConnectionClosedData } from './interfaces'; + +export function connectionClosed({ reason, reasonStr }: ConnectionClosedData): void { + let message: string; + + // @TODO (5) + if (reasonStr) { + message = reasonStr; + } else { + const { CloseReason } = ProtoController.root.Event_ConnectionClosed; + switch (reason) { + case CloseReason.USER_LIMIT_REACHED: + message = 'The server has reached its maximum user capacity'; + break; + case CloseReason.TOO_MANY_CONNECTIONS: + message = 'There are too many concurrent connections from your address'; + break; + case CloseReason.BANNED: + message = 'You are banned'; + break; + case CloseReason.DEMOTED: + message = 'You were demoted'; + break; + case CloseReason.SERVER_SHUTDOWN: + message = 'Scheduled server shutdown'; + break; + case CloseReason.USERNAMEINVALID: + message = 'Invalid username'; + break; + case CloseReason.LOGGEDINELSEWERE: + message = 'You have been logged out due to logging in at another location'; + break; + case CloseReason.OTHER: + default: + message = 'Unknown reason'; + break; + } + } + + updateStatus(StatusEnum.DISCONNECTED, message); +} diff --git a/webclient/src/websocket/events/session/gameJoined.ts b/webclient/src/websocket/events/session/gameJoined.ts new file mode 100644 index 000000000..8c0d49006 --- /dev/null +++ b/webclient/src/websocket/events/session/gameJoined.ts @@ -0,0 +1,6 @@ +import { SessionPersistence } from '../../persistence'; +import { GameJoinedData } from './interfaces'; + +export function gameJoined(gameJoined: GameJoinedData): void { + SessionPersistence.gameJoined(gameJoined); +} diff --git a/webclient/src/websocket/events/session/index.ts b/webclient/src/websocket/events/session/index.ts new file mode 100644 index 000000000..5b3ab198e --- /dev/null +++ b/webclient/src/websocket/events/session/index.ts @@ -0,0 +1,32 @@ +import { ProtobufEvents } from '../../services/ProtobufService'; +import { addToList } from './addToList'; +import { connectionClosed } from './connectionClosed'; +import { listRooms } from './listRooms'; +import { notifyUser } from './notifyUser'; +import { removeFromList } from './removeFromList'; +import { replayAdded } from './replayAdded'; +import { serverCompleteList } from './serverCompleteList'; +import { serverIdentification } from './serverIdentification'; +import { serverMessage } from './serverMessage'; +import { serverShutdown } from './serverShutdown'; +import { userJoined } from './userJoined'; +import { userLeft } from './userLeft'; +import { userMessage } from './userMessage'; +import { gameJoined } from './gameJoined'; + +export const SessionEvents: ProtobufEvents = { + '.Event_AddToList.ext': addToList, + '.Event_ConnectionClosed.ext': connectionClosed, + '.Event_GameJoined.ext': gameJoined, + '.Event_ListRooms.ext': listRooms, + '.Event_NotifyUser.ext': notifyUser, + '.Event_RemoveFromList.ext': removeFromList, + '.Event_ReplayAdded.ext': replayAdded, + '.Event_ServerCompleteList.ext': serverCompleteList, + '.Event_ServerIdentification.ext': serverIdentification, + '.Event_ServerMessage.ext': serverMessage, + '.Event_ServerShutdown.ext': serverShutdown, + '.Event_UserJoined.ext': userJoined, + '.Event_UserLeft.ext': userLeft, + '.Event_UserMessage.ext': userMessage, +} diff --git a/webclient/src/websocket/events/session/interfaces.ts b/webclient/src/websocket/events/session/interfaces.ts new file mode 100644 index 000000000..ddc10d103 --- /dev/null +++ b/webclient/src/websocket/events/session/interfaces.ts @@ -0,0 +1,90 @@ +import { Game, NotificationType, ReplayMatch, Room, User } from 'types'; + +export interface AddToListData { + listName: string; + userInfo: User; +} + +export interface ConnectionClosedData { + endTime: number; + reason: number; + reasonStr: string; +} + +export interface GameJoinedData { + gameInfo: Game; + gameTypes: any[]; + hostId: number; + playerId: number; + spectator: boolean; + resuming: boolean; + judge: boolean; +} + +export interface ListRoomsData { + roomList: Room[]; +} + +export interface NotifyUserData { + type: NotificationType; + warningReason: string; + customTitle: string; + customContent: string; +} + +export interface PlayerGamePropertiesData { + playerId: number; + userInfo: User; + spectator: boolean; + conceded: boolean; + readyStart: boolean; + deckHash: string; + pingSeconds: number; + sideboardLocked: boolean; + judge: boolean; +} + +export interface RemoveFromListData { + listName: string; + userName: string; +} + +export interface ServerIdentificationData { + protocolVersion: number; + serverName: string; + serverVersion: string; + serverOptions: number; +} + +export interface ServerMessageData { + message: string; +} + +export interface ServerShutdownData { + reason: string; + minutes: number; +} + +export interface UserJoinedData { + userInfo: User; +} + +export interface UserLeftData { + name: string; +} + +export interface UserMessageData { + senderName: string; + receiverName: string; + message: string; +} + +export interface ReplayAddedData { + matchInfo: ReplayMatch; +} + +export interface ServerCompleteListData { + serverId: number; + userList: User[]; + roomList: Room[]; +} diff --git a/webclient/src/websocket/events/session/listRooms.ts b/webclient/src/websocket/events/session/listRooms.ts new file mode 100644 index 000000000..faba2968e --- /dev/null +++ b/webclient/src/websocket/events/session/listRooms.ts @@ -0,0 +1,16 @@ +import webClient from '../../WebClient'; +import { joinRoom } from '../../commands/session'; +import { RoomPersistence } from '../../persistence'; +import { ListRoomsData } from './interfaces'; + +export function listRooms({ roomList }: ListRoomsData): void { + RoomPersistence.updateRooms(roomList); + + if (webClient.clientOptions.autojoinrooms) { + roomList.forEach(({ autoJoin, roomId }) => { + if (autoJoin) { + joinRoom(roomId); + } + }); + } +} diff --git a/webclient/src/websocket/events/session/notifyUser.ts b/webclient/src/websocket/events/session/notifyUser.ts new file mode 100644 index 000000000..f5673fe3f --- /dev/null +++ b/webclient/src/websocket/events/session/notifyUser.ts @@ -0,0 +1,7 @@ +import { SessionPersistence } from '../../persistence'; +import { NotifyUserData } from './interfaces'; + + +export function notifyUser(payload: NotifyUserData): void { + SessionPersistence.notifyUser(payload); +} diff --git a/webclient/src/websocket/events/session/removeFromList.ts b/webclient/src/websocket/events/session/removeFromList.ts new file mode 100644 index 000000000..20e2d7f54 --- /dev/null +++ b/webclient/src/websocket/events/session/removeFromList.ts @@ -0,0 +1,18 @@ +import { SessionPersistence } from '../../persistence'; +import { RemoveFromListData } from './interfaces'; + +export function removeFromList({ listName, userName }: RemoveFromListData): void { + switch (listName) { + case 'buddy': { + SessionPersistence.removeFromBuddyList(userName); + break; + } + case 'ignore': { + SessionPersistence.removeFromIgnoreList(userName); + break; + } + default: { + console.log(`Attempted to remove from unknown list: ${listName}`); + } + } +} diff --git a/webclient/src/websocket/events/session/replayAdded.ts b/webclient/src/websocket/events/session/replayAdded.ts new file mode 100644 index 000000000..18a4ea82d --- /dev/null +++ b/webclient/src/websocket/events/session/replayAdded.ts @@ -0,0 +1,6 @@ +import { SessionPersistence } from '../../persistence'; +import { ReplayAddedData } from './interfaces'; + +export function replayAdded({ matchInfo }: ReplayAddedData): void { + SessionPersistence.replayAdded(matchInfo); +} diff --git a/webclient/src/websocket/events/session/serverCompleteList.ts b/webclient/src/websocket/events/session/serverCompleteList.ts new file mode 100644 index 000000000..77d37a31e --- /dev/null +++ b/webclient/src/websocket/events/session/serverCompleteList.ts @@ -0,0 +1,7 @@ +import { RoomPersistence, SessionPersistence } from '../../persistence'; +import { ServerCompleteListData } from './interfaces'; + +export function serverCompleteList({ userList, roomList }: ServerCompleteListData): void { + SessionPersistence.updateUsers(userList); + RoomPersistence.updateRooms(roomList); +} diff --git a/webclient/src/websocket/events/session/serverIdentification.ts b/webclient/src/websocket/events/session/serverIdentification.ts new file mode 100644 index 000000000..87ae79453 --- /dev/null +++ b/webclient/src/websocket/events/session/serverIdentification.ts @@ -0,0 +1,71 @@ +import { StatusEnum, WebSocketConnectOptions, WebSocketConnectReason } from 'types'; + +import webClient from '../../WebClient'; +import { + activate, + disconnect, + login, + register, + requestPasswordSalt, + forgotPasswordChallenge, + forgotPasswordRequest, + forgotPasswordReset, + updateStatus, +} from '../../commands/session'; +import { generateSalt, passwordSaltSupported } from '../../utils'; +import { ServerIdentificationData } from './interfaces'; +import { SessionPersistence } from '../../persistence'; + +export function serverIdentification(info: ServerIdentificationData): void { + const { serverName, serverVersion, protocolVersion, serverOptions } = info; + if (protocolVersion !== webClient.protocolVersion) { + updateStatus(StatusEnum.DISCONNECTED, `Protocol version mismatch: ${protocolVersion}`); + disconnect(); + return; + } + + const getPasswordSalt = passwordSaltSupported(serverOptions); + const connectOptions = { ...webClient.options }; + + switch (connectOptions.reason) { + case WebSocketConnectReason.LOGIN: + updateStatus(StatusEnum.LOGGING_IN, 'Logging In...'); + if (getPasswordSalt) { + requestPasswordSalt(connectOptions); + } else { + login(connectOptions); + } + break; + case WebSocketConnectReason.REGISTER: + const passwordSalt = getPasswordSalt ? generateSalt() : null; + register(connectOptions, passwordSalt); + break; + case WebSocketConnectReason.ACTIVATE_ACCOUNT: + if (getPasswordSalt) { + requestPasswordSalt(connectOptions); + } else { + activate(connectOptions); + } + break; + case WebSocketConnectReason.PASSWORD_RESET_REQUEST: + forgotPasswordRequest(connectOptions); + break; + case WebSocketConnectReason.PASSWORD_RESET_CHALLENGE: + forgotPasswordChallenge(connectOptions); + break; + case WebSocketConnectReason.PASSWORD_RESET: + if (getPasswordSalt) { + requestPasswordSalt(connectOptions); + } else { + forgotPasswordReset(connectOptions); + } + break; + default: + updateStatus(StatusEnum.DISCONNECTED, 'Unknown Connection Reason: ' + connectOptions.reason); + disconnect(); + break; + } + + webClient.options = {} as WebSocketConnectOptions; + SessionPersistence.updateInfo(serverName, serverVersion); +} diff --git a/webclient/src/websocket/events/session/serverMessage.ts b/webclient/src/websocket/events/session/serverMessage.ts new file mode 100644 index 000000000..f9e52aa7a --- /dev/null +++ b/webclient/src/websocket/events/session/serverMessage.ts @@ -0,0 +1,6 @@ +import { SessionPersistence } from '../../persistence'; +import { ServerMessageData } from './interfaces'; + +export function serverMessage({ message }: ServerMessageData): void { + SessionPersistence.serverMessage(message); +} diff --git a/webclient/src/websocket/events/session/serverShutdown.ts b/webclient/src/websocket/events/session/serverShutdown.ts new file mode 100644 index 000000000..cdda893a4 --- /dev/null +++ b/webclient/src/websocket/events/session/serverShutdown.ts @@ -0,0 +1,7 @@ +import { SessionPersistence } from '../../persistence'; +import { ServerShutdownData } from './interfaces'; + + +export function serverShutdown(payload: ServerShutdownData): void { + SessionPersistence.serverShutdown(payload); +} diff --git a/webclient/src/websocket/events/session/userJoined.ts b/webclient/src/websocket/events/session/userJoined.ts new file mode 100644 index 000000000..cb512db60 --- /dev/null +++ b/webclient/src/websocket/events/session/userJoined.ts @@ -0,0 +1,6 @@ +import { SessionPersistence } from '../../persistence'; +import { UserJoinedData } from './interfaces'; + +export function userJoined({ userInfo }: UserJoinedData): void { + SessionPersistence.userJoined(userInfo); +} diff --git a/webclient/src/websocket/events/session/userLeft.ts b/webclient/src/websocket/events/session/userLeft.ts new file mode 100644 index 000000000..9e00e59e1 --- /dev/null +++ b/webclient/src/websocket/events/session/userLeft.ts @@ -0,0 +1,6 @@ +import { SessionPersistence } from '../../persistence'; +import { UserLeftData } from './interfaces'; + +export function userLeft({ name }: UserLeftData): void { + SessionPersistence.userLeft(name); +} diff --git a/webclient/src/websocket/events/session/userMessage.ts b/webclient/src/websocket/events/session/userMessage.ts new file mode 100644 index 000000000..bff08460b --- /dev/null +++ b/webclient/src/websocket/events/session/userMessage.ts @@ -0,0 +1,8 @@ +import { SessionPersistence } from '../../persistence'; +import { UserMessageData } from './interfaces'; + + + +export function userMessage(payload: UserMessageData): void { + SessionPersistence.userMessage(payload); +} diff --git a/webclient/src/websocket/index.ts b/webclient/src/websocket/index.ts new file mode 100644 index 000000000..b52c97168 --- /dev/null +++ b/webclient/src/websocket/index.ts @@ -0,0 +1,3 @@ +export * from './commands'; + +export { default as webClient } from './WebClient'; diff --git a/webclient/src/websocket/persistence/AdminPersistence.ts b/webclient/src/websocket/persistence/AdminPersistence.ts new file mode 100644 index 000000000..9552d8abf --- /dev/null +++ b/webclient/src/websocket/persistence/AdminPersistence.ts @@ -0,0 +1,19 @@ +import { ServerDispatch } from 'store'; + +export class AdminPersistence { + static adjustMod(userName: string, shouldBeMod: boolean, shouldBeJudge: boolean) { + ServerDispatch.adjustMod(userName, shouldBeMod, shouldBeJudge) + } + + static reloadConfig() { + ServerDispatch.reloadConfig(); + } + + static shutdownServer() { + ServerDispatch.shutdownServer(); + } + + static updateServerMessage() { + ServerDispatch.updateServerMessage(); + } +} diff --git a/webclient/src/websocket/persistence/GamePersistence.ts b/webclient/src/websocket/persistence/GamePersistence.ts new file mode 100644 index 000000000..e39c1b5a9 --- /dev/null +++ b/webclient/src/websocket/persistence/GamePersistence.ts @@ -0,0 +1,12 @@ +import { PlayerGamePropertiesData } from '../events/session/interfaces'; +import { LeaveGameReason } from '../../types'; + +export class GamePersistence { + static joinGame(playerGamePropertiesData: PlayerGamePropertiesData) { + console.log('joinGame', playerGamePropertiesData); + } + + static leaveGame(reason: LeaveGameReason) { + console.log('leaveGame', reason); + } +} diff --git a/webclient/src/websocket/persistence/ModeratorPersistence.ts b/webclient/src/websocket/persistence/ModeratorPersistence.ts new file mode 100644 index 000000000..d1b991fa0 --- /dev/null +++ b/webclient/src/websocket/persistence/ModeratorPersistence.ts @@ -0,0 +1,46 @@ +import { ServerDispatch } from 'store'; +import { BanHistoryItem, LogItem, WarnHistoryItem, WarnListItem } from 'types'; + +import NormalizeService from '../utils/NormalizeService'; + +export class ModeratorPersistence { + static banFromServer(userName: string): void { + ServerDispatch.banFromServer(userName); + } + + static banHistory(userName: string, banHistory: BanHistoryItem[]): void { + ServerDispatch.banHistory(userName, banHistory); + } + + static viewLogs(logs: LogItem[]): void { + ServerDispatch.viewLogs(NormalizeService.normalizeLogs(logs)); + } + + static warnHistory(userName: string, warnHistory: WarnHistoryItem[]): void { + ServerDispatch.warnHistory(userName, warnHistory); + } + + static warnListOptions(warnList: WarnListItem[]): void { + ServerDispatch.warnListOptions(warnList); + } + + static warnUser(userName: string): void { + ServerDispatch.warnUser(userName); + } + + static grantReplayAccess(replayId: number, moderatorName: string): void { + ServerDispatch.grantReplayAccess(replayId, moderatorName); + } + + static forceActivateUser(usernameToActivate: string, moderatorName: string): void { + ServerDispatch.forceActivateUser(usernameToActivate, moderatorName); + } + + static getAdminNotes(userName: string, notes: string): void { + ServerDispatch.getAdminNotes(userName, notes); + } + + static updateAdminNotes(userName: string, notes: string): void { + ServerDispatch.updateAdminNotes(userName, notes); + } +} diff --git a/webclient/src/websocket/persistence/RoomPersistence.ts b/webclient/src/websocket/persistence/RoomPersistence.ts new file mode 100644 index 000000000..a69e79338 --- /dev/null +++ b/webclient/src/websocket/persistence/RoomPersistence.ts @@ -0,0 +1,63 @@ +import { store, RoomsDispatch, RoomsSelectors } from 'store'; +import { Game, Message, Room, User } from 'types'; +import NormalizeService from '../utils/NormalizeService'; + +export class RoomPersistence { + static clearStore() { + RoomsDispatch.clearStore(); + } + + static joinRoom(roomInfo: Room) { + NormalizeService.normalizeRoomInfo(roomInfo); + RoomsDispatch.joinRoom(roomInfo); + } + + static leaveRoom(roomId: number) { + RoomsDispatch.leaveRoom(roomId); + } + + static updateRooms(rooms: Room[]) { + RoomsDispatch.updateRooms(rooms); + } + + static updateGames(roomId: number, gameList: Game[]) { + const game = gameList[0]; + + if (!game.gameType) { + const room = RoomsSelectors.getRoom(store.getState(), roomId); + + if (room) { + const { gametypeMap } = room; + NormalizeService.normalizeGameObject(game, gametypeMap); + } + } + + RoomsDispatch.updateGames(roomId, gameList); + } + + static addMessage(roomId: number, message: Message) { + NormalizeService.normalizeUserMessage(message); + + RoomsDispatch.addMessage(roomId, message); + } + + static userJoined(roomId: number, user: User) { + RoomsDispatch.userJoined(roomId, user); + } + + static userLeft(roomId: number, name: string) { + RoomsDispatch.userLeft(roomId, name); + } + + static removeMessages(roomId: number, name: string, amount: number): void { + RoomsDispatch.removeMessages(roomId, name, amount); + }; + + static gameCreated(roomId: number) { + RoomsDispatch.gameCreated(roomId); + } + + static joinedGame(roomId: number, gameId: number) { + RoomsDispatch.joinedGame(roomId, gameId); + } +} diff --git a/webclient/src/websocket/persistence/SessionPersistence.ts b/webclient/src/websocket/persistence/SessionPersistence.ts new file mode 100644 index 000000000..9962af968 --- /dev/null +++ b/webclient/src/websocket/persistence/SessionPersistence.ts @@ -0,0 +1,247 @@ +import { ServerDispatch } from 'store'; +import { DeckList, DeckStorageTreeItem, ReplayMatch, StatusEnum, User, WebSocketConnectOptions } from 'types'; + +import { sanitizeHtml } from 'websocket/utils'; +import { + GameJoinedData, + NotifyUserData, + PlayerGamePropertiesData, + ServerShutdownData, + UserMessageData +} from '../events/session/interfaces'; +import NormalizeService from '../utils/NormalizeService'; + +export class SessionPersistence { + static initialized() { + ServerDispatch.initialized(); + } + + static clearStore() { + ServerDispatch.clearStore(); + } + + static loginSuccessful(options: WebSocketConnectOptions) { + ServerDispatch.loginSuccessful(options); + } + + static loginFailed() { + ServerDispatch.loginFailed(); + } + + static connectionClosed(reason: number) { + ServerDispatch.connectionClosed(reason); + } + + static connectionFailed() { + ServerDispatch.connectionFailed(); + } + + static testConnectionSuccessful() { + ServerDispatch.testConnectionSuccessful(); + } + + static testConnectionFailed() { + ServerDispatch.testConnectionFailed(); + } + + static updateBuddyList(buddyList) { + ServerDispatch.updateBuddyList(buddyList); + } + + static addToBuddyList(user: User) { + ServerDispatch.addToBuddyList(user); + } + + static removeFromBuddyList(userName: string) { + ServerDispatch.removeFromBuddyList(userName); + } + + static updateIgnoreList(ignoreList) { + ServerDispatch.updateIgnoreList(ignoreList); + } + + static addToIgnoreList(user: User) { + ServerDispatch.addToIgnoreList(user); + } + + static removeFromIgnoreList(userName: string) { + ServerDispatch.removeFromIgnoreList(userName); + } + + static updateInfo(name: string, version: string) { + ServerDispatch.updateInfo(name, version); + } + + static updateStatus(state: number, description: string) { + ServerDispatch.updateStatus(state, description); + + if (state === StatusEnum.DISCONNECTED) { + this.connectionClosed(state); + } + } + + static updateUser(user: User) { + ServerDispatch.updateUser(user); + } + + static updateUsers(users: User[]) { + ServerDispatch.updateUsers(users); + } + + static userJoined(user: User) { + ServerDispatch.userJoined(user); + } + + static userLeft(userName: string) { + ServerDispatch.userLeft(userName); + } + + static serverMessage(message: string) { + ServerDispatch.serverMessage(sanitizeHtml(message)); + } + + static accountAwaitingActivation(options: WebSocketConnectOptions) { + ServerDispatch.accountAwaitingActivation(options); + } + + static accountActivationSuccess() { + ServerDispatch.accountActivationSuccess(); + } + + static accountActivationFailed() { + ServerDispatch.accountActivationFailed(); + } + + static registrationRequiresEmail() { + ServerDispatch.registrationRequiresEmail(); + } + + static registrationSuccess() { + ServerDispatch.registrationSuccess(); + } + + static registrationFailed(reason: string, endTime?: number) { + const reasonMsg = endTime ? NormalizeService.normalizeBannedUserError(reason, endTime) : reason; + + ServerDispatch.registrationFailed(reasonMsg); + } + + static registrationEmailError(error: string) { + ServerDispatch.registrationEmailError(error); + } + + static registrationPasswordError(error: string) { + ServerDispatch.registrationPasswordError(error); + } + + static registrationUserNameError(error: string) { + ServerDispatch.registrationUserNameError(error); + } + + static resetPasswordChallenge() { + ServerDispatch.resetPasswordChallenge(); + } + + static resetPassword() { + ServerDispatch.resetPassword(); + } + + static resetPasswordSuccess() { + ServerDispatch.resetPasswordSuccess(); + } + + static resetPasswordFailed() { + ServerDispatch.resetPasswordFailed(); + } + + static accountPasswordChange(): void { + ServerDispatch.accountPasswordChange(); + } + + static accountEditChanged(realName?: string, email?: string, country?: string): void { + ServerDispatch.accountEditChanged({ realName, email, country }); + } + + static accountImageChanged(avatarBmp: Uint8Array): void { + ServerDispatch.accountImageChanged({ avatarBmp }); + } + + static directMessageSent(userName: string, message: string): void { + ServerDispatch.directMessageSent(userName, message); + } + + static getUserInfo(userInfo: User) { + ServerDispatch.getUserInfo(userInfo); + } + + static getGamesOfUser(userName: string, response: any): void { + // Response_GetGamesOfUser contains a gameList field — log for now until game layer is complete + console.log('getGamesOfUser', userName, response); + } + + static gameJoined(gameJoinedData: GameJoinedData): void { + console.log('gameJoined', gameJoinedData); + } + + static notifyUser(notification: NotifyUserData): void { + ServerDispatch.notifyUser(notification); + } + + static playerPropertiesChanged(payload: PlayerGamePropertiesData): void { + console.log('playerPropertiesChanged', payload); + } + + static serverShutdown(data: ServerShutdownData): void { + ServerDispatch.serverShutdown(data); + } + + static userMessage(messageData: UserMessageData): void { + ServerDispatch.userMessage(messageData); + } + + static addToList(list: string, userName: string): void { + ServerDispatch.addToList(list, userName) + } + + static removeFromList(list: string, userName: string): void { + ServerDispatch.removeFromList(list, userName); + } + + static deleteServerDeck(deckId: number): void { + ServerDispatch.deckDelete(deckId); + } + + static updateServerDecks(deckList: DeckList): void { + ServerDispatch.backendDecks(deckList); + } + + static uploadServerDeck(path: string, treeItem: DeckStorageTreeItem): void { + ServerDispatch.deckUpload(path, treeItem); + } + + static createServerDeckDir(path: string, dirName: string): void { + ServerDispatch.deckNewDir(path, dirName); + } + + static deleteServerDeckDir(path: string): void { + ServerDispatch.deckDelDir(path); + } + + static replayList(matchList: ReplayMatch[]): void { + ServerDispatch.replayList(matchList); + } + + static replayAdded(matchInfo: ReplayMatch): void { + ServerDispatch.replayAdded(matchInfo); + } + + static replayModifyMatch(gameId: number, doNotHide: boolean): void { + ServerDispatch.replayModifyMatch(gameId, doNotHide); + } + + static replayDeleteMatch(gameId: number): void { + ServerDispatch.replayDeleteMatch(gameId); + } +} + + diff --git a/webclient/src/websocket/persistence/index.ts b/webclient/src/websocket/persistence/index.ts new file mode 100644 index 000000000..a1e34fffa --- /dev/null +++ b/webclient/src/websocket/persistence/index.ts @@ -0,0 +1,5 @@ +export { AdminPersistence } from './AdminPersistence'; +export { RoomPersistence } from './RoomPersistence'; +export { SessionPersistence } from './SessionPersistence'; +export { ModeratorPersistence } from './ModeratorPersistence'; +export { GamePersistence } from './GamePersistence'; diff --git a/webclient/src/websocket/services/BackendService.ts b/webclient/src/websocket/services/BackendService.ts new file mode 100644 index 000000000..fce741d35 --- /dev/null +++ b/webclient/src/websocket/services/BackendService.ts @@ -0,0 +1,82 @@ +import webClient from '../WebClient'; +import { ProtoController } from './ProtoController'; + +export interface CommandOptions { + responseName?: string; + onSuccess?: (response: any, raw: any) => void; + onError?: (responseCode: number, raw: any) => void; + onResponseCode?: { [code: number]: (raw: any) => void }; + onResponse?: (raw: any) => void; +} + +export class BackendService { + static sendSessionCommand(commandName: string, params: any, options: CommandOptions): void { + const command = ProtoController.root[commandName].create(params || {}); + const sc = ProtoController.root.SessionCommand.create({ + [`.${commandName}.ext`]: command, + }); + webClient.protobuf.sendSessionCommand(sc, raw => { + BackendService.handleResponse(commandName, raw, options); + }); + } + + static sendRoomCommand(roomId: number, commandName: string, params: any, options: CommandOptions): void { + const command = ProtoController.root[commandName].create(params || {}); + const rc = ProtoController.root.RoomCommand.create({ + [`.${commandName}.ext`]: command, + }); + webClient.protobuf.sendRoomCommand(roomId, rc, raw => { + BackendService.handleResponse(commandName, raw, options); + }); + } + + static sendModeratorCommand(commandName: string, params: any, options: CommandOptions): void { + const command = ProtoController.root[commandName].create(params || {}); + const mc = ProtoController.root.ModeratorCommand.create({ + [`.${commandName}.ext`]: command, + }); + webClient.protobuf.sendModeratorCommand(mc, raw => { + BackendService.handleResponse(commandName, raw, options); + }); + } + + static sendAdminCommand(commandName: string, params: any, options: CommandOptions): void { + const command = ProtoController.root[commandName].create(params || {}); + const ac = ProtoController.root.AdminCommand.create({ + [`.${commandName}.ext`]: command, + }); + webClient.protobuf.sendAdminCommand(ac, raw => { + BackendService.handleResponse(commandName, raw, options); + }); + } + + private static handleResponse(commandName: string, raw: any, options: CommandOptions): void { + if (options.onResponse) { + options.onResponse(raw); + return; + } + + const { responseCode } = raw; + + if (responseCode === ProtoController.root.Response.ResponseCode.RespOk) { + if (options.onSuccess) { + const response = options.responseName + ? raw[`.${options.responseName}.ext`] + : raw; + options.onSuccess(response, raw); + } + return; + } + + if (options.onResponseCode?.[responseCode]) { + options.onResponseCode[responseCode](raw); + return; + } + + if (options.onError) { + options.onError(responseCode, raw); + } else { + console.error(`${commandName} failed with response code: ${responseCode}`); + } + } +} diff --git a/webclient/src/websocket/services/KeepAliveService.spec.ts b/webclient/src/websocket/services/KeepAliveService.spec.ts new file mode 100644 index 000000000..df5169b4c --- /dev/null +++ b/webclient/src/websocket/services/KeepAliveService.spec.ts @@ -0,0 +1,68 @@ +import { KeepAliveService } from './KeepAliveService'; + +import webClient from '../WebClient'; + +describe('KeepAliveService', () => { + let service: KeepAliveService; + + beforeEach(() => { + jest.useFakeTimers(); + + service = new KeepAliveService(webClient.socket); + }); + + it('should create', () => { + expect(service).toBeDefined(); + }); + + describe('startPingLoop', () => { + let resolvePing; + let interval; + let promise; + let ping; + let checkReadyStateSpy; + + beforeEach(() => { + interval = 100; + promise = new Promise(resolve => resolvePing = resolve); + ping = (done) => promise.then(done); + + checkReadyStateSpy = jest.spyOn(webClient.socket, 'checkReadyState'); + checkReadyStateSpy.mockImplementation(() => true); + + service.startPingLoop(interval, ping); + jest.advanceTimersByTime(interval); + }); + + it('should start ping loop', () => { + expect((service as any).keepalivecb).toBeDefined(); + expect((service as any).lastPingPending).toBeTruthy(); + }); + + it('should call ping callback when done', (done: jest.DoneCallback) => { + resolvePing(); + + promise.then(() => { + expect((service as any).lastPingPending).toBeFalsy(); + done(); + }); + }); + + it('should fire disconnected$ if lastPingPending is still true', () => { + jest.spyOn(service.disconnected$, 'next').mockImplementation(() => {}); + jest.advanceTimersByTime(interval); + + expect(service.disconnected$.next).toHaveBeenCalled(); + }); + + it('should endPingLoop if socket is not open', () => { + jest.spyOn(service, 'endPingLoop').mockImplementation(() => {}); + checkReadyStateSpy.mockImplementation(() => false); + + resolvePing(); + jest.advanceTimersByTime(interval); + + expect(service.endPingLoop).toHaveBeenCalled(); + }); + }); +}); diff --git a/webclient/src/websocket/services/KeepAliveService.ts b/webclient/src/websocket/services/KeepAliveService.ts new file mode 100644 index 000000000..070a532e6 --- /dev/null +++ b/webclient/src/websocket/services/KeepAliveService.ts @@ -0,0 +1,40 @@ +import { Subject } from 'rxjs'; + +import { WebSocketService } from './WebSocketService'; + +export class KeepAliveService { + private socket: WebSocketService; + + private keepalivecb: NodeJS.Timeout; + private lastPingPending: boolean; + + public disconnected$ = new Subject(); + + constructor(socket: WebSocketService) { + this.socket = socket; + } + + public startPingLoop(interval: number, ping: Function): void { + this.keepalivecb = setInterval(() => { + // check if the previous ping got no reply + if (this.lastPingPending) { + this.disconnected$.next(); + } + + // stop the ping loop if we"re disconnected + if (!this.socket.checkReadyState(WebSocket.OPEN)) { + this.endPingLoop(); + return; + } + + this.lastPingPending = true; + ping(() => this.lastPingPending = false); + }, interval); + } + + public endPingLoop() { + clearInterval(this.keepalivecb); + this.keepalivecb = null; + this.lastPingPending = false; + } +} diff --git a/webclient/src/websocket/services/ProtoController.ts b/webclient/src/websocket/services/ProtoController.ts new file mode 100644 index 000000000..130d5e196 --- /dev/null +++ b/webclient/src/websocket/services/ProtoController.ts @@ -0,0 +1,24 @@ +import protobuf from 'protobufjs'; + +import { SessionPersistence } from '../persistence'; +import ProtoFiles from '../../proto-files.json'; + +const PB_FILE_DIR = `${process.env.PUBLIC_URL}/pb`; + +// Leaf module — no imports from the websocket layer other than persistence. +// Both BackendService and ProtobufService import this; neither should import +// the other for controller access, avoiding circular dependency cycles. +export const ProtoController = { + root: null as any, + + load(): void { + const files = ProtoFiles.map(file => `${PB_FILE_DIR}/${file}`); + ProtoController.root = new protobuf.Root(); + ProtoController.root.load(files, { keepCase: false }, (err: Error) => { + if (err) { + throw err; + } + SessionPersistence.initialized(); + }); + }, +}; diff --git a/webclient/src/websocket/services/ProtobufService.ts b/webclient/src/websocket/services/ProtobufService.ts new file mode 100644 index 000000000..20c4e8599 --- /dev/null +++ b/webclient/src/websocket/services/ProtobufService.ts @@ -0,0 +1,138 @@ +import { CommonEvents, GameEvents, RoomEvents, SessionEvents } from '../events'; +import { WebClient } from '../WebClient'; +import { SessionCommands } from 'websocket'; +import { ProtoController } from './ProtoController'; + +export interface ProtobufEvents { + [event: string]: Function; +} + +export class ProtobufService { + private cmdId = 0; + private pendingCommands: { [cmdId: string]: Function } = {}; + + private webClient: WebClient; + + constructor(webClient: WebClient) { + this.webClient = webClient; + ProtoController.load(); + } + + public resetCommands() { + this.cmdId = 0; + this.pendingCommands = {}; + } + + public sendRoomCommand(roomId: number, roomCmd: any, callback?: Function) { + const cmd = ProtoController.root.CommandContainer.create({ + 'roomId': roomId, + 'roomCommand': [roomCmd] + }); + + this.sendCommand(cmd, raw => callback && callback(raw)); + } + + public sendSessionCommand(sesCmd: any, callback?: Function) { + const cmd = ProtoController.root.CommandContainer.create({ + 'sessionCommand': [sesCmd] + }); + + this.sendCommand(cmd, (raw) => callback && callback(raw)); + } + + public sendModeratorCommand(modCmd: any, callback?: Function) { + const cmd = ProtoController.root.CommandContainer.create({ + 'moderatorCommand': [modCmd] + }); + + this.sendCommand(cmd, (raw) => callback && callback(raw)); + } + + public sendAdminCommand(adminCmd: any, callback?: Function) { + const cmd = ProtoController.root.CommandContainer.create({ + 'adminCommand': [adminCmd] + }); + + this.sendCommand(cmd, (raw) => callback && callback(raw)); + } + + public sendCommand(cmd: any, callback: Function) { + this.cmdId++; + + cmd['cmdId'] = this.cmdId; + this.pendingCommands[this.cmdId] = callback; + + if (this.webClient.socket.checkReadyState(WebSocket.OPEN)) { + this.webClient.socket.send(ProtoController.root.CommandContainer.encode(cmd).finish()); + } + } + + public sendKeepAliveCommand(pingReceived: Function) { + SessionCommands.ping(pingReceived); + } + + public handleMessageEvent({ data }: MessageEvent): void { + try { + const uint8msg = new Uint8Array(data); + const msg = ProtoController.root.ServerMessage.decode(uint8msg); + + if (msg) { + switch (msg.messageType) { + case ProtoController.root.ServerMessage.MessageType.RESPONSE: + this.processServerResponse(msg.response); + break; + case ProtoController.root.ServerMessage.MessageType.ROOM_EVENT: + this.processRoomEvent(msg.roomEvent, msg); + break; + case ProtoController.root.ServerMessage.MessageType.SESSION_EVENT: + this.processSessionEvent(msg.sessionEvent, msg); + break; + case ProtoController.root.ServerMessage.MessageType.GAME_EVENT_CONTAINER: + this.processGameEvent(msg.gameEvent, msg); + break; + default: + console.log(msg); + break; + } + } + } catch (err) { + console.error('Processing failed:', err); + } + } + + private processServerResponse(response: any) { + const { cmdId } = response; + + if (this.pendingCommands[cmdId]) { + this.pendingCommands[cmdId](response); + delete this.pendingCommands[cmdId]; + } + } + + private processCommonEvent(response: any, raw: any) { + this.processEvent(response, CommonEvents, raw); + } + + private processRoomEvent(response: any, raw: any) { + this.processEvent(response, RoomEvents, raw); + } + + private processSessionEvent(response: any, raw: any) { + this.processEvent(response, SessionEvents, raw); + } + + private processGameEvent(response: any, raw: any): void { + this.processEvent(response, GameEvents, raw); + } + + private processEvent(response: any, events: ProtobufEvents, raw: any) { + for (const event in events) { + const payload = response[event]; + + if (payload) { + events[event](payload, raw); + return; + } + } + } +} diff --git a/webclient/src/websocket/services/WebSocketService.ts b/webclient/src/websocket/services/WebSocketService.ts new file mode 100644 index 000000000..95fa2d356 --- /dev/null +++ b/webclient/src/websocket/services/WebSocketService.ts @@ -0,0 +1,129 @@ +import { Subject } from 'rxjs'; + +import { StatusEnum, WebSocketConnectOptions } from 'types'; + +import { KeepAliveService } from './KeepAliveService'; +import { WebClient } from '../WebClient'; +import { SessionPersistence } from '../persistence'; +import { updateStatus } from '../commands/session'; + +export class WebSocketService { + private socket: WebSocket; + private testSocket: WebSocket; + + private webClient: WebClient; + private keepAliveService: KeepAliveService; + + public message$: Subject = new Subject(); + + private keepalive: number; + + constructor(webClient: WebClient) { + this.webClient = webClient; + + this.keepAliveService = new KeepAliveService(this); + this.keepAliveService.disconnected$.subscribe(() => { + this.disconnect(); + updateStatus(StatusEnum.DISCONNECTED, 'Connection timeout'); + }); + } + + public connect(options: WebSocketConnectOptions, protocol: string = 'wss'): void { + if (window.location.hostname === 'localhost') { + protocol = 'ws'; + } + + const { host, port } = options; + this.keepalive = this.webClient.clientOptions.keepalive; + + this.socket = this.createWebSocket(`${protocol}://${host}:${port}`); + } + + public testConnect(options: WebSocketConnectOptions, protocol: string = 'wss'): void { + if (window.location.hostname === 'localhost') { + protocol = 'ws'; + } + + const { host, port } = options; + + this.testWebSocket(`${protocol}://${host}:${port}`); + } + + public disconnect(): void { + if (this.socket) { + this.socket.close(); + } + } + + public checkReadyState(state: number): boolean { + return this.socket?.readyState === state; + } + + public send(message): void { + this.socket.send(message); + } + + private createWebSocket(url: string): WebSocket { + const socket = new WebSocket(url); + socket.binaryType = 'arraybuffer'; + + const connectionTimer = setTimeout(() => socket.close(), this.keepalive); + + socket.onopen = () => { + clearTimeout(connectionTimer); + updateStatus(StatusEnum.CONNECTED, 'Connected'); + + this.keepAliveService.startPingLoop(this.keepalive, (pingReceived: Function) => { + this.webClient.keepAlive(pingReceived); + }); + }; + + socket.onclose = () => { + // dont overwrite failure messages + if (this.webClient.status !== StatusEnum.DISCONNECTED) { + updateStatus(StatusEnum.DISCONNECTED, 'Connection Closed'); + } + + this.keepAliveService.endPingLoop(); + }; + + socket.onerror = () => { + updateStatus(StatusEnum.DISCONNECTED, 'Connection Failed'); + SessionPersistence.connectionFailed(); + }; + + socket.onmessage = (event: MessageEvent) => { + this.message$.next(event); + } + + return socket; + } + + private testWebSocket(url: string): void { + if (this.testSocket) { + this.testSocket.onerror = null; + this.testSocket.close(); + } + + const socket = new WebSocket(url); + socket.binaryType = 'arraybuffer'; + + const connectionTimer = setTimeout(() => socket.close(), this.webClient.clientOptions.keepalive); + + socket.onopen = () => { + clearTimeout(connectionTimer); + SessionPersistence.testConnectionSuccessful(); + socket.close(); + }; + + socket.onerror = () => { + SessionPersistence.testConnectionFailed(); + }; + + socket.onclose = () => { + this.testSocket = null; + } + + this.testSocket = socket; + } +} diff --git a/webclient/src/websocket/utils/NormalizeService.ts b/webclient/src/websocket/utils/NormalizeService.ts new file mode 100644 index 000000000..4d9d37ba6 --- /dev/null +++ b/webclient/src/websocket/utils/NormalizeService.ts @@ -0,0 +1,63 @@ +import { Game, GametypeMap, LogItem, LogGroups, Message, Room } from 'types'; + +export default class NormalizeService { + // Flatten room gameTypes into map object + static normalizeRoomInfo(roomInfo: Room): void { + roomInfo.gametypeMap = {}; + + const { gametypeList, gametypeMap, gameList } = roomInfo; + + gametypeList.reduce((map, type) => { + map[type.gameTypeId] = type.description; + return map; + }, gametypeMap); + + gameList.forEach((game) => NormalizeService.normalizeGameObject(game, gametypeMap)); + } + + // Flatten gameTypes[] into gameType field + // Default sortable values ("" || 0 || -1) + static normalizeGameObject(game: Game, gametypeMap: GametypeMap): void { + const { gameTypes, description } = game; + const hasType = gameTypes && gameTypes.length; + game.gameType = hasType ? gametypeMap[gameTypes[0]] : ''; + + game.description = description || ''; + } + + // Flatten logs[] into object mapped by targetType (room, game, chat) + static normalizeLogs(logs: LogItem[]): LogGroups { + return logs.reduce((obj, log) => { + const { targetType } = log; + obj[targetType] = obj[targetType] || []; + obj[targetType].push(log); + return obj; + }, {} as LogGroups); + } + + // messages sent by current user dont have their username prepended + static normalizeUserMessage(message: Message): void { + const { name } = message; + + if (name) { + message.message = `${name}: ${message.message}`; + } + } + + // Banned reason string is not being exposed by the server + static normalizeBannedUserError(reasonStr: string, endTime: number): string { + let error; + + if (endTime) { + error = 'You are banned until ' + new Date(endTime).toString(); + } else { + error = 'You are permanently banned'; + } + + if (reasonStr) { + error += '\n\n' + reasonStr; + } + + return error; + } +} diff --git a/webclient/src/websocket/utils/guid.util.ts b/webclient/src/websocket/utils/guid.util.ts new file mode 100644 index 000000000..73b17d719 --- /dev/null +++ b/webclient/src/websocket/utils/guid.util.ts @@ -0,0 +1,8 @@ +function s4(): string { + const s4 = Math.floor((1 + Math.random()) * 0x10000); + return s4.toString(16).substring(1); +} + +export function guid(): string { + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); +} diff --git a/webclient/src/websocket/utils/index.ts b/webclient/src/websocket/utils/index.ts new file mode 100644 index 000000000..4147f4af8 --- /dev/null +++ b/webclient/src/websocket/utils/index.ts @@ -0,0 +1,3 @@ +export * from './guid.util'; +export * from './sanitizeHtml.util'; +export * from './passwordHasher'; diff --git a/webclient/src/websocket/utils/passwordHasher.ts b/webclient/src/websocket/utils/passwordHasher.ts new file mode 100644 index 000000000..164a91823 --- /dev/null +++ b/webclient/src/websocket/utils/passwordHasher.ts @@ -0,0 +1,32 @@ +import sha512 from 'crypto-js/sha512'; +import Base64 from 'crypto-js/enc-base64'; +import { ProtoController } from '../services/ProtoController'; + +const HASH_ROUNDS = 1_000; +const SALT_LENGTH = 16; + +export const hashPassword = (salt: string, password: string): string => { + let hashedPassword = salt + password; + for (let i = 0; i < HASH_ROUNDS; i++) { + // WHY DO WE DO IT THIS WAY? + hashedPassword = sha512(hashedPassword); + } + + return salt + Base64.stringify(hashedPassword); +}; + +export const generateSalt = (): string => { + const characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + + let salt = ''; + for (let i = 0; i < SALT_LENGTH; i++) { + salt += characters.charAt(Math.floor(Math.random() * characters.length)); + } + + return salt; +} + +export const passwordSaltSupported = (serverOptions: number): number => { + // Intentional use of Bitwise operator b/c of how Servatrice Enums work + return serverOptions & ProtoController.root.Event_ServerIdentification.ServerOptions.SupportsPasswordHash; +} diff --git a/webclient/src/websocket/utils/sanitizeHtml.util.ts b/webclient/src/websocket/utils/sanitizeHtml.util.ts new file mode 100644 index 000000000..e5321213b --- /dev/null +++ b/webclient/src/websocket/utils/sanitizeHtml.util.ts @@ -0,0 +1,17 @@ +import sanitize from 'sanitize-html'; + +export function sanitizeHtml(msg: string): string { + return sanitize(msg, { + allowedTags: ['br', 'a', 'img', 'center', 'b', 'font'], + allowedAttributes: { + '*': ['href', 'color', 'rel', 'target'], + }, + allowedSchemes: ['http', 'https', 'ftp'], + transformTags: { + 'a': sanitize.simpleTransform('a', { + target: '_blank', + rel: 'noopener noreferrer', + }), + } + }); +} diff --git a/webclient/tsconfig.json b/webclient/tsconfig.json new file mode 100644 index 000000000..0e35af354 --- /dev/null +++ b/webclient/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "baseUrl": "src", + "target": "es6", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "typeRoots": [ + "node_modules/@types" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "noFallthroughCasesInSwitch": false + }, + "include": [ + "src" + ] +} diff --git a/zonebg/CMakeLists.txt b/zonebg/CMakeLists.txt deleted file mode 100644 index b250a44b0..000000000 --- a/zonebg/CMakeLists.txt +++ /dev/null @@ -1,16 +0,0 @@ -# CMakeLists for zonebg/ directory -# -# Installs default "zone background" files - -FILE(GLOB zonebg "${CMAKE_CURRENT_SOURCE_DIR}/*.png" "${CMAKE_CURRENT_SOURCE_DIR}/*.jpg") - -if(UNIX) - if(APPLE) - INSTALL(FILES ${zonebg} DESTINATION cockatrice.app/Contents/Resources/zonebg/) - else() - # Assume linux - INSTALL(FILES ${zonebg} DESTINATION share/cockatrice/zonebg/) - endif() -elseif(WIN32) - INSTALL(FILES ${zonebg} DESTINATION zonebg/) -endif() \ No newline at end of file diff --git a/zonebg/VelvetMarble_VerticalHand_Hand.jpg b/zonebg/VelvetMarble_VerticalHand_Hand.jpg deleted file mode 100644 index 83e8915ad..000000000 Binary files a/zonebg/VelvetMarble_VerticalHand_Hand.jpg and /dev/null differ diff --git a/zonebg/VelvetMarble_VerticalHand_Player.jpg b/zonebg/VelvetMarble_VerticalHand_Player.jpg deleted file mode 100644 index d86987bff..000000000 Binary files a/zonebg/VelvetMarble_VerticalHand_Player.jpg and /dev/null differ