diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1912c72 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +cmake-build-debug/ +cmake-build-wsl/ +.idea/ +build/ +Dockerfile +.git diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..245ceb0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +on: push + +jobs: + check: + runs-on: ubuntu-latest + container: ilyaaid/watch_up + steps: + - uses: actions/checkout@v3 + - run: make check + + build: + runs-on: ubuntu-latest + container: ilyaaid/watch_up + steps: + - uses: actions/checkout@v3 + - run: make build + + test: + runs-on: ubuntu-latest + container: ilyaaid/watch_up + steps: + - uses: actions/checkout@v3 + - run: make test + + coverage: + runs-on: ubuntu-latest + container: ilyaaid/watch_up + steps: + - uses: actions/checkout@v3 + - run: git config --global --add safe.directory /__w/2022_2_cash_map/2022_2_cash_map + - run: git submodule update --init + - run: make coverage + - run: gcovr -r . -s | sed '/tests/d' | sed '/server/d' > client/README.txt + - run: gcovr -r . -s | sed '/tests/d' | sed '/client/d' > server/README.txt + - run: git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + - run: git config --global user.name "github-actions[bot]" + - run: git add client/README.txt server/README.txt + - run: git commit -m "auto commit coverage report" && git push || echo "coverage not changed." + - uses: actions/upload-artifact@v3 + with: + name: coverage-report + path: build/lcov/html + retention-days: 5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92b9f01 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +cmake-build-debug/ +cmake-build-docker/ +cmake-build-wsl/ +cmake-build-mingw/ +.idea/ +build/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5fcaa96 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "externals/CMake-codecov"] + path = externals/CMake-codecov + url = https://github.com/ivan12093/CMake-codecov.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..8a50b04 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.18) + +include("cmake/HunterGate.cmake") +HunterGate( + URL "https://github.com/cpp-pm/hunter/archive/v0.23.314.tar.gz" + SHA1 "95c47c92f68edb091b5d6d18924baabe02a6962a" +) + +project(watch_up_project) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +option(BUILD_TESTS "Build tests" ON) +option(BUILD_DOCS "Build documentation" OFF) +option(BUILD_COVERAGE "Build code coverage" OFF) + +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wpedantic -Werror -Wall -Wextra") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-command-line-argument") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wshadow -Wnon-virtual-dtor") + +find_package(GTest CONFIG) +if (NOT GTest_FOUND) + hunter_add_package(GTest) +endif() +find_package(GTest CONFIG REQUIRED) + +if (BUILD_TESTS) + include(GoogleTest) + enable_testing() +endif() + +if (BUILD_COVERAGE) + set(ENABLE_COVERAGE ON CACHE BOOL "Enable coverage build." FORCE) + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/externals/CMake-codecov/cmake") + find_package(codecov) + string(APPEND CMAKE_CXX_FLAGS " --coverage") +endif() + +add_subdirectory(server) +add_subdirectory(client) + +if (BUILD_COVERAGE) + coverage_evaluate() +endif() diff --git a/CPPLINT.cfg b/CPPLINT.cfg new file mode 100644 index 0000000..eb8ba28 --- /dev/null +++ b/CPPLINT.cfg @@ -0,0 +1,8 @@ +headers=h,hpp +linelength=110 +filter=-whitespace/tab +filter=-runtime/int +filter=-legal/copyright +filter=-build/include_subdir +filter=-build/include +filter=-readability/casting diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..aea7085 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM ubuntu +ENV DEBIAN_FRONTEND noninteractive +WORKDIR cash_map +COPY . . +RUN apt -y update && \ + apt -y install python3 cppcheck clang-tidy make git lcov \ + gcovr python3-pip libgtest-dev build-essential gcc g++ gdb clang cmake && \ + pip3 install cpplint && \ + pip3 install --upgrade cmake && \ + apt clean && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8478bd4 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +.PHONY: all build rebuild check test testextra memtest memtestextra clean coverage + +all: clean check build test coverage + +clean: + rm -rf build + +check: + cd scripts && chmod +x run_linters.sh && ./run_linters.sh + +build: + cd scripts && chmod +x build.sh && ./build.sh + +test: build + cd scripts && chmod +x run_tests.sh && ./run_tests.sh + +coverage: + cd scripts && chmod +x coverage.sh && ./coverage.sh diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt new file mode 100644 index 0000000..3ec0098 --- /dev/null +++ b/client/CMakeLists.txt @@ -0,0 +1,24 @@ +project(client) + +file(GLOB SOURCES "src/*.cpp") + +list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/client.cpp") + +add_library(${PROJECT_NAME}_lib STATIC ${SOURCES}) + +target_include_directories(${PROJECT_NAME}_lib PUBLIC include) + +add_executable(${PROJECT_NAME} ./src/client.cpp) + +target_include_directories(${PROJECT_NAME} PUBLIC include) + +target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}_lib) + +if (BUILD_TESTS) + add_subdirectory(tests) +endif() + +if (BUILD_COVERAGE) + add_coverage(${PROJECT_NAME}) + add_coverage(${PROJECT_NAME}_lib) +endif() diff --git a/client/README.txt b/client/README.txt new file mode 100644 index 0000000..166cc18 --- /dev/null +++ b/client/README.txt @@ -0,0 +1,13 @@ +------------------------------------------------------------------------------ + GCC Code Coverage Report +Directory: . +------------------------------------------------------------------------------ +File Lines Exec Cover Missing +------------------------------------------------------------------------------ +client/src/client.cpp 5 0 0% 5-9 +client/src/lib.cpp 2 2 100% +------------------------------------------------------------------------------ +TOTAL 21 9 42% +------------------------------------------------------------------------------ +lines: 42.9% (9 out of 21) +branches: 18.4% (7 out of 38) diff --git a/client/include/lib.hpp b/client/include/lib.hpp new file mode 100644 index 0000000..e3fc9b0 --- /dev/null +++ b/client/include/lib.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include + +std::string foo(); diff --git a/client/src/client.cpp b/client/src/client.cpp new file mode 100644 index 0000000..7b0b505 --- /dev/null +++ b/client/src/client.cpp @@ -0,0 +1,10 @@ +#include + +#include "lib.hpp" + +int main() { + std::cout << "Client exe" << "\n"; + std::cout << foo() << "\n"; + for (int i = 1; i < 2; ++i) {} + return 0; +} diff --git a/client/src/lib.cpp b/client/src/lib.cpp new file mode 100644 index 0000000..c4b52a4 --- /dev/null +++ b/client/src/lib.cpp @@ -0,0 +1,7 @@ +#include + +#include "lib.hpp" + +std::string foo() { + return "Foo lib"; +} diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt new file mode 100644 index 0000000..652be47 --- /dev/null +++ b/client/tests/CMakeLists.txt @@ -0,0 +1,11 @@ +project(test_client) + +file(GLOB TEST_SOURCES "*.cpp") + +add_executable(${PROJECT_NAME} ${TEST_SOURCES}) + +target_link_libraries(${PROJECT_NAME} client_lib GTest::gtest_main) + +#add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) +gtest_discover_tests(${PROJECT_NAME}) + diff --git a/client/tests/test.cpp b/client/tests/test.cpp new file mode 100644 index 0000000..54c4d27 --- /dev/null +++ b/client/tests/test.cpp @@ -0,0 +1,9 @@ +#include +#include + +#include "lib.hpp" + +TEST(Lib_test, test_foo) { + std::string a = "Foo lib"; + EXPECT_EQ(a, foo()); +} \ No newline at end of file diff --git a/cmake/HunterGate.cmake b/cmake/HunterGate.cmake new file mode 100644 index 0000000..04886ef --- /dev/null +++ b/cmake/HunterGate.cmake @@ -0,0 +1,528 @@ +# Copyright (c) 2013-2019, Ruslan Baratov +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT HOLDER 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. + +# This is a gate file to Hunter package manager. +# Include this file using `include` command and add package you need, example: +# +# cmake_minimum_required(VERSION 3.2) +# +# include("cmake/HunterGate.cmake") +# HunterGate( +# URL "https://github.com/path/to/hunter/archive.tar.gz" +# SHA1 "798501e983f14b28b10cda16afa4de69eee1da1d" +# ) +# +# project(MyProject) +# +# hunter_add_package(Foo) +# hunter_add_package(Boo COMPONENTS Bar Baz) +# +# Projects: +# * https://github.com/hunter-packages/gate/ +# * https://github.com/ruslo/hunter + +option(HUNTER_ENABLED "Enable Hunter package manager support" ON) + +if(HUNTER_ENABLED) + if(CMAKE_VERSION VERSION_LESS "3.2") + message( + FATAL_ERROR + "At least CMake version 3.2 required for Hunter dependency management." + " Update CMake or set HUNTER_ENABLED to OFF." + ) + endif() +endif() + +include(CMakeParseArguments) # cmake_parse_arguments + +option(HUNTER_STATUS_PRINT "Print working status" ON) +option(HUNTER_STATUS_DEBUG "Print a lot info" OFF) +option(HUNTER_TLS_VERIFY "Enable/disable TLS certificate checking on downloads" ON) + +set(HUNTER_ERROR_PAGE "https://docs.hunter.sh/en/latest/reference/errors") + +function(hunter_gate_status_print) + if(HUNTER_STATUS_PRINT OR HUNTER_STATUS_DEBUG) + foreach(print_message ${ARGV}) + message(STATUS "[hunter] ${print_message}") + endforeach() + endif() +endfunction() + +function(hunter_gate_status_debug) + if(HUNTER_STATUS_DEBUG) + foreach(print_message ${ARGV}) + string(TIMESTAMP timestamp) + message(STATUS "[hunter *** DEBUG *** ${timestamp}] ${print_message}") + endforeach() + endif() +endfunction() + +function(hunter_gate_error_page error_page) + message("------------------------------ ERROR ------------------------------") + message(" ${HUNTER_ERROR_PAGE}/${error_page}.html") + message("-------------------------------------------------------------------") + message("") + message(FATAL_ERROR "") +endfunction() + +function(hunter_gate_internal_error) + message("") + foreach(print_message ${ARGV}) + message("[hunter ** INTERNAL **] ${print_message}") + endforeach() + message("[hunter ** INTERNAL **] [Directory:${CMAKE_CURRENT_LIST_DIR}]") + message("") + hunter_gate_error_page("error.internal") +endfunction() + +function(hunter_gate_fatal_error) + cmake_parse_arguments(hunter "" "ERROR_PAGE" "" "${ARGV}") + if("${hunter_ERROR_PAGE}" STREQUAL "") + hunter_gate_internal_error("Expected ERROR_PAGE") + endif() + message("") + foreach(x ${hunter_UNPARSED_ARGUMENTS}) + message("[hunter ** FATAL ERROR **] ${x}") + endforeach() + message("[hunter ** FATAL ERROR **] [Directory:${CMAKE_CURRENT_LIST_DIR}]") + message("") + hunter_gate_error_page("${hunter_ERROR_PAGE}") +endfunction() + +function(hunter_gate_user_error) + hunter_gate_fatal_error(${ARGV} ERROR_PAGE "error.incorrect.input.data") +endfunction() + +function(hunter_gate_self root version sha1 result) + string(COMPARE EQUAL "${root}" "" is_bad) + if(is_bad) + hunter_gate_internal_error("root is empty") + endif() + + string(COMPARE EQUAL "${version}" "" is_bad) + if(is_bad) + hunter_gate_internal_error("version is empty") + endif() + + string(COMPARE EQUAL "${sha1}" "" is_bad) + if(is_bad) + hunter_gate_internal_error("sha1 is empty") + endif() + + string(SUBSTRING "${sha1}" 0 7 archive_id) + + set( + hunter_self + "${root}/_Base/Download/Hunter/${version}/${archive_id}/Unpacked" + ) + + set("${result}" "${hunter_self}" PARENT_SCOPE) +endfunction() + +# Set HUNTER_GATE_ROOT cmake variable to suitable value. +function(hunter_gate_detect_root) + # Check CMake variable + string(COMPARE NOTEQUAL "${HUNTER_ROOT}" "" not_empty) + if(not_empty) + set(HUNTER_GATE_ROOT "${HUNTER_ROOT}" PARENT_SCOPE) + hunter_gate_status_debug("HUNTER_ROOT detected by cmake variable") + return() + endif() + + # Check environment variable + string(COMPARE NOTEQUAL "$ENV{HUNTER_ROOT}" "" not_empty) + if(not_empty) + set(HUNTER_GATE_ROOT "$ENV{HUNTER_ROOT}" PARENT_SCOPE) + hunter_gate_status_debug("HUNTER_ROOT detected by environment variable") + return() + endif() + + # Check HOME environment variable + string(COMPARE NOTEQUAL "$ENV{HOME}" "" result) + if(result) + set(HUNTER_GATE_ROOT "$ENV{HOME}/.hunter" PARENT_SCOPE) + hunter_gate_status_debug("HUNTER_ROOT set using HOME environment variable") + return() + endif() + + # Check SYSTEMDRIVE and USERPROFILE environment variable (windows only) + if(WIN32) + string(COMPARE NOTEQUAL "$ENV{SYSTEMDRIVE}" "" result) + if(result) + set(HUNTER_GATE_ROOT "$ENV{SYSTEMDRIVE}/.hunter" PARENT_SCOPE) + hunter_gate_status_debug( + "HUNTER_ROOT set using SYSTEMDRIVE environment variable" + ) + return() + endif() + + string(COMPARE NOTEQUAL "$ENV{USERPROFILE}" "" result) + if(result) + set(HUNTER_GATE_ROOT "$ENV{USERPROFILE}/.hunter" PARENT_SCOPE) + hunter_gate_status_debug( + "HUNTER_ROOT set using USERPROFILE environment variable" + ) + return() + endif() + endif() + + hunter_gate_fatal_error( + "Can't detect HUNTER_ROOT" + ERROR_PAGE "error.detect.hunter.root" + ) +endfunction() + +function(hunter_gate_download dir) + string( + COMPARE + NOTEQUAL + "$ENV{HUNTER_DISABLE_AUTOINSTALL}" + "" + disable_autoinstall + ) + if(disable_autoinstall AND NOT HUNTER_RUN_INSTALL) + hunter_gate_fatal_error( + "Hunter not found in '${dir}'" + "Set HUNTER_RUN_INSTALL=ON to auto-install it from '${HUNTER_GATE_URL}'" + "Settings:" + " HUNTER_ROOT: ${HUNTER_GATE_ROOT}" + " HUNTER_SHA1: ${HUNTER_GATE_SHA1}" + ERROR_PAGE "error.run.install" + ) + endif() + string(COMPARE EQUAL "${dir}" "" is_bad) + if(is_bad) + hunter_gate_internal_error("Empty 'dir' argument") + endif() + + string(COMPARE EQUAL "${HUNTER_GATE_SHA1}" "" is_bad) + if(is_bad) + hunter_gate_internal_error("HUNTER_GATE_SHA1 empty") + endif() + + string(COMPARE EQUAL "${HUNTER_GATE_URL}" "" is_bad) + if(is_bad) + hunter_gate_internal_error("HUNTER_GATE_URL empty") + endif() + + set(done_location "${dir}/DONE") + set(sha1_location "${dir}/SHA1") + + set(build_dir "${dir}/Build") + set(cmakelists "${dir}/CMakeLists.txt") + + hunter_gate_status_debug("Locking directory: ${dir}") + file(LOCK "${dir}" DIRECTORY GUARD FUNCTION) + hunter_gate_status_debug("Lock done") + + if(EXISTS "${done_location}") + # while waiting for lock other instance can do all the job + hunter_gate_status_debug("File '${done_location}' found, skip install") + return() + endif() + + file(REMOVE_RECURSE "${build_dir}") + file(REMOVE_RECURSE "${cmakelists}") + + file(MAKE_DIRECTORY "${build_dir}") # check directory permissions + + # Disabling languages speeds up a little bit, reduces noise in the output + # and avoids path too long windows error + file( + WRITE + "${cmakelists}" + "cmake_minimum_required(VERSION 3.2)\n" + "project(HunterDownload LANGUAGES NONE)\n" + "include(ExternalProject)\n" + "ExternalProject_Add(\n" + " Hunter\n" + " URL\n" + " \"${HUNTER_GATE_URL}\"\n" + " URL_HASH\n" + " SHA1=${HUNTER_GATE_SHA1}\n" + " DOWNLOAD_DIR\n" + " \"${dir}\"\n" + " TLS_VERIFY\n" + " ${HUNTER_TLS_VERIFY}\n" + " SOURCE_DIR\n" + " \"${dir}/Unpacked\"\n" + " CONFIGURE_COMMAND\n" + " \"\"\n" + " BUILD_COMMAND\n" + " \"\"\n" + " INSTALL_COMMAND\n" + " \"\"\n" + ")\n" + ) + + if(HUNTER_STATUS_DEBUG) + set(logging_params "") + else() + set(logging_params OUTPUT_QUIET) + endif() + + hunter_gate_status_debug("Run generate") + + # Need to add toolchain file too. + # Otherwise on Visual Studio + MDD this will fail with error: + # "Could not find an appropriate version of the Windows 10 SDK installed on this machine" + if(EXISTS "${CMAKE_TOOLCHAIN_FILE}") + get_filename_component(absolute_CMAKE_TOOLCHAIN_FILE "${CMAKE_TOOLCHAIN_FILE}" ABSOLUTE) + set(toolchain_arg "-DCMAKE_TOOLCHAIN_FILE=${absolute_CMAKE_TOOLCHAIN_FILE}") + else() + # 'toolchain_arg' can't be empty + set(toolchain_arg "-DCMAKE_TOOLCHAIN_FILE=") + endif() + + string(COMPARE EQUAL "${CMAKE_MAKE_PROGRAM}" "" no_make) + if(no_make) + set(make_arg "") + else() + # Test case: remove Ninja from PATH but set it via CMAKE_MAKE_PROGRAM + set(make_arg "-DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM}") + endif() + + execute_process( + COMMAND + "${CMAKE_COMMAND}" + "-H${dir}" + "-B${build_dir}" + "-G${CMAKE_GENERATOR}" + "${toolchain_arg}" + ${make_arg} + WORKING_DIRECTORY "${dir}" + RESULT_VARIABLE download_result + ${logging_params} + ) + + if(NOT download_result EQUAL 0) + hunter_gate_internal_error( + "Configure project failed." + "To reproduce the error run: ${CMAKE_COMMAND} -H${dir} -B${build_dir} -G${CMAKE_GENERATOR} ${toolchain_arg} ${make_arg}" + "In directory ${dir}" + ) + endif() + + hunter_gate_status_print( + "Initializing Hunter workspace (${HUNTER_GATE_SHA1})" + " ${HUNTER_GATE_URL}" + " -> ${dir}" + ) + execute_process( + COMMAND "${CMAKE_COMMAND}" --build "${build_dir}" + WORKING_DIRECTORY "${dir}" + RESULT_VARIABLE download_result + ${logging_params} + ) + + if(NOT download_result EQUAL 0) + hunter_gate_internal_error("Build project failed") + endif() + + file(REMOVE_RECURSE "${build_dir}") + file(REMOVE_RECURSE "${cmakelists}") + + file(WRITE "${sha1_location}" "${HUNTER_GATE_SHA1}") + file(WRITE "${done_location}" "DONE") + + hunter_gate_status_debug("Finished") +endfunction() + +# Must be a macro so master file 'cmake/Hunter' can +# apply all variables easily just by 'include' command +# (otherwise PARENT_SCOPE magic needed) +macro(HunterGate) + if(HUNTER_GATE_DONE) + # variable HUNTER_GATE_DONE set explicitly for external project + # (see `hunter_download`) + set_property(GLOBAL PROPERTY HUNTER_GATE_DONE YES) + endif() + + # First HunterGate command will init Hunter, others will be ignored + get_property(_hunter_gate_done GLOBAL PROPERTY HUNTER_GATE_DONE SET) + + if(NOT HUNTER_ENABLED) + # Empty function to avoid error "unknown function" + function(hunter_add_package) + endfunction() + + set( + _hunter_gate_disabled_mode_dir + "${CMAKE_CURRENT_LIST_DIR}/cmake/Hunter/disabled-mode" + ) + if(EXISTS "${_hunter_gate_disabled_mode_dir}") + hunter_gate_status_debug( + "Adding \"disabled-mode\" modules: ${_hunter_gate_disabled_mode_dir}" + ) + list(APPEND CMAKE_PREFIX_PATH "${_hunter_gate_disabled_mode_dir}") + endif() + elseif(_hunter_gate_done) + hunter_gate_status_debug("Secondary HunterGate (use old settings)") + hunter_gate_self( + "${HUNTER_CACHED_ROOT}" + "${HUNTER_VERSION}" + "${HUNTER_SHA1}" + _hunter_self + ) + include("${_hunter_self}/cmake/Hunter") + else() + set(HUNTER_GATE_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}") + + string(COMPARE NOTEQUAL "${PROJECT_NAME}" "" _have_project_name) + if(_have_project_name) + hunter_gate_fatal_error( + "Please set HunterGate *before* 'project' command. " + "Detected project: ${PROJECT_NAME}" + ERROR_PAGE "error.huntergate.before.project" + ) + endif() + + cmake_parse_arguments( + HUNTER_GATE "LOCAL" "URL;SHA1;GLOBAL;FILEPATH" "" ${ARGV} + ) + + string(COMPARE EQUAL "${HUNTER_GATE_SHA1}" "" _empty_sha1) + string(COMPARE EQUAL "${HUNTER_GATE_URL}" "" _empty_url) + string( + COMPARE + NOTEQUAL + "${HUNTER_GATE_UNPARSED_ARGUMENTS}" + "" + _have_unparsed + ) + string(COMPARE NOTEQUAL "${HUNTER_GATE_GLOBAL}" "" _have_global) + string(COMPARE NOTEQUAL "${HUNTER_GATE_FILEPATH}" "" _have_filepath) + + if(_have_unparsed) + hunter_gate_user_error( + "HunterGate unparsed arguments: ${HUNTER_GATE_UNPARSED_ARGUMENTS}" + ) + endif() + if(_empty_sha1) + hunter_gate_user_error("SHA1 suboption of HunterGate is mandatory") + endif() + if(_empty_url) + hunter_gate_user_error("URL suboption of HunterGate is mandatory") + endif() + if(_have_global) + if(HUNTER_GATE_LOCAL) + hunter_gate_user_error("Unexpected LOCAL (already has GLOBAL)") + endif() + if(_have_filepath) + hunter_gate_user_error("Unexpected FILEPATH (already has GLOBAL)") + endif() + endif() + if(HUNTER_GATE_LOCAL) + if(_have_global) + hunter_gate_user_error("Unexpected GLOBAL (already has LOCAL)") + endif() + if(_have_filepath) + hunter_gate_user_error("Unexpected FILEPATH (already has LOCAL)") + endif() + endif() + if(_have_filepath) + if(_have_global) + hunter_gate_user_error("Unexpected GLOBAL (already has FILEPATH)") + endif() + if(HUNTER_GATE_LOCAL) + hunter_gate_user_error("Unexpected LOCAL (already has FILEPATH)") + endif() + endif() + + hunter_gate_detect_root() # set HUNTER_GATE_ROOT + + # Beautify path, fix probable problems with windows path slashes + get_filename_component( + HUNTER_GATE_ROOT "${HUNTER_GATE_ROOT}" ABSOLUTE + ) + hunter_gate_status_debug("HUNTER_ROOT: ${HUNTER_GATE_ROOT}") + if(NOT HUNTER_ALLOW_SPACES_IN_PATH) + string(FIND "${HUNTER_GATE_ROOT}" " " _contain_spaces) + if(NOT _contain_spaces EQUAL -1) + hunter_gate_fatal_error( + "HUNTER_ROOT (${HUNTER_GATE_ROOT}) contains spaces." + "Set HUNTER_ALLOW_SPACES_IN_PATH=ON to skip this error" + "(Use at your own risk!)" + ERROR_PAGE "error.spaces.in.hunter.root" + ) + endif() + endif() + + string( + REGEX + MATCH + "[0-9]+\\.[0-9]+\\.[0-9]+[-_a-z0-9]*" + HUNTER_GATE_VERSION + "${HUNTER_GATE_URL}" + ) + string(COMPARE EQUAL "${HUNTER_GATE_VERSION}" "" _is_empty) + if(_is_empty) + set(HUNTER_GATE_VERSION "unknown") + endif() + + hunter_gate_self( + "${HUNTER_GATE_ROOT}" + "${HUNTER_GATE_VERSION}" + "${HUNTER_GATE_SHA1}" + _hunter_self + ) + + set(_master_location "${_hunter_self}/cmake/Hunter") + get_filename_component(_archive_id_location "${_hunter_self}/.." ABSOLUTE) + set(_done_location "${_archive_id_location}/DONE") + set(_sha1_location "${_archive_id_location}/SHA1") + + # Check Hunter already downloaded by HunterGate + if(NOT EXISTS "${_done_location}") + hunter_gate_download("${_archive_id_location}") + endif() + + if(NOT EXISTS "${_done_location}") + hunter_gate_internal_error("hunter_gate_download failed") + endif() + + if(NOT EXISTS "${_sha1_location}") + hunter_gate_internal_error("${_sha1_location} not found") + endif() + file(READ "${_sha1_location}" _sha1_value) + string(COMPARE EQUAL "${_sha1_value}" "${HUNTER_GATE_SHA1}" _is_equal) + if(NOT _is_equal) + hunter_gate_internal_error( + "Short SHA1 collision:" + " ${_sha1_value} (from ${_sha1_location})" + " ${HUNTER_GATE_SHA1} (HunterGate)" + ) + endif() + if(NOT EXISTS "${_master_location}") + hunter_gate_user_error( + "Master file not found:" + " ${_master_location}" + "try to update Hunter/HunterGate" + ) + endif() + include("${_master_location}") + set_property(GLOBAL PROPERTY HUNTER_GATE_DONE YES) + endif() +endmacro() \ No newline at end of file diff --git a/externals/CMake-codecov b/externals/CMake-codecov new file mode 160000 index 0000000..1974181 --- /dev/null +++ b/externals/CMake-codecov @@ -0,0 +1 @@ +Subproject commit 1974181d8515441329e3a03ccf47a99518402491 diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..6bce93e --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e + +cd ../ + +mkdir -p build +cd build +cmake .. +cmake --build . diff --git a/scripts/build_image.sh b/scripts/build_image.sh new file mode 100644 index 0000000..986884e --- /dev/null +++ b/scripts/build_image.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +cd ../ +docker build -t cash_map . diff --git a/scripts/coverage.sh b/scripts/coverage.sh new file mode 100644 index 0000000..ce7673c --- /dev/null +++ b/scripts/coverage.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e +export GTEST_COLOR=1 +export CTEST_OUTPUT_ON_FAILURE=true + +cd ../ + +cmake -H. -Bbuild -DBUILD_COVERAGE=ON +cmake --build build +cmake --build build --target test --verbose +cmake --build build --target lcov +gcovr -r . | sed '/tests/d' diff --git a/scripts/run_linters.sh b/scripts/run_linters.sh new file mode 100644 index 0000000..19fae24 --- /dev/null +++ b/scripts/run_linters.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -o pipefail + +cd ../ + +echo -e "\n==============cppcheck server================" +cppcheck server --enable=all --inconclusive --error-exitcode=1 \ +-I server/include -I server/tests/include --suppress=missingIncludeSystem + +echo -e "\n==============cppcheck client================" +cppcheck client --enable=all --inconclusive --error-exitcode=1 \ +-I client/include -I client/tests/include --suppress=missingIncludeSystem + + + +echo -e "\n==============clang-tidy server================" +clang-tidy server/src/* server/include/* -warnings-as-errors=* -extra-arg=-std=c++17 -- -Iserver/include + +echo -e "\n==============clang-tidy client================" +clang-tidy client/src/* client/include/* -warnings-as-errors=* -extra-arg=-std=c++17 -- -Iclient/include + + +echo -e "\n==============cpplint server====================" +cpplint --extensions=c server/include/* server/src/* + +echo -e "\n==============cpplint client====================" +cpplint --extensions=cpp client/include/* client/src/* \ No newline at end of file diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh new file mode 100644 index 0000000..0d9f7e5 --- /dev/null +++ b/scripts/run_tests.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e + +cd ../build +ctest -V -R + diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt new file mode 100644 index 0000000..99b2316 --- /dev/null +++ b/server/CMakeLists.txt @@ -0,0 +1,31 @@ +project(server) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Boost REQUIRED) +find_package(Threads REQUIRED) + +add_compile_definitions(_GLIBCXX_USE_CXX11_ABI=0) + +add_executable(${PROJECT_NAME} main.cpp) + +target_link_libraries(${PROJECT_NAME} PUBLIC server_core) +target_link_libraries(${PROJECT_NAME} PRIVATE ${Boost_LIBRARIES}) +target_link_libraries(${PROJECT_NAME} PRIVATE Threads::Threads) +#target_link_libraries(${PROJECT_NAME} PUBLIC /usr/lib/x86_64-linux-gnu/libboost_date_time.a) + + +add_subdirectory(api) +add_subdirectory(lib) +add_subdirectory(core) +add_subdirectory(DB_client) + + +if (BUILD_TESTS) + add_subdirectory(tests) +endif() + +if (BUILD_COVERAGE) + add_coverage(${PROJECT_NAME}) + #add_coverage(${PROJECT_NAME}_lib) +endif() diff --git a/server/DB_client/CMakeLists.txt b/server/DB_client/CMakeLists.txt new file mode 100644 index 0000000..d4df130 --- /dev/null +++ b/server/DB_client/CMakeLists.txt @@ -0,0 +1,7 @@ +project(db_client) + +file(GLOB SOURCES "src/*") +add_library(${PROJECT_NAME} ${SOURCES}) + +target_include_directories(${PROJECT_NAME} PUBLIC include) +target_link_libraries(${PROJECT_NAME} PUBLIC user_session) \ No newline at end of file diff --git a/server/DB_client/include/DB.h b/server/DB_client/include/DB.h new file mode 100644 index 0000000..d704838 --- /dev/null +++ b/server/DB_client/include/DB.h @@ -0,0 +1,25 @@ +#ifndef SERVER_DB_H +#define SERVER_DB_H + +#include +#include + +#include "user.h" + + +class DB { +public: + static DB& get_instance(); + std::unique_ptr get_user_by_id(const std::string& id, const std::string& pass); + bool add_user(const std::string& nickname, const std::string& login, const std::string& password); + + DB(const DB& ) = delete; + void operator=(const DB&) = delete; +private: + DB()= default; + + + std::unordered_map> users; +}; + +#endif //SERVER_DB_H diff --git a/server/DB_client/src/DB.cpp b/server/DB_client/src/DB.cpp new file mode 100644 index 0000000..d2249a6 --- /dev/null +++ b/server/DB_client/src/DB.cpp @@ -0,0 +1,32 @@ +#include "DB.h" + + +DB &DB::get_instance() { + static DB instance; + return instance; +} + +std::unique_ptr DB::get_user_by_id(const std::string& id, const std::string& pass) { + auto it = users.find(id); + if(it == users.end()){ + return {}; + } + if(it->second.second != pass){ + return {}; + } + return std::make_unique(it->second.first); +} + +bool DB::add_user(const std::string& nickname, const std::string& login, const std::string& password) { + if(login.empty()) + return false; + if(users.find(login) == users.end()){ + User new_user; + new_user.set_nick(nickname); + new_user.set_id(login); + new_user.set_pass(password); + users.insert({login, {new_user,password}}); + return true; + } + return false; +} diff --git a/server/README.txt b/server/README.txt new file mode 100644 index 0000000..aea75ef --- /dev/null +++ b/server/README.txt @@ -0,0 +1,13 @@ +------------------------------------------------------------------------------ + GCC Code Coverage Report +Directory: . +------------------------------------------------------------------------------ +File Lines Exec Cover Missing +------------------------------------------------------------------------------ +server/main.cpp 4 0 0% 5,7-9 +server/src/server.cpp 3 0 0% 5,8-9 +------------------------------------------------------------------------------ +TOTAL 21 9 42% +------------------------------------------------------------------------------ +lines: 42.9% (9 out of 21) +branches: 18.4% (7 out of 38) diff --git a/server/api/CMakeLists.txt b/server/api/CMakeLists.txt new file mode 100644 index 0000000..0f904e1 --- /dev/null +++ b/server/api/CMakeLists.txt @@ -0,0 +1,4 @@ +add_subdirectory(room) +add_subdirectory(viewer) +add_subdirectory(ws_session) +add_subdirectory(user) diff --git a/server/api/isession/CMakeLists.txt b/server/api/isession/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/server/api/isession/session.cpp b/server/api/isession/session.cpp new file mode 100644 index 0000000..ece15fd --- /dev/null +++ b/server/api/isession/session.cpp @@ -0,0 +1,26 @@ +#include "session.h" + +session::session(tcp::socket &&socket) +{} +session::session(stream_ptr&& ws){} + + +void session::run(){ + +} +void session::do_read(){ + +} +void session::do_close(){ + +} +void session::handle_request(){ + +} +void session::send_msg(const std::string& msg){ + +} +void session::on_write(error_code ec){ + +} + diff --git a/server/api/isession/session.h b/server/api/isession/session.h new file mode 100644 index 0000000..2682bca --- /dev/null +++ b/server/api/isession/session.h @@ -0,0 +1,41 @@ +#ifndef SERVER_SESSION_H +#define SERVER_SESSION_H + +#include +#include + +#include +#include + + +namespace beast = boost::beast; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from +using error_code = boost::system::error_code; + +typedef websocket::stream stream; +typedef std::unique_ptr stream_ptr; + +class session : public std::enable_shared_from_this{ +public: + session(tcp::socket &&socket); + session(stream_ptr&& ws); + + void run(); + void do_read(); + void do_close(); + void handle_request(); + void send_msg(const std::string& msg); + void on_write(error_code ec); +protected: + stream_ptr ws_; + beast::flat_buffer buffer_; + + std::queue response_q; +}; + + +#endif //SERVER_SESSION_H + + diff --git a/server/api/room/CMakeLists.txt b/server/api/room/CMakeLists.txt new file mode 100644 index 0000000..87e5d7f --- /dev/null +++ b/server/api/room/CMakeLists.txt @@ -0,0 +1,11 @@ +project(room) + +file(GLOB SOURCES "src/*") + + +add_library(${PROJECT_NAME} ${SOURCES}) + +target_include_directories(${PROJECT_NAME} PUBLIC include) + +target_link_libraries(${PROJECT_NAME} PUBLIC viewer server_core serializer response) + diff --git a/server/api/room/include/room.h b/server/api/room/include/room.h new file mode 100644 index 0000000..10e6ceb --- /dev/null +++ b/server/api/room/include/room.h @@ -0,0 +1,123 @@ +#ifndef WATCH_UP_PROJECT_ROOM_H +#define WATCH_UP_PROJECT_ROOM_H + +#include +#include +#include + +#include +#include +#include +#include +#include + + +namespace beast = boost::beast; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from +using error_code = boost::system::error_code; +using uuid = boost::uuids::uuid; + + +class IViewer; +class shared_state; + +typedef std::weak_ptr w_viewer_ptr; +typedef std::shared_ptr viewer_ptr; +typedef std::shared_ptr state_ptr; + + +class IRoom { +public: + virtual ~IRoom() = default; + + virtual void start() =0; + virtual void join (w_viewer_ptr) =0; + virtual void leave(const std::string& id, const std::string& nick) =0; + + virtual void ping() =0; + virtual void pause(const boost::posix_time::time_duration &, const std::string&)=0; + virtual void play(const std::string& sender_id) =0; + virtual void synchronize(const boost::posix_time::time_duration &) =0; + + virtual void set_resource(const std::string &, const std::string &viewer) =0; + virtual void send_chat_msg(const std::string &nick, const std::string& msg) =0; + virtual void set_nickname(const std::string& viewer, std::string&& nick) =0; + virtual void set_service(const std::string& service, const std::string& viewer) =0; + + virtual w_viewer_ptr get_viewer(const std::string& ) =0; + virtual const uuid get_id() const =0; + virtual state_ptr get_state() =0; + + virtual void start_timer() =0; + virtual void stop_timer() =0; + virtual void on_pong(std::unordered_map &req) =0; + virtual bool is_playing() =0; + + virtual void service(const std::string&) =0; +}; + + +class Room: public IRoom, public std::enable_shared_from_this{ +public: + Room(std::string&& host, uuid&& id, state_ptr state); + ~Room() override; + void start() override; + void join (w_viewer_ptr) override; + void leave(const std::string& id, const std::string& nick) override; + void ping() override; + void pause(const boost::posix_time::time_duration &, const std::string&)override; + void play(const std::string& sender_id) override; + void synchronize(const boost::posix_time::time_duration&) override; + void send_chat_msg(const std::string &nick, const std::string& msg) override; + void set_resource(const std::string &, const std::string &viewer) override; + void set_nickname(const std::string& viewer, std::string&& nick) override; + void set_service(const std::string& service, const std::string& viewer) override; + + void start_timer() override; + void stop_timer() override; + + const uuid get_id() const override; + w_viewer_ptr get_viewer(const std::string& ) override; + state_ptr get_state() override; + void on_pong(std::unordered_map &req) override; + + bool is_playing() override; + + void service(const std::string&) override; + +private: + std::string calculate_timestamp(); +private: + struct ping_info{ + boost::posix_time::time_duration player_time; + boost::posix_time::time_duration latency; + boost::posix_time::ptime response; + std::string viewer_id; + }; + state_ptr state_; + + bool playing = false; + + uuid id_; + std::string host_; + std::string src_; + std::string service_; + + boost::posix_time::time_duration current_timestamp; + boost::posix_time::ptime last_update; + boost::posix_time::ptime last_server_time; + + std::unordered_map participants_; + std::vector> viewers_; + + boost::asio::deadline_timer check_timer_; + + //user id -> latency + std::unordered_map latency_; + + +}; + +#endif //WATCH_UP_PROJECT_ROOM_H diff --git a/server/api/room/include/room_creator.hpp b/server/api/room/include/room_creator.hpp new file mode 100644 index 0000000..ecf8c2a --- /dev/null +++ b/server/api/room/include/room_creator.hpp @@ -0,0 +1,20 @@ +#ifndef WATCH_UP_PROJECT_ROOM_CREATOR_HPP +#define WATCH_UP_PROJECT_ROOM_CREATOR_HPP + +#include +#include + +#include "room.h" +#include "shared_state.h" + +typedef boost::uuids::uuid uuid; + + +class room_creator{ +public: + static std::shared_ptr create_room(const std::weak_ptr& host, state_ptr state){ + return std::make_shared(host.lock()->get_id(), boost::uuids::random_generator()(), std::move(state)); + } +}; + +#endif //WATCH_UP_PROJECT_ROOM_CREATOR_HPP diff --git a/server/api/room/src/room.cpp b/server/api/room/src/room.cpp new file mode 100644 index 0000000..e299d50 --- /dev/null +++ b/server/api/room/src/room.cpp @@ -0,0 +1,250 @@ +#include "room.h" + +#include +#include + +#include +#include + +#include "viewer.h" +#include "shared_state.h" +#include "serializer.h" +#include "response_creator.hpp" + +Room::Room(std::string&& host, uuid&& id, state_ptr state): + playing(false), + id_(std::forward(id)), + host_(std::move(host)), + state_(std::move(state)), + check_timer_(state_->get_io_context(), boost::posix_time::seconds(5)) +{ + + std::cout << "[room constructor]\n"; +} + +void Room::start() { + std::cout << "[room start]\n"; + start_timer(); +} + +void Room::join(w_viewer_ptr viewer) { + std::cout << "[ROOM JOIN]\n"; + if(participants_.find(viewer.lock()->get_id()) != participants_.end()){ + participants_[viewer.lock()->get_id()].lock()->set_room(nullptr); + participants_[viewer.lock()->get_id()] = viewer; + auto welcome_msg = serializer::serialize_welcome_msg("join", viewers_, src_, calculate_timestamp(), + playing, id_, service_, viewer.lock()->get_id()); + viewer.lock()->send_message(welcome_msg); + return; + } + if(!viewers_.empty()) { + + auto welcome_msg = serializer::serialize_welcome_msg("join", viewers_, src_, calculate_timestamp(), playing, id_, service_); + viewer.lock()->send_message(welcome_msg); + + auto incomer_msg = serializer::serialize_viewers("incomer", {std::make_pair(viewer.lock()->get_id(), viewer.lock()->get_nickname())}); + for (const auto &p: participants_) { + std::cout << incomer_msg << std::endl; + p.second.lock()->send_message(incomer_msg); + } + } else { + auto res = response_creator::create_with_room(200, "created", id_); + viewer.lock()->send_message(serializer::serialize_response("create", res)); + } + + participants_.insert(std::make_pair(viewer.lock()->get_id(), viewer)); + viewers_.emplace_back(viewer.lock()->get_id(), viewer.lock()->get_nickname()); + latency_.insert(std::make_pair(viewer.lock()->get_id(), boost::posix_time::time_duration(0,0,0,0))); +} + +void Room::leave(const std::string& id, const std::string& nick){ + std::cout << "[ROOM LEAVE]\n"; + auto viewer_dto = std::make_pair(id, nick); + auto it = find(viewers_.begin(), viewers_.end(), viewer_dto); + viewers_.erase(it); + participants_.erase(id); + latency_.erase(id); + auto leave_msg = serializer::serialize_viewers("leave", {viewer_dto}); + for(const auto& p : participants_){ + p.second.lock()->send_message(leave_msg); + } + if (participants_.empty()){ + stop_timer(); + } +} + + + +void Room::pause(const boost::posix_time::time_duration & time, const std::string& sender_id) { + if(!playing){ + return; + } + playing = false; + auto msg = serializer::serialize_response("pause", {{"time", boost::lexical_cast(time)}}); + for(const auto& p : participants_){ + if(p.first != sender_id) + p.second.lock()->send_message(msg); + } +} + +void Room::play(const std::string& sender_id) { + if(playing){ + return; + } + playing = true; + auto msg = serializer::serialize_response("play", {}); + for(const auto& p : participants_){ + if (p.first != sender_id){ + p.second.lock()->send_message(msg); + + std::cout << p.second.lock()->get_nickname() << " " << msg << std::endl; + } + } + +} + +void Room::synchronize(const boost::posix_time::time_duration & timing) { + auto msg = serializer::serialize_response("s_time", + response_creator::create_with_timing(timing)); + + for(const auto& p : participants_){ + p.second.lock()->send_message(msg); + } +} + +void Room::send_chat_msg(const std::string& nick, const std::string &msg) { + auto res = response_creator::create_chat_mag(nick, msg); + for(const auto& p : participants_){ + p.second.lock()->send_message(res); + } +} + + +const uuid Room::get_id() const { + return id_; +} + +Room::~Room() { + state_->remove_room(id_); + std::cout << "[room destructor]\n"; +} + +void Room::start_timer() { + check_timer_.expires_from_now(boost::posix_time::seconds(5)); + check_timer_.async_wait([self{shared_from_this()}](const error_code& ec){ + if(ec){ + std::cout << "[check timings error] " << ec.message() << std::endl; + } + self->ping(); + }); +} + +void Room::stop_timer() { + check_timer_.cancel(); +} + +void Room::set_resource(const std::string &src, const std::string &viewer_id) { + src_ = src; + playing = false; + current_timestamp = boost::posix_time::time_duration(0,0,0,0); + + std::unordered_map res{ + {"src", src} + }; + auto msg = serializer::serialize_response("s_src", res); + for(const auto& p : participants_){ + if(p.first != viewer_id){ + p.second.lock()->send_message(msg); + } + } +} + +w_viewer_ptr Room::get_viewer(const std::string& v_id) { + return participants_[v_id]; +} + +void Room::set_nickname(const std::string &viewer, std::string &&nick) { + auto msg = serializer::serialize_viewers("s_nick", {std::make_pair(viewer, std::move(nick))}); + for(const auto& v: participants_){ + v.second.lock()->send_message(msg); + } +} + + +void Room::ping(){ + std::cout << "[check timings] id: "<< boost::uuids::to_string(id_)<< std::endl; + if(participants_.empty()){ + return; + } + auto ping_time = boost::posix_time::microsec_clock::universal_time(); + auto msg = response_creator::create_ping(ping_time); + + for(const auto& viewer : participants_){ + viewer.second.lock()->send_message(msg); + } + last_server_time = ping_time; + start_timer(); +} + + +void Room::on_pong(std::unordered_map &req) { + ping_info ping{ + boost::posix_time::duration_from_string(req["time"]), + // TODO server time from response + boost::posix_time::microsec_clock::universal_time() - last_server_time + /*boost::posix_time::time_from_string(req["server_time"])*/, + boost::posix_time::microsec_clock::universal_time(), + req["v_id"] + }; + if(ping.player_time > current_timestamp){ + if(playing){ + current_timestamp = ping.player_time + ping.latency; + } else { + current_timestamp = ping.player_time; + } + last_update = boost::posix_time::microsec_clock::universal_time(); + } + + latency_[ping.viewer_id] = ping.latency; + for(const auto& v : participants_){ + boost::posix_time::time_duration pong_time = playing ? ping.player_time + latency_[v.first] : ping.player_time; + auto msg = response_creator::create_pong(pong_time, ping.viewer_id); + v.second.lock()->send_message(msg); + } +} + +state_ptr Room::get_state() { + return state_; +} + + +bool Room::is_playing(){ + return playing; +} + +std::string Room::calculate_timestamp() { + std::stringstream ss_time; + auto timestamp = current_timestamp + (boost::posix_time::microsec_clock::universal_time() - last_update); + ss_time << timestamp; + return ss_time.str(); +} + +void Room::set_service(const std::string &service, const std::string &viewer) { + playing = false; + service_ = service; + src_ = ""; + current_timestamp = boost::posix_time::time_duration(0,0,0,0); + std::unordered_map res{ + {"service", service} + }; + auto msg = serializer::serialize_response("s_service", res); + for(const auto& p : participants_){ + if(p.first != viewer){ + p.second.lock()->send_message(msg); + } + } +} + +void Room::service(const std::string & service) { + service_ = service; +} diff --git a/server/api/user/CMakeLists.txt b/server/api/user/CMakeLists.txt new file mode 100644 index 0000000..a488634 --- /dev/null +++ b/server/api/user/CMakeLists.txt @@ -0,0 +1,6 @@ +project(user_session) + +file(GLOB SOURCES "src/*") +add_library(${PROJECT_NAME} ${SOURCES}) +target_include_directories(${PROJECT_NAME} PUBLIC include) +target_link_libraries(${PROJECT_NAME} PUBLIC serializer ws_session response) diff --git a/server/api/user/include/us_types.hpp b/server/api/user/include/us_types.hpp new file mode 100644 index 0000000..11caac2 --- /dev/null +++ b/server/api/user/include/us_types.hpp @@ -0,0 +1,26 @@ +#ifndef SERVER_US_TYPES_HPP +#define SERVER_US_TYPES_HPP + + +enum class us_type{ + invalid, + create, + join, + logout, +}; + +static std::unordered_map const us_types = { + {"invalid", us_type::invalid}, + {"create", us_type::create}, + {"join", us_type::join}, + {"logout", us_type::logout} +}; + + +static std::unordered_map const us_types_to_string ={ + {us_type::invalid, "invalid"}, + {us_type::create,"create"}, + {us_type::join,"join"}, + {us_type::logout,"logout"} +}; +#endif //SERVER_US_TYPES_HPP diff --git a/server/api/user/include/user.h b/server/api/user/include/user.h new file mode 100644 index 0000000..3a799ee --- /dev/null +++ b/server/api/user/include/user.h @@ -0,0 +1,35 @@ +#ifndef SERVER_USER_H +#define SERVER_USER_H + +#include + +class User { +public: + const std::string get_nick() const{ + return nickname_; + } + const std::string get_id() const{ + return uuid_; + } + + const std::string get_pass() const{ + return password_; + + } + void set_nick(const std::string& nick){ + nickname_ = nick; + } + void set_id(const std::string id){ + uuid_ = id; + } + + void set_pass(const std::string& pass){ + password_ = pass; + } +private: + std::string nickname_; + std::string uuid_; + std::string password_; +}; + +#endif //SERVER_USER_H diff --git a/server/api/user/include/user_creator.h b/server/api/user/include/user_creator.h new file mode 100644 index 0000000..5f393d0 --- /dev/null +++ b/server/api/user/include/user_creator.h @@ -0,0 +1,12 @@ +#ifndef SERVER_USER_CREATOR_H +#define SERVER_USER_CREATOR_H + +#include "user_session.h" + +class user_creator{ +public: + static std::shared_ptr create(stream_ptr&& ws, user_ptr&& user, const state_ptr& state){ + return std::make_shared(std::move(ws), std::move(user), state); + } +}; +#endif //SERVER_USER_CREATOR_H diff --git a/server/api/user/include/user_session.h b/server/api/user/include/user_session.h new file mode 100644 index 0000000..7381d5b --- /dev/null +++ b/server/api/user/include/user_session.h @@ -0,0 +1,51 @@ +#ifndef SERVER_USER_SESSION_H +#define SERVER_USER_SESSION_H + +#include +#include +#include + +#include +#include + +#include "user.h" +#include "us_types.hpp" + +namespace beast = boost::beast; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from +using error_code = boost::system::error_code; + +class shared_state; + +typedef websocket::stream stream; +typedef std::unique_ptr stream_ptr; +typedef std::shared_ptr state_ptr; +typedef std::unique_ptr user_ptr; + + +class user_session : public std::enable_shared_from_this{ +public: + user_session(stream_ptr&& ws, user_ptr && user, state_ptr state); + ~user_session() = default; + + void run(); + void do_read(); + void do_close(); + void logout(); + void handle_request(); + void on_write(error_code ec); + void send_message(const std::string& msg); + +private: + user_ptr user_; + + stream_ptr ws_; + beast::flat_buffer buffer_; + state_ptr state_; + + std::queue response_q; +}; + +#endif //SERVER_USER_SESSION_H diff --git a/server/api/user/src/user_session.cpp b/server/api/user/src/user_session.cpp new file mode 100644 index 0000000..17e491a --- /dev/null +++ b/server/api/user/src/user_session.cpp @@ -0,0 +1,97 @@ +#include "user_session.h" +#include "ws_session.hpp" +#include "serializer.h" +#include "response_creator.hpp" +#include "viewer_manager.hpp" + +#include + +user_session::user_session(stream_ptr &&ws, user_ptr &&user, state_ptr state) : + ws_(std::move(ws)), + user_(std::move(user)), + state_(std::move(state)) {} + +void user_session::run() { + std::cout << "[user run]\n"; + auto msg = response_creator::create_login("login", 200, user_->get_id(), user_->get_nick(), user_->get_pass()); + send_message(msg); + do_read(); +} + +void user_session::do_read() { + ws_->async_read(buffer_, + [self{shared_from_this()}](error_code ec, std::size_t) { + if (ec && ec != boost::asio::error::eof) { + std::cout << "[us async read error]" << ec.message() << std::endl; + return; + } + self->handle_request(); + }); +} + +void user_session::handle_request() { + std::cout << "[user session handle request]\n"; + try{ + auto req = serializer::deserialize(beast::buffers_to_string(buffer_.data())); + std::cout << beast::buffers_to_string(buffer_.data()) << std::endl; + auto t = us_types.at(req["type"]); + switch(t){ + case us_type::create: + case us_type::join:{ + std::make_shared + (std::move(req), std::move(ws_), state_, std::move(user_))->handle_request(); + return; + } + case us_type::logout: + logout(); + return; + default: + { + auto msg = response_creator::create_with_status(req["type"], 405); + send_message(msg); + } + } + } + catch(...){ + auto msg = response_creator::create_with_status("invalid", 400); + send_message(msg); + + } + do_read(); +} + +void user_session::on_write(error_code ec) { + if(ec){ + std::cout << "[user write error] " << ec.message() << std::endl; + return; + } + response_q.pop(); + + if(!response_q.empty()){ + ws_->async_write(net::buffer(response_q.front()), + [self{shared_from_this()}](error_code ec_, std::size_t){ + self->on_write(ec_); + }); + } +} + +void user_session::do_close() { + ws_->close(""); +} + +void user_session::logout() { + auto session = std::make_shared(std::move(ws_), state_); + session->do_read(); + auto msg = response_creator::create_with_status("logout", 200); + session->send_msg(msg); +} + +void user_session::send_message(const std::string& msg){ + response_q.push(msg); + if(response_q.size() > 1) + return; + ws_->async_write(net::buffer(msg), + [self{shared_from_this()}](error_code ec, std::size_t){ + self->on_write(ec); + }); +} \ No newline at end of file diff --git a/server/api/viewer/CMakeLists.txt b/server/api/viewer/CMakeLists.txt new file mode 100644 index 0000000..320c881 --- /dev/null +++ b/server/api/viewer/CMakeLists.txt @@ -0,0 +1,11 @@ +project(viewer) + +file(GLOB SOURCES "src/*") + + +add_library(${PROJECT_NAME} ${SOURCES}) + +target_include_directories(${PROJECT_NAME} PUBLIC include) + +target_link_libraries(${PROJECT_NAME} PUBLIC room serializer ws_session user_session response) + diff --git a/server/api/viewer/include/handler.h b/server/api/viewer/include/handler.h new file mode 100644 index 0000000..8a1eb97 --- /dev/null +++ b/server/api/viewer/include/handler.h @@ -0,0 +1,167 @@ +#ifndef WATCH_UP_PROJECT_HANDLER_H +#define WATCH_UP_PROJECT_HANDLER_H + +#include +#include +#include + +#include "room.h" +#include "vs_types.hpp" +#include "serializer.h" +#include "response_creator.hpp" + +typedef std::shared_ptr room_ptr; +typedef std::shared_ptr viewer_ptr; + +class handler { +public: + handler(std::string&& msg, viewer_ptr viewer, room_ptr room): + viewer_(std::move(viewer)), + room_(std::move(room)), + str_req_(std::move(msg)) + {} + void define_type(){ + auto it = types.find(req_["type"]); + std::cout << req_["type"] << std::endl; + if (it == types.end()) + return; + type_ = it->second; + } + void handle_request(){ + try{ + req_=serializer::deserialize(str_req_); + std::cout <<"[viewer handle request]\n" << str_req_ << std::endl; + define_type(); + switch (type_) { + case vs_type::leave: + viewer_->do_close(); + return; + case vs_type::pong: + req_["v_id"] = boost::lexical_cast(viewer_->get_id()); + room_->on_pong(req_); + viewer_->do_read(); + return; + case vs_type::play: { + if (!viewer_->get_a_opts().can_pause) { + status_ = 403; + break; + } + if (room_->is_playing()){ + status_ = 409; + break; + } + status_ = 200; + room_->play(viewer_->get_id()); + } + break; + case vs_type::pause_: { + if (!viewer_->get_a_opts().can_pause) { + status_ = 403; + break; + } + if (!room_->is_playing()){ + status_ = 409; + break; + } + room_->pause(boost::lexical_cast(req_["time"]), viewer_->get_id()); + status_ = 200; + } + break; + case vs_type::s_time: { + if (!viewer_->get_a_opts().can_rewind) { + status_ = 403; + break; + } + status_ = 200; + room_->synchronize(boost::lexical_cast(req_["time"])); + } + break; + case vs_type::s_nick: + if (viewer_->get_nickname() == req_["nick"]) { + status_ = 200; + break; + } + viewer_->set_nickname(req_["nick"]); + room_->set_nickname(viewer_->get_id(), std::move(req_["nick"])); + status_ = 200; + break; + case vs_type::sync_: { + if (!viewer_->get_a_opts().is_host) { + status_ = 403; + break; + } + status_ = 200; + room_->synchronize(boost::posix_time::duration_from_string(req_["time"])); + } + break; + case vs_type::s_src: { + if (!viewer_->get_a_opts().is_host) { + status_ = 403; + break; + } + status_ = 200; + room_->set_resource(req_["src"], viewer_->get_id()); + + } + break; + case vs_type::s_access: { + if (!viewer_->get_a_opts().is_host) { + status_ = 403; + break; + } + access_options to_set {boost::lexical_cast(req_["pause"]), + boost::lexical_cast(req_["rewind"]), + boost::lexical_cast(req_["host"])}; + room_->get_viewer(req_["viewer"]).lock()->set_access_opts(to_set); + status_ = 200; + break; + } + case vs_type::chat: { + room_->send_chat_msg(viewer_->get_nickname(), req_["msg"]); + break; + } + case vs_type::s_service: { + if(!viewer_->get_a_opts().is_host){ + status_ = 403; + break; + } + status_=200; + room_->set_service(req_["service"], viewer_->get_id()); + break; + } + default: + status_ = 405; + break; + } + } + catch (nlohmann::json::exception& ec){ + status_ = 400; + std::cout << ec.what() << std::endl; + } + catch(boost::bad_lexical_cast const& e){ + std::cout << "Error: " << e.what() << "\n"; + } + catch(...){ + std::cout << "ex\n"; + status_ = 400; + } + std::string msg; + if(type_ == vs_type::s_src){ + msg = response_creator::create_src_success(type_to_string.at(type_), status_, req_["src"]); + viewer_->send_message(msg); + } else if(type_ != vs_type::chat){ + msg = response_creator::create_with_status(type_to_string.at(type_), status_); + viewer_->send_message(msg); + } + viewer_->do_read(); + } +private: + vs_type type_ = vs_type::invalid; + unsigned short status_; + viewer_ptr viewer_; + room_ptr room_; + std::string str_req_; + std::unordered_map req_; +}; + +#endif //WATCH_UP_PROJECT_HANDLER_H diff --git a/server/api/viewer/include/viewer.h b/server/api/viewer/include/viewer.h new file mode 100644 index 0000000..6451a1d --- /dev/null +++ b/server/api/viewer/include/viewer.h @@ -0,0 +1,98 @@ +#ifndef WATCH_UP_PROJECT_VIEWER_H +#define WATCH_UP_PROJECT_VIEWER_H + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "room.h" +#include "user.h" + +namespace beast = boost::beast; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from +using error_code = boost::system::error_code; +using uuid = boost::uuids::uuid; + +typedef std::shared_ptr room_ptr; +typedef websocket::stream ws_stream; +typedef std::unique_ptr stream_ptr; + +typedef std::unique_ptr user_ptr; +class handler; + +struct access_options{ + bool can_pause = true; + bool can_rewind = true; + bool is_host = true; +}; + + +class IViewer{ +public: + virtual ~IViewer() = default; + virtual void start() = 0; + + + virtual void do_read() = 0; + virtual void do_close() = 0; + virtual void send_message(const std::string&) = 0; + virtual void on_write(error_code ec) = 0; + + virtual std::string get_id() const = 0; + virtual std::string get_nickname() const = 0; + virtual access_options get_a_opts() const = 0; + + virtual void set_nickname(std::string) = 0; + virtual void set_room(const room_ptr& room) = 0; + virtual void set_access_opts(const access_options& opts)= 0; + + +}; + +class Viewer: public IViewer, public std::enable_shared_from_this { +public: + Viewer(stream_ptr && ws, user_ptr&& user = nullptr, room_ptr&& room = nullptr); + Viewer(stream_ptr && ws, access_options a_opts, user_ptr&& user = nullptr, room_ptr&& room = nullptr); + Viewer(stream_ptr&& ws, std::string&& id, std::string&& nick, room_ptr&& room = nullptr); + + ~Viewer() override; + + void start() override; + + void do_read() override; + void do_close() override; + void send_message(const std::string&) override; + void on_write(error_code ec) override; + + std::string get_id() const override; + std::string get_nickname() const override; + access_options get_a_opts() const override; + + void set_nickname(std::string) override; + void set_room(const room_ptr& room) override; + void set_access_opts(const access_options& )override; + +protected: + std::string id_; + std::unique_ptr user_; + room_ptr room_; + + std::string nickname_; + access_options a_opts_; + + stream_ptr ws_; + beast::flat_buffer buffer_; + std::queue response_q; +}; + +#endif //WATCH_UP_PROJECT_VIEWER_H + + diff --git a/server/api/viewer/include/viewer_creator.hpp b/server/api/viewer/include/viewer_creator.hpp new file mode 100644 index 0000000..1715c6d --- /dev/null +++ b/server/api/viewer/include/viewer_creator.hpp @@ -0,0 +1,34 @@ +#ifndef WATCH_UP_PROJECT_VIEWER_CREATOR_HPP +#define WATCH_UP_PROJECT_VIEWER_CREATOR_HPP + +#include +#include + +#include "viewer.h" +#include "user.h" + +typedef boost::uuids::uuid uuid; + +class viewer_creator { +public: + static std::shared_ptr create_anonymous(stream_ptr&& ws, room_ptr&& room = nullptr){ + return std::make_shared + (std::move(ws), boost::lexical_cast(boost::uuids::random_generator()()), "anonymous", std::move(room)); + } + + static std::shared_ptr create(stream_ptr && ws, user_ptr&& user, room_ptr&& room = nullptr){ + return std::make_shared + (std::move(ws), std::move(user), std::move(room)); + } + + static std::shared_ptr create_host(stream_ptr && ws, user_ptr&& user){ + if (!user){ + return create_anonymous(std::move(ws)); + } + access_options opts{true, true, true}; + return std::make_shared(std::move(ws), opts, std::move(user)); + } +}; + + +#endif //WATCH_UP_PROJECT_VIEWER_CREATOR_HPP diff --git a/server/api/viewer/include/viewer_manager.hpp b/server/api/viewer/include/viewer_manager.hpp new file mode 100644 index 0000000..82e1f27 --- /dev/null +++ b/server/api/viewer/include/viewer_manager.hpp @@ -0,0 +1,51 @@ +#ifndef WATCH_UP_PROJECT_VIEWER_MANAGER_HPP +#define WATCH_UP_PROJECT_VIEWER_MANAGER_HPP + +#include +#include +#include + +#include + +#include "us_types.hpp" +#include "serializer.h" +#include "viewer_creator.hpp" +#include "room_creator.hpp" +#include "response_creator.hpp" +#include "ws_session.hpp" + + +namespace beast = boost::beast; +namespace websocket = beast::websocket; + +typedef websocket::stream stream; +typedef std::unique_ptr stream_ptr; + +class viewer_manager: public std::enable_shared_from_this { +public: + viewer_manager() = delete; + viewer_manager(std::string&& ws_req, stream_ptr&& ws, state_ptr state, user_ptr&& user = nullptr); + viewer_manager(std::unordered_map&& req, stream_ptr&& ws, state_ptr state, user_ptr&& user = nullptr); + + void do_close(); + void handle_request(); + void send_message(const std::string& msg); + void on_write(error_code ec); + void define_type(); + + + +private: + stream_ptr ws_; + state_ptr state_; + std::queue response_q; + + user_ptr user_; + us_type type_ = us_type::invalid; + + std::string str_req_; + std::unordered_map req_; + +}; + +#endif //WATCH_UP_PROJECT_VIEWER_MANAGER_HPP diff --git a/server/api/viewer/include/vs_types.hpp b/server/api/viewer/include/vs_types.hpp new file mode 100644 index 0000000..49dda01 --- /dev/null +++ b/server/api/viewer/include/vs_types.hpp @@ -0,0 +1,71 @@ +#ifndef WATCH_UP_PROJECT_TYPES_HPP +#define WATCH_UP_PROJECT_TYPES_HPP + +#include "nlohmann/json.hpp" + +enum class vs_type { + invalid = -1, + leave, + ping, + pong, + play, + pause_, + s_time, + s_nick, + sync_, + s_src, + s_access, + chat, + s_service, +}; + +static std::unordered_map const types = { + {"invalid", vs_type::invalid }, + {"leave", vs_type::leave }, + {"ping", vs_type::ping }, + {"pong", vs_type::pong }, + {"play", vs_type::play }, + {"pause", vs_type::pause_ }, + {"s_time", vs_type::s_time }, + {"s_nick", vs_type::s_nick }, + {"sync", vs_type::sync_ }, + {"s_src", vs_type::s_src }, + {"s_access", vs_type::s_access}, + {"chat", vs_type::chat }, + {"s_service",vs_type::s_service} +}; + +static std::unordered_map const type_to_string = { + {vs_type::invalid, "invalid" }, + {vs_type::leave, "leave" }, + {vs_type::ping, "ping" }, + {vs_type::pong, "pong" }, + {vs_type::play, "play" }, + {vs_type::pause_, "pause" }, + {vs_type::s_time, "s_time" }, + {vs_type::s_nick, "s_nick" }, + {vs_type::sync_ , "sync" }, + {vs_type::s_src , "s_src" }, + {vs_type::s_access, "s_access"}, + {vs_type::chat, "chat"}, + {vs_type::s_service, "s_service"}, +}; + +NLOHMANN_JSON_SERIALIZE_ENUM(vs_type, { + {vs_type::invalid, nullptr }, + {vs_type::leave, "leave" }, + {vs_type::ping, "ping" }, + {vs_type::pong, "pong" }, + {vs_type::play, "play" }, + {vs_type::pause_, "pause" }, + {vs_type::s_time, "s_time" }, + {vs_type::s_nick, "s_nick" }, + {vs_type::sync_, "sync" }, + {vs_type::s_src, "s_src" }, + {vs_type::s_access, "s_access"}, + {vs_type::chat, "chat" }, + {vs_type::s_service, "s_service"}, +}) + + +#endif //WATCH_UP_PROJECT_TYPES_HPP diff --git a/server/api/viewer/src/viewer.cpp b/server/api/viewer/src/viewer.cpp new file mode 100644 index 0000000..677f07c --- /dev/null +++ b/server/api/viewer/src/viewer.cpp @@ -0,0 +1,123 @@ +#include "viewer.h" + +#include + +#include "handler.h" +#include "ws_session.hpp" +#include "user_session.h" + +Viewer::Viewer(stream_ptr &&ws, user_ptr&& user, room_ptr&& room): + user_(std::move(user)), + room_(std::move(room)), + ws_(std::move(ws)) +{ + id_ = user_->get_id(); + nickname_ = user_->get_nick(); +} + +Viewer::Viewer(stream_ptr &&ws, access_options a_opts, user_ptr&& user, room_ptr&& room): + user_(std::move(user)), + room_(std::move(room)), + ws_(std::move(ws)), + + a_opts_(a_opts) +{ + id_ = user_->get_id(); + nickname_ = user_->get_nick(); +} + +Viewer::Viewer(stream_ptr &&ws, std::string &&id, std::string &&nick, room_ptr&& room) : + id_(id), + nickname_(std::move(nick)), + + user_(user_ptr(nullptr)), + room_(std::move(room)), + ws_(std::move(ws)) +{} + +Viewer::~Viewer(){ + std::cout << "[viewer destructor]\n"; + if(room_) + room_->leave(id_, nickname_); +} + + +void Viewer::start() { + room_->join(shared_from_this()->weak_from_this()); + do_read(); +} + +void Viewer::do_read() { + ws_->async_read(buffer_, + // expands the lifetime of viewer instance + [self{shared_from_this()}](error_code ec, std::size_t ){ + if(ec && ec != boost::asio::error::eof){ + std::cout << "[viewer async read error] " << ec.message() << std::endl; + return; + } + auto in = beast::buffers_to_string(self->buffer_.cdata()); + self->buffer_.consume(self->buffer_.size()); + // creates an event loop + handler(std::move(in), self->shared_from_this(), self->room_).handle_request(); + }); +} + + +void Viewer::do_close() { + if(user_){ + std::make_shared(std::move(ws_), std::move(user_), room_->get_state())->do_read(); + return; + } + std::make_shared(std::move(ws_), room_->get_state())->do_read(); +} + +void Viewer::send_message(const std::string & msg) { + response_q.push(msg); + if(response_q.size() > 1) + return; + ws_->async_write(net::buffer(msg), + [self{shared_from_this()}](error_code ec, std::size_t){ + self->on_write(ec); + }); +} + +void Viewer::on_write(error_code ec) { + if(ec){ + std::cout << "[viewer write error] " << ec.message() << std::endl; + return; + } + response_q.pop(); + + if(!response_q.empty()){ + ws_->async_write(net::buffer(response_q.front()), + [self{shared_from_this()}](error_code ec_, std::size_t){ + self->on_write(ec_); + }); + } +} + +std::string Viewer::get_id() const { + return id_; +} + + +std::string Viewer::get_nickname() const { + return nickname_; +} + +access_options Viewer::get_a_opts() const { + return a_opts_; +} + +void Viewer::set_nickname(std::string nickname) { + nickname_ = nickname; +} + +void Viewer::set_room(const room_ptr &room) { + room_ = room; +} + +void Viewer::set_access_opts(const access_options& opts) { + a_opts_ = opts; + std::cout << "can pause " << a_opts_.can_pause << std::endl; +} diff --git a/server/api/viewer/src/viewer_manager.cpp b/server/api/viewer/src/viewer_manager.cpp new file mode 100644 index 0000000..62e06d1 --- /dev/null +++ b/server/api/viewer/src/viewer_manager.cpp @@ -0,0 +1,96 @@ +#include "viewer_manager.hpp" +#include "us_types.hpp" + +viewer_manager::viewer_manager(std::string&& ws_req, stream_ptr&& ws, state_ptr state, user_ptr&& user): + ws_(std::move(ws)), + state_(std::move(state)), + str_req_(std::move(ws_req)), + user_(std::move(user)) +{} + +viewer_manager::viewer_manager(std::unordered_map&& req, stream_ptr&& ws, state_ptr state, user_ptr&& user): + ws_(std::move(ws)), + state_(std::move(state)), + req_(std::move(req)), + user_(std::move(user)) +{} + +void viewer_manager::do_close(){ + // pass to low level + std::make_shared(std::move(ws_), state_)->do_read(); +} + + +void viewer_manager::define_type(){ + auto it = us_types.find(req_["type"]); + if(it == us_types.end()) + return; + type_ = it->second; +} + +void viewer_manager::handle_request(){ + try { + if (req_.empty()){ + req_ = serializer::deserialize(str_req_); + } + define_type(); + if (type_ == us_type::create) { + auto host = viewer_creator::create_host(std::move(ws_), std::move(user_)); + auto room = room_creator::create_room(std::weak_ptr(host), state_); + room->service(req_["service"]); + state_->add_room(room->get_id(), std::weak_ptr(room)); + host->set_room(room); + + host->start(); + room->start(); + + } else if (type_ == us_type::join) { + auto room_id = boost::lexical_cast(req_["room_id"]); + auto room = state_->get_room(room_id); + if (!room.lock()) { + auto msg = response_creator::create_with_status("join", 404); + send_message(msg); + do_close(); + return; + } + if(!user_){ + viewer_creator::create_anonymous(std::move(ws_), room.lock())->start(); + return; + } + viewer_creator::create(std::move(ws_),std::move(user_), room.lock())->start(); + } else { + + } + } + catch (...){ + auto msg = response_creator::create_with_status(us_types_to_string.at(type_), 400); + send_message(msg); + do_close(); + return; + } +} + +void viewer_manager::send_message(const std::string &msg) { + response_q.push(msg); + if(response_q.size() > 1) + return; + ws_->async_write(net::buffer(msg), + [self{shared_from_this()}](error_code ec, std::size_t){ + self->on_write(ec); + }); +} + +void viewer_manager::on_write(error_code ec) { + if(ec){ + std::cout << "[viewer manager write error] " << ec.message() << std::endl; + return; + } + response_q.pop(); + + if(!response_q.empty()){ + ws_->async_write(net::buffer(response_q.front()), + [self{shared_from_this()}](error_code ec_, std::size_t){ + self->on_write(ec_); + }); + } +} diff --git a/server/api/ws_session/CMakeLists.txt b/server/api/ws_session/CMakeLists.txt new file mode 100644 index 0000000..998e18c --- /dev/null +++ b/server/api/ws_session/CMakeLists.txt @@ -0,0 +1,6 @@ +project(ws_session) + +file(GLOB SOURCES "src/*") +add_library(${PROJECT_NAME} ${SOURCES}) +target_include_directories(${PROJECT_NAME} PUBLIC include) +target_link_libraries(${PROJECT_NAME} PUBLIC room serializer db_client) diff --git a/server/api/ws_session/include/ps_types.hpp b/server/api/ws_session/include/ps_types.hpp new file mode 100644 index 0000000..9a1f8d9 --- /dev/null +++ b/server/api/ws_session/include/ps_types.hpp @@ -0,0 +1,26 @@ +#ifndef SERVER_PS_TYPES_HPP +#define SERVER_PS_TYPES_HPP + +#include +#include + +enum class ps_type{ + login, + reg, + join, + +}; + +static std::unordered_map const ps_types = { + {"login", ps_type::login}, + {"reg", ps_type::reg }, + {"join", ps_type::join } +}; + +static std::unordered_map const ps_type_to_string = { + {ps_type::login, "login"}, + {ps_type::reg, "reg" }, + {ps_type::join, "join" }, +}; + +#endif //SERVER_PS_TYPES_HPP diff --git a/server/api/ws_session/include/ws_session.hpp b/server/api/ws_session/include/ws_session.hpp new file mode 100644 index 0000000..b2adc3e --- /dev/null +++ b/server/api/ws_session/include/ws_session.hpp @@ -0,0 +1,48 @@ +#ifndef WATCH_UP_PROJECT_WS_SESSION_HPP +#define WATCH_UP_PROJECT_WS_SESSION_HPP +#include +#include +#include + +#include +#include + +#include "shared_state.h" + +namespace beast = boost::beast; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from +using error_code = boost::system::error_code; + +typedef websocket::stream stream; +typedef std::unique_ptr stream_ptr; +typedef std::shared_ptr state_ptr; + + +class ws_session : public std::enable_shared_from_this { +public: + // TODO rm virtual + ws_session(tcp::socket &&socket, state_ptr state); + ws_session(stream_ptr&& ws, state_ptr state); + ~ws_session() = default; + + void run(); + void do_read(); + void do_close(); + void handle_request(); + void send_msg(const std::string& msg); + void on_write(error_code ec); + +protected: + stream_ptr ws_; + beast::flat_buffer buffer_; + state_ptr state_; + + std::queue response_q; +}; + + +#endif //WATCH_UP_PROJECT_WS_SESSION_HPP + + diff --git a/server/api/ws_session/src/ws_session.cpp b/server/api/ws_session/src/ws_session.cpp new file mode 100644 index 0000000..f992033 --- /dev/null +++ b/server/api/ws_session/src/ws_session.cpp @@ -0,0 +1,124 @@ +#include "ws_session.hpp" +#include "serializer.h" +#include "viewer_manager.hpp" +#include "user_session.h" +#include "user_creator.h" +#include "DB.h" + + +#include "ps_types.hpp" + +ws_session::ws_session(tcp::socket &&socket, state_ptr state): + ws_(std::make_unique(std::move(socket))), + state_(std::move(state)) +{} + +ws_session::ws_session(stream_ptr &&ws, state_ptr state): + ws_(std::move(ws)), + state_(std::move(state)) +{} + + +void ws_session::run(){ + std::cout << "[ws run]" << std::endl; + ws_->async_accept([self{shared_from_this()}](error_code ec){ + if(ec){ + std::cout << "[ws accepted] " << ec.message() << std::endl; + } + self->do_read(); + }); +} + +void ws_session::do_read() { + ws_->async_read(buffer_, + [self{shared_from_this()}](error_code ec, std::size_t){ + if(ec && ec != boost::asio::error::eof){ + std::cout << "[ws async read error]" << ec.message() << std::endl; + return; + } + self->handle_request(); + }); +} + + +void ws_session::do_close(){ + ws_->close(""); +} + +void ws_session::on_write(error_code ec) { + if(ec){ + std::cout << "[ws_session write error] " << ec.message() << std::endl; + return; + } + response_q.pop(); + + if(!response_q.empty()){ + ws_->async_write(net::buffer(response_q.front()), + [self{shared_from_this()}](error_code ec_, std::size_t){ + self->on_write(ec_); + }); + } +} + +void ws_session::handle_request(){ + std::cout << "[ws_session handle request]" << std::endl; + try { + auto req = serializer::deserialize(beast::buffers_to_string(buffer_.data())); + std::cout << beast::buffers_to_string(buffer_.data()) << std::endl; + buffer_.consume(buffer_.size()); + auto t = ps_types.at(req["type"]); + switch (t) { + case ps_type::login: + { + // TODO normal db client + auto user = DB::get_instance().get_user_by_id(req["login"], req["password"]); + if (!user){ + auto msg = response_creator::create_with_status("login", 403); + send_msg(msg); + break; + } + user_creator::create(std::move(ws_), std::move(user), state_)->run(); + return; + } + case ps_type::reg: + { + bool pass = DB::get_instance().add_user(req["nick"], req["login"], req["password"]); + if(!pass){ + auto msg = response_creator::create_with_status("reg", 409); + send_msg(msg); + break; + } + auto user = DB::get_instance().get_user_by_id(req["login"], req["password"]); + user_creator::create(std::move(ws_), std::move(user), state_)->run(); + return; + } + case ps_type::join: + std::make_shared + (std::move(req), std::move(ws_), state_)->handle_request(); + return; + default: + { + auto msg = response_creator::create_with_status("invalid", 400); + send_msg(msg); + return; + } + } + } + catch(...){ + auto msg = response_creator::create_with_status("invalid", 400); + send_msg(msg); + } + do_read(); + +} + +void ws_session::send_msg(const std::string &msg) { + response_q.push(msg); + if(response_q.size() > 1) + return; + ws_->async_write(net::buffer(msg), + [self{shared_from_this()}](error_code ec, std::size_t){ + self->on_write(ec); + }); +} + diff --git a/server/core/CMakeLists.txt b/server/core/CMakeLists.txt new file mode 100644 index 0000000..3f4e8a4 --- /dev/null +++ b/server/core/CMakeLists.txt @@ -0,0 +1,11 @@ +project(server_core) + +file(GLOB SOURCES "src/*") + +add_library(${PROJECT_NAME} ${SOURCES}) + +target_include_directories(${PROJECT_NAME} PUBLIC include) + +target_link_libraries(${PROJECT_NAME} PUBLIC ws_session) +target_link_libraries(${PROJECT_NAME} PRIVATE ${Boost_LIBRARIES}) +target_link_libraries(${PROJECT_NAME} PRIVATE Threads::Threads) diff --git a/server/core/include/listener.h b/server/core/include/listener.h new file mode 100644 index 0000000..a85f68f --- /dev/null +++ b/server/core/include/listener.h @@ -0,0 +1,28 @@ +#ifndef WATCH_UP_PROJECT_LISTENER_H +#define WATCH_UP_PROJECT_LISTENER_H + +#include + +#include + +#include "shared_state.h" + +typedef std::shared_ptr state_ptr; + +class listener: public std::enable_shared_from_this{ +public: + listener(boost::asio::io_context& ioc, boost::asio::ip::tcp::endpoint&& endpoint); + ~listener(); + + void async_accept(); + void on_accept(boost::system::error_code& ec); + +private: + state_ptr state_; + boost::asio::io_context& ioc_; + boost::asio::ip::tcp::acceptor acceptor_; + boost::asio::ip::tcp::socket socket_; + +}; + +#endif //WATCH_UP_PROJECT_LISTENER_H diff --git a/server/core/include/server.h b/server/core/include/server.h new file mode 100644 index 0000000..d33111b --- /dev/null +++ b/server/core/include/server.h @@ -0,0 +1,22 @@ +#ifndef WATCH_UP_PROJECT_SERVER_HPP +#define WATCH_UP_PROJECT_SERVER_HPP + +#include + +struct Options{ + std::string ip; + unsigned short port; +}; + +class Server{ +public: + Server(); + Server(const Options& opts); + void run(); + void stop(); + +private: + bool started; + Options opts_; +}; +#endif //WATCH_UP_PROJECT_SERVER_HPP diff --git a/server/core/include/shared_state.h b/server/core/include/shared_state.h new file mode 100644 index 0000000..2c95a6e --- /dev/null +++ b/server/core/include/shared_state.h @@ -0,0 +1,38 @@ +#ifndef WATCH_UP_PROJECT_SHARED_STATE_H +#define WATCH_UP_PROJECT_SHARED_STATE_H + +#include +#include + +#include +#include +#include +#include + +namespace beast = boost::beast; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from +using error_code = boost::system::error_code; + +class IRoom; + +typedef std::shared_ptr room_ptr; +typedef std::weak_ptr w_room_ptr; +typedef boost::uuids::uuid uuid; + +class shared_state: public std::enable_shared_from_this{ +public: + explicit shared_state(net::io_context &ioc); + void add_room(const uuid &room_id, w_room_ptr&& room); + void remove_room(uuid room_id); + w_room_ptr get_room(uuid room_id); + net::io_context &get_io_context() const; +private: + std::unordered_map> rooms_; + net::io_context &ioc_; +}; + + + +#endif //WATCH_UP_PROJECT_SHARED_STATE_H diff --git a/server/core/src/listener.cpp b/server/core/src/listener.cpp new file mode 100644 index 0000000..176d56c --- /dev/null +++ b/server/core/src/listener.cpp @@ -0,0 +1,68 @@ +#include "listener.h" + +#include + +#include + +#include "ws_session.hpp" + +namespace beast = boost::beast; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from +using error_code = boost::system::error_code; + + +listener::listener(net::io_context &ioc, tcp::endpoint&& endpoint): + state_(std::make_shared(ioc)), ioc_(ioc), acceptor_(ioc), socket_(ioc) +{ + error_code ec; + acceptor_.open(endpoint.protocol(), ec); + if(ec){ + std::cout << "[acceptor open error] " << ec.message() << std::endl; + } + acceptor_.set_option(net::socket_base::reuse_address(true), ec); + if (ec){ + std::cout << "[acceptor set_option error] " << ec.message() << std::endl; + } + acceptor_.bind(endpoint, ec); + if(ec){ + std::cout << "[acceptor bind error] " << ec.message() << std::endl; + } + acceptor_.listen(boost::asio::socket_base::max_listen_connections, ec); + if(ec){ + std::cout << "[acceptor listen error] " << ec.message() << std::endl; + } + //TODO remove + std::cout << endpoint.address().to_string() << std::endl; +} + + +void listener::async_accept() { + acceptor_.async_accept(socket_, [self{shared_from_this()}](error_code ec){ + //TODO check for ec + //TODO log + if(ec){ + std::cout <<"[listener async_accept error] " << ec.message() << std::endl; + } + + std::cout << "[connection accepted]" << std::endl; + self->on_accept(ec); + }); +} + + +void listener::on_accept(error_code& ec) { + // TODO check ec + if(ec){ + std::cout << "[on accept error] "<< ec.message() << std::endl; + } + + std::make_shared(std::move(socket_), state_)->run(); + async_accept(); +} + +listener::~listener() { + acceptor_.close(); +} + diff --git a/server/core/src/server.cpp b/server/core/src/server.cpp new file mode 100644 index 0000000..dcf71f2 --- /dev/null +++ b/server/core/src/server.cpp @@ -0,0 +1,30 @@ +#include "server.h" + +#include +#include + +#include + +#include "listener.h" + + +namespace beast = boost::beast; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; + +Server::Server(): started(false), opts_({"127.0.0.1", 8080}) +{} + +void Server::run() { + std::cout << "Server run" << std::endl; + net::io_context ioc; + + tcp::endpoint endpoint{{net::ip::make_address(opts_.ip)}, opts_.port}; + + + std::make_shared(ioc, std::move(endpoint))->async_accept(); + + ioc.run(); + started = true; +} diff --git a/server/core/src/shared_state.cpp b/server/core/src/shared_state.cpp new file mode 100644 index 0000000..5ce5ad2 --- /dev/null +++ b/server/core/src/shared_state.cpp @@ -0,0 +1,28 @@ +#include "shared_state.h" + +#include + +shared_state::shared_state(net::io_context &ioc): + ioc_(ioc) +{} + +void shared_state::add_room(const uuid &room_id, w_room_ptr&& room){ + rooms_.insert(std::make_pair(room_id, std::forward(room))); +} + +void shared_state::remove_room(uuid room_id) { + rooms_.erase(room_id); + std::cout << "[rooms count] : " << rooms_.size() << std::endl; +} + +net::io_context &shared_state::get_io_context() const { + return ioc_; +} + +w_room_ptr shared_state::get_room(uuid room_id) { + auto it = rooms_.find(room_id); + if(it == rooms_.end()){ + return {}; + } + return it->second; +} diff --git a/server/lib/CMakeLists.txt b/server/lib/CMakeLists.txt new file mode 100644 index 0000000..0571c1c --- /dev/null +++ b/server/lib/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(serializer) +add_subdirectory(response) diff --git a/server/lib/response/CMakeLists.txt b/server/lib/response/CMakeLists.txt new file mode 100644 index 0000000..aa7cf6e --- /dev/null +++ b/server/lib/response/CMakeLists.txt @@ -0,0 +1,6 @@ +project(response) + +file(GLOB SOURCES "src/*") +add_library(${PROJECT_NAME} ${SOURCES}) +target_include_directories(${PROJECT_NAME} PUBLIC include) +target_link_libraries(${PROJECT_NAME} PUBLIC serializer) \ No newline at end of file diff --git a/server/lib/response/include/response_creator.hpp b/server/lib/response/include/response_creator.hpp new file mode 100644 index 0000000..29484b5 --- /dev/null +++ b/server/lib/response/include/response_creator.hpp @@ -0,0 +1,33 @@ +#ifndef WATCH_UP_PROJECT_RESPONSE_CREATOR_HPP +#define WATCH_UP_PROJECT_RESPONSE_CREATOR_HPP + +#include +#include + +#include + +#include "serializer.h" + +class response_creator { +public: + static std::string create_with_status(std::string type_, unsigned short code); + + static std::unordered_map + create_with_room(unsigned short code, const std::string &msg, const uuid &room_id); + + static std::unordered_map create_with_timing + (const boost::posix_time::time_duration &time); + + static std::string create_ping(const boost::posix_time::ptime &server_time); + + static std::string + create_login(std::string type_, unsigned short code, const std::string &login, const std::string &nick, + const std::string &pass); + + static std::string create_src_success(std::string type_, unsigned short code, const std::string &src); + + static std::string create_chat_mag(const std::string &nick, const std::string &msg); + + static std::string create_pong(const boost::posix_time::time_duration& timestamp, const std::string& viewer_id); +}; +#endif //WATCH_UP_PROJECT_RESPONSE_CREATOR_HPP diff --git a/server/lib/response/src/response_creator.cpp b/server/lib/response/src/response_creator.cpp new file mode 100644 index 0000000..31906fe --- /dev/null +++ b/server/lib/response/src/response_creator.cpp @@ -0,0 +1,68 @@ +#include "response_creator.hpp" + +std::string response_creator::create_with_status(std::string type_, unsigned short code){ + std::unordered_map data{}; + data["code"] = std::to_string(code); + auto out = serializer::serialize_response(type_, data); + return out; +} +std::unordered_map response_creator::create_with_room(unsigned short code, const std::string& msg, const uuid& room_id){ + std::unordered_map data{}; + data["code"] = std::to_string(code); + data["message"] = msg; + data["room_id"] = boost::lexical_cast(room_id); + return data; +} + +std::unordered_map response_creator::create_with_timing + (const boost::posix_time::time_duration& time){ + std::unordered_map data{}; + std::stringstream ss_time; + ss_time << time; + auto str_time = ss_time.str(); + data["time"] = str_time; + return data; +} + +std::string response_creator::create_ping(const boost::posix_time::ptime& server_time){ + std::unordered_map data{}; + std::stringstream ss_time; + ss_time << server_time; + data["server_time"] = ss_time.str(); + return serializer::serialize_response("ping", data); +} + +std::string response_creator::create_login(std::string type_, unsigned short code, const std::string& login, const std::string& nick, const std::string& pass){ + std::unordered_map data{}; + data["code"] = std::to_string(code); + data["login"] = login; + data["nick"] = nick; + data["password"] = pass; + auto out = serializer::serialize_response(type_, data); + return out; +} + +std::string response_creator::create_src_success(std::string type_, unsigned short code,const std::string& src){ + std::unordered_map data{}; + data["code"] = std::to_string(code); + data["src"] = src; + auto out = serializer::serialize_response(type_, data); + return out; +} + +std::string response_creator::create_chat_mag(const std::string& nick, const std::string& msg){ + std::unordered_map data{}; + data["sender"] = nick; + data["msg"] = msg; + auto out = serializer::serialize_response("chat", data); + return out; +} + +std::string response_creator::create_pong(const boost::posix_time::time_duration ×tamp, const std::string &viewer_id) { + std::unordered_map data{}; + std::stringstream ss_time; + ss_time << timestamp; + data["time"] = ss_time.str(); + data["viewer"] = viewer_id; + return serializer::serialize_response("pong", data); +} diff --git a/server/lib/serializer/CMakeLists.txt b/server/lib/serializer/CMakeLists.txt new file mode 100644 index 0000000..7a5850c --- /dev/null +++ b/server/lib/serializer/CMakeLists.txt @@ -0,0 +1,8 @@ +project(serializer) + +file(GLOB SOURCES "src/*") +find_package(nlohmann_json 3.10.5 REQUIRED) + +add_library(${PROJECT_NAME} ${SOURCES}) +target_include_directories(${PROJECT_NAME} PUBLIC include) +target_link_libraries(${PROJECT_NAME} PRIVATE nlohmann_json::nlohmann_json) diff --git a/server/lib/serializer/include/serializer.h b/server/lib/serializer/include/serializer.h new file mode 100644 index 0000000..36b60ae --- /dev/null +++ b/server/lib/serializer/include/serializer.h @@ -0,0 +1,35 @@ +#ifndef WATCH_UP_PROJECT_SERIALIZER_H +#define WATCH_UP_PROJECT_SERIALIZER_H + +#include +#include +#include + +#include "nlohmann/json.hpp" +#include +#include +#include + +using uuid = boost::uuids::uuid; + +class serializer { +public: + + static std::unordered_map deserialize(const std::string& json_str); + + static std::unordered_map deserialize_access_opts(const std::string& json_str); + + + static std::string serialize_viewers(std::string type, const std::vector>& viewers); + + static std::string serialize_welcome_msg(std::string type, const std::vector>& viewers, + std::string src, std::string timing, bool state, const uuid& room_id, const std::string& service, + const std::string& self = ""); + + static std::string serialize_response(std::string type, const std::unordered_map& fields); + + static std::string serialize_timings(std::string type, const std::vector>& timings); + + + }; +#endif //WATCH_UP_PROJECT_SERIALIZER_H diff --git a/server/lib/serializer/src/serializer.cpp b/server/lib/serializer/src/serializer.cpp new file mode 100644 index 0000000..b687cec --- /dev/null +++ b/server/lib/serializer/src/serializer.cpp @@ -0,0 +1,84 @@ +#include "serializer.h" + +std::unordered_map serializer::deserialize(const std::string &json_str) { + //TODO try-catch + nlohmann::json json = nlohmann::json::parse(json_str); + std::unordered_map data{}; + + for(const auto& item: json.items()){ + + data[item.key()] = item.value(); + } + return data; +} + +std::unordered_map serializer::deserialize_access_opts(const std::string &json_str) { + nlohmann::json json = nlohmann::json::parse(json_str); + std::unordered_map data{}; + + for(const auto& item: json.items()){ + data[item.key()] = boost::lexical_cast(item.value()); + } + return data; +} + +std::string serializer::serialize_viewers(std::string type, const std::vector> &viewers) { + nlohmann::json data{}; + data["type"] = type; + auto viewers_arr = nlohmann::json::array(); + for(const auto& viewer: viewers){ + auto v_obj = nlohmann::json{}; + v_obj["id"] = viewer.first; + v_obj["nick"] = viewer.second; + viewers_arr.push_back(v_obj); + } + data["viewers"] = viewers_arr; + return nlohmann::to_string(data); +} + +std::string serializer::serialize_welcome_msg(std::string type, const std::vector> &viewers, + std::string src, std::string timing, bool state, const uuid &room_id, const std::string& service, + const std::string &self) { + nlohmann::json data{}; + data["type"] = type; + data["src"] = src; + data["time"] = timing; + data["code"] = "200"; + data["state"] = state ? "True" : "False"; + data["room_id"] = boost::lexical_cast(room_id); + data["service"] = service; + auto viewers_arr = nlohmann::json::array(); + for(const auto& viewer: viewers){ + if (viewer.first == self){ + continue; + } + auto v_obj = nlohmann::json{}; + v_obj["id"] = viewer.first; + v_obj["nick"] = viewer.second; + viewers_arr.push_back(v_obj); + } + data["viewers"] = viewers_arr; + return nlohmann::to_string(data); +} + +std::string serializer::serialize_response(std::string type, const std::unordered_map& fields){ + nlohmann::json data{}; + data["type"] = type; + for(const auto& [key, value] : fields){ + data[key] = value; + } + return nlohmann::to_string(data); +} +std::string serializer::serialize_timings(std::string type, const std::vector>& timings){ + nlohmann::json data{}; + data["type"] = type; + auto viewers_arr = nlohmann::json::array(); + for(const auto& viewer: timings){ + auto v_obj = nlohmann::json{}; + v_obj["id"] = viewer.first; + v_obj["time"] = viewer.second; + viewers_arr.push_back(v_obj); + } + data["timings"] = viewers_arr; + return nlohmann::to_string(data); +} diff --git a/server/main.cpp b/server/main.cpp new file mode 100644 index 0000000..4b09b97 --- /dev/null +++ b/server/main.cpp @@ -0,0 +1,9 @@ +#include + +#include "server.h" + +int main(){ + Server server; + server.run(); + return 0; +} diff --git a/server/tests/CMakeLists.txt b/server/tests/CMakeLists.txt new file mode 100644 index 0000000..d31a4ed --- /dev/null +++ b/server/tests/CMakeLists.txt @@ -0,0 +1,10 @@ +project(test_server) + +file(GLOB TEST_SOURCES "*.cpp") + +add_executable(${PROJECT_NAME} ${TEST_SOURCES}) + +target_link_libraries(${PROJECT_NAME} server_lib GTest::gtest_main) + +#add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) +gtest_discover_tests(${PROJECT_NAME}) diff --git a/server/tests/test.cpp b/server/tests/test.cpp new file mode 100644 index 0000000..c68e28c --- /dev/null +++ b/server/tests/test.cpp @@ -0,0 +1,7 @@ +#include +#include + + +TEST(Lib_test, test_foo) { + EXPECT_EQ(1,1); +} diff --git a/server/uml/uml_server b/server/uml/uml_server new file mode 100644 index 0000000..4b1882c --- /dev/null +++ b/server/uml/uml_server @@ -0,0 +1 @@ +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 diff --git a/server/uml/uml_server.jpg b/server/uml/uml_server.jpg new file mode 100644 index 0000000..0780221 Binary files /dev/null and b/server/uml/uml_server.jpg differ