mirror of
https://github.com/vcmi/vcmi.git
synced 2026-06-19 22:57:37 +02:00
Merge pull request #6748 from Laserlicht/api_lobby
[1.8?] API for lobby
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
# CMake script to embed multiple files as C++ string constants
|
||||
# Usage: cmake -DINPUT_FILES="file1.txt|file2.yaml" -DOUTPUT_FILE=output.h -P embed_file.cmake
|
||||
|
||||
if(NOT DEFINED INPUT_FILES)
|
||||
message(FATAL_ERROR "INPUT_FILES must be defined")
|
||||
endif()
|
||||
|
||||
if(NOT DEFINED OUTPUT_FILE)
|
||||
message(FATAL_ERROR "OUTPUT_FILE must be defined")
|
||||
endif()
|
||||
|
||||
# Convert pipe-separated string to list
|
||||
string(REPLACE "|" ";" INPUT_FILES_LIST "${INPUT_FILES}")
|
||||
|
||||
# Start building the header content
|
||||
set(HEADER_CONTENT "// Auto-generated file - DO NOT EDIT\n")
|
||||
set(HEADER_CONTENT "${HEADER_CONTENT}// Generated from embedded files\n\n")
|
||||
set(HEADER_CONTENT "${HEADER_CONTENT}#pragma once\n\n")
|
||||
set(HEADER_CONTENT "${HEADER_CONTENT}namespace EmbeddedFiles\n{\n")
|
||||
|
||||
# Process each input file
|
||||
foreach(INPUT_FILE ${INPUT_FILES_LIST})
|
||||
if(NOT EXISTS "${INPUT_FILE}")
|
||||
message(FATAL_ERROR "Input file does not exist: ${INPUT_FILE}")
|
||||
endif()
|
||||
|
||||
# Read the input file
|
||||
file(READ "${INPUT_FILE}" FILE_CONTENT)
|
||||
|
||||
# Escape special characters for C++ string literal
|
||||
string(REPLACE "\\" "\\\\" FILE_CONTENT "${FILE_CONTENT}")
|
||||
string(REPLACE "\"" "\\\"" FILE_CONTENT "${FILE_CONTENT}")
|
||||
string(REPLACE "\n" "\\n\"\n\t\"" FILE_CONTENT "${FILE_CONTENT}")
|
||||
|
||||
# Get the base name for the variable
|
||||
get_filename_component(VAR_NAME "${INPUT_FILE}" NAME_WE)
|
||||
string(TOUPPER "${VAR_NAME}" VAR_NAME)
|
||||
string(REPLACE "-" "_" VAR_NAME "${VAR_NAME}")
|
||||
|
||||
# Add to header content
|
||||
set(HEADER_CONTENT "${HEADER_CONTENT}\tconst char* ${VAR_NAME}_CONTENT = \"${FILE_CONTENT}\";\n\n")
|
||||
|
||||
message(STATUS "Embedded ${INPUT_FILE} as ${VAR_NAME}_CONTENT")
|
||||
endforeach()
|
||||
|
||||
# Close the namespace
|
||||
set(HEADER_CONTENT "${HEADER_CONTENT}}\n")
|
||||
|
||||
# Write the output file
|
||||
file(WRITE "${OUTPUT_FILE}" "${HEADER_CONTENT}")
|
||||
|
||||
message(STATUS "Generated ${OUTPUT_FILE}")
|
||||
@@ -30,6 +30,11 @@ std::unique_ptr<INetworkServer> NetworkHandler::createServerTCP(INetworkServerLi
|
||||
return std::make_unique<NetworkServer>(listener, *context);
|
||||
}
|
||||
|
||||
NetworkContext & NetworkHandler::getContext()
|
||||
{
|
||||
return *context;
|
||||
}
|
||||
|
||||
std::shared_ptr<INetworkConnection> NetworkHandler::createAsyncConnection(INetworkConnectionListener & listener)
|
||||
{
|
||||
auto loopbackConnection = std::make_shared<InternalConnection>(listener, *context);
|
||||
|
||||
@@ -30,6 +30,8 @@ public:
|
||||
|
||||
void run() override;
|
||||
void stop() override;
|
||||
|
||||
NetworkContext & getContext() override;
|
||||
};
|
||||
|
||||
VCMI_LIB_NAMESPACE_END
|
||||
|
||||
@@ -184,6 +184,8 @@ public:
|
||||
/// Starts network processing on this thread. Does not returns until networking processing has been terminated
|
||||
virtual void run() = 0;
|
||||
virtual void stop() = 0;
|
||||
|
||||
virtual NetworkContext & getContext() = 0;
|
||||
};
|
||||
|
||||
VCMI_LIB_NAMESPACE_END
|
||||
|
||||
@@ -2,7 +2,9 @@ set(lobby_SRCS
|
||||
StdInc.cpp
|
||||
|
||||
EntryPoint.cpp
|
||||
HttpServer.cpp
|
||||
LobbyDatabase.cpp
|
||||
LobbyHttpApi.cpp
|
||||
LobbyServer.cpp
|
||||
SQLiteConnection.cpp
|
||||
)
|
||||
@@ -10,12 +12,36 @@ set(lobby_SRCS
|
||||
set(lobby_HEADERS
|
||||
StdInc.h
|
||||
|
||||
HttpServer.h
|
||||
LobbyDatabase.h
|
||||
LobbyDefines.h
|
||||
LobbyHttpApi.h
|
||||
LobbyServer.h
|
||||
SQLiteConnection.h
|
||||
)
|
||||
|
||||
# Generate header file from web assets
|
||||
set(WEB_ASSETS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/web/openapi.yaml"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/web/swagger.html"
|
||||
)
|
||||
set(WEB_HEADER_FILE "${CMAKE_CURRENT_BINARY_DIR}/EmbeddedWebAssets.h")
|
||||
|
||||
# Convert list to semicolon-separated string for passing to script
|
||||
string(REPLACE ";" "|" WEB_ASSETS_STRING "${WEB_ASSETS}")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${WEB_HEADER_FILE}"
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-DINPUT_FILES=${WEB_ASSETS_STRING}
|
||||
-DOUTPUT_FILE=${WEB_HEADER_FILE}
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/../cmake_modules/embed_file.cmake
|
||||
DEPENDS ${WEB_ASSETS}
|
||||
COMMENT "Embedding web assets into C++ header"
|
||||
VERBATIM
|
||||
)
|
||||
list(APPEND lobby_HEADERS "${WEB_HEADER_FILE}")
|
||||
|
||||
assign_source_group(${lobby_SRCS} ${lobby_HEADERS})
|
||||
|
||||
add_executable(vcmilobby ${lobby_SRCS} ${lobby_HEADERS})
|
||||
@@ -29,6 +55,7 @@ target_link_libraries(vcmilobby PRIVATE ${lobby_LIBS} ${SQLite3_LIBRARIES})
|
||||
|
||||
target_include_directories(vcmilobby PRIVATE ${SQLite3_INCLUDE_DIRS})
|
||||
target_include_directories(vcmilobby PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_include_directories(vcmilobby PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
if(WIN32)
|
||||
set_target_properties(vcmilobby
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include "StdInc.h"
|
||||
|
||||
#include "LobbyServer.h"
|
||||
#include "HttpServer.h"
|
||||
#include "LobbyHttpApi.h"
|
||||
|
||||
#include "../lib/CConsoleHandler.h"
|
||||
#include "../lib/logging/CBasicLogConfigurator.h"
|
||||
@@ -18,6 +20,8 @@
|
||||
#include "../lib/VCMIDirs.h"
|
||||
|
||||
static const int LISTENING_PORT = 3031;
|
||||
static const int HTTP_API_PORT = 3032;
|
||||
static const bool HTTP_API_LOCALHOST_ONLY = true;
|
||||
|
||||
int main(int argc, const char * argv[])
|
||||
{
|
||||
@@ -45,7 +49,23 @@ int main(int argc, const char * argv[])
|
||||
logGlobal->error("Failed to start server! Another server already uses the same port? Reason: '%s'", e.what());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Start HTTP API Server
|
||||
LobbyHttpApi lobbyApi(*server.getDatabase());
|
||||
HttpServer httpServer(server.getNetworkContext(), lobbyApi, HTTP_API_PORT, HTTP_API_LOCALHOST_ONLY);
|
||||
try
|
||||
{
|
||||
httpServer.start();
|
||||
}
|
||||
catch (const std::exception & e)
|
||||
{
|
||||
logGlobal->error("Failed to start HTTP API server! Reason: '%s'", e.what());
|
||||
return 1;
|
||||
}
|
||||
|
||||
server.run();
|
||||
|
||||
httpServer.stop();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* HttpServer.cpp, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
#include "StdInc.h"
|
||||
#include "HttpServer.h"
|
||||
#include "EmbeddedWebAssets.h"
|
||||
|
||||
#include "../lib/logging/CLogger.h"
|
||||
|
||||
namespace beast = boost::beast;
|
||||
namespace http = beast::http;
|
||||
namespace net = boost::asio;
|
||||
using tcp = net::ip::tcp;
|
||||
|
||||
/// Extracts the value of a URL query parameter by key (e.g. "hours" from "/api?hours=24")
|
||||
static std::string extractQueryParameter(boost::beast::string_view target, const std::string & key)
|
||||
{
|
||||
std::string t(target);
|
||||
auto qpos = t.find('?');
|
||||
if (qpos == std::string::npos)
|
||||
return {};
|
||||
std::string query = t.substr(qpos + 1);
|
||||
const std::string prefix = key + '=';
|
||||
for (std::string::size_type pos = 0; pos < query.size(); )
|
||||
{
|
||||
auto amp = query.find('&', pos);
|
||||
std::string part = query.substr(pos, amp == std::string::npos ? std::string::npos : amp - pos);
|
||||
if (part.substr(0, prefix.size()) == prefix)
|
||||
return part.substr(prefix.size());
|
||||
if (amp == std::string::npos) break;
|
||||
pos = amp + 1;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
HttpServer::HttpServer(boost::asio::io_context & ioc, ILobbyHttpHandler & handler, unsigned short port, bool localhostOnly)
|
||||
: handler(handler)
|
||||
, port(port)
|
||||
, localhostOnly(localhostOnly)
|
||||
, ioc(ioc)
|
||||
{
|
||||
}
|
||||
|
||||
HttpServer::~HttpServer()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
void HttpServer::start()
|
||||
{
|
||||
acceptor = std::make_unique<tcp::acceptor>(ioc);
|
||||
if (localhostOnly)
|
||||
{
|
||||
tcp::endpoint ep{boost::asio::ip::address_v4::loopback(), port};
|
||||
acceptor->open(ep.protocol());
|
||||
acceptor->set_option(tcp::acceptor::reuse_address(true));
|
||||
acceptor->bind(ep);
|
||||
}
|
||||
else
|
||||
{
|
||||
tcp::endpoint ep{tcp::v6(), port};
|
||||
acceptor->open(ep.protocol());
|
||||
acceptor->set_option(tcp::acceptor::reuse_address(true));
|
||||
acceptor->set_option(boost::asio::ip::v6_only(false));
|
||||
acceptor->bind(ep);
|
||||
}
|
||||
acceptor->listen();
|
||||
|
||||
doAccept();
|
||||
logGlobal->info("HTTP API Server started on port %d", port);
|
||||
}
|
||||
|
||||
void HttpServer::stop()
|
||||
{
|
||||
if (acceptor && acceptor->is_open())
|
||||
{
|
||||
acceptor->close();
|
||||
logGlobal->info("HTTP API Server stopped");
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServer::doAccept()
|
||||
{
|
||||
acceptor->async_accept([this](boost::system::error_code ec, tcp::socket socket)
|
||||
{
|
||||
if (!ec)
|
||||
{
|
||||
auto stream = std::make_shared<beast::tcp_stream>(std::move(socket));
|
||||
auto buffer = std::make_shared<beast::flat_buffer>();
|
||||
auto req = std::make_shared<http::request<http::string_body>>();
|
||||
|
||||
http::async_read(*stream, *buffer, *req,
|
||||
[this, stream, buffer, req](boost::system::error_code readEc, std::size_t) mutable
|
||||
{
|
||||
if (readEc)
|
||||
{
|
||||
logGlobal->error("HTTP read error: %s", readEc.message());
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
auto res = std::make_shared<http::response<http::string_body>>(handleRequest(std::move(*req), *stream));
|
||||
http::async_write(*stream, *res,
|
||||
[stream, res](boost::system::error_code, std::size_t)
|
||||
{
|
||||
beast::error_code shutdownEc;
|
||||
stream->socket().shutdown(tcp::socket::shutdown_send, shutdownEc);
|
||||
});
|
||||
}
|
||||
catch (const std::exception & e)
|
||||
{
|
||||
logGlobal->error("HTTP session error: %s", e.what());
|
||||
}
|
||||
});
|
||||
}
|
||||
if (acceptor && acceptor->is_open())
|
||||
doAccept();
|
||||
});
|
||||
}
|
||||
|
||||
HttpServer::Response HttpServer::makeResponse(const Request & req, http::status status, std::string body, const std::string & contentType)
|
||||
{
|
||||
Response res{status, req.version()};
|
||||
res.set(http::field::server, "VCMI-Lobby-API");
|
||||
res.set(http::field::content_type, contentType);
|
||||
res.keep_alive(req.keep_alive());
|
||||
res.body() = std::move(body);
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
}
|
||||
|
||||
HttpServer::Response HttpServer::handleStatsV1(const Request & req)
|
||||
{
|
||||
return makeResponse(req, http::status::ok, handler.getApiStats());
|
||||
}
|
||||
|
||||
HttpServer::Response HttpServer::handleChatsV1(const Request & req)
|
||||
{
|
||||
std::string channelName = "english";
|
||||
if (auto val = extractQueryParameter(req.target(), "channelName"); !val.empty())
|
||||
channelName = val;
|
||||
|
||||
return makeResponse(req, http::status::ok, handler.getApiChats(channelName));
|
||||
}
|
||||
|
||||
HttpServer::Response HttpServer::handleRoomsV1(const Request & req)
|
||||
{
|
||||
int hours = -1;
|
||||
int limit = 50;
|
||||
if (auto val = extractQueryParameter(req.target(), "hours"); !val.empty())
|
||||
{
|
||||
try { hours = std::stoi(val); }
|
||||
catch (const std::invalid_argument &) { return makeResponse(req, http::status::bad_request, R"({"error":"Parameter 'hours' must be an integer"})"); }
|
||||
catch (const std::out_of_range &) { return makeResponse(req, http::status::bad_request, R"({"error":"Parameter 'hours' is out of range"})"); }
|
||||
}
|
||||
if (auto val = extractQueryParameter(req.target(), "limit"); !val.empty())
|
||||
{
|
||||
try
|
||||
{
|
||||
limit = std::stoi(val);
|
||||
if (limit < 1 || limit > 250)
|
||||
return makeResponse(req, http::status::bad_request, R"({"error":"Parameter 'limit' must be between 1 and 250"})");
|
||||
}
|
||||
catch (const std::invalid_argument &) { return makeResponse(req, http::status::bad_request, R"({"error":"Parameter 'limit' must be an integer"})"); }
|
||||
catch (const std::out_of_range &) { return makeResponse(req, http::status::bad_request, R"({"error":"Parameter 'limit' must be between 1 and 250"})"); }
|
||||
}
|
||||
|
||||
return makeResponse(req, http::status::ok, handler.getApiRooms(hours, limit));
|
||||
}
|
||||
|
||||
HttpServer::Response HttpServer::handleDocs(const Request & req)
|
||||
{
|
||||
return makeResponse(req, http::status::ok, EmbeddedFiles::SWAGGER_CONTENT, "text/html");
|
||||
}
|
||||
|
||||
HttpServer::Response HttpServer::handleOpenApiSpec(const Request & req)
|
||||
{
|
||||
return makeResponse(req, http::status::ok, EmbeddedFiles::OPENAPI_CONTENT, "text/yaml");
|
||||
}
|
||||
|
||||
HttpServer::Response HttpServer::handleRequest(Request && req, beast::tcp_stream & stream)
|
||||
{
|
||||
std::string clientIP = "unknown";
|
||||
try
|
||||
{
|
||||
clientIP = stream.socket().remote_endpoint().address().to_string();
|
||||
}
|
||||
catch(const boost::system::system_error & e)
|
||||
{
|
||||
logGlobal->warn("HTTP API: could not get client IP: %s", e.what());
|
||||
}
|
||||
|
||||
std::string userAgent = std::string(req[http::field::user_agent]);
|
||||
if (userAgent.empty())
|
||||
userAgent = "unknown";
|
||||
|
||||
logGlobal->info("HTTP API Request: %s %s from %s (User-Agent: %s)",
|
||||
req.method_string().data(),
|
||||
req.target().data(),
|
||||
clientIP.c_str(),
|
||||
userAgent.c_str());
|
||||
|
||||
try
|
||||
{
|
||||
if (req.target() == "/api/v1/stats") return handleStatsV1(req);
|
||||
if (req.target().starts_with("/api/v1/chats")) return handleChatsV1(req);
|
||||
if (req.target().starts_with("/api/v1/rooms")) return handleRoomsV1(req);
|
||||
if (req.target() == "/api/docs" ||
|
||||
req.target() == "/") return handleDocs(req);
|
||||
if (req.target() == "/api/openapi.yaml") return handleOpenApiSpec(req);
|
||||
|
||||
return makeResponse(req, http::status::not_found,
|
||||
R"({ "error": "Not Found", "message": "The requested endpoint does not exist" })");
|
||||
}
|
||||
catch (const std::exception & e)
|
||||
{
|
||||
logGlobal->error("Error handling HTTP request: %s", e.what());
|
||||
return makeResponse(req, http::status::internal_server_error, R"({"error":"Internal Server Error"})");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* HttpServer.h, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/beast.hpp>
|
||||
|
||||
/// Interface that must be implemented to handle the concrete API endpoints
|
||||
class ILobbyHttpHandler
|
||||
{
|
||||
public:
|
||||
virtual ~ILobbyHttpHandler() = default;
|
||||
|
||||
virtual std::string getApiStats() = 0;
|
||||
virtual std::string getApiChats(const std::string & channelName) = 0;
|
||||
virtual std::string getApiRooms(int hours, int limit) = 0;
|
||||
};
|
||||
|
||||
/// Generic HTTP/REST server built on Boost.Beast.
|
||||
/// Handles the TCP accept loop, request parsing, routing, and response writing.
|
||||
/// Delegates concrete API data to ILobbyHttpHandler.
|
||||
class HttpServer
|
||||
{
|
||||
public:
|
||||
HttpServer(boost::asio::io_context & ioc, ILobbyHttpHandler & handler, unsigned short port, bool localhostOnly);
|
||||
~HttpServer();
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
private:
|
||||
using Request = boost::beast::http::request<boost::beast::http::string_body>;
|
||||
using Response = boost::beast::http::response<boost::beast::http::string_body>;
|
||||
|
||||
void doAccept();
|
||||
Response handleRequest(Request && req, boost::beast::tcp_stream & stream);
|
||||
|
||||
static Response makeResponse(const Request & req, boost::beast::http::status status, std::string body, const std::string & contentType = "application/json");
|
||||
|
||||
Response handleStatsV1(const Request & req);
|
||||
Response handleChatsV1(const Request & req);
|
||||
Response handleRoomsV1(const Request & req);
|
||||
Response handleDocs(const Request & req);
|
||||
Response handleOpenApiSpec(const Request & req);
|
||||
|
||||
ILobbyHttpHandler & handler;
|
||||
unsigned short port;
|
||||
bool localhostOnly;
|
||||
boost::asio::io_context & ioc;
|
||||
std::unique_ptr<boost::asio::ip::tcp::acceptor> acceptor;
|
||||
};
|
||||
@@ -272,6 +272,15 @@ void LobbyDatabase::prepareStatements()
|
||||
ORDER BY secondsElapsed ASC
|
||||
)");
|
||||
|
||||
getRoomsStatement = database->prepare(R"(
|
||||
SELECT roomID, hostAccountID, displayName, description, status, playerLimit, version, mods, strftime('%s',CURRENT_TIMESTAMP)- strftime('%s',gr.creationTime) AS secondsElapsed
|
||||
FROM gameRooms gr
|
||||
LEFT JOIN accounts a ON gr.hostAccountID = a.accountID
|
||||
WHERE (? = -1 OR strftime('%s',CURRENT_TIMESTAMP) - strftime('%s',gr.creationTime) < ? * 3600)
|
||||
ORDER BY gr.creationTime DESC
|
||||
LIMIT ?
|
||||
)");
|
||||
|
||||
getGameRoomInvitesStatement = database->prepare(R"(
|
||||
SELECT a.accountID, a.displayName
|
||||
FROM gameRoomInvites gri
|
||||
@@ -304,6 +313,36 @@ void LobbyDatabase::prepareStatements()
|
||||
WHERE accountID = ?
|
||||
)");
|
||||
|
||||
getActiveAccountsCountsBatchStatement = database->prepare(R"(
|
||||
SELECT
|
||||
SUM(lastLoginTime >= datetime('now', '-1 hours')),
|
||||
SUM(lastLoginTime >= datetime('now', '-24 hours')),
|
||||
SUM(lastLoginTime >= datetime('now', '-168 hours')),
|
||||
SUM(lastLoginTime >= datetime('now', '-720 hours')),
|
||||
SUM(lastLoginTime >= datetime('now', '-8760 hours'))
|
||||
FROM accounts
|
||||
)");
|
||||
|
||||
getRegisteredAccountsCountsBatchStatement = database->prepare(R"(
|
||||
SELECT
|
||||
COUNT(*),
|
||||
SUM(creationTime >= datetime('now', '-24 hours')),
|
||||
SUM(creationTime >= datetime('now', '-168 hours')),
|
||||
SUM(creationTime >= datetime('now', '-720 hours')),
|
||||
SUM(creationTime >= datetime('now', '-8760 hours'))
|
||||
FROM accounts
|
||||
)");
|
||||
|
||||
getClosedGameRoomsCountsBatchStatement = database->prepare(R"(
|
||||
SELECT
|
||||
COUNT(*),
|
||||
SUM(creationTime >= datetime('now', '-24 hours')),
|
||||
SUM(creationTime >= datetime('now', '-168 hours')),
|
||||
SUM(creationTime >= datetime('now', '-720 hours')),
|
||||
SUM(creationTime >= datetime('now', '-8760 hours'))
|
||||
FROM gameRooms WHERE status = 5
|
||||
)");
|
||||
|
||||
isAccountCookieValidStatement = database->prepare(R"(
|
||||
SELECT COUNT(accountID)
|
||||
FROM accountCookies
|
||||
@@ -481,6 +520,48 @@ std::string LobbyDatabase::getAccountDisplayName(const std::string & accountID)
|
||||
return result;
|
||||
}
|
||||
|
||||
LobbyDatabase::ActiveAccountsCounts LobbyDatabase::getActiveAccountsCounts()
|
||||
{
|
||||
ActiveAccountsCounts result{};
|
||||
|
||||
getActiveAccountsCountsBatchStatement->reset();
|
||||
|
||||
if(getActiveAccountsCountsBatchStatement->execute())
|
||||
getActiveAccountsCountsBatchStatement->getColumns(result.h1, result.h24, result.h168, result.h720, result.h8760);
|
||||
|
||||
getActiveAccountsCountsBatchStatement->reset();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
LobbyDatabase::RegisteredAccountsCounts LobbyDatabase::getRegisteredAccountsCounts()
|
||||
{
|
||||
RegisteredAccountsCounts result{};
|
||||
|
||||
getRegisteredAccountsCountsBatchStatement->reset();
|
||||
|
||||
if(getRegisteredAccountsCountsBatchStatement->execute())
|
||||
getRegisteredAccountsCountsBatchStatement->getColumns(result.total, result.h24, result.h168, result.h720, result.h8760);
|
||||
|
||||
getRegisteredAccountsCountsBatchStatement->reset();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
LobbyDatabase::ClosedGameRoomsCounts LobbyDatabase::getClosedGameRoomsCounts()
|
||||
{
|
||||
ClosedGameRoomsCounts result{};
|
||||
|
||||
getClosedGameRoomsCountsBatchStatement->reset();
|
||||
|
||||
if(getClosedGameRoomsCountsBatchStatement->execute())
|
||||
getClosedGameRoomsCountsBatchStatement->getColumns(result.total, result.h24, result.h168, result.h720, result.h8760);
|
||||
|
||||
getClosedGameRoomsCountsBatchStatement->reset();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
LobbyCookieStatus LobbyDatabase::getAccountCookieStatus(const std::string & accountID, const std::string & accessCookieUUID)
|
||||
{
|
||||
bool result = false;
|
||||
@@ -645,6 +726,45 @@ std::vector<LobbyAccount> LobbyDatabase::getActiveAccounts()
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<LobbyGameRoom> LobbyDatabase::getRooms(int hours, int limit)
|
||||
{
|
||||
std::vector<LobbyGameRoom> result;
|
||||
|
||||
getRoomsStatement->reset();
|
||||
getRoomsStatement->setBinds(hours, hours, limit);
|
||||
|
||||
while(getRoomsStatement->execute())
|
||||
{
|
||||
LobbyGameRoom entry;
|
||||
std::string hostAccountDisplayName;
|
||||
int64_t secondsElapsed;
|
||||
getRoomsStatement->getColumns(entry.roomID, entry.hostAccountID, hostAccountDisplayName, entry.description, entry.roomState, entry.playerLimit, entry.version, entry.modsJson, secondsElapsed);
|
||||
entry.age = std::chrono::seconds(secondsElapsed);
|
||||
|
||||
LobbyAccount hostAccount;
|
||||
hostAccount.accountID = entry.hostAccountID;
|
||||
hostAccount.displayName = hostAccountDisplayName;
|
||||
entry.participants.push_back(hostAccount);
|
||||
|
||||
result.push_back(entry);
|
||||
}
|
||||
getRoomsStatement->reset();
|
||||
|
||||
for (auto & room : result)
|
||||
{
|
||||
getGameRoomPlayersStatement->setBinds(room.roomID);
|
||||
while(getGameRoomPlayersStatement->execute())
|
||||
{
|
||||
LobbyAccount account;
|
||||
getGameRoomPlayersStatement->getColumns(account.accountID, account.displayName);
|
||||
room.participants.push_back(account);
|
||||
}
|
||||
getGameRoomPlayersStatement->reset();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string LobbyDatabase::getIdleGameRoom(const std::string & hostAccountID)
|
||||
{
|
||||
std::string result;
|
||||
|
||||
@@ -47,6 +47,10 @@ class LobbyDatabase
|
||||
SQLiteStatementPtr getAccountInviteStatusStatement;
|
||||
SQLiteStatementPtr getAccountGameRoomStatement;
|
||||
SQLiteStatementPtr getAccountDisplayNameStatement;
|
||||
SQLiteStatementPtr getActiveAccountsCountsBatchStatement;
|
||||
SQLiteStatementPtr getRegisteredAccountsCountsBatchStatement;
|
||||
SQLiteStatementPtr getClosedGameRoomsCountsBatchStatement;
|
||||
SQLiteStatementPtr getRoomsStatement;
|
||||
SQLiteStatementPtr getGameRoomPlayersStatement;
|
||||
SQLiteStatementPtr getGameRoomInvitesStatement;
|
||||
SQLiteStatementPtr countRoomUsedSlotsStatement;
|
||||
@@ -89,12 +93,24 @@ public:
|
||||
std::vector<LobbyGameRoom> getAccountGameHistory(const std::string & accountID);
|
||||
std::vector<LobbyGameRoom> getActiveGameRooms();
|
||||
std::vector<LobbyAccount> getActiveAccounts();
|
||||
std::vector<LobbyGameRoom> getRooms(int hours, int limit);
|
||||
std::vector<LobbyChatMessage> getRecentMessageHistory(const std::string & channelType, const std::string & channelName);
|
||||
std::vector<LobbyChatMessage> getFullMessageHistory(const std::string & channelType, const std::string & channelName);
|
||||
|
||||
std::string getIdleGameRoom(const std::string & hostAccountID);
|
||||
std::string getAccountGameRoom(const std::string & accountID);
|
||||
std::string getAccountDisplayName(const std::string & accountID);
|
||||
/// Batch account activity counts: total registered, plus active in last 1h/24h/1w/1m/1y
|
||||
struct ActiveAccountsCounts { int h1, h24, h168, h720, h8760; };
|
||||
ActiveAccountsCounts getActiveAccountsCounts();
|
||||
|
||||
/// Batch registration counts: total, plus registered in last 24h/1w/1m/1y
|
||||
struct RegisteredAccountsCounts { int total, h24, h168, h720, h8760; };
|
||||
RegisteredAccountsCounts getRegisteredAccountsCounts();
|
||||
|
||||
/// Batch closed game room counts: total, plus closed in last 24h/1w/1m/1y
|
||||
struct ClosedGameRoomsCounts { int total, h24, h168, h720, h8760; };
|
||||
ClosedGameRoomsCounts getClosedGameRoomsCounts();
|
||||
|
||||
LobbyCookieStatus getAccountCookieStatus(const std::string & accountID, const std::string & accessCookieUUID);
|
||||
LobbyInviteStatus getAccountInviteStatus(const std::string & accountID, const std::string & roomID);
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* LobbyHttpApi.cpp, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
#include "StdInc.h"
|
||||
#include "LobbyHttpApi.h"
|
||||
#include "LobbyDatabase.h"
|
||||
|
||||
#include "../lib/json/JsonNode.h"
|
||||
#include "../lib/logging/CLogger.h"
|
||||
|
||||
LobbyHttpApi::LobbyHttpApi(LobbyDatabase & database)
|
||||
: database(database)
|
||||
, startTime(std::chrono::system_clock::now())
|
||||
{
|
||||
}
|
||||
|
||||
bool LobbyHttpApi::isCacheValid(const CacheEntry & entry) const
|
||||
{
|
||||
return std::chrono::system_clock::now() - entry.timestamp < std::chrono::seconds(CACHE_TTL_SECONDS);
|
||||
}
|
||||
|
||||
std::string LobbyHttpApi::formatTimestamp(std::chrono::system_clock::time_point timePoint)
|
||||
{
|
||||
auto tt = std::chrono::system_clock::to_time_t(timePoint);
|
||||
std::tm tm{};
|
||||
localtime_r(&tt, &tm);
|
||||
std::ostringstream oss;
|
||||
oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S%z");
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string LobbyHttpApi::getApiStats()
|
||||
{
|
||||
if (statsCache && isCacheValid(*statsCache))
|
||||
return statsCache->json;
|
||||
JsonNode stats;
|
||||
stats["onlinePlayers"].Vector() = JsonVector();
|
||||
for (const auto & player : database.getActiveAccounts())
|
||||
stats["onlinePlayers"].Vector().push_back(JsonNode(player.displayName));
|
||||
auto activeCounts = database.getActiveAccountsCounts();
|
||||
stats["onlinePlayersCount"].Struct() = JsonMap{
|
||||
{"current", JsonNode(static_cast<int64_t>(stats["onlinePlayers"].Vector().size()))},
|
||||
{"lastHour", JsonNode(static_cast<int64_t>(activeCounts.h1))},
|
||||
{"lastDay", JsonNode(static_cast<int64_t>(activeCounts.h24))},
|
||||
{"lastWeek", JsonNode(static_cast<int64_t>(activeCounts.h168))},
|
||||
{"lastMonth", JsonNode(static_cast<int64_t>(activeCounts.h720))},
|
||||
{"lastYear", JsonNode(static_cast<int64_t>(activeCounts.h8760))}
|
||||
};
|
||||
auto registeredCounts = database.getRegisteredAccountsCounts();
|
||||
stats["registeredPlayersCount"].Struct() = JsonMap{
|
||||
{"total", JsonNode(static_cast<int64_t>(registeredCounts.total))},
|
||||
{"lastDay", JsonNode(static_cast<int64_t>(registeredCounts.h24))},
|
||||
{"lastWeek", JsonNode(static_cast<int64_t>(registeredCounts.h168))},
|
||||
{"lastMonth", JsonNode(static_cast<int64_t>(registeredCounts.h720))},
|
||||
{"lastYear", JsonNode(static_cast<int64_t>(registeredCounts.h8760))}
|
||||
};
|
||||
std::map<LobbyRoomState, int> lobbysCount;
|
||||
for (const auto & room : database.getActiveGameRooms())
|
||||
lobbysCount[room.roomState]++;
|
||||
auto closedCounts = database.getClosedGameRoomsCounts();
|
||||
stats["gameCount"].Struct() = JsonMap{
|
||||
{"current", JsonNode(lobbysCount[LobbyRoomState::BUSY])},
|
||||
{"total", JsonNode(static_cast<int64_t>(closedCounts.total))},
|
||||
{"lastDay", JsonNode(static_cast<int64_t>(closedCounts.h24))},
|
||||
{"lastWeek", JsonNode(static_cast<int64_t>(closedCounts.h168))},
|
||||
{"lastMonth", JsonNode(static_cast<int64_t>(closedCounts.h720))},
|
||||
{"lastYear", JsonNode(static_cast<int64_t>(closedCounts.h8760))}
|
||||
};
|
||||
stats["lobbyCount"].Struct() = JsonMap{
|
||||
{"current", JsonNode(static_cast<int64_t>(lobbysCount[LobbyRoomState::PUBLIC] + lobbysCount[LobbyRoomState::PRIVATE]))},
|
||||
{"public", JsonNode(static_cast<int64_t>(lobbysCount[LobbyRoomState::PUBLIC]))},
|
||||
{"private", JsonNode(static_cast<int64_t>(lobbysCount[LobbyRoomState::PRIVATE]))}
|
||||
};
|
||||
stats["lobbyStartTime"].String() = formatTimestamp(startTime);
|
||||
|
||||
stats["server"].String() = "VCMI Lobby";
|
||||
stats["apiVersion"].String() = "1.0";
|
||||
|
||||
std::string json = stats.toCompactString();
|
||||
statsCache = CacheEntry{json, std::chrono::system_clock::now()};
|
||||
return json;
|
||||
}
|
||||
|
||||
std::string LobbyHttpApi::getApiChats(const std::string & channelName)
|
||||
{
|
||||
auto it = chatsCache.find(channelName);
|
||||
if (it != chatsCache.end() && isCacheValid(it->second))
|
||||
return it->second.json;
|
||||
|
||||
JsonNode chats;
|
||||
chats["messages"].Vector() = JsonVector();
|
||||
chats["channelName"].String() = channelName;
|
||||
|
||||
auto messages = database.getRecentMessageHistory("global", channelName);
|
||||
|
||||
for (const auto & msg : messages)
|
||||
{
|
||||
JsonNode message;
|
||||
message["displayName"].String() = msg.displayName;
|
||||
message["messageText"].String() = msg.messageText;
|
||||
message["ageSeconds"].Integer() = msg.age.count();
|
||||
|
||||
auto messageTime = std::chrono::system_clock::now() - msg.age;
|
||||
message["timestamp"].String() = formatTimestamp(messageTime);
|
||||
|
||||
chats["messages"].Vector().push_back(message);
|
||||
}
|
||||
|
||||
chats["count"].Integer() = chats["messages"].Vector().size();
|
||||
|
||||
std::string json = chats.toCompactString();
|
||||
chatsCache[channelName] = CacheEntry{json, std::chrono::system_clock::now()};
|
||||
return json;
|
||||
}
|
||||
|
||||
std::string LobbyHttpApi::serializeRooms(const std::vector<LobbyGameRoom> & rooms, int hours, int limit)
|
||||
{
|
||||
JsonNode result;
|
||||
result["rooms"].Vector() = JsonVector();
|
||||
result["hours"].Integer() = hours;
|
||||
result["limit"].Integer() = limit;
|
||||
|
||||
int count = 0;
|
||||
for (const auto & room : rooms)
|
||||
{
|
||||
if (hours != -1 && room.age > std::chrono::hours(hours))
|
||||
continue;
|
||||
if (count >= limit)
|
||||
break;
|
||||
|
||||
JsonNode roomNode;
|
||||
roomNode["description"].String() = room.description;
|
||||
roomNode["status"].Integer() = static_cast<int>(room.roomState);
|
||||
roomNode["playerLimit"].Integer() = room.playerLimit;
|
||||
roomNode["version"].String() = room.version;
|
||||
roomNode["secondsElapsed"].Integer() = room.age.count();
|
||||
|
||||
auto creationTime = std::chrono::system_clock::now() - room.age;
|
||||
roomNode["createdAt"].String() = formatTimestamp(creationTime);
|
||||
|
||||
// Parse mods JSON string
|
||||
try {
|
||||
JsonNode modsNode(reinterpret_cast<const std::byte*>(room.modsJson.data()), room.modsJson.size(), "");
|
||||
roomNode["mods"] = modsNode;
|
||||
} catch(const std::exception & e) {
|
||||
logGlobal->warn("HTTP API: failed to parse mods JSON: %s", e.what());
|
||||
roomNode["mods"].Struct() = JsonMap{};
|
||||
}
|
||||
|
||||
result["rooms"].Vector().push_back(roomNode);
|
||||
++count;
|
||||
}
|
||||
|
||||
result["count"].Integer() = result["rooms"].Vector().size();
|
||||
return result.toCompactString();
|
||||
}
|
||||
|
||||
std::string LobbyHttpApi::getApiRooms(int hours, int limit)
|
||||
{
|
||||
if (roomsCache)
|
||||
{
|
||||
const auto & c = *roomsCache;
|
||||
const bool ttlOk = std::chrono::system_clock::now() - c.timestamp < std::chrono::seconds(CACHE_TTL_SECONDS);
|
||||
const bool hoursOk = c.fetchedHours == -1 || (hours != -1 && c.fetchedHours >= hours);
|
||||
const bool limitOk = c.fetchedLimit >= limit;
|
||||
if (ttlOk && hoursOk && limitOk)
|
||||
return serializeRooms(c.rooms, hours, limit);
|
||||
}
|
||||
|
||||
auto fetchedRooms = database.getRooms(hours, limit);
|
||||
|
||||
roomsCache = RoomsCacheEntry{fetchedRooms, hours, limit, std::chrono::system_clock::now()};
|
||||
|
||||
return serializeRooms(fetchedRooms, hours, limit);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* LobbyHttpApi.h, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "HttpServer.h"
|
||||
#include "LobbyDefines.h"
|
||||
|
||||
class LobbyDatabase;
|
||||
|
||||
/// Concrete implementation of the VCMI lobby REST API.
|
||||
/// Fetches data from LobbyDatabase, applies caching, and serializes to JSON.
|
||||
class LobbyHttpApi : public ILobbyHttpHandler
|
||||
{
|
||||
public:
|
||||
static constexpr int CACHE_TTL_SECONDS = 30;
|
||||
|
||||
explicit LobbyHttpApi(LobbyDatabase & database);
|
||||
|
||||
std::string getApiStats() override;
|
||||
std::string getApiChats(const std::string & channelName) override;
|
||||
std::string getApiRooms(int hours, int limit) override;
|
||||
|
||||
private:
|
||||
struct CacheEntry
|
||||
{
|
||||
std::string json;
|
||||
std::chrono::system_clock::time_point timestamp;
|
||||
};
|
||||
|
||||
bool isCacheValid(const CacheEntry & entry) const;
|
||||
std::string formatTimestamp(std::chrono::system_clock::time_point timePoint);
|
||||
std::string serializeRooms(const std::vector<LobbyGameRoom> & rooms, int hours, int limit);
|
||||
|
||||
LobbyDatabase & database;
|
||||
std::chrono::system_clock::time_point startTime;
|
||||
|
||||
std::optional<CacheEntry> statsCache;
|
||||
std::map<std::string, CacheEntry> chatsCache;
|
||||
|
||||
struct RoomsCacheEntry
|
||||
{
|
||||
std::vector<LobbyGameRoom> rooms;
|
||||
int fetchedHours; // -1 means all time
|
||||
int fetchedLimit;
|
||||
std::chrono::system_clock::time_point timestamp;
|
||||
};
|
||||
std::optional<RoomsCacheEntry> roomsCache;
|
||||
};
|
||||
@@ -841,6 +841,16 @@ LobbyServer::LobbyServer(const boost::filesystem::path & databasePath)
|
||||
{
|
||||
}
|
||||
|
||||
LobbyDatabase * LobbyServer::getDatabase() const
|
||||
{
|
||||
return database.get();
|
||||
}
|
||||
|
||||
NetworkContext & LobbyServer::getNetworkContext()
|
||||
{
|
||||
return networkHandler->getContext();
|
||||
}
|
||||
|
||||
void LobbyServer::start(uint16_t port)
|
||||
{
|
||||
networkServer->start(port);
|
||||
|
||||
+4
-1
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "../lib/network/NetworkInterface.h"
|
||||
#include "../lib/network/NetworkDefines.h"
|
||||
#include "LobbyDefines.h"
|
||||
|
||||
VCMI_LIB_NAMESPACE_BEGIN
|
||||
@@ -101,4 +101,7 @@ public:
|
||||
|
||||
void start(uint16_t port);
|
||||
void run();
|
||||
|
||||
LobbyDatabase * getDatabase() const;
|
||||
NetworkContext & getNetworkContext();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: VCMI Lobby API
|
||||
description: REST API for VCMI Lobby Server statistics and information
|
||||
version: 1.0.0
|
||||
servers:
|
||||
- url: /api/v1
|
||||
description: API v1
|
||||
paths:
|
||||
/stats:
|
||||
get:
|
||||
summary: Get lobby statistics
|
||||
description: Returns comprehensive statistics about the VCMI lobby including online players, registered players, games, and lobbies
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
onlinePlayers:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of currently online player display names
|
||||
onlinePlayersCount:
|
||||
type: object
|
||||
properties:
|
||||
current:
|
||||
type: integer
|
||||
description: Currently online players
|
||||
lastHour:
|
||||
type: integer
|
||||
description: Active in last hour
|
||||
lastDay:
|
||||
type: integer
|
||||
description: Active in last 24 hours
|
||||
lastWeek:
|
||||
type: integer
|
||||
description: Active in last week
|
||||
lastMonth:
|
||||
type: integer
|
||||
description: Active in last month
|
||||
lastYear:
|
||||
type: integer
|
||||
description: Active in last year
|
||||
registeredPlayersCount:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
description: Total registered players
|
||||
lastDay:
|
||||
type: integer
|
||||
description: Registered in last 24 hours
|
||||
lastWeek:
|
||||
type: integer
|
||||
description: Registered in last week
|
||||
lastMonth:
|
||||
type: integer
|
||||
description: Registered in last month
|
||||
lastYear:
|
||||
type: integer
|
||||
description: Registered in last year
|
||||
gameCount:
|
||||
type: object
|
||||
properties:
|
||||
current:
|
||||
type: integer
|
||||
description: Currently running games
|
||||
total:
|
||||
type: integer
|
||||
description: Total completed games
|
||||
lastDay:
|
||||
type: integer
|
||||
description: Games in last 24 hours
|
||||
lastWeek:
|
||||
type: integer
|
||||
description: Games in last week
|
||||
lastMonth:
|
||||
type: integer
|
||||
description: Games in last month
|
||||
lastYear:
|
||||
type: integer
|
||||
description: Games in last year
|
||||
lobbyCount:
|
||||
type: object
|
||||
properties:
|
||||
current:
|
||||
type: integer
|
||||
description: Currently open lobbies
|
||||
public:
|
||||
type: integer
|
||||
description: Public lobbies
|
||||
private:
|
||||
type: integer
|
||||
description: Private lobbies
|
||||
lobbyStartTime:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Server start timestamp
|
||||
server:
|
||||
type: string
|
||||
description: Server name
|
||||
apiVersion:
|
||||
type: string
|
||||
description: API version
|
||||
/chats:
|
||||
get:
|
||||
summary: Get public chat messages
|
||||
description: Returns recent messages from a public chat channel
|
||||
parameters:
|
||||
- in: query
|
||||
name: channelName
|
||||
schema:
|
||||
type: string
|
||||
default: english
|
||||
required: false
|
||||
description: Name of the chat channel (default is 'english')
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
channelName:
|
||||
type: string
|
||||
description: Name of the requested chat channel
|
||||
messages:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
displayName:
|
||||
type: string
|
||||
description: Display name of the message sender
|
||||
messageText:
|
||||
type: string
|
||||
description: The message content
|
||||
ageSeconds:
|
||||
type: integer
|
||||
description: Message age in seconds
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Message creation timestamp
|
||||
/rooms:
|
||||
get:
|
||||
summary: Get game rooms
|
||||
description: Returns recent game rooms with optional time filter
|
||||
parameters:
|
||||
- in: query
|
||||
name: hours
|
||||
schema:
|
||||
type: integer
|
||||
default: -1
|
||||
required: false
|
||||
description: Filter rooms created in last N hours (-1 for all rooms, default is -1)
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
default: 50
|
||||
minimum: 1
|
||||
maximum: 250
|
||||
required: false
|
||||
description: Maximum number of rooms to return (default 50, max 250)
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
hours:
|
||||
type: integer
|
||||
description: Hours filter applied
|
||||
limit:
|
||||
type: integer
|
||||
description: Limit applied
|
||||
count:
|
||||
type: integer
|
||||
description: Number of rooms returned
|
||||
rooms:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
description: Room description
|
||||
status:
|
||||
type: integer
|
||||
description: Room status (0=preparing, 1=public, 2=private, 3=busy, 4=closed, 5=abandoned)
|
||||
playerLimit:
|
||||
type: integer
|
||||
description: Maximum number of players
|
||||
version:
|
||||
type: string
|
||||
description: Game version
|
||||
secondsElapsed:
|
||||
type: integer
|
||||
description: Seconds since room creation
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Room creation timestamp
|
||||
mods:
|
||||
type: object
|
||||
description: JSON object containing mod configuration
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>VCMI Lobby API - Swagger UI</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
SwaggerUIBundle({
|
||||
url: '/api/openapi.yaml',
|
||||
dom_id: '#swagger-ui',
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
layout: 'StandaloneLayout'
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user