From b6beee1d76917881160b0b85ed061982ad69d0aa Mon Sep 17 00:00:00 2001 From: Green Sky Date: Wed, 6 Dec 2023 15:12:34 +0100 Subject: [PATCH] inital working port of old sdbot from llmtox --- .gitignore | 26 + CMakeLists.txt | 73 ++ external/CMakeLists.txt | 98 +++ external/cmake/Findsodium.cmake | 297 +++++++ external/happyhttp/CMakeLists.txt | 10 + external/happyhttp/happyhttp/happyhttp.cpp | 940 +++++++++++++++++++++ external/happyhttp/happyhttp/happyhttp.h | 333 ++++++++ plugins/CMakeLists.txt | 11 + plugins/plugin_sdbot-webui.cpp | 78 ++ src/CMakeLists.txt | 16 + src/sd_bot.cpp | 224 +++++ src/sd_bot.hpp | 58 ++ 12 files changed, 2164 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 external/CMakeLists.txt create mode 100644 external/cmake/Findsodium.cmake create mode 100644 external/happyhttp/CMakeLists.txt create mode 100644 external/happyhttp/happyhttp/happyhttp.cpp create mode 100644 external/happyhttp/happyhttp/happyhttp.h create mode 100644 plugins/CMakeLists.txt create mode 100644 plugins/plugin_sdbot-webui.cpp create mode 100644 src/CMakeLists.txt create mode 100644 src/sd_bot.cpp create mode 100644 src/sd_bot.hpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..56f48bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +.vs/ +*.o +*.swp +~* +*~ +.idea/ +cmake-build-debug/ +cmake-build-debugandtest/ +cmake-build-release/ +*.stackdump +*.coredump +compile_commands.json +/build* +/result* +.clangd +.cache + +.DS_Store +.AppleDouble +.LSOverride + +CMakeLists.txt.user* +CMakeCache.txt + +*.tox +imgui.ini diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..f2951a1 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,73 @@ +cmake_minimum_required(VERSION 3.9 FATAL_ERROR) + +# cmake setup begin +project(solanaceae_sdbot-webui) + +if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + set(SOLANACEAE_SDBOT_WEBUI_STANDALONE ON) + # why the f do i need this >:( + set(NOT_SOLANACEAE_SDBOT_WEBUI_STANDALONE OFF) +else() + set(SOLANACEAE_SDBOT_WEBUI_STANDALONE OFF) + set(NOT_SOLANACEAE_SDBOT_WEBUI_STANDALONE ON) +endif() +message("II SOLANACEAE_SDBOT_WEBUI_STANDALONE " ${SOLANACEAE_SDBOT_WEBUI_STANDALONE}) + +option(SOLANACEAE_SDBOT_WEBUI_BUILD_PLUGINS "Build the toxic_games plugins" ${SOLANACEAE_SDBOT_WEBUI_STANDALONE}) + +if (SOLANACEAE_SDBOT_WEBUI_STANDALONE) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) + + # defaulting to debug mode, if not specified + if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Debug") + endif() + + # setup my vim ycm :D + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + + # more paths + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +endif() + +# external libs +add_subdirectory(./external) # before increasing warn levels, sad :( + +if (SOLANACEAE_SDBOT_WEBUI_STANDALONE) + set(CMAKE_CXX_EXTENSIONS OFF) + + # bump up warning levels appropriately for clang, gcc & msvc + if (${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU" OR ${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") + add_compile_options( + -Wall -Wextra # Reasonable and standard + -Wpedantic # Warn if non-standard C++ is used + -Wunused # Warn on anything being unused + #-Wconversion # Warn on type conversions that may lose data + #-Wsign-conversion # Warn on sign conversions + -Wshadow # Warn if a variable declaration shadows one from a parent context + ) + + if (NOT WIN32) + #link_libraries(-fsanitize=address,undefined) + #link_libraries(-fsanitize=undefined) + endif() + elseif (${CMAKE_CXX_COMPILER_ID} STREQUAL "MSVC") + if (CMAKE_CXX_FLAGS MATCHES "/W[0-4]") + string(REGEX REPLACE "/W[0-4]" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") + endif() + endif() +endif() + +# cmake setup end + +add_subdirectory(./src) + +if (SOLANACEAE_SDBOT_WEBUI_BUILD_PLUGINS) + message("II SOLANACEAE_SDBOT_WEBUI_BUILD_PLUGINS " ${SOLANACEAE_SDBOT_WEBUI_BUILD_PLUGINS}) + add_subdirectory(./plugins) +endif() + diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt new file mode 100644 index 0000000..4d96766 --- /dev/null +++ b/external/CMakeLists.txt @@ -0,0 +1,98 @@ +cmake_minimum_required(VERSION 3.14...3.24 FATAL_ERROR) + +include(FetchContent) + +add_subdirectory(./happyhttp) + +if ( + NOT TARGET libsodium AND + NOT TARGET unofficial-sodium::sodium AND + NOT TARGET unofficial-sodium::sodium_config_public AND + NOT TARGET sodium +) + # for find sodium + list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) + + find_package(unofficial-sodium CONFIG QUIET) + find_package(sodium QUIET) + if(unofficial-sodium_FOUND) # vcpkg + if(TARGET unofficial-sodium::sodium) + #TODO: alias can not target another alias + #target_link_libraries(toxcore unofficial-sodium::sodium) + #add_library(libsodium ALIAS unofficial-sodium::sodium) + + add_library(libsodium INTERFACE) + target_link_libraries(libsodium INTERFACE unofficial-sodium::sodium) + endif() + if(TARGET unofficial-sodium::sodium_config_public) + #TODO: alias can not target another alias + #target_link_libraries(toxcore unofficial-sodium::sodium_config_public) + #add_library(libsodium ALIAS unofficial-sodium::sodium_config_public) + + add_library(libsodium INTERFACE) + target_link_libraries(libsodium INTERFACE unofficial-sodium::sodium_config_public) + endif() + elseif(sodium_FOUND) + #add_library(libsodium ALIAS sodium) + add_library(libsodium INTERFACE) + target_link_libraries(libsodium INTERFACE sodium) + else() + message(SEND_ERROR "missing libsodium") + endif() +endif() + +# TODO: move entt dep into solanaceae_contact +if (NOT TARGET EnTT::EnTT) + FetchContent_Declare(EnTT + GIT_REPOSITORY https://github.com/skypjack/entt.git + GIT_TAG v3.12.2 + EXCLUDE_FROM_ALL + ) + FetchContent_MakeAvailable(EnTT) +endif() + +if (NOT TARGET solanaceae_util) + FetchContent_Declare(solanaceae_util + GIT_REPOSITORY https://github.com/Green-Sky/solanaceae_util.git + GIT_TAG master + EXCLUDE_FROM_ALL + ) + FetchContent_MakeAvailable(solanaceae_util) +endif() + +if (NOT TARGET solanaceae_contact) + FetchContent_Declare(solanaceae_contact + GIT_REPOSITORY https://github.com/Green-Sky/solanaceae_contact.git + GIT_TAG master + EXCLUDE_FROM_ALL + ) + FetchContent_MakeAvailable(solanaceae_contact) +endif() + +if (NOT TARGET solanaceae_message3) + FetchContent_Declare(solanaceae_message3 + GIT_REPOSITORY https://github.com/Green-Sky/solanaceae_message3.git + GIT_TAG master + EXCLUDE_FROM_ALL + ) + FetchContent_MakeAvailable(solanaceae_message3) +endif() + +if (NOT TARGET solanaceae_plugin) + FetchContent_Declare(solanaceae_plugin + GIT_REPOSITORY https://github.com/Green-Sky/solanaceae_plugin.git + GIT_TAG master + EXCLUDE_FROM_ALL + ) + FetchContent_MakeAvailable(solanaceae_plugin) +endif() + +if (NOT TARGET nlohmann_json::nlohmann_json) + FetchContent_Declare(json + URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz + URL_HASH SHA256=d6c65aca6b1ed68e7a182f4757257b107ae403032760ed6ef121c9d55e81757d + EXCLUDE_FROM_ALL + ) + FetchContent_MakeAvailable(json) +endif() + diff --git a/external/cmake/Findsodium.cmake b/external/cmake/Findsodium.cmake new file mode 100644 index 0000000..a210c00 --- /dev/null +++ b/external/cmake/Findsodium.cmake @@ -0,0 +1,297 @@ +# Written in 2016 by Henrik Steffen Gaßmann +# +# To the extent possible under law, the author(s) have dedicated all +# copyright and related and neighboring rights to this software to the +# public domain worldwide. This software is distributed without any warranty. +# +# You should have received a copy of the CC0 Public Domain Dedication +# along with this software. If not, see +# +# http://creativecommons.org/publicdomain/zero/1.0/ +# +######################################################################## +# Tries to find the local libsodium installation. +# +# On Windows the sodium_DIR environment variable is used as a default +# hint which can be overridden by setting the corresponding cmake variable. +# +# Once done the following variables will be defined: +# +# sodium_FOUND +# sodium_INCLUDE_DIR +# sodium_LIBRARY_DEBUG +# sodium_LIBRARY_RELEASE +# +# +# Furthermore an imported "sodium" target is created. +# + +if (CMAKE_C_COMPILER_ID STREQUAL "GNU" + OR CMAKE_C_COMPILER_ID STREQUAL "Clang") + set(_GCC_COMPATIBLE 1) +endif() + +# static library option +if (NOT DEFINED sodium_USE_STATIC_LIBS) + option(sodium_USE_STATIC_LIBS "enable to statically link against sodium" OFF) +endif() +if(NOT (sodium_USE_STATIC_LIBS EQUAL sodium_USE_STATIC_LIBS_LAST)) + unset(sodium_LIBRARY CACHE) + unset(sodium_LIBRARY_DEBUG CACHE) + unset(sodium_LIBRARY_RELEASE CACHE) + unset(sodium_DLL_DEBUG CACHE) + unset(sodium_DLL_RELEASE CACHE) + set(sodium_USE_STATIC_LIBS_LAST ${sodium_USE_STATIC_LIBS} CACHE INTERNAL "internal change tracking variable") +endif() + + +######################################################################## +# UNIX +if (UNIX) + # import pkg-config + find_package(PkgConfig QUIET) + if (PKG_CONFIG_FOUND) + pkg_check_modules(sodium_PKG QUIET libsodium) + endif() + + if(sodium_USE_STATIC_LIBS) + foreach(_libname ${sodium_PKG_STATIC_LIBRARIES}) + if (NOT _libname MATCHES "^lib.*\\.a$") # ignore strings already ending with .a + list(INSERT sodium_PKG_STATIC_LIBRARIES 0 "lib${_libname}.a") + endif() + endforeach() + list(REMOVE_DUPLICATES sodium_PKG_STATIC_LIBRARIES) + + # if pkgconfig for libsodium doesn't provide + # static lib info, then override PKG_STATIC here.. + if (NOT sodium_PKG_STATIC_FOUND) + set(sodium_PKG_STATIC_LIBRARIES libsodium.a) + endif() + + set(XPREFIX sodium_PKG_STATIC) + else() + if (NOT sodium_PKG_FOUND) + set(sodium_PKG_LIBRARIES sodium) + endif() + + set(XPREFIX sodium_PKG) + endif() + + find_path(sodium_INCLUDE_DIR sodium.h + HINTS ${${XPREFIX}_INCLUDE_DIRS} + ) + find_library(sodium_LIBRARY_DEBUG NAMES ${${XPREFIX}_LIBRARIES} + HINTS ${${XPREFIX}_LIBRARY_DIRS} + ) + find_library(sodium_LIBRARY_RELEASE NAMES ${${XPREFIX}_LIBRARIES} + HINTS ${${XPREFIX}_LIBRARY_DIRS} + ) + + +######################################################################## +# Windows +elseif (WIN32) + set(sodium_DIR "$ENV{sodium_DIR}" CACHE FILEPATH "sodium install directory") + mark_as_advanced(sodium_DIR) + + find_path(sodium_INCLUDE_DIR sodium.h + HINTS ${sodium_DIR} + PATH_SUFFIXES include + ) + + if (MSVC) + # detect target architecture + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/arch.cpp" [=[ + #if defined _M_IX86 + #error ARCH_VALUE x86_32 + #elif defined _M_X64 + #error ARCH_VALUE x86_64 + #endif + #error ARCH_VALUE unknown + ]=]) + try_compile(_UNUSED_VAR "${CMAKE_CURRENT_BINARY_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/arch.cpp" + OUTPUT_VARIABLE _COMPILATION_LOG + ) + string(REGEX REPLACE ".*ARCH_VALUE ([a-zA-Z0-9_]+).*" "\\1" _TARGET_ARCH "${_COMPILATION_LOG}") + + # construct library path + if (_TARGET_ARCH STREQUAL "x86_32") + string(APPEND _PLATFORM_PATH "Win32") + elseif(_TARGET_ARCH STREQUAL "x86_64") + string(APPEND _PLATFORM_PATH "x64") + else() + message(FATAL_ERROR "the ${_TARGET_ARCH} architecture is not supported by Findsodium.cmake.") + endif() + string(APPEND _PLATFORM_PATH "/$$CONFIG$$") + + if (MSVC_VERSION LESS 1900) + math(EXPR _VS_VERSION "${MSVC_VERSION} / 10 - 60") + else() + math(EXPR _VS_VERSION "${MSVC_VERSION} / 10 - 50") + endif() + string(APPEND _PLATFORM_PATH "/v${_VS_VERSION}") + + if (sodium_USE_STATIC_LIBS) + string(APPEND _PLATFORM_PATH "/static") + else() + string(APPEND _PLATFORM_PATH "/dynamic") + endif() + + string(REPLACE "$$CONFIG$$" "Debug" _DEBUG_PATH_SUFFIX "${_PLATFORM_PATH}") + string(REPLACE "$$CONFIG$$" "Release" _RELEASE_PATH_SUFFIX "${_PLATFORM_PATH}") + + find_library(sodium_LIBRARY_DEBUG libsodium.lib + HINTS ${sodium_DIR} + PATH_SUFFIXES ${_DEBUG_PATH_SUFFIX} + ) + find_library(sodium_LIBRARY_RELEASE libsodium.lib + HINTS ${sodium_DIR} + PATH_SUFFIXES ${_RELEASE_PATH_SUFFIX} + ) + if (NOT sodium_USE_STATIC_LIBS) + set(CMAKE_FIND_LIBRARY_SUFFIXES_BCK ${CMAKE_FIND_LIBRARY_SUFFIXES}) + set(CMAKE_FIND_LIBRARY_SUFFIXES ".dll") + find_library(sodium_DLL_DEBUG libsodium + HINTS ${sodium_DIR} + PATH_SUFFIXES ${_DEBUG_PATH_SUFFIX} + ) + find_library(sodium_DLL_RELEASE libsodium + HINTS ${sodium_DIR} + PATH_SUFFIXES ${_RELEASE_PATH_SUFFIX} + ) + set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES_BCK}) + endif() + + elseif(_GCC_COMPATIBLE) + if (sodium_USE_STATIC_LIBS) + find_library(sodium_LIBRARY_DEBUG libsodium.a + HINTS ${sodium_DIR} + PATH_SUFFIXES lib + ) + find_library(sodium_LIBRARY_RELEASE libsodium.a + HINTS ${sodium_DIR} + PATH_SUFFIXES lib + ) + else() + find_library(sodium_LIBRARY_DEBUG libsodium.dll.a + HINTS ${sodium_DIR} + PATH_SUFFIXES lib + ) + find_library(sodium_LIBRARY_RELEASE libsodium.dll.a + HINTS ${sodium_DIR} + PATH_SUFFIXES lib + ) + + file(GLOB _DLL + LIST_DIRECTORIES false + RELATIVE "${sodium_DIR}/bin" + "${sodium_DIR}/bin/libsodium*.dll" + ) + find_library(sodium_DLL_DEBUG ${_DLL} libsodium + HINTS ${sodium_DIR} + PATH_SUFFIXES bin + ) + find_library(sodium_DLL_RELEASE ${_DLL} libsodium + HINTS ${sodium_DIR} + PATH_SUFFIXES bin + ) + endif() + else() + message(FATAL_ERROR "this platform is not supported by FindSodium.cmake") + endif() + + +######################################################################## +# unsupported +else() + message(FATAL_ERROR "this platform is not supported by FindSodium.cmake") +endif() + + +######################################################################## +# common stuff + +# extract sodium version +if (sodium_INCLUDE_DIR) + set(_VERSION_HEADER "${_INCLUDE_DIR}/sodium/version.h") + if (EXISTS _VERSION_HEADER) + file(READ "${_VERSION_HEADER}" _VERSION_HEADER_CONTENT) + string(REGEX REPLACE ".*#[ \t]*define[ \t]*SODIUM_VERSION_STRING[ \t]*\"([^\n]*)\".*" "\\1" + sodium_VERSION "${_VERSION_HEADER_CONTENT}") + set(sodium_VERSION "${sodium_VERSION}" PARENT_SCOPE) + endif() +endif() + +# communicate results +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args( + sodium # The name must be either uppercase or match the filename case. + REQUIRED_VARS + sodium_LIBRARY_RELEASE + sodium_LIBRARY_DEBUG + sodium_INCLUDE_DIR + VERSION_VAR + sodium_VERSION +) + +if(Sodium_FOUND) + set(sodium_LIBRARIES + optimized ${sodium_LIBRARY_RELEASE} debug ${sodium_LIBRARY_DEBUG}) +endif() + +# mark file paths as advanced +mark_as_advanced(sodium_INCLUDE_DIR) +mark_as_advanced(sodium_LIBRARY_DEBUG) +mark_as_advanced(sodium_LIBRARY_RELEASE) +if (WIN32) + mark_as_advanced(sodium_DLL_DEBUG) + mark_as_advanced(sodium_DLL_RELEASE) +endif() + +# create imported target +if(sodium_USE_STATIC_LIBS) + set(_LIB_TYPE STATIC) +else() + set(_LIB_TYPE SHARED) +endif() + +if(NOT TARGET sodium) + add_library(sodium ${_LIB_TYPE} IMPORTED) +endif() + +set_target_properties(sodium PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${sodium_INCLUDE_DIR}" + IMPORTED_LINK_INTERFACE_LANGUAGES "C" +) + +if (sodium_USE_STATIC_LIBS) + set_target_properties(sodium PROPERTIES + INTERFACE_COMPILE_DEFINITIONS "SODIUM_STATIC" + IMPORTED_LOCATION "${sodium_LIBRARY_RELEASE}" + IMPORTED_LOCATION_DEBUG "${sodium_LIBRARY_DEBUG}" + ) +else() + if (UNIX) + set_target_properties(sodium PROPERTIES + IMPORTED_LOCATION "${sodium_LIBRARY_RELEASE}" + IMPORTED_LOCATION_DEBUG "${sodium_LIBRARY_DEBUG}" + ) + elseif (WIN32) + set_target_properties(sodium PROPERTIES + IMPORTED_IMPLIB "${sodium_LIBRARY_RELEASE}" + IMPORTED_IMPLIB_DEBUG "${sodium_LIBRARY_DEBUG}" + ) + if (NOT (sodium_DLL_DEBUG MATCHES ".*-NOTFOUND")) + set_target_properties(sodium PROPERTIES + IMPORTED_LOCATION_DEBUG "${sodium_DLL_DEBUG}" + ) + endif() + if (NOT (sodium_DLL_RELEASE MATCHES ".*-NOTFOUND")) + set_target_properties(sodium PROPERTIES + IMPORTED_LOCATION_RELWITHDEBINFO "${sodium_DLL_RELEASE}" + IMPORTED_LOCATION_MINSIZEREL "${sodium_DLL_RELEASE}" + IMPORTED_LOCATION_RELEASE "${sodium_DLL_RELEASE}" + ) + endif() + endif() +endif() diff --git a/external/happyhttp/CMakeLists.txt b/external/happyhttp/CMakeLists.txt new file mode 100644 index 0000000..525e88e --- /dev/null +++ b/external/happyhttp/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.9 FATAL_ERROR) + +add_library(happyhttp STATIC + ./happyhttp/happyhttp.h + ./happyhttp/happyhttp.cpp +) + +target_include_directories(happyhttp PUBLIC .) +target_compile_features(happyhttp PUBLIC cxx_std_11) + diff --git a/external/happyhttp/happyhttp/happyhttp.cpp b/external/happyhttp/happyhttp/happyhttp.cpp new file mode 100644 index 0000000..a805b80 --- /dev/null +++ b/external/happyhttp/happyhttp/happyhttp.cpp @@ -0,0 +1,940 @@ +/* + * HappyHTTP - a simple HTTP library + * Version 0.1 + * + * Copyright (c) 2006 Ben Campbell + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software in a + * product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * + * 2. Altered source versions must be plainly marked as such, and must not + * be misrepresented as being the original software. + * + * 3. This notice may not be removed or altered from any source distribution. + * + */ + + +#include "happyhttp.h" + +#ifndef _WIN32 +// #include + #include + #include + #include + #include + #include // for gethostbyname() + #include +#else + #include + #define vsnprintf _vsnprintf +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include + +#ifndef _WIN32 + #include + #define _stricmp strcasecmp +#endif + + +using namespace std; + + +namespace happyhttp +{ + +#ifdef WIN32 +const char* GetWinsockErrorString( int err ); +#endif + + +//--------------------------------------------------------------------- +// Helper functions +//--------------------------------------------------------------------- + + + +void BailOnSocketError( const char* context ) +{ +#ifdef WIN32 + + int e = WSAGetLastError(); + const char* msg = GetWinsockErrorString( e ); +#else + const char* msg = strerror( errno ); +#endif + throw Wobbly( "%s: %s", context, msg ); +} + + +#ifdef WIN32 + +const char* GetWinsockErrorString( int err ) +{ + switch( err) + { + case 0: return "No error"; + case WSAEINTR: return "Interrupted system call"; + case WSAEBADF: return "Bad file number"; + case WSAEACCES: return "Permission denied"; + case WSAEFAULT: return "Bad address"; + case WSAEINVAL: return "Invalid argument"; + case WSAEMFILE: return "Too many open sockets"; + case WSAEWOULDBLOCK: return "Operation would block"; + case WSAEINPROGRESS: return "Operation now in progress"; + case WSAEALREADY: return "Operation already in progress"; + case WSAENOTSOCK: return "Socket operation on non-socket"; + case WSAEDESTADDRREQ: return "Destination address required"; + case WSAEMSGSIZE: return "Message too long"; + case WSAEPROTOTYPE: return "Protocol wrong type for socket"; + case WSAENOPROTOOPT: return "Bad protocol option"; + case WSAEPROTONOSUPPORT: return "Protocol not supported"; + case WSAESOCKTNOSUPPORT: return "Socket type not supported"; + case WSAEOPNOTSUPP: return "Operation not supported on socket"; + case WSAEPFNOSUPPORT: return "Protocol family not supported"; + case WSAEAFNOSUPPORT: return "Address family not supported"; + case WSAEADDRINUSE: return "Address already in use"; + case WSAEADDRNOTAVAIL: return "Can't assign requested address"; + case WSAENETDOWN: return "Network is down"; + case WSAENETUNREACH: return "Network is unreachable"; + case WSAENETRESET: return "Net connection reset"; + case WSAECONNABORTED: return "Software caused connection abort"; + case WSAECONNRESET: return "Connection reset by peer"; + case WSAENOBUFS: return "No buffer space available"; + case WSAEISCONN: return "Socket is already connected"; + case WSAENOTCONN: return "Socket is not connected"; + case WSAESHUTDOWN: return "Can't send after socket shutdown"; + case WSAETOOMANYREFS: return "Too many references, can't splice"; + case WSAETIMEDOUT: return "Connection timed out"; + case WSAECONNREFUSED: return "Connection refused"; + case WSAELOOP: return "Too many levels of symbolic links"; + case WSAENAMETOOLONG: return "File name too long"; + case WSAEHOSTDOWN: return "Host is down"; + case WSAEHOSTUNREACH: return "No route to host"; + case WSAENOTEMPTY: return "Directory not empty"; + case WSAEPROCLIM: return "Too many processes"; + case WSAEUSERS: return "Too many users"; + case WSAEDQUOT: return "Disc quota exceeded"; + case WSAESTALE: return "Stale NFS file handle"; + case WSAEREMOTE: return "Too many levels of remote in path"; + case WSASYSNOTREADY: return "Network system is unavailable"; + case WSAVERNOTSUPPORTED: return "Winsock version out of range"; + case WSANOTINITIALISED: return "WSAStartup not yet called"; + case WSAEDISCON: return "Graceful shutdown in progress"; + case WSAHOST_NOT_FOUND: return "Host not found"; + case WSANO_DATA: return "No host data of that type was found"; + } + + return "unknown"; +}; + +#endif // WIN32 + + +// return true if socket has data waiting to be read +bool datawaiting( int sock ) +{ + fd_set fds; + FD_ZERO( &fds ); + FD_SET( sock, &fds ); + + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 0; + + int r = select( sock+1, &fds, NULL, NULL, &tv); + if (r < 0) + BailOnSocketError( "select" ); + + if( FD_ISSET( sock, &fds ) ) + return true; + else + return false; +} + + +// Try to work out address from string +// returns 0 if bad +struct in_addr *atoaddr( const char* address) +{ + struct hostent *host; + static struct in_addr saddr; + + // First try nnn.nnn.nnn.nnn form + saddr.s_addr = inet_addr(address); + if (saddr.s_addr != -1) + return &saddr; + + host = gethostbyname(address); + if( host ) + return (struct in_addr *) *host->h_addr_list; + + return 0; +} + + + + + + + +//--------------------------------------------------------------------- +// +// Exception class +// +//--------------------------------------------------------------------- + + +Wobbly::Wobbly( const char* fmt, ... ) +{ + va_list ap; + va_start( ap,fmt); + int n = vsnprintf( m_Message, MAXLEN, fmt, ap ); + va_end( ap ); + if(n==MAXLEN) + m_Message[MAXLEN-1] = '\0'; +} + + + + + + + + +//--------------------------------------------------------------------- +// +// Connection +// +//--------------------------------------------------------------------- +Connection::Connection( const char* host, int port ) : + m_ResponseBeginCB(0), + m_ResponseDataCB(0), + m_ResponseCompleteCB(0), + m_UserData(0), + m_State( IDLE ), + m_Host( host ), + m_Port( port ), + m_Sock(-1) +{ +} + + +void Connection::setcallbacks( + ResponseBegin_CB begincb, + ResponseData_CB datacb, + ResponseComplete_CB completecb, + void* userdata ) +{ + m_ResponseBeginCB = begincb; + m_ResponseDataCB = datacb; + m_ResponseCompleteCB = completecb; + m_UserData = userdata; +} + + +void Connection::connect() +{ + in_addr* addr = atoaddr( m_Host.c_str() ); + if( !addr ) + throw Wobbly( "Invalid network address" ); + + sockaddr_in address; + memset( (char*)&address, 0, sizeof(address) ); + address.sin_family = AF_INET; + address.sin_port = htons( m_Port ); + address.sin_addr.s_addr = addr->s_addr; + + m_Sock = socket( AF_INET, SOCK_STREAM, 0 ); + if( m_Sock < 0 ) + BailOnSocketError( "socket()" ); + +// printf("Connecting to %s on port %d.\n",inet_ntoa(*addr), port); + + if( ::connect( m_Sock, (sockaddr const*)&address, sizeof(address) ) < 0 ) + BailOnSocketError( "connect()" ); +} + + +void Connection::close() +{ +#ifdef WIN32 + if( m_Sock >= 0 ) + ::closesocket( m_Sock ); +#else + if( m_Sock >= 0 ) + ::close( m_Sock ); +#endif + m_Sock = -1; + + // discard any incomplete responses + while( !m_Outstanding.empty() ) + { + delete m_Outstanding.front(); + m_Outstanding.pop_front(); + } +} + + +Connection::~Connection() +{ + close(); +} + +void Connection::request( const char* method, + const char* url, + const char* headers[], + const unsigned char* body, + int bodysize ) +{ + + bool gotcontentlength = false; // already in headers? + + // check headers for content-length + // TODO: check for "Host" and "Accept-Encoding" too + // and avoid adding them ourselves in putrequest() + if( headers ) + { + const char** h = headers; + while( *h ) + { + const char* name = *h++; + const char* value = *h++; + assert( value != 0 ); // name with no value! + + if( 0==_stricmp( name, "content-length" ) ) + gotcontentlength = true; + } + } + + putrequest( method, url ); + + if( body && !gotcontentlength ) + putheader( "Content-Length", bodysize ); + + if( headers ) + { + const char** h = headers; + while( *h ) + { + const char* name = *h++; + const char* value = *h++; + putheader( name, value ); + } + } + endheaders(); + + if( body ) + send( body, bodysize ); + +} + + + + +void Connection::putrequest( const char* method, const char* url ) +{ + if( m_State != IDLE ) + throw Wobbly( "Request already issued" ); + + m_State = REQ_STARTED; + + char req[ 512 ]; + sprintf( req, "%s %s HTTP/1.1", method, url ); + m_Buffer.push_back( req ); + + putheader( "Host", m_Host.c_str() ); // required for HTTP1.1 + + // don't want any fancy encodings please + putheader("Accept-Encoding", "identity"); + + // Push a new response onto the queue + Response *r = new Response( method, *this ); + m_Outstanding.push_back( r ); +} + + +void Connection::putheader( const char* header, const char* value ) +{ + if( m_State != REQ_STARTED ) + throw Wobbly( "putheader() failed" ); + m_Buffer.push_back( string(header) + ": " + string( value ) ); +} + +void Connection::putheader( const char* header, int numericvalue ) +{ + char buf[32]; + sprintf( buf, "%d", numericvalue ); + putheader( header, buf ); +} + +void Connection::endheaders() +{ + if( m_State != REQ_STARTED ) + throw Wobbly( "Cannot send header" ); + m_State = IDLE; + + m_Buffer.push_back( "" ); + + string msg; + vector< string>::const_iterator it; + for( it = m_Buffer.begin(); it != m_Buffer.end(); ++it ) + msg += (*it) + "\r\n"; + + m_Buffer.clear(); + +// printf( "%s", msg.c_str() ); + send( (const unsigned char*)msg.c_str(), msg.size() ); +} + + + +void Connection::send( const unsigned char* buf, int numbytes ) +{ +// fwrite( buf, 1,numbytes, stdout ); + + if( m_Sock < 0 ) + connect(); + + while( numbytes > 0 ) + { +#ifdef WIN32 + int n = ::send( m_Sock, (const char*)buf, numbytes, 0 ); +#else + int n = ::send( m_Sock, buf, numbytes, 0 ); +#endif + if( n<0 ) + BailOnSocketError( "send()" ); + numbytes -= n; + buf += n; + } +} + + +void Connection::pump(int milisec) +{ + if( m_Outstanding.empty() ) + return; // no requests outstanding + + assert( m_Sock >0 ); // outstanding requests but no connection! + + if( !datawaiting( m_Sock ) ) + return; // recv will block + + unsigned char buf[ 2048 ]; + int a = recv( m_Sock, (char*)buf, sizeof(buf), 0 ); + if( a<0 ) + BailOnSocketError( "recv()" ); + + if( a== 0 ) + { + // connection has closed + + Response* r = m_Outstanding.front(); + r->notifyconnectionclosed(); + assert( r->completed() ); + delete r; + m_Outstanding.pop_front(); + + // any outstanding requests will be discarded + close(); + } + else + { + int used = 0; + while( used < a && !m_Outstanding.empty() ) + { + + Response* r = m_Outstanding.front(); + int u = r->pump( &buf[used], a-used ); + + // delete response once completed + if( r->completed() ) + { + delete r; + m_Outstanding.pop_front(); + } + used += u; + } + + // NOTE: will lose bytes if response queue goes empty + // (but server shouldn't be sending anything if we don't have + // anything outstanding anyway) + assert( used == a ); // all bytes should be used up by here. + } +} + + + + + + +//--------------------------------------------------------------------- +// +// Response +// +//--------------------------------------------------------------------- + + +Response::Response( const char* method, Connection& conn ) : + m_Connection( conn ), + m_State( STATUSLINE ), + m_Method( method ), + m_Version( 0 ), + m_Status(0), + m_BytesRead(0), + m_Chunked(false), + m_ChunkLeft(0), + m_Length(-1), + m_WillClose(false) +{ +} + + +const char* Response::getheader( const char* name ) const +{ + std::string lname( name ); +#ifdef _MSC_VER + std::transform( lname.begin(), lname.end(), lname.begin(), tolower ); +#else + std::transform( lname.begin(), lname.end(), lname.begin(), ::tolower ); +#endif + + std::map< std::string, std::string >::const_iterator it = m_Headers.find( lname ); + if( it == m_Headers.end() ) + return 0; + else + return it->second.c_str(); +} + + +int Response::getstatus() const +{ + // only valid once we've got the statusline + assert( m_State != STATUSLINE ); + return m_Status; +} + + +const char* Response::getreason() const +{ + // only valid once we've got the statusline + assert( m_State != STATUSLINE ); + return m_Reason.c_str(); +} + + + +// Connection has closed +void Response::notifyconnectionclosed() +{ + if( m_State == COMPLETE ) + return; + + // eof can be valid... + if( m_State == BODY && + !m_Chunked && + m_Length == -1 ) + { + Finish(); // we're all done! + } + else + { + throw Wobbly( "Connection closed unexpectedly" ); + } +} + + + +int Response::pump( const unsigned char* data, int datasize ) +{ + assert( datasize != 0 ); + int count = datasize; + + while( count > 0 && m_State != COMPLETE ) + { + if( m_State == STATUSLINE || + m_State == HEADERS || + m_State == TRAILERS || + m_State == CHUNKLEN || + m_State == CHUNKEND ) + { + // we want to accumulate a line + while( count > 0 ) + { + char c = (char)*data++; + --count; + if( c == '\n' ) + { + // now got a whole line! + switch( m_State ) + { + case STATUSLINE: + ProcessStatusLine( m_LineBuf ); + break; + case HEADERS: + ProcessHeaderLine( m_LineBuf ); + break; + case TRAILERS: + ProcessTrailerLine( m_LineBuf ); + break; + case CHUNKLEN: + ProcessChunkLenLine( m_LineBuf ); + break; + case CHUNKEND: + // just soak up the crlf after body and go to next state + assert( m_Chunked == true ); + m_State = CHUNKLEN; + break; + default: + break; + } + m_LineBuf.clear(); + break; // break out of line accumulation! + } + else + { + if( c != '\r' ) // just ignore CR + m_LineBuf += c; + } + } + } + else if( m_State == BODY ) + { + int bytesused = 0; + if( m_Chunked ) + bytesused = ProcessDataChunked( data, count ); + else + bytesused = ProcessDataNonChunked( data, count ); + data += bytesused; + count -= bytesused; + } + } + + // return number of bytes used + return datasize - count; +} + + + +void Response::ProcessChunkLenLine( std::string const& line ) +{ + // chunklen in hex at beginning of line + m_ChunkLeft = strtol( line.c_str(), NULL, 16 ); + + if( m_ChunkLeft == 0 ) + { + // got the whole body, now check for trailing headers + m_State = TRAILERS; + m_HeaderAccum.clear(); + } + else + { + m_State = BODY; + } +} + + +// handle some body data in chunked mode +// returns number of bytes used. +int Response::ProcessDataChunked( const unsigned char* data, int count ) +{ + assert( m_Chunked ); + + int n = count; + if( n>m_ChunkLeft ) + n = m_ChunkLeft; + + // invoke callback to pass out the data + if( m_Connection.m_ResponseDataCB ) + (m_Connection.m_ResponseDataCB)( this, m_Connection.m_UserData, data, n ); + + m_BytesRead += n; + + m_ChunkLeft -= n; + assert( m_ChunkLeft >= 0); + if( m_ChunkLeft == 0 ) + { + // chunk completed! now soak up the trailing CRLF before next chunk + m_State = CHUNKEND; + } + return n; +} + +// handle some body data in non-chunked mode. +// returns number of bytes used. +int Response::ProcessDataNonChunked( const unsigned char* data, int count ) +{ + int n = count; + if( m_Length != -1 ) + { + // we know how many bytes to expect + int remaining = m_Length - m_BytesRead; + if( n > remaining ) + n = remaining; + } + + // invoke callback to pass out the data + if( m_Connection.m_ResponseDataCB ) + (m_Connection.m_ResponseDataCB)( this, m_Connection.m_UserData, data, n ); + + m_BytesRead += n; + + // Finish if we know we're done. Else we're waiting for connection close. + if( m_Length != -1 && m_BytesRead == m_Length ) + Finish(); + + return n; +} + + +void Response::Finish() +{ + m_State = COMPLETE; + + // invoke the callback + if( m_Connection.m_ResponseCompleteCB ) + (m_Connection.m_ResponseCompleteCB)( this, m_Connection.m_UserData ); +} + + +void Response::ProcessStatusLine( std::string const& line ) +{ + const char* p = line.c_str(); + + // skip any leading space + while( *p && *p == ' ' ) + ++p; + + // get version + while( *p && *p != ' ' ) + m_VersionString += *p++; + while( *p && *p == ' ' ) + ++p; + + // get status code + std::string status; + while( *p && *p != ' ' ) + status += *p++; + while( *p && *p == ' ' ) + ++p; + + // rest of line is reason + while( *p ) + m_Reason += *p++; + + m_Status = atoi( status.c_str() ); + if( m_Status < 100 || m_Status > 999 ) + throw Wobbly( "BadStatusLine (%s)", line.c_str() ); + +/* + printf( "version: '%s'\n", m_VersionString.c_str() ); + printf( "status: '%d'\n", m_Status ); + printf( "reason: '%s'\n", m_Reason.c_str() ); +*/ + + if( m_VersionString == "HTTP:/1.0" ) + m_Version = 10; + else if( 0==m_VersionString.compare( 0,7,"HTTP/1." ) ) + m_Version = 11; + else + throw Wobbly( "UnknownProtocol (%s)", m_VersionString.c_str() ); + // TODO: support for HTTP/0.9 + + + // OK, now we expect headers! + m_State = HEADERS; + m_HeaderAccum.clear(); +} + + +// process accumulated header data +void Response::FlushHeader() +{ + if( m_HeaderAccum.empty() ) + return; // no flushing required + + const char* p = m_HeaderAccum.c_str(); + + std::string header; + std::string value; + while( *p && *p != ':' ) + header += tolower( *p++ ); + + // skip ':' + if( *p ) + ++p; + + // skip space + while( *p && (*p ==' ' || *p=='\t') ) + ++p; + + value = p; // rest of line is value + + m_Headers[ header ] = value; +// printf("header: ['%s': '%s']\n", header.c_str(), value.c_str() ); + + m_HeaderAccum.clear(); +} + + +void Response::ProcessHeaderLine( std::string const& line ) +{ + const char* p = line.c_str(); + if( line.empty() ) + { + FlushHeader(); + // end of headers + + // HTTP code 100 handling (we ignore 'em) + if( m_Status == CONTINUE ) + m_State = STATUSLINE; // reset parsing, expect new status line + else + BeginBody(); // start on body now! + return; + } + + if( isspace(*p) ) + { + // it's a continuation line - just add it to previous data + ++p; + while( *p && isspace( *p ) ) + ++p; + + m_HeaderAccum += ' '; + m_HeaderAccum += p; + } + else + { + // begin a new header + FlushHeader(); + m_HeaderAccum = p; + } +} + + +void Response::ProcessTrailerLine( std::string const& line ) +{ + // TODO: handle trailers? + // (python httplib doesn't seem to!) + if( line.empty() ) + Finish(); + + // just ignore all the trailers... +} + + + +// OK, we've now got all the headers read in, so we're ready to start +// on the body. But we need to see what info we can glean from the headers +// first... +void Response::BeginBody() +{ + + m_Chunked = false; + m_Length = -1; // unknown + m_WillClose = false; + + // using chunked encoding? + const char* trenc = getheader( "transfer-encoding" ); + if( trenc && 0==_stricmp( trenc, "chunked") ) + { + m_Chunked = true; + m_ChunkLeft = -1; // unknown + } + + m_WillClose = CheckClose(); + + // length supplied? + const char* contentlen = getheader( "content-length" ); + if( contentlen && !m_Chunked ) + { + m_Length = atoi( contentlen ); + } + + // check for various cases where we expect zero-length body + if( m_Status == NO_CONTENT || + m_Status == NOT_MODIFIED || + ( m_Status >= 100 && m_Status < 200 ) || // 1xx codes have no body + m_Method == "HEAD" ) + { + m_Length = 0; + } + + + // if we're not using chunked mode, and no length has been specified, + // assume connection will close at end. + if( !m_WillClose && !m_Chunked && m_Length == -1 ) + m_WillClose = true; + + + + // Invoke the user callback, if any + if( m_Connection.m_ResponseBeginCB ) + (m_Connection.m_ResponseBeginCB)( this, m_Connection.m_UserData ); + +/* + printf("---------BeginBody()--------\n"); + printf("Length: %d\n", m_Length ); + printf("WillClose: %d\n", (int)m_WillClose ); + printf("Chunked: %d\n", (int)m_Chunked ); + printf("ChunkLeft: %d\n", (int)m_ChunkLeft ); + printf("----------------------------\n"); +*/ + // now start reading body data! + if( m_Chunked ) + m_State = CHUNKLEN; + else + m_State = BODY; +} + + +// return true if we think server will automatically close connection +bool Response::CheckClose() +{ + if( m_Version == 11 ) + { + // HTTP1.1 + // the connection stays open unless "connection: close" is specified. + const char* conn = getheader( "connection" ); + if( conn && 0==_stricmp( conn, "close" ) ) + return true; + else + return false; + } + + // Older HTTP + // keep-alive header indicates persistant connection + if( getheader( "keep-alive" ) ) + return false; + + // TODO: some special case handling for Akamai and netscape maybe? + // (see _check_close() in python httplib.py for details) + + return true; +} + + + +} // end namespace happyhttp + + diff --git a/external/happyhttp/happyhttp/happyhttp.h b/external/happyhttp/happyhttp/happyhttp.h new file mode 100644 index 0000000..17ee52d --- /dev/null +++ b/external/happyhttp/happyhttp/happyhttp.h @@ -0,0 +1,333 @@ +/* + * HappyHTTP - a simple HTTP library + * Version 0.1 + * + * Copyright (c) 2006 Ben Campbell + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software in a + * product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * + * 2. Altered source versions must be plainly marked as such, and must not + * be misrepresented as being the original software. + * + * 3. This notice may not be removed or altered from any source distribution. + * + */ + + +#ifndef HAPPYHTTP_H +#define HAPPYHTTP_H + + +#include +#include +#include +#include + + + + +// forward decl +struct in_addr; + +namespace happyhttp +{ + + +class Response; + +// Helper Functions +void BailOnSocketError( const char* context ); +struct in_addr *atoaddr( const char* address); + + +typedef void (*ResponseBegin_CB)( const Response* r, void* userdata ); +typedef void (*ResponseData_CB)( const Response* r, void* userdata, const unsigned char* data, int numbytes ); +typedef void (*ResponseComplete_CB)( const Response* r, void* userdata ); + + +// HTTP status codes +enum { + // 1xx informational + CONTINUE = 100, + SWITCHING_PROTOCOLS = 101, + PROCESSING = 102, + + // 2xx successful + OK = 200, + CREATED = 201, + ACCEPTED = 202, + NON_AUTHORITATIVE_INFORMATION = 203, + NO_CONTENT = 204, + RESET_CONTENT = 205, + PARTIAL_CONTENT = 206, + MULTI_STATUS = 207, + IM_USED = 226, + + // 3xx redirection + MULTIPLE_CHOICES = 300, + MOVED_PERMANENTLY = 301, + FOUND = 302, + SEE_OTHER = 303, + NOT_MODIFIED = 304, + USE_PROXY = 305, + TEMPORARY_REDIRECT = 307, + + // 4xx client error + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + PAYMENT_REQUIRED = 402, + FORBIDDEN = 403, + NOT_FOUND = 404, + METHOD_NOT_ALLOWED = 405, + NOT_ACCEPTABLE = 406, + PROXY_AUTHENTICATION_REQUIRED = 407, + REQUEST_TIMEOUT = 408, + CONFLICT = 409, + GONE = 410, + LENGTH_REQUIRED = 411, + PRECONDITION_FAILED = 412, + REQUEST_ENTITY_TOO_LARGE = 413, + REQUEST_URI_TOO_LONG = 414, + UNSUPPORTED_MEDIA_TYPE = 415, + REQUESTED_RANGE_NOT_SATISFIABLE = 416, + EXPECTATION_FAILED = 417, + UNPROCESSABLE_ENTITY = 422, + LOCKED = 423, + FAILED_DEPENDENCY = 424, + UPGRADE_REQUIRED = 426, + + // 5xx server error + INTERNAL_SERVER_ERROR = 500, + NOT_IMPLEMENTED = 501, + BAD_GATEWAY = 502, + SERVICE_UNAVAILABLE = 503, + GATEWAY_TIMEOUT = 504, + HTTP_VERSION_NOT_SUPPORTED = 505, + INSUFFICIENT_STORAGE = 507, + NOT_EXTENDED = 510, +}; + + + +// Exception class + +class Wobbly +{ +public: + Wobbly( const char* fmt, ... ); + const char* what() const + { return m_Message; } +protected: + enum { MAXLEN=256 }; + char m_Message[ MAXLEN ]; +}; + + + +//------------------------------------------------- +// Connection +// +// Handles the socket connection, issuing of requests and managing +// responses. +// ------------------------------------------------ + +class Connection +{ + friend class Response; +public: + // doesn't connect immediately + Connection( const char* host, int port ); + ~Connection(); + + // Set up the response handling callbacks. These will be invoked during + // calls to pump(). + // begincb - called when the responses headers have been received + // datacb - called repeatedly to handle body data + // completecb - response is completed + // userdata is passed as a param to all callbacks. + void setcallbacks( + ResponseBegin_CB begincb, + ResponseData_CB datacb, + ResponseComplete_CB completecb, + void* userdata ); + + // Don't need to call connect() explicitly as issuing a request will + // call it automatically if needed. + // But it could block (for name lookup etc), so you might prefer to + // call it in advance. + void connect(); + + // close connection, discarding any pending requests. + void close(); + + // Update the connection (non-blocking) + // Just keep calling this regularly to service outstanding requests. + void pump(int milisec=10); //10 miliseconds to prevent high cpu load + + // any requests still outstanding? + bool outstanding() const + { return !m_Outstanding.empty(); } + + // --------------------------- + // high-level request interface + // --------------------------- + + // method is "GET", "POST" etc... + // url is only path part: eg "/index.html" + // headers is array of name/value pairs, terminated by a null-ptr + // body & bodysize specify body data of request (eg values for a form) + void request( const char* method, const char* url, const char* headers[]=0, + const unsigned char* body=0, int bodysize=0 ); + + // --------------------------- + // low-level request interface + // --------------------------- + + // begin request + // method is "GET", "POST" etc... + // url is only path part: eg "/index.html" + void putrequest( const char* method, const char* url ); + + // Add a header to the request (call after putrequest() ) + void putheader( const char* header, const char* value ); + void putheader( const char* header, int numericvalue ); // alternate version + + // Finished adding headers, issue the request. + void endheaders(); + + // send body data if any. + // To be called after endheaders() + void send( const unsigned char* buf, int numbytes ); + +protected: + // some bits of implementation exposed to Response class + + // callbacks + ResponseBegin_CB m_ResponseBeginCB; + ResponseData_CB m_ResponseDataCB; + ResponseComplete_CB m_ResponseCompleteCB; + void* m_UserData; + +private: + enum { IDLE, REQ_STARTED, REQ_SENT } m_State; + std::string m_Host; + int m_Port; + int m_Sock; + std::vector< std::string > m_Buffer; // lines of request + + std::deque< Response* > m_Outstanding; // responses for outstanding requests +}; + + + + + + +//------------------------------------------------- +// Response +// +// Handles parsing of response data. +// ------------------------------------------------ + + +class Response +{ + friend class Connection; +public: + + // retrieve a header (returns 0 if not present) + const char* getheader( const char* name ) const; + + bool completed() const + { return m_State == COMPLETE; } + + + // get the HTTP status code + int getstatus() const; + + // get the HTTP response reason string + const char* getreason() const; + + // true if connection is expected to close after this response. + bool willclose() const + { return m_WillClose; } +protected: + // interface used by Connection + + // only Connection creates Responses. + Response( const char* method, Connection& conn ); + + // pump some data in for processing. + // Returns the number of bytes used. + // Will always return 0 when response is complete. + int pump( const unsigned char* data, int datasize ); + + // tell response that connection has closed + void notifyconnectionclosed(); + +private: + enum { + STATUSLINE, // start here. status line is first line of response. + HEADERS, // reading in header lines + BODY, // waiting for some body data (all or a chunk) + CHUNKLEN, // expecting a chunk length indicator (in hex) + CHUNKEND, // got the chunk, now expecting a trailing blank line + TRAILERS, // reading trailers after body. + COMPLETE, // response is complete! + } m_State; + + Connection& m_Connection; // to access callback ptrs + std::string m_Method; // req method: "GET", "POST" etc... + + // status line + std::string m_VersionString; // HTTP-Version + int m_Version; // 10: HTTP/1.0 11: HTTP/1.x (where x>=1) + int m_Status; // Status-Code + std::string m_Reason; // Reason-Phrase + + // header/value pairs + std::map m_Headers; + + int m_BytesRead; // body bytes read so far + bool m_Chunked; // response is chunked? + int m_ChunkLeft; // bytes left in current chunk + int m_Length; // -1 if unknown + bool m_WillClose; // connection will close at response end? + + std::string m_LineBuf; // line accumulation for states that want it + std::string m_HeaderAccum; // accumulation buffer for headers + + + void FlushHeader(); + void ProcessStatusLine( std::string const& line ); + void ProcessHeaderLine( std::string const& line ); + void ProcessTrailerLine( std::string const& line ); + void ProcessChunkLenLine( std::string const& line ); + + int ProcessDataChunked( const unsigned char* data, int count ); + int ProcessDataNonChunked( const unsigned char* data, int count ); + + void BeginBody(); + bool CheckClose(); + void Finish(); +}; + + + +} // end namespace happyhttp + + +#endif // HAPPYHTTP_H + + diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt new file mode 100644 index 0000000..dbd5922 --- /dev/null +++ b/plugins/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.14...3.24 FATAL_ERROR) + +add_library(plugin_sdbot-webui SHARED + ./plugin_sdbot-webui.cpp +) + +target_link_libraries(plugin_sdbot-webui PUBLIC + solanaceae_plugin + solanaceae_sdbot-webui +) + diff --git a/plugins/plugin_sdbot-webui.cpp b/plugins/plugin_sdbot-webui.cpp new file mode 100644 index 0000000..40f05b0 --- /dev/null +++ b/plugins/plugin_sdbot-webui.cpp @@ -0,0 +1,78 @@ +#include + +#include "../src/sd_bot.hpp" + +#include +#include + +#define RESOLVE_INSTANCE(x) static_cast(solana_api->resolveInstance(#x)) +#define PROVIDE_INSTANCE(x, p, v) solana_api->provideInstance(#x, p, static_cast(v)) + +static std::unique_ptr g_sdbot = nullptr; + +extern "C" { + +SOLANA_PLUGIN_EXPORT const char* solana_plugin_get_name(void) { + return "SDBot-webui"; +} + +SOLANA_PLUGIN_EXPORT uint32_t solana_plugin_get_version(void) { + return SOLANA_PLUGIN_VERSION; +} + +SOLANA_PLUGIN_EXPORT uint32_t solana_plugin_start(struct SolanaAPI* solana_api) { + std::cout << "PLUGIN SDB START()\n"; + + if (solana_api == nullptr) { + return 1; + } + + Contact3Registry* cr; + RegistryMessageModel* rmm = nullptr; + ConfigModelI* conf = nullptr; + + { // make sure required types are loaded + cr = RESOLVE_INSTANCE(Contact3Registry); + rmm = RESOLVE_INSTANCE(RegistryMessageModel); + conf = RESOLVE_INSTANCE(ConfigModelI); + + if (cr == nullptr) { + std::cerr << "PLUGIN SDB missing Contact3Registry\n"; + return 2; + } + + if (rmm == nullptr) { + std::cerr << "PLUGIN SDB missing RegistryMessageModel\n"; + return 2; + } + + if (conf == nullptr) { + std::cerr << "PLUGIN SDB missing ConfigModelI\n"; + return 2; + } + } + + // static store, could be anywhere tho + // construct with fetched dependencies + g_sdbot = std::make_unique(*cr, *rmm, *conf); + + // register types + PROVIDE_INSTANCE(SDBot, "SDBot", g_sdbot.get()); + + return 0; +} + +SOLANA_PLUGIN_EXPORT void solana_plugin_stop(void) { + std::cout << "PLUGIN SDB STOP()\n"; + + g_sdbot.reset(); +} + +SOLANA_PLUGIN_EXPORT void solana_plugin_tick(float delta) { + (void)delta; + //std::cout << "PLUGIN SDB TICK()\n"; + g_sdbot->iterate(); +} + +} // extern C + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..064d569 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.9 FATAL_ERROR) + +add_library(solanaceae_sdbot-webui STATIC + ./sd_bot.hpp + ./sd_bot.cpp +) + +target_compile_features(solanaceae_sdbot-webui PUBLIC cxx_std_17) +target_link_libraries(solanaceae_sdbot-webui PUBLIC + happyhttp + solanaceae_contact + solanaceae_message3 + nlohmann_json::nlohmann_json + libsodium +) + diff --git a/src/sd_bot.cpp b/src/sd_bot.cpp new file mode 100644 index 0000000..d4b4bf1 --- /dev/null +++ b/src/sd_bot.cpp @@ -0,0 +1,224 @@ +#include "./sd_bot.hpp" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +SDBot::SDBot( + Contact3Registry& cr, + RegistryMessageModel& rmm, + ConfigModelI& conf +) : _cr(cr), _rmm(rmm), _conf(conf) { + _rmm.subscribe(this, RegistryMessageModel_Event::message_construct); +} + +SDBot::~SDBot(void) { +} + +void SDBot::iterate(void) { + if (static_cast(_con) && _con->outstanding()) { + _con->pump(); + } else if (!_prompt_queue.empty()) { // dequeue new task + const auto& [task_id, prompt] = _prompt_queue.front(); + + _current_task = task_id; + + // TODO: reuse connection? + // TODO: read from config + _con = std::make_unique("127.0.0.1", 7860); + _con->setcallbacks( + +[](const happyhttp::Response* r, void* ud) { static_cast(ud)->onHttpBegin(r); }, + +[](const happyhttp::Response* r, void* ud, const uint8_t* data, int n) { static_cast(ud)->onHttpData(r, data, n); }, + +[](const happyhttp::Response* r, void* ud) { static_cast(ud)->onHttpComplete(r); }, + this + ); + + static const char* headers [] { + "accept: application/json", + "Content-Type: application/json", + nullptr, + }; + + nlohmann::json j_body; + // TODO: read from config +#if 1 + j_body["width"] = 512; + j_body["height"] = 512; +#elif 0 + j_body["width"] = 768; + j_body["height"] = 768; +#else + j_body["width"] = 128; + j_body["height"] = 128; +#endif + + j_body["prompt"] = prompt; + + j_body["seed"] = -1; + j_body["steps"] = 20; + //j_body["steps"] = 5; + j_body["cfg_scale"] = 6.5; + j_body["sampler_index"] = "Euler a"; + + j_body["batch_size"] = 1; + j_body["n_iter"] = 1; + j_body["restore_faces"] = false; + j_body["tiling"] = false; + j_body["enable_hr"] = false; + + std::string body = j_body.dump(); + + try { + _con->request("POST", "/sdapi/v1/txt2img", headers, reinterpret_cast(body.data()), body.size()); + } catch (const happyhttp::Wobbly& e) { + std::cerr << "SDB http request error: " << e.what() << "\n"; + // cleanup + _task_map.erase(_current_task.value()); + _current_task = std::nullopt; + _con.reset(); + } + + _prompt_queue.pop(); + } +} + +bool SDBot::onEvent(const Message::Events::MessageConstruct& e) { + if (!e.e.all_of()) { + std::cout << "SDB: got message that is not"; + + if (!e.e.all_of()) { + std::cout << " contact_to"; + } + if (!e.e.all_of()) { + std::cout << " contact_from"; + } + if (!e.e.all_of()) { + std::cout << " text"; + } + if (!e.e.all_of()) { + std::cout << " unread"; + } + + std::cout << "\n"; + return false; + } + + if (e.e.any_of()) { + std::cout << "SDB: got message that is"; + if (e.e.all_of()) { + std::cout << " action"; + } + std::cout << "\n"; + return false; + } + + std::string_view message_text = e.e.get().text; + + if (message_text.empty()) { + // empty message? + return false; + } + + const auto contact_to = e.e.get().c; + const auto contact_from = e.e.get().c; + + const bool is_private = _cr.any_of(contact_to); + + if (is_private) { + std::cout << "SDB private message " << message_text << " (l:" << message_text.size() << ")\n"; + { // queue task + const auto id = ++_last_task_counter; + _task_map[id] = contact_from; // reply privately + _prompt_queue.push(std::make_pair(uint64_t{id}, std::string{message_text})); + } + } else { + assert(_cr.all_of(contact_to)); + const auto contact_self = _cr.get(contact_to).self; + if (!_cr.all_of(contact_self)) { + std::cerr << "SDB error: dont have self name\n"; + return false; + } + const auto& self_name = _cr.get(contact_self).name; + + const auto self_prefix = self_name + ": "; + + // check if for us. (starts with : ) + if (message_text.substr(0, self_prefix.size()) == self_prefix) { + std::cout << "SDB public message " << message_text << " (l:" << message_text.size() << ")\n"; + const auto id = ++_last_task_counter; + _task_map[id] = contact_to; // reply publicly + _prompt_queue.push(std::make_pair(uint64_t{id}, std::string{message_text.substr(self_prefix.size())})); + } + } + + // TODO: mark message read? + + return true; +} + +void SDBot::onHttpBegin(const happyhttp::Response* r) { + std::cout << "SDB http begin " << r->getstatus() << " " << r->getreason() << "\n"; + // TODO: handle errors + _con_data.clear(); +} + +void SDBot::onHttpData(const happyhttp::Response* /*r*/, const unsigned char* data, int n) { + //std::cout << "SDB http data\n"; + // TODO: handle errors + for (int i = 0; i < n; i++) { + _con_data.push_back(data[i]); + } +} + +void SDBot::onHttpComplete(const happyhttp::Response* r) { + std::cout << "SDB http complete " << r->getstatus() << " " << r->getreason() << "\n"; + if (r->getstatus() == happyhttp::OK) { + std::cout << "SDB data\n"; + //std::cout << std::string_view{reinterpret_cast(_con_data.data()), _con_data.size()} << "\n"; + + // extract json result + const auto j = nlohmann::json::parse(std::string_view{reinterpret_cast(_con_data.data()), _con_data.size()}); + + if (j.count("images") && !j.at("images").empty() && j.at("images").is_array()) { + for (const auto& i_j : j.at("images").items()) { + // decode data (base64) + std::vector png_data(_con_data.size()); // just init to upper bound + size_t decoded_size {0}; + sodium_base642bin( + png_data.data(), png_data.size(), + i_j.value().get().data(), i_j.value().get().size(), + " \n\t", + &decoded_size, + nullptr, + sodium_base64_VARIANT_ORIGINAL + ); + png_data.resize(decoded_size); + + // hand png to download manager + const auto& contact = _task_map.at(_current_task.value()); + + std::filesystem::create_directories("sdbot_img_send"); + const std::string tmp_img_file_name = "sdbot_img_" + std::to_string(_current_task.value()) + ".png"; + const std::string tmp_img_file_path = "sdbot_img_send/" + tmp_img_file_name; + + std::ofstream(tmp_img_file_path).write(reinterpret_cast(png_data.data()), png_data.size()); + _rmm.sendFilePath(contact, tmp_img_file_name, tmp_img_file_path); + } + } else { + std::cerr << "SDB json response did not contain images?\n"; + } + + _task_map.erase(_current_task.value()); + _current_task = std::nullopt; + _con.reset(); + } +} + diff --git a/src/sd_bot.hpp b/src/sd_bot.hpp new file mode 100644 index 0000000..1f2d1f5 --- /dev/null +++ b/src/sd_bot.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include + +#include + +#include +#include +#include +#include +#include + +// fwd +struct ConfigModelI; + +class SDBot : public RegistryMessageModelEventI { + Contact3Registry& _cr; + RegistryMessageModel& _rmm; + ConfigModelI& _conf; + + //TransferManager& _tm; + + //std::map> _task_map; + std::map _task_map; + std::queue> _prompt_queue; + uint64_t _last_task_counter = 0; + + std::optional _current_task; + std::unique_ptr _con; + std::vector _con_data; + + public: + SDBot( + Contact3Registry& cr, + RegistryMessageModel& rmm, + ConfigModelI& conf + ); + ~SDBot(void); + + void iterate(void); + + public: // conf + bool use_webp_for_friends = true; + bool use_webp_for_groups = true; + + //protected: // tox events + //bool onToxEvent(const Tox_Event_Friend_Message* e) override; + //bool onToxEvent(const Tox_Event_Group_Message* e) override; + protected: // mm + bool onEvent(const Message::Events::MessageConstruct& e) override; + + public: // http cb + void onHttpBegin(const happyhttp::Response* r); + void onHttpData(const happyhttp::Response* r, const unsigned char* data, int n); + void onHttpComplete(const happyhttp::Response* r); +}; +