diff --git a/CMakeLists.txt b/CMakeLists.txt index dab32fa..5914018 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.25) project(creature-server - VERSION "2.0.0" + VERSION "2.1.0" DESCRIPTION "Server for April's Creatures" HOMEPAGE_URL https://github.com/opsnlops/creature-server LANGUAGES C CXX) @@ -93,8 +93,9 @@ add_definitions( ) -# ...and our own library +# ...and our own libraries add_subdirectory(lib/e131_service) +add_subdirectory(lib/CreatureVoicesLib) # Set up a base64 encoder/decoder @@ -257,6 +258,8 @@ add_executable(creature-server src/server/ws/websocket/ClientCafe.cpp src/server/ws/websocket/ClientConnection.cpp src/server/ws/messaging/NoticeMessageCommandDTO.h + src/server/ws/service/VoiceService.h + src/server/ws/service/VoiceService.cpp ) @@ -277,6 +280,7 @@ target_link_libraries(creature-server PRIVATE mongo::bsoncxx_shared argparse e131_service + CreatureVoicesLib ) @@ -358,6 +362,7 @@ target_link_libraries(creature-server-test PRIVATE mongo::bsoncxx_shared argparse e131_service + CreatureVoicesLib gtest_main gmock_main ) diff --git a/lib/CreatureVoicesLib/CMakeLists.txt b/lib/CreatureVoicesLib/CMakeLists.txt new file mode 100644 index 0000000..a1fa0a6 --- /dev/null +++ b/lib/CreatureVoicesLib/CMakeLists.txt @@ -0,0 +1,97 @@ +cmake_minimum_required(VERSION 3.25) + + +project(CreatureVoicesLib + VERSION "0.1.0" + DESCRIPTION "Voice library for April's Creatures" + HOMEPAGE_URL https://github.com/opsnlops/creature-server + LANGUAGES C CXX) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 20) + +# Overshadowed declarations keep biting me in the butt 😅 +add_compile_options(-Wshadow) +add_compile_options(-Wall) +add_compile_options(-Wextra) +add_compile_options(-Wpedantic) + +set(PACKAGE_AUTHOR "April White") + + +# Log loudly +include(FetchContent) +set(FETCHCONTENT_QUIET OFF) + +# This project uses threads +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) + + +find_package(PkgConfig REQUIRED) +find_package(CURL REQUIRED) + +# fmt +message(STATUS "fmt") +FetchContent_Declare( + fmt + GIT_REPOSITORY https://github.com/fmtlib/fmt + GIT_TAG 9.1.0 +) +FetchContent_MakeAvailable(fmt) +set(FMT_HEADER_ONLY ON) +set(FMT_LOCALE ON) + + +# spdlog +FetchContent_Declare( + spdlog + GIT_REPOSITORY https://github.com/gabime/spdlog + GIT_TAG v1.11.0 +) +FetchContent_MakeAvailable(spdlog) + + +# json +FetchContent_Declare( + json + URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz) +FetchContent_MakeAvailable(json) + + +# Use the oatpp that we've already made +find_package(oatpp REQUIRED PATHS ${CMAKE_SOURCE_DIR}/../../externals/install/lib/cmake/oatpp-1.3.0) + +# Define our library +add_library(CreatureVoicesLib STATIC + + src/model/HttpMethod.h + src/model/HttpMethod.cpp + src/model/Voice.h + src/model/Voice.cpp + + + src/CreatureVoices.h + src/CreatureVoices.cpp + src/VoiceResult.h + src/CurlBase.h + src/CurlBase.cpp + src/CurlHandle.h + src/CurlHandle.cpp + + +) + +# Include directories for this library +target_include_directories(CreatureVoicesLib PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${CURL_INCLUDE_DIR} + ${oatpp_INCLUDE_DIRS} +) + +target_link_libraries(CreatureVoicesLib + ${CURL_LIBRARIES} + spdlog::spdlog $<$:ws2_32> + fmt::fmt + nlohmann_json::nlohmann_json +) diff --git a/lib/CreatureVoicesLib/src/CreatureVoices.cpp b/lib/CreatureVoicesLib/src/CreatureVoices.cpp new file mode 100644 index 0000000..5dbf4f2 --- /dev/null +++ b/lib/CreatureVoicesLib/src/CreatureVoices.cpp @@ -0,0 +1,71 @@ + +#include +#include +#include +#include + +#include +#include +#include +#include + + +#include "model/HttpMethod.h" +#include "VoiceResult.h" +#include "CreatureVoices.h" + +using json = nlohmann::json; + +namespace creatures::voice { + + + CreatureVoices::CreatureVoices(std::string apiKey) : apiKey(std::move(apiKey)) {} + + + VoiceResult> CreatureVoices::listAllAvailableVoices() { + const std::string url ="https://api.elevenlabs.io/v1/voices"; + + debug("Fetching available voices"); + + auto curlHandle = createCurlHandle(url); + auto res = performRequest(curlHandle, HttpMethod::GET, ""); + if(!res.isSuccess()) { + auto error = res.getError(); + std::string errorMessage = fmt::format("Failed to fetch available voices: {}", error->getMessage()); + curl_easy_cleanup(curlHandle.get()); + return VoiceResult>{VoiceError(error->getCode(), error->getMessage())}; + } + curl_easy_cleanup(curlHandle.get()); + + auto httpResponse = res.getValue().value(); + trace("httpResponse was: {}", httpResponse); + + + json jsonResponse; + try { + jsonResponse = json::parse(httpResponse); + } catch (const json::parse_error& e) { + std::string errorMessage = fmt::format("Failed to parse JSON response: {}", e.what()); + warn(errorMessage); + return VoiceResult>{VoiceError(VoiceError::InvalidData, errorMessage)}; + } + + std::vector voices; + for (const auto& item : jsonResponse["voices"]) { + Voice voice; + voice.voiceId = item["voice_id"].get(); + voice.name = item["name"].get(); + voices.push_back(voice); + } + debug("ElevenLabs gave us {} voices", voices.size()); + + + // Sort the voices by name + std::sort(voices.begin(), voices.end(), [](const Voice& a, const Voice& b) { + return a.name < b.name; + }); + + return voices; + } + +} diff --git a/lib/CreatureVoicesLib/src/CreatureVoices.h b/lib/CreatureVoicesLib/src/CreatureVoices.h new file mode 100644 index 0000000..bfd3b54 --- /dev/null +++ b/lib/CreatureVoicesLib/src/CreatureVoices.h @@ -0,0 +1,28 @@ + +#pragma once + + +#include + +#include +#include + +#include "CurlBase.h" +#include "VoiceResult.h" +#include "model/HttpMethod.h" +#include "model/Voice.h" + + +namespace creatures :: voice { + + class CreatureVoices : public CurlBase { + public: + CreatureVoices(std::string apiKey); + VoiceResult> listAllAvailableVoices(); + + private: + std::string apiKey; + + }; + +} \ No newline at end of file diff --git a/lib/CreatureVoicesLib/src/CurlBase.cpp b/lib/CreatureVoicesLib/src/CurlBase.cpp new file mode 100644 index 0000000..7238dfe --- /dev/null +++ b/lib/CreatureVoicesLib/src/CurlBase.cpp @@ -0,0 +1,128 @@ + +#include +#include +#include + +#include +#include +#include + + +#include "model/HttpMethod.h" +#include "VoiceResult.h" +#include "CurlBase.h" + + +namespace creatures::voice { + + + size_t CurlBase::WriteCallback(char* ptr, size_t size, size_t nmemb, std::string* data) { + data->append(ptr, size * nmemb); + return size * nmemb; + } + + + CurlHandle CurlBase::createCurlHandle(const std::string& url) { + debug("Creating a curl handle for URL: {}", url); + return CurlHandle(url); + } + + VoiceResult CurlBase::performRequest(CurlHandle& curlHandle, + HttpMethod method, + const std::string& data) { + + debug("performing a request! method: {}, data: {}", httpMethodToString(method), data); + + std::string response; + std::string errorMessage; + + // Make sure CURL is initialized + if(!curlHandle.get()) { + errorMessage = "Unable to perform request because CURL handle is not initialized"; + error(errorMessage); + return VoiceResult{VoiceError(VoiceError::InternalError, errorMessage)}; + } + + curl_easy_setopt(curlHandle.get(), CURLOPT_WRITEDATA, &response); + trace("CURL handle set up for writing"); + + switch (method) { + case HttpMethod::GET: + // GET is the default method, no need to set anything special + break; + case HttpMethod::POST: + curl_easy_setopt(curlHandle.get(), CURLOPT_POST, 1L); + curl_easy_setopt(curlHandle.get(), CURLOPT_POSTFIELDS, data.c_str()); + break; + case HttpMethod::PUT: + curl_easy_setopt(curlHandle.get(), CURLOPT_CUSTOMREQUEST, httpMethodToString(method).c_str()); + curl_easy_setopt(curlHandle.get(), CURLOPT_POSTFIELDS, data.c_str()); + break; + case HttpMethod::DELETE: + curl_easy_setopt(curlHandle.get(), CURLOPT_CUSTOMREQUEST, httpMethodToString(method).c_str()); + break; + default: + errorMessage = fmt::format("Unknown HTTP method: {}", httpMethodToString(method)); + error(errorMessage); + curl_easy_cleanup(curlHandle.get()); + return VoiceResult{VoiceError(VoiceError::InternalError, errorMessage)}; + } + + + CURLcode res = curl_easy_perform(curlHandle.get()); + if (res != CURLE_OK) { + errorMessage = fmt::format("CURL request failed: {}", curl_easy_strerror(res)); + error(errorMessage); + curl_easy_cleanup(curlHandle.get()); + return VoiceResult{VoiceError(VoiceError::InternalError, errorMessage)}; + } + + long http_code = 0; + curl_easy_getinfo(curlHandle.get(), CURLINFO_RESPONSE_CODE, &http_code); + + // Let's look at the response code + switch(http_code) { + case 200: + case 201: + case 204: + // These are all good responses + break; + case 301: + case 302: + errorMessage = fmt::format("HTTP error: {} - redirect", http_code); + warn(errorMessage); + curl_easy_cleanup(curlHandle.get()); + return VoiceResult{VoiceError(VoiceError::InvalidData, errorMessage)}; + case 400: + errorMessage = fmt::format("HTTP error: {} - bad request", http_code); + warn(errorMessage); + curl_easy_cleanup(curlHandle.get()); + return VoiceResult{VoiceError(VoiceError::InvalidData, errorMessage)}; + case 401: + case 403: + errorMessage = fmt::format("HTTP error: {} - unauthorized", http_code); + warn(errorMessage); + curl_easy_cleanup(curlHandle.get()); + return VoiceResult{VoiceError(VoiceError::InvalidApiKey, errorMessage)}; + case 404: + errorMessage = fmt::format("HTTP error: {} - not found", http_code); + warn(errorMessage); + curl_easy_cleanup(curlHandle.get()); + return VoiceResult{VoiceError(VoiceError::NotFound, errorMessage)}; + + // Map everything else to an error + default: + errorMessage = fmt::format("HTTP error: {}", http_code); + error(errorMessage); + curl_easy_cleanup(curlHandle.get()); + return VoiceResult{VoiceError(VoiceError::InternalError, errorMessage)}; + } + + + // Looks good! We have good data + curl_easy_cleanup(curlHandle.get()); + + debug("request successful!"); + return VoiceResult{response}; + } +} diff --git a/lib/CreatureVoicesLib/src/CurlBase.h b/lib/CreatureVoicesLib/src/CurlBase.h new file mode 100644 index 0000000..937c1e4 --- /dev/null +++ b/lib/CreatureVoicesLib/src/CurlBase.h @@ -0,0 +1,34 @@ + +#pragma once + +#include +#include + +#include +#include + +#include "CurlHandle.h" +#include "VoiceResult.h" +#include "model/HttpMethod.h" + +using spdlog::trace; +using spdlog::debug; +using spdlog::info; +using spdlog::warn; +using spdlog::error; +using spdlog::critical; + +namespace creatures :: voice { + + class CurlBase { + public: + CurlBase() = default; + ~CurlBase() = default; + + protected: + CurlHandle createCurlHandle(const std::string& url); + VoiceResult performRequest(CurlHandle& curlHandle, HttpMethod method, const std::string& data); + static size_t WriteCallback(char* ptr, size_t size, size_t nmemb, std::string* data); + }; + +} \ No newline at end of file diff --git a/lib/CreatureVoicesLib/src/CurlHandle.cpp b/lib/CreatureVoicesLib/src/CurlHandle.cpp new file mode 100644 index 0000000..7071c88 --- /dev/null +++ b/lib/CreatureVoicesLib/src/CurlHandle.cpp @@ -0,0 +1,41 @@ + +#include +#include +#include + +#include "CurlHandle.h" + + +namespace creatures :: voice { + + CurlHandle::CurlHandle(const std::string& url) { + curl = curl_easy_init(); + if (curl) { + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + } else { + spdlog::error("Failed to initialize curl handle"); + } + } + + /** + * Make sure to clean up the curl handle when the object is destroyed. + */ + CurlHandle::~CurlHandle() { + if (curl) { + curl_easy_cleanup(curl); + } + } + + + CURL* CurlHandle::get() const { + return curl; + } + + size_t CurlHandle::WriteCallback(char* ptr, size_t size, size_t nmemb, std::string* data) { + data->append(ptr, size * nmemb); + return size * nmemb; + } + +} \ No newline at end of file diff --git a/lib/CreatureVoicesLib/src/CurlHandle.h b/lib/CreatureVoicesLib/src/CurlHandle.h new file mode 100644 index 0000000..8108a86 --- /dev/null +++ b/lib/CreatureVoicesLib/src/CurlHandle.h @@ -0,0 +1,34 @@ + +#pragma once + +#include +#include +#include + +namespace creatures :: voice { + + /** + * This is a wrapper around the curl handle to make sure we're not leaking handles over time. Since + * we call curl_easy_init() and curl_easy_cleanup() in the constructor and destructor, respectively, + * we can be sure that the handle is always cleaned up properly. + */ + class CurlHandle { + public: + CurlHandle(const std::string& url); + ~CurlHandle(); + //CurlHandle(CurlHandle&& other) noexcept; + + // Delete copy constructor and copy assignment to avoid double cleanup + CurlHandle(const CurlHandle&) = delete; + CurlHandle& operator=(const CurlHandle&) = delete; + + CURL* get() const; + + private: + CURL* curl = nullptr; + static size_t WriteCallback(char* ptr, size_t size, size_t nmemb, std::string* data); + + }; + + +} // namespace creatures :: voice \ No newline at end of file diff --git a/lib/CreatureVoicesLib/src/VoiceResult.h b/lib/CreatureVoicesLib/src/VoiceResult.h new file mode 100644 index 0000000..bbdc10e --- /dev/null +++ b/lib/CreatureVoicesLib/src/VoiceResult.h @@ -0,0 +1,105 @@ +#pragma once + +#include +#include +#include +#include + +namespace creatures :: voice { + +// Define the ServerError struct + class VoiceError { + public: + enum Code { + NotFound, + Forbidden, + InternalError, + InvalidData, + InvalidApiKey + }; + + VoiceError(Code code, const std::string& message); + Code getCode() const; + std::string getMessage() const; + + private: + Code code; + std::string message; + }; + +// Function to convert ServerError code to HTTP status code + int serverErrorToStatusCode(VoiceError::Code code); + +// Define a generic Result type + template + class VoiceResult { + public: + // Constructors for success and error + VoiceResult(const T& value); + VoiceResult(const VoiceError& error); + + // Check if the result is a success + [[nodiscard]] bool isSuccess() const; + + // Get the value (if success) + std::optional getValue() const; + + // Get the error (if failure) + [[nodiscard]] std::optional getError() const; + + private: + std::variant m_result; + }; + +// Implement ServerError methods + inline VoiceError::VoiceError(Code code, const std::string& message) : code(code), message(message) {} + + inline VoiceError::Code VoiceError::getCode() const { + return code; + } + + inline std::string VoiceError::getMessage() const { + return message; + } + +// Function to convert ServerError code to HTTP status code + inline int serverErrorToStatusCode(VoiceError::Code code) { + switch (code) { + case VoiceError::NotFound: return 404; + case VoiceError::Forbidden: return 403; + case VoiceError::InternalError: return 500; + case VoiceError::InvalidData: return 400; + case VoiceError::InvalidApiKey: return 401; + default: return 500; + } + } + +// Implement Result methods + template + VoiceResult::VoiceResult(const T& value) : m_result(value) {} + + template + VoiceResult::VoiceResult(const VoiceError& error) : m_result(error) {} + + template + bool VoiceResult::isSuccess() const { + return std::holds_alternative(m_result); + } + + template + std::optional VoiceResult::getValue() const { + if (isSuccess()) { + return std::get(m_result); + } + return std::nullopt; + } + + template + std::optional VoiceResult::getError() const { + if (!isSuccess()) { + return std::get(m_result); + } + return std::nullopt; + } + +} diff --git a/lib/CreatureVoicesLib/src/model/HttpMethod.cpp b/lib/CreatureVoicesLib/src/model/HttpMethod.cpp new file mode 100644 index 0000000..df250b0 --- /dev/null +++ b/lib/CreatureVoicesLib/src/model/HttpMethod.cpp @@ -0,0 +1,19 @@ + +#include + +#include "HttpMethod.h" + +namespace creatures :: voice { + + std::string httpMethodToString(HttpMethod method) { + switch (method) { + case HttpMethod::GET: return{"GET"}; + case HttpMethod::POST: return {"POST"}; + case HttpMethod::PUT: return {"PUT"}; + case HttpMethod::DELETE: return {"DELETE"}; + default: return {"UNKNOWN"}; + } + } + + +} // namespace creatures :: voice \ No newline at end of file diff --git a/lib/CreatureVoicesLib/src/model/HttpMethod.h b/lib/CreatureVoicesLib/src/model/HttpMethod.h new file mode 100644 index 0000000..8a2e4d1 --- /dev/null +++ b/lib/CreatureVoicesLib/src/model/HttpMethod.h @@ -0,0 +1,16 @@ + +#pragma once + +/** + * Simple enum to make it easier to work with HTTP methods + */ +namespace creatures :: voice { + enum class HttpMethod { + GET, + POST, + PUT, + DELETE + }; + + std::string httpMethodToString(HttpMethod method); +} \ No newline at end of file diff --git a/lib/CreatureVoicesLib/src/model/Voice.cpp b/lib/CreatureVoicesLib/src/model/Voice.cpp new file mode 100644 index 0000000..a00a770 --- /dev/null +++ b/lib/CreatureVoicesLib/src/model/Voice.cpp @@ -0,0 +1,27 @@ + +#include + +#include + +#include "Voice.h" + +namespace creatures::voice { + + Voice convertFromDto(const std::shared_ptr &voiceDto) { + Voice voice; + voice.voiceId = voiceDto->voice_id; + voice.name = voiceDto->name; + + return voice; + } + + // Convert this into its DTO + oatpp::Object convertToDto(const Voice &voice) { + auto voiceDto = VoiceDto::createShared(); + voiceDto->voice_id = voice.voiceId; + voiceDto->name = voice.name; + + return voiceDto; + } + +} \ No newline at end of file diff --git a/lib/CreatureVoicesLib/src/model/Voice.h b/lib/CreatureVoicesLib/src/model/Voice.h new file mode 100644 index 0000000..230eea1 --- /dev/null +++ b/lib/CreatureVoicesLib/src/model/Voice.h @@ -0,0 +1,43 @@ + +#pragma once + +#include + +#include +#include + + +namespace creatures :: voice { + + struct Voice { + std::string voiceId; + std::string name; + }; + +#include OATPP_CODEGEN_BEGIN(DTO) + + class VoiceDto : public oatpp::DTO { + + DTO_INIT(VoiceDto, DTO /* extends */) + + DTO_FIELD_INFO(voice_id) { + info->description = "The ID of the voice"; + } + DTO_FIELD(String, voice_id); + + DTO_FIELD_INFO(name) { + info->description = "The name of the voice"; + } + DTO_FIELD(String, name); + + }; + +#include OATPP_CODEGEN_END(DTO) + + + oatpp::Object convertToDto(const Voice &voice); + Voice convertFromDto(const std::shared_ptr &voiceDto); + + +} + diff --git a/src/server/config.h b/src/server/config.h index 43d77f8..026555f 100644 --- a/src/server/config.h +++ b/src/server/config.h @@ -49,6 +49,10 @@ #define NETWORK_DEVICE_NAME_ENV "NETWORK_DEVICE_NAME" #define DEFAULT_NETWORK_DEVICE_NAME "eth0" +#define VOICE_API_KEY_ENV "VOICE_API_KEY" +#define DEFAULT_VOICE_API_KEY "" + + #define SOUND_BUFFER_SIZE 2048 // Higher = less CPU, lower = less latency // Should we use the GPIO devices for LEDs? This only works on the Raspberry Pi, diff --git a/src/server/config/CommandLine.cpp b/src/server/config/CommandLine.cpp index aaab257..d57fd64 100644 --- a/src/server/config/CommandLine.cpp +++ b/src/server/config/CommandLine.cpp @@ -78,6 +78,11 @@ namespace creatures { .default_value(environmentToString(NETWORK_DEVICE_NAME_ENV, DEFAULT_NETWORK_DEVICE_NAME)) .nargs(1); + program.add_argument("-v", "--voice-api-key") + .help("ElevenLabs API key") + .default_value(environmentToString(VOICE_API_KEY_ENV, DEFAULT_VOICE_API_KEY)) + .nargs(1); + auto &oneShots = program.add_mutually_exclusive_group(); oneShots.add_argument("--list-sound-devices") .help("list available sound devices and exit") @@ -164,6 +169,13 @@ namespace creatures { debug("set our sound file location to {}", soundsLocation); } + auto voiceApiKey = program.get("-v"); + debug("read voice API key {} from command line", voiceApiKey); + if(!voiceApiKey.empty()) { + config->setVoiceApiKey(voiceApiKey); + debug("set our voice API key to {}", voiceApiKey); + } + auto networkDeviceName = program.get("-n"); debug("read network device name {} from command line", networkDeviceName); diff --git a/src/server/config/Configuration.cpp b/src/server/config/Configuration.cpp index 8ffbebc..b144a29 100644 --- a/src/server/config/Configuration.cpp +++ b/src/server/config/Configuration.cpp @@ -59,4 +59,11 @@ namespace creatures { void Configuration::setNetworkDevice(uint16_t _networkDevice) { this->networkDevice = _networkDevice; } + + std::string Configuration::getVoiceApiKey() const { + return this->voiceApiKey; + } + void Configuration::setVoiceApiKey(std::string _voiceApiKey) { + this->voiceApiKey = std::move(_voiceApiKey); + } } \ No newline at end of file diff --git a/src/server/config/Configuration.h b/src/server/config/Configuration.h index ad58f2e..55008aa 100644 --- a/src/server/config/Configuration.h +++ b/src/server/config/Configuration.h @@ -25,6 +25,8 @@ namespace creatures { std::string getSoundFileLocation() const; uint16_t getNetworkDevice() const; + std::string getVoiceApiKey() const; + protected: void setUseGPIO(bool _useGPIO); @@ -36,6 +38,8 @@ namespace creatures { void setSoundFileLocation(std::string _soundFileLocation); void setNetworkDevice(uint16_t _networkDevice); + void setVoiceApiKey(std::string _voiceApiKey); + private: // Should we use the GPIO pins? @@ -53,6 +57,9 @@ namespace creatures { // Network stuff uint16_t networkDevice = 0; + + // ElevenLabs (Voice) stuff + std::string voiceApiKey = DEFAULT_VOICE_API_KEY; }; diff --git a/src/server/ws/App.cpp b/src/server/ws/App.cpp index 741c46e..8115bc4 100644 --- a/src/server/ws/App.cpp +++ b/src/server/ws/App.cpp @@ -19,6 +19,7 @@ #include "controller/MetricsController.h" #include "controller/SoundController.h" #include "controller/StaticController.h" +#include "controller/VoiceController.h" #include "controller/WebSocketController.h" #include "server/ws/websocket/ClientCafe.h" @@ -71,6 +72,7 @@ namespace creatures ::ws { router->addController(MetricsController::createShared()); router->addController(SoundController::createShared()); router->addController(StaticController::createShared()); + router->addController(VoiceController::createShared()); router->addController(WebSocketController::createShared()); /* Get connection handler component */ diff --git a/src/server/ws/AppComponent.h b/src/server/ws/AppComponent.h index 7e6f778..c376388 100644 --- a/src/server/ws/AppComponent.h +++ b/src/server/ws/AppComponent.h @@ -18,14 +18,22 @@ #include +#include + + #include "SwaggerComponent.h" #include "ErrorHandler.h" +#include "server/config/Configuration.h" #include "server/ws/messaging/MessageProcessor.h" #include "server/ws/websocket/ClientCafe.h" #include "util/loggingUtils.h" #include "util/MessageQueue.h" +namespace creatures { + extern std::shared_ptr config; +} + namespace creatures :: ws { class AppComponent { @@ -101,6 +109,14 @@ namespace creatures :: ws { }()); + /** + * Register the voice service + */ + OATPP_CREATE_COMPONENT(std::shared_ptr, voiceService)([] { + return std::make_shared(config->getVoiceApiKey()); + }()); + + /** * Create the MessageProcessor */ diff --git a/src/server/ws/controller/VoiceController.h b/src/server/ws/controller/VoiceController.h new file mode 100644 index 0000000..7e5a7f4 --- /dev/null +++ b/src/server/ws/controller/VoiceController.h @@ -0,0 +1,65 @@ + +#pragma once + +#include +#include +#include +#include + + +#include + +#include "server/database.h" + + +#include "server/ws/dto/ListDto.h" +#include "server/ws/dto/StatusDto.h" +#include "server/ws/service/VoiceService.h" + +#include "server/metrics/counters.h" + +namespace creatures { + extern std::shared_ptr metrics; +} + +#include OATPP_CODEGEN_BEGIN(ApiController) //<- Begin Codegen + +namespace creatures :: ws { + + class VoiceController : public oatpp::web::server::api::ApiController { + public: + VoiceController(OATPP_COMPONENT(std::shared_ptr, objectMapper)): + oatpp::web::server::api::ApiController(objectMapper) {} + private: + VoiceService m_voiceService; // Create the sound service + public: + + static std::shared_ptr createShared( + OATPP_COMPONENT(std::shared_ptr, + objectMapper) + ) { + return std::make_shared(objectMapper); + } + + + ENDPOINT_INFO(getAllVoices) { + info->summary = "Lists all of the voices files"; + + info->addResponse>(Status::CODE_200, "application/json; charset=utf-8"); + info->addResponse>(Status::CODE_404, "application/json; charset=utf-8"); + info->addResponse>(Status::CODE_500, "application/json; charset=utf-8"); + } + ENDPOINT("GET", "api/v1/voice", getAllVoices) + { + creatures::metrics->incrementRestRequestsProcessed(); + return createDtoResponse(Status::CODE_200, m_voiceService.getAllVoices()); + } + + + + + }; + +} + +#include OATPP_CODEGEN_END(ApiController) //<- End Codegen \ No newline at end of file diff --git a/src/server/ws/service/VoiceService.cpp b/src/server/ws/service/VoiceService.cpp new file mode 100644 index 0000000..cd212ca --- /dev/null +++ b/src/server/ws/service/VoiceService.cpp @@ -0,0 +1,83 @@ + + +#include +#include + + +#include "exception/exception.h" + + +#include +#include +#include + +#include "server/config/Configuration.h" + + +#include "server/ws/dto/ListDto.h" +#include "server/ws/dto/StatusDto.h" + +#include "util/helpers.h" + + +#include "VoiceService.h" + + +namespace creatures { + extern std::shared_ptr config; +} + +namespace creatures :: ws { + + using oatpp::web::protocol::http::Status; + + + oatpp::Object>> VoiceService::getAllVoices() { + OATPP_COMPONENT(std::shared_ptr, appLogger); + OATPP_COMPONENT(std::shared_ptr, voiceService); + + appLogger->debug("Asked to return all of the voices"); + + + bool error = false; + oatpp::String errorMessage; + Status status = Status::CODE_200; + + + auto voiceResult = voiceService->listAllAvailableVoices(); + if(!voiceResult.isSuccess()) { + error = true; + errorMessage = voiceResult.getError()->getMessage(); + switch (voiceResult.getError()->getCode()) { + case creatures::voice::VoiceError::InvalidApiKey: + status = Status::CODE_401; + break; + case creatures::voice::VoiceError::InvalidData: + status = Status::CODE_400; + break; + default: + status = Status::CODE_500; + break; + } + } + OATPP_ASSERT_HTTP(!error, status, errorMessage) + + // Looks good, let's keep going! + auto voices = voiceResult.getValue().value(); + debug("Found {} voices", voices.size()); + + auto voiceList = oatpp::Vector>::createShared(); + for(auto& voice : voices) { + voiceList->push_back(convertToDto(voice)); + trace("adding voice: {}", voice.name); + } + + auto list = ListDto>::createShared(); + list->count = voiceList->size(); + list->items = voiceList; + + return list; + + } + +} \ No newline at end of file diff --git a/src/server/ws/service/VoiceService.h b/src/server/ws/service/VoiceService.h new file mode 100644 index 0000000..6f9f2a7 --- /dev/null +++ b/src/server/ws/service/VoiceService.h @@ -0,0 +1,35 @@ +#pragma once + +#include "spdlog/spdlog.h" + +#include +#include + +// From our CreatureVoiceLib +#include + +#include "server/ws/dto/ListDto.h" + + +namespace creatures :: ws { + + class VoiceService { + + private: + typedef oatpp::web::protocol::http::Status Status; + + public: + + VoiceService() = default; + virtual ~VoiceService() = default; + + /** + * Get all of the voices + */ + oatpp::Object>> getAllVoices(); + + }; + + + +} // creatures :: ws