From 72abd7e20ea47c61c8e5d01ec82a06a1b6f7dab4 Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Thu, 30 Jul 2020 20:25:45 +0200 Subject: [PATCH] First draft of the new snapshot system and database changes --- git-teaspeak | 2 +- server/CMakeLists.txt | 2 + server/src/DatabaseHelper.cpp | 283 +++---- server/src/DatabaseHelper.h | 200 ++--- server/src/VirtualServerManager.cpp | 56 +- server/src/VirtualServerManager.h | 24 +- .../ConnectedClientTextCommandHandler.cpp | 4 +- server/src/client/DataClient.cpp | 128 ++-- server/src/client/command_handler/client.cpp | 314 ++++---- server/src/client/command_handler/misc.cpp | 12 +- server/src/client/music/MusicClientPlayer.cpp | 2 +- .../src/client/query/QueryClientCommands.cpp | 35 +- server/src/manager/SqlDataManager.cpp | 180 +++-- server/src/snapshots/deploy.cpp | 701 ++++++++---------- server/src/snapshots/music.cpp | 102 +++ server/src/snapshots/music.h | 70 ++ server/src/snapshots/parser.cpp | 355 +++++++++ server/src/snapshots/snapshot.h | 15 +- server/src/snapshots/snapshot_data.h | 32 + shared | 2 +- 20 files changed, 1558 insertions(+), 961 deletions(-) create mode 100644 server/src/snapshots/music.cpp create mode 100644 server/src/snapshots/music.h create mode 100644 server/src/snapshots/parser.cpp create mode 100644 server/src/snapshots/snapshot_data.h diff --git a/git-teaspeak b/git-teaspeak index d75ecb6..0dc8e28 160000 --- a/git-teaspeak +++ b/git-teaspeak @@ -1 +1 @@ -Subproject commit d75ecb67041c3b9958d75848c253e35774d36578 +Subproject commit 0dc8e28125f9727f2dffef4b6f93b397099db1f0 diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index c392bc0..faab19b 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -139,6 +139,8 @@ set(SERVER_SOURCE_FILES src/snapshots/server.cpp src/snapshots/groups.cpp src/snapshots/deploy.cpp + src/snapshots/music.cpp + src/snapshots/parser.cpp src/manager/ActionLogger.cpp src/manager/ActionLoggerImpl.cpp diff --git a/server/src/DatabaseHelper.cpp b/server/src/DatabaseHelper.cpp index da8c580..efafc8f 100644 --- a/server/src/DatabaseHelper.cpp +++ b/server/src/DatabaseHelper.cpp @@ -48,98 +48,97 @@ void DatabaseHelper::tick() { } } -int collectData(deque>* list, int length, char** values, char** columns){ - shared_ptr entry = std::make_shared(); +constexpr static std::string_view kSqlBase{"SELECT `client_unique_id`, `client_database_id`, `client_nickname`, `client_created`, `client_last_connected`, `client_total_connections` FROM `clients_server`"}; +inline std::deque> query_database_client_info(sql::SqlManager* sql_manager, ServerId server_id, const std::string& query, const std::vector& variables) { + std::deque> result{}; - for(int index = 0; index < length; index++) - if(strcmp(columns[index], "cldbid") == 0) - entry->cldbid = static_cast(stol(values[index])); - else if(strcmp(columns[index], "clientUid") == 0) - entry->uniqueId = values[index]; - else if(strcmp(columns[index], "firstConnect") == 0) - entry->created = time_point() + seconds(stoll(values[index])); - else if(strcmp(columns[index], "lastConnect") == 0) - entry->lastjoin = time_point() + seconds(stoll(values[index])); - else if(strcmp(columns[index], "connections") == 0) - entry->connections = static_cast(stoi(values[index])); - else if(strcmp(columns[index], "lastName") == 0) - entry->lastName = values[index]; - else if(strcmp(columns[index], "serverId") == 0); - else logError(LOG_GENERAL, "Invalid db key for manager data. Key: {}", columns[index]); + sql::command command{sql_manager, query}; + for(const auto& variable : variables) + command.value(variable); + auto sql_result = command.query([&](int length, std::string* values, std::string* names) { + auto entry = std::make_shared(); - list->push_back(entry); - return 0; -} + auto index{0}; + try { + assert(names[index] == "client_unique_id"); + entry->client_unique_id = values[index++]; -#define MAX_QUERY 32 -std::deque> DatabaseHelper::queryDatabaseInfo(const std::shared_ptr& server, const std::deque& list) { - if(list.empty()) return {}; + assert(names[index] == "client_database_id"); + entry->client_database_id = std::stoull(values[index++]); - deque> result; + assert(names[index] == "client_nickname"); + entry->client_nickname = values[index++]; - if(list.size() <= MAX_QUERY){ - std::string query = "SELECT * FROM `clients` WHERE `serverId` = :serverId AND ("; - for(auto elm : list) - query += " `cldbid` = " + to_string(elm) + " OR"; - query = query.substr(0, query.length() - 3) + ")"; - logTrace(server ? server->getServerId() : 0, "[SQL] queryDatabseInfo(...) -> {}", query); - auto state = sql::command(this->sql, query, variable{":serverId", server ? server->getServerId() : 0}).query(std::function(collectData), &result); - auto pf = LOG_SQL_CMD; - pf(state); - if(!state) return {}; - } else { - std::deque sub; - do { - sub.insert(sub.begin(), list.begin(), list.begin() + min(list.size(), (size_t) MAX_QUERY)); - list.erase(list.begin(), list.begin() + min(list.size(), (size_t) MAX_QUERY)); + assert(names[index] == "client_created"); + entry->client_created = std::chrono::system_clock::time_point{} + std::chrono::seconds{std::stoull(values[index++])}; - auto res = this->queryDatabaseInfo(server, sub); - result.insert(result.end(), res.begin(), res.end()); - sub.clear(); - } while(!list.empty()); + assert(names[index] == "client_last_connected"); + entry->client_last_connected = std::chrono::system_clock::time_point{} + std::chrono::seconds{std::stoull(values[index++])}; + + assert(names[index] == "client_total_connections"); + entry->client_total_connections = std::stoull(values[index++]); + + assert(index == length); + } catch (std::exception& ex) { + logError(server_id, "Failed to parse client base properties at index {}: {}. Query: {}", + index - 1, + ex.what(), + query + ); + } + + result.push_back(std::move(entry)); + }); + if(!sql_result) { + logError(server_id, "Failed to query client database infos: {}; Query: {}", sql_result.fmtStr(), query); + return result; } return result; } +std::deque> DatabaseHelper::queryDatabaseInfo(const std::shared_ptr& server, const std::deque& list) { + if(list.empty()) + return {}; + + std::string valueList{}; + for(const auto& element : list) + valueList += ", " + std::to_string(element); + valueList = valueList.substr(2); + + auto serverId = server ? server->getServerId() : 0; + return query_database_client_info(this->sql, serverId, std::string{kSqlBase} + "WHERE `server_id` = :sid AND `client_database_id` IN (" + valueList + ")", {variable{":sid", serverId}}); +} + std::deque> DatabaseHelper::queryDatabaseInfoByUid(const std::shared_ptr &server, std::deque list) { - if(list.empty()) return {}; + if(list.empty()) + return {}; - deque> result; + std::string valueList{}; + for(size_t value_index{0}; value_index < list.size(); value_index++) + valueList += ", :v" + std::to_string(value_index); + valueList = valueList.substr(2); - if(list.size() <= MAX_QUERY){ - std::string query = "SELECT * FROM `clients` WHERE `serverId` = :serverId AND ("; - for(const auto &elm : list) - query += " `clientUid` = '" + elm + "' OR"; - query = query.substr(0, query.length() - 3) + ")"; - logTrace(server ? server->getServerId() : 0, "[SQL] queryDatabseInfoByUid(...) -> {}", query); - auto state = sql::command(this->sql, query, variable{":serverId", server ? server->getServerId() : 0}).query(function(collectData), &result); - auto pf = LOG_SQL_CMD; - pf(state); - if(!state) return {}; - } else { - std::deque sub; - do { - sub.insert(sub.begin(), list.begin(), list.begin() + min(list.size(), (size_t) MAX_QUERY)); - list.erase(list.begin(), list.begin() + min(list.size(), (size_t) MAX_QUERY)); + auto serverId = server ? server->getServerId() : 0; - auto res = this->queryDatabaseInfoByUid(server, sub); - result.insert(result.end(), res.begin(), res.end()); - sub.clear(); - } while(!list.empty()); - } + std::vector values{}; + values.reserve(list.size() + 1); - return result; + values.emplace_back(":sid", serverId); + for(size_t value_index{0}; value_index < list.size(); value_index++) + values.emplace_back(":v" + std::to_string(value_index), list[value_index]); + + return query_database_client_info(this->sql, serverId, std::string{kSqlBase} + "WHERE `server_id` = :sid AND `client_unique_id` IN (" + valueList + ")", values); } bool DatabaseHelper::validClientDatabaseId(const std::shared_ptr& server, ClientDbId cldbid) { return cldbid > 0; } //TODO here check void DatabaseHelper::deleteClient(const std::shared_ptr& server, ClientDbId cldbid) { - ServerId sid = static_cast(server ? server->getServerId() : 0); + auto serverId = (ServerId) (server ? server->getServerId() : 0); { lock_guard lock(permManagerLock); for(auto permMgr : this->cachedPermissionManagers) - if(permMgr->cldbid == cldbid && permMgr->sid == sid) { + if(serverId == 0 || (permMgr->cldbid == cldbid && permMgr->sid == serverId)) { this->cachedPermissionManagers.erase(std::find(this->cachedPermissionManagers.begin(), this->cachedPermissionManagers.end(), permMgr)); delete permMgr; break; @@ -147,12 +146,19 @@ void DatabaseHelper::deleteClient(const std::shared_ptr& server, } //TODO remove from props cache? + sql::result state{}; - auto state = sql::command(this->sql, "DELETE FROM `properties` WHERE `serverId` = :sid AND (`type` = :type1 OR `type` = :type2) AND `id` = :id", variable{":sid", sid}, variable{":type1", property::PROP_TYPE_CONNECTION}, variable{":type2", property::PROP_TYPE_CLIENT}, variable{":id", cldbid}).execute(); - state = sql::command(this->sql, "DELETE FROM `permissions` WHERE `serverId` = :sid AND `type` = :type AND `id` = :id", variable{":sid", sid}, variable{":type", permission::SQL_PERM_USER}, variable{":id", cldbid}).execute(); - state = sql::command(this->sql, "DELETE FROM `clients` WHERE `serverId` = :sid AND `cldbid` = :id", variable{":sid", sid}, variable{":id", cldbid}).execute(); - state = sql::command(this->sql, "DELETE FROM `bannedClients` WHERE `serverId` = :sid AND `invokerDbid` = :id", variable{":sid", sid}, variable{":id", cldbid}).execute(); - state = sql::command(this->sql, "DELETE FROM `assignedGroups` WHERE `serverId` = :sid AND `cldbid` = :id", variable{":sid", sid}, variable{":id", cldbid}).execute(); + state = sql::command(this->sql, "DELETE FROM `properties` WHERE `serverId` = :sid AND (`type` = :type1 OR `type` = :type2) AND `id` = :id", variable{":sid", serverId}, variable{":type1", property::PROP_TYPE_CONNECTION}, variable{":type2", property::PROP_TYPE_CLIENT}, variable{":id", cldbid}).execute(); + state = sql::command(this->sql, "DELETE FROM `permissions` WHERE `serverId` = :sid AND `type` = :type AND `id` = :id", variable{":sid", serverId}, variable{":type", permission::SQL_PERM_USER}, variable{":id", cldbid}).execute(); + state = sql::command(this->sql, "DELETE FROM `bannedClients` WHERE `serverId` = :sid AND `invokerDbid` = :id", variable{":sid", serverId}, variable{":id", cldbid}).execute(); + state = sql::command(this->sql, "DELETE FROM `assignedGroups` WHERE `serverId` = :sid AND `cldbid` = :id", variable{":sid", serverId}, variable{":id", cldbid}).execute(); + + if(serverId == 0) { + state = sql::command(this->sql, "DELETE FROM `clients_server` WHERE `server_id` = :sid AND `client_database_id` = :id", variable{":sid", serverId}, variable{":id", cldbid}).execute(); + } else { + state = sql::command(this->sql, "DELETE FROM `clients_server` WHERE `client_database_id` = :id", variable{":id", cldbid}).execute(); + state = sql::command(this->sql, "DELETE FROM `clients` WHERE `client_database_id` = :id", variable{":id", cldbid}).execute(); + } //TODO delete letters //TODO delete query @@ -560,52 +566,44 @@ std::shared_ptr DatabaseHelper::default_properties_client(std::share return properties; } -std::mutex DatabaseHelper::database_id_mutex{}; - -bool DatabaseHelper::assignDatabaseId(sql::SqlManager *sql, ServerId id, std::shared_ptr cl) { +bool DatabaseHelper::assignDatabaseId(sql::SqlManager *sql, ServerId serverId, std::shared_ptr cl) { cl->loadDataForCurrentServer(); - if(cl->getClientDatabaseId() == 0){ //Client does not exist - ClientDbId new_client_database_id{0}; - auto res = sql::command(sql, "SELECT `cldbid` FROM `clients` WHERE `serverId` = 0 AND `clientUid` = :cluid", variable{":cluid", cl->getUid()}).query([](ClientDbId* ptr, int length, char** values, char** names){ - *ptr = static_cast(stoll(values[0])); - return 0; - }, &new_client_database_id); - auto pf = LOG_SQL_CMD; - pf(res); - if(!res) return false; + if(cl->getClientDatabaseId() == 0) { + /* client does not exists, create a new one */ - auto insertTemplate = sql::model(sql, "INSERT INTO `clients` (`serverId`, `cldbId`, `clientUid`, `lastName`,`firstConnect`,`lastConnect`, `connections`) VALUES (:serverId, :cldbid, :cluid, :name, :fconnect, :lconnect, :connections)", - variable{":cluid", cl->getUid()}, variable{":name", cl->getDisplayName()}, - variable{":fconnect", duration_cast(system_clock::now().time_since_epoch()).count()}, variable{":lconnect", 0}, - variable{":connections", 0}); - if(new_client_database_id == 0) { /* we've a completely new user */ - std::lock_guard db_id_lock{DatabaseHelper::database_id_mutex}; - res = sql::command(sql, "SELECT `cldbid` FROM `clients` WHERE `serverId` = 0 ORDER BY `cldbid` DESC LIMIT 1").query([&](int length, std::string* values, std::string* names) { - assert(length == 1); - new_client_database_id = (ClientDbId) stoll(values[0]); - }); - LOG_SQL_CMD(res); - if(!res) return false; + sql::result sql_result{}; - new_client_database_id += 1; - res = insertTemplate.command().values(variable{":serverId", 0}, variable{":cldbid", new_client_database_id}).execute(); //Insert global - LOG_SQL_CMD(res); - if(!res) return false; - debugMessage(LOG_INSTANCE, "Registered a new client. Unique id: {}, First server: {}, Database ID: {}", cl->getUid(), id, new_client_database_id); - } else { - debugMessage(id, "Having new client, which is already known on this instance. Unique id: {}, First server: {}, Database ID: {}", cl->getUid(), id, new_client_database_id); + std::string insert_or_ignore{sql->getType() == sql::TYPE_SQLITE ? "INSERT OR IGNORE" : "INSERT IGNORE"}; + + auto currentTimeSeconds = duration_cast(system_clock::now().time_since_epoch()).count(); + sql_result = sql::command{sql, insert_or_ignore + " INTO `clients` (`client_unique_id`, `client_created`) VALUES (:uniqueId, :now)", + variable{":uniqueId", cl->getUid()}, + variable{":now", currentTimeSeconds} + }.execute(); + + if(!sql_result) { + logCritical(LOG_INSTANCE, "Failed to execute client insert command for {}: {}", cl->getUid(), sql_result.fmtStr()); + return false; } - if(id != 0){ //Else already inserted - res = insertTemplate.command().values(variable{":serverId", id}, variable{":cldbid", new_client_database_id}).execute(); - pf(res); - if(!res) return false; + sql_result = sql::command{sql, "INSERT INTO `clients_server` (`server_id`, `client_unique_id`, `client_database_id`, `client_created`) SELECT :serverId, :uniqueId, `client_database_id`, :now FROM `clients` WHERE `client_unique_id` = :uniqueId;", + variable{":serverId", serverId}, + variable{":uniqueId", cl->getUid()}, + variable{":now", currentTimeSeconds} + }.execute(); + + if(!sql_result) { + logCritical(LOG_INSTANCE, "Failed to execute client server insert command for {}: {}", cl->getUid(), sql_result.fmtStr()); + return false; } - return assignDatabaseId(sql, id, cl); + if(!cl->loadDataForCurrentServer()) + return false; + + debugMessage(serverId, "Successfully registered client {} for server {} with database id {}.", cl->getUid(), serverId, cl->getClientDatabaseId()); + return true; } - logTrace(id, "Loaded client successfully from database. Database id: {} Unique id: {}", cl->getClientDatabaseId(), cl->getUid()); return true; } @@ -886,6 +884,7 @@ std::shared_ptr DatabaseHelper::loadClientProperties(const std::shar if(server) { props->operator[](property::CLIENT_DESCRIPTION) = server->properties()[property::VIRTUALSERVER_DEFAULT_CLIENT_DESCRIPTION].value(); } + bool loaded = false; if(use_startup_cache && server) { shared_ptr entry; @@ -910,6 +909,7 @@ std::shared_ptr DatabaseHelper::loadClientProperties(const std::shar loaded = true; } } + if(!loaded) { auto command = sql::command(this->sql, "SELECT `key`, `value`, `type` FROM properties WHERE `serverId` = :serverId AND (`type` = :type1 OR `type` = :type2) AND `id` = :id", variable{":serverId", server ? server->getServerId() : 0}, variable{":type1", property::PropertyType::PROP_TYPE_CONNECTION}, variable{":type2", property::PropertyType::PROP_TYPE_CLIENT}, variable{":id", cldbid}); @@ -943,15 +943,15 @@ std::shared_ptr DatabaseHelper::loadClientProperties(const std::shar if(!prop.hasDbReference() && (prop.default_value() == prop.value())) return; //No changes to default value prop.setModified(false); - std::string sql; + std::string sqlCommand; if(prop.hasDbReference()) - sql = "UPDATE `properties` SET `value` = :value WHERE `serverId` = :serverId AND `type` = :type AND `id` = :id AND `key` = :key"; + sqlCommand = "UPDATE `properties` SET `value` = :value WHERE `serverId` = :serverId AND `type` = :type AND `id` = :id AND `key` = :key"; else { prop.setDbReference(true); - sql = "INSERT INTO `properties` (`serverId`, `type`, `id`, `key`, `value`) VALUES (:serverId, :type, :id, :key, :value)"; + sqlCommand = "INSERT INTO `properties` (`serverId`, `type`, `id`, `key`, `value`) VALUES (:serverId, :type, :id, :key, :value)"; } logTrace(server ? server->getServerId() : 0, "[Property] Changed property in db key: " + std::string{prop.type().name} + " value: " + prop.value()); - sql::command(this->sql, sql, + sql::command(this->sql, sqlCommand, variable{":serverId", server ? server->getServerId() : 0}, variable{":type", prop.type().type_property}, variable{":id", cldbid}, @@ -967,16 +967,51 @@ std::shared_ptr DatabaseHelper::loadClientProperties(const std::shar return; } - std::string query; - if(prop.type() == property::CLIENT_TOTALCONNECTIONS) - query = "UPDATE `clients` SET `connections` = :value WHERE `serverId` = :serverId AND `cldbid` = :cldbid"; - else if(prop.type() == property::CLIENT_NICKNAME) - query = "UPDATE `clients` SET `lastName` = :value WHERE `serverId` = :serverId AND `cldbid` = :cldbid"; - else if(prop.type() == property::CLIENT_LASTCONNECTED) - query = "UPDATE `clients` SET `lastConnect` = :value WHERE `serverId` = :serverId AND `cldbid` = :cldbid"; - if(query.empty()) return; - debugMessage(server ? server->getServerId() : 0, "[Property] Changing client property '" + std::string{prop.type().name} + "' for " + to_string(cldbid) + " (New value: " + prop.value() + ", SQL: " + query + ")"); - sql::command(this->sql, query, variable{":serverId", server ? server->getServerId() : 0}, variable{":cldbid", cldbid}, variable{":value", prop.value()}).executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); + std::string column; + switch (prop.type().property_index) { + case property::CLIENT_NICKNAME: + column = "client_nickname"; + break; + + case property::CLIENT_LASTCONNECTED: + column = "client_last_connected"; + break; + + case property::CLIENT_TOTALCONNECTIONS: + column = "client_total_connections"; + break; + + case property::CLIENT_MONTH_BYTES_UPLOADED: + column = "client_month_upload"; + break; + + case property::CLIENT_TOTAL_BYTES_UPLOADED: + column = "client_total_upload"; + break; + + case property::CLIENT_MONTH_BYTES_DOWNLOADED: + column = "client_month_download"; + break; + + case property::CLIENT_TOTAL_BYTES_DOWNLOADED: + column = "client_total_download"; + break; + + default: + return; + } + + debugMessage(server ? server->getServerId() : 0, "[Property] Changing client property '{}' for {} (New value: {], Column: {})", + prop.type().name, + cldbid, + prop.value(), + column + ); + sql::command(this->sql, "UPDATE `clients_server` SET `" + column + "` = :value WHERE `server_id` = :server_id AND `client_database_id` = :cldbid", + variable{":serverId", server ? server->getServerId() : 0}, + variable{":cldbid", cldbid}, + variable{":value", prop.value()} + ).executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); }); return props; diff --git a/server/src/DatabaseHelper.h b/server/src/DatabaseHelper.h index 4eee7de..2da23fd 100644 --- a/server/src/DatabaseHelper.h +++ b/server/src/DatabaseHelper.h @@ -8,126 +8,126 @@ #include #include -namespace ts { - namespace server { - class VirtualServer; - class DataClient; +namespace ts::server { + class VirtualServer; + class DataClient; - struct ClientDatabaseInfo { - ServerId sid; - ClientDbId cldbid; - std::chrono::time_point created; - std::chrono::time_point lastjoin; - std::string uniqueId; - std::string lastName; - uint32_t connections; - }; + struct ClientDatabaseInfo { + ServerId server_id; - struct CachedPermissionManager { - ServerId sid; - ClientDbId cldbid; - std::weak_ptr manager; - std::shared_ptr ownLock; - std::chrono::time_point lastAccess; - }; + ClientDbId client_database_id; + std::string client_unique_id; + std::string client_nickname; - struct CachedProperties { - ServerId sid; - ClientDbId cldbid; - std::weak_ptr properties; - std::shared_ptr ownLock; - std::chrono::time_point lastAccess; - }; + std::chrono::time_point client_created; + std::chrono::time_point client_last_connected; - /* - CREATE_TABLE("properties", "`serverId` INTEGER DEFAULT -1, `type` INTEGER, `id` INTEGER, `key` VARCHAR(" UNKNOWN_KEY_LENGTH "), `value` TEXT", command_append_utf8); - CREATE_TABLE("permissions", "`serverId` INT NOT NULL, `type` INT, `id` INT, `channelId` INT, `permId` VARCHAR(" UNKNOWN_KEY_LENGTH "), `value` INT, `grant` INT", command_append_utf8); - */ - struct StartupPermissionEntry { - permission::PermissionSqlType type = permission::SQL_PERM_CHANNEL; - uint64_t id = 0; - ChannelId channelId = 0; - std::shared_ptr permission = permission::PermissionTypeEntry::unknown; - permission::PermissionValue value = 0; - permission::PermissionValue grant = 0; + uint32_t client_total_connections; + }; - bool flag_skip = false; - bool flag_negate = false; - }; + struct CachedPermissionManager { + ServerId sid; + ClientDbId cldbid; + std::weak_ptr manager; + std::shared_ptr ownLock; + std::chrono::time_point lastAccess; + }; - struct StartupPropertyEntry { - property::PropertyType type = property::PropertyType::PROP_TYPE_UNKNOWN; - uint64_t id{0}; - const property::PropertyDescription* info{&property::undefined_property_description}; - std::string value; - }; + struct CachedProperties { + ServerId sid; + ClientDbId cldbid; + std::weak_ptr properties; + std::shared_ptr ownLock; + std::chrono::time_point lastAccess; + }; - struct StartupCacheEntry { - ServerId sid; + /* + CREATE_TABLE("properties", "`serverId` INTEGER DEFAULT -1, `type` INTEGER, `id` INTEGER, `key` VARCHAR(" UNKNOWN_KEY_LENGTH "), `value` TEXT", command_append_utf8); + CREATE_TABLE("permissions", "`serverId` INT NOT NULL, `type` INT, `id` INT, `channelId` INT, `permId` VARCHAR(" UNKNOWN_KEY_LENGTH "), `value` INT, `grant` INT", command_append_utf8); + */ + struct StartupPermissionEntry { + permission::PermissionSqlType type = permission::SQL_PERM_CHANNEL; + uint64_t id = 0; + ChannelId channelId = 0; + std::shared_ptr permission = permission::PermissionTypeEntry::unknown; + permission::PermissionValue value = 0; + permission::PermissionValue grant = 0; - std::deque> permissions; - std::deque> properties; - }; + bool flag_skip = false; + bool flag_negate = false; + }; - struct FastPropertyEntry { - const property::PropertyDescription* type; - std::string value; - }; + struct StartupPropertyEntry { + property::PropertyType type = property::PropertyType::PROP_TYPE_UNKNOWN; + uint64_t id{0}; + const property::PropertyDescription* info{&property::undefined_property_description}; + std::string value; + }; - class DatabaseHelper { - public: - static std::shared_ptr default_properties_client(std::shared_ptr /* properties */, ClientType /* type */); - static bool assignDatabaseId(sql::SqlManager *, ServerId id, std::shared_ptr); - static std::mutex database_id_mutex; + struct StartupCacheEntry { + ServerId sid; - explicit DatabaseHelper(sql::SqlManager*); - ~DatabaseHelper(); + std::deque> permissions; + std::deque> properties; + }; - void loadStartupCache(); - size_t cacheBinarySize(); - void clearStartupCache(ServerId sid = 0); + struct FastPropertyEntry { + const property::PropertyDescription* type; + std::string value; + }; - void deleteClient(const std::shared_ptr&,ClientDbId); - bool validClientDatabaseId(const std::shared_ptr&, ClientDbId); - std::deque> queryDatabaseInfo(const std::shared_ptr&, const std::deque&); - std::deque> queryDatabaseInfoByUid(const std::shared_ptr &, std::deque); + class DatabaseHelper { + public: + static std::shared_ptr default_properties_client(std::shared_ptr /* properties */, ClientType /* type */); + static bool assignDatabaseId(sql::SqlManager *, ServerId serverId, std::shared_ptr); - std::shared_ptr loadClientPermissionManager(const std::shared_ptr&, ClientDbId); - void saveClientPermissions(const std::shared_ptr&, ClientDbId , const std::shared_ptr& /* permission manager */); + explicit DatabaseHelper(sql::SqlManager*); + ~DatabaseHelper(); - std::shared_ptr loadChannelPermissions(const std::shared_ptr&, ChannelId); - void saveChannelPermissions(const std::shared_ptr&, ChannelId, const std::shared_ptr& /* permission manager */); + void loadStartupCache(); + size_t cacheBinarySize(); + void clearStartupCache(ServerId sid = 0); - std::shared_ptr loadGroupPermissions(const std::shared_ptr&, GroupId); - void saveGroupPermissions(const std::shared_ptr&, GroupId, const std::shared_ptr& /* permission manager */); + void deleteClient(const std::shared_ptr&,ClientDbId); + bool validClientDatabaseId(const std::shared_ptr&, ClientDbId); + std::deque> queryDatabaseInfo(const std::shared_ptr&, const std::deque&); + std::deque> queryDatabaseInfoByUid(const std::shared_ptr &, std::deque); - std::shared_ptr loadPlaylistPermissions(const std::shared_ptr&, PlaylistId /* playlist id */); - void savePlaylistPermissions(const std::shared_ptr&, PlaylistId, const std::shared_ptr& /* permission manager */); + std::shared_ptr loadClientPermissionManager(const std::shared_ptr&, ClientDbId); + void saveClientPermissions(const std::shared_ptr&, ClientDbId , const std::shared_ptr& /* permission manager */); - std::shared_ptr loadServerProperties(const std::shared_ptr&); //Read and write - std::shared_ptr loadPlaylistProperties(const std::shared_ptr&, PlaylistId); //Read and write - std::shared_ptr loadChannelProperties(const std::shared_ptr&, ChannelId); //Read and write - std::shared_ptr loadClientProperties(const std::shared_ptr&, ClientDbId, ClientType); + std::shared_ptr loadChannelPermissions(const std::shared_ptr&, ChannelId); + void saveChannelPermissions(const std::shared_ptr&, ChannelId, const std::shared_ptr& /* permission manager */); - bool deleteGroupPermissions(const std::shared_ptr&, GroupId); - bool deleteChannelPermissions(const std::shared_ptr&, ChannelId); - bool deletePlaylist(const std::shared_ptr&, PlaylistId /* playlist id */); - std::deque> query_properties(ServerId /* server */, property::PropertyType /* type */, uint64_t /* id */); /* required for server snapshots */ + std::shared_ptr loadGroupPermissions(const std::shared_ptr&, GroupId); + void saveGroupPermissions(const std::shared_ptr&, GroupId, const std::shared_ptr& /* permission manager */); - void tick(); - private: - void loadStartupPermissionCache(); - void loadStartupPropertyCache(); + std::shared_ptr loadPlaylistPermissions(const std::shared_ptr&, PlaylistId /* playlist id */); + void savePlaylistPermissions(const std::shared_ptr&, PlaylistId, const std::shared_ptr& /* permission manager */); - bool use_startup_cache = false; - threads::Mutex startup_lock; - std::deque> startup_entries; + std::shared_ptr loadServerProperties(const std::shared_ptr&); //Read and write + std::shared_ptr loadPlaylistProperties(const std::shared_ptr&, PlaylistId); //Read and write + std::shared_ptr loadChannelProperties(const std::shared_ptr&, ChannelId); //Read and write + std::shared_ptr loadClientProperties(const std::shared_ptr&, ClientDbId, ClientType); - sql::SqlManager* sql = nullptr; - threads::Mutex permManagerLock; - std::deque cachedPermissionManagers; - threads::Mutex propsLock; - std::deque cachedProperties; - }; - } + bool deleteGroupPermissions(const std::shared_ptr&, GroupId); + bool deleteChannelPermissions(const std::shared_ptr&, ChannelId); + bool deletePlaylist(const std::shared_ptr&, PlaylistId /* playlist id */); + std::deque> query_properties(ServerId /* server */, property::PropertyType /* type */, uint64_t /* id */); /* required for server snapshots */ + + void tick(); + private: + void loadStartupPermissionCache(); + void loadStartupPropertyCache(); + + bool use_startup_cache = false; + threads::Mutex startup_lock; + std::deque> startup_entries; + + sql::SqlManager* sql = nullptr; + threads::Mutex permManagerLock; + std::deque cachedPermissionManagers; + threads::Mutex propsLock; + std::deque cachedProperties; + }; } \ No newline at end of file diff --git a/server/src/VirtualServerManager.cpp b/server/src/VirtualServerManager.cpp index e436406..83f4d65 100644 --- a/server/src/VirtualServerManager.cpp +++ b/server/src/VirtualServerManager.cpp @@ -256,6 +256,7 @@ uint16_t VirtualServerManager::next_available_port(const std::string& host_strin ts::ServerId VirtualServerManager::next_available_server_id(bool& success) { auto server_id_base = this->handle->properties()[property::SERVERINSTANCE_VIRTUAL_SERVER_ID_INDEX].as(); + /* ensure we're not using 0xFFFF (This is the snapshot server) */ if(server_id_base > 65530) { success = false; return 0; @@ -340,7 +341,7 @@ shared_ptr VirtualServerManager::create_server(std::string hosts, if(!sid_success) return nullptr; - this->delete_server_in_db(serverId); /* just to ensure */ + this->delete_server_in_db(serverId, true); /* just to ensure */ sql::command(this->handle->getSql(), "INSERT INTO `servers` (`serverId`, `host`, `port`) VALUES (:sid, :host, :port)", variable{":sid", serverId}, variable{":host", hosts}, variable{":port", port}).executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); auto prop_copy = sql::command(this->handle->getSql(), "INSERT INTO `properties` (`serverId`, `type`, `id`, `key`, `value`) SELECT :target_sid AS `serverId`, `type`, `id`, `key`, `value` FROM `properties` WHERE `type` = :type AND `id` = 0 AND `serverId` = 0;", @@ -418,7 +419,7 @@ bool VirtualServerManager::deleteServer(shared_ptr server) { } this->handle->properties()[property::SERVERINSTANCE_SPOKEN_TIME_DELETED] += server->properties()[property::VIRTUALSERVER_SPOKEN_TIME].as(); - this->delete_server_in_db(server->serverId); + this->delete_server_in_db(server->serverId, true); if(auto file_vs = file::server()->find_virtual_server(server->getServerId()); file_vs) { using ErrorType = file::filesystem::ServerCommandErrorType; @@ -480,7 +481,7 @@ void VirtualServerManager::tickHandshakeClients() { } } -void VirtualServerManager::delete_server_in_db(ts::ServerId server_id) { +void VirtualServerManager::delete_server_in_db(ts::ServerId server_id, bool data_only) { #define execute_delete(statement) \ result = sql::command(this->handle->getSql(), statement, variable{":sid", server_id}).execute(); \ if(!result) { \ @@ -490,21 +491,60 @@ if(!result) { \ sql::result result{}; + if(!data_only) { + execute_delete("DELETE FROM `servers` WHERE `serverId` = :sid"); + } + execute_delete("DELETE FROM `tokens` WHERE `serverId` = :sid"); execute_delete("DELETE FROM `properties` WHERE `serverId` = :sid"); execute_delete("DELETE FROM `permissions` WHERE `serverId` = :sid"); - execute_delete("DELETE FROM `clients` WHERE `serverId` = :sid"); execute_delete("DELETE FROM `channels` WHERE `serverId` = :sid"); execute_delete("DELETE FROM `bannedClients` WHERE `serverId` = :sid"); - execute_delete("DELETE FROM `ban_trigger` WHERE `server_id` = :sid"); execute_delete("DELETE FROM `groups` WHERE `serverId` = :sid"); execute_delete("DELETE FROM `assignedGroups` WHERE `serverId` = :sid"); - execute_delete("DELETE FROM `servers` WHERE `serverId` = :sid"); + execute_delete("DELETE FROM `complains` WHERE `serverId` = :sid"); + execute_delete("DELETE FROM `letters` WHERE `serverId` = :sid"); execute_delete("DELETE FROM `musicbots` WHERE `serverId` = :sid"); - execute_delete("DELETE FROM `conversations` WHERE `server_id` = :sid"); - execute_delete("DELETE FROM `conversation_blocks` WHERE `server_id` = :sid"); execute_delete("DELETE FROM `playlists` WHERE `serverId` = :sid"); execute_delete("DELETE FROM `playlist_songs` WHERE `serverId` = :sid"); + + execute_delete("DELETE FROM `conversations` WHERE `server_id` = :sid"); + execute_delete("DELETE FROM `conversation_blocks` WHERE `server_id` = :sid"); + execute_delete("DELETE FROM `ban_trigger` WHERE `server_id` = :sid"); + execute_delete("DELETE FROM `clients_server` WHERE `server_id` = :sid"); +} + +#define execute_change(table, column) \ +result = sql::command(this->handle->getSql(), "UPDATE TABLE `" table "` SET `" column "` = :nsid WHERE `" column "` = :osid;", \ + variable{":osid", old_id}, variable{":nsid", new_id}).execute(); \ +if(!result) { \ + logWarning(LOG_INSTANCE, "Failed to execute server id change on table {} (column {}): {}", table, column, result.fmtStr()); \ + result = sql::result{}; \ +} + +void VirtualServerManager::change_server_id_in_db(ts::ServerId old_id, ts::ServerId new_id) { + sql::result result{}; + + + execute_change("tokens", "serverId"); + execute_change("properties", "serverId"); + execute_change("permissions", "serverId"); + execute_change("channels", "serverId"); + execute_change("bannedClients", "serverId"); + execute_change("groups", "serverId"); + execute_change("assignedGroups", "serverId"); + execute_change("complains", "serverId"); + execute_change("letters", "serverId"); + execute_change("musicbots", "serverId"); + execute_change("playlists", "serverId"); + execute_change("playlist_songs", "serverId"); + + execute_change("conversations", "server_id"); + execute_change("conversation_blocks", "server_id"); + execute_change("ban_trigger", "server_id"); + execute_change("clients_server", "server_id"); + + execute_change("servers", "serverId"); } \ No newline at end of file diff --git a/server/src/VirtualServerManager.h b/server/src/VirtualServerManager.h index bf583db..f0e8c93 100644 --- a/server/src/VirtualServerManager.h +++ b/server/src/VirtualServerManager.h @@ -28,6 +28,16 @@ namespace ts::server { STOPPING }; + enum struct SnapshotDeployResult { + SUCCESS, + + REACHED_SOFTWARE_SERVER_LIMIT, + REACHED_CONFIG_SERVER_LIMIT, + REACHED_SERVER_ID_LIMIT, + + CUSTOM_ERROR /* error message set */ + }; + explicit VirtualServerManager(InstanceHandler*); ~VirtualServerManager(); @@ -57,7 +67,9 @@ namespace ts::server { //Dotn use shared_ptr references to keep sure that they be hold in memory bool createServerSnapshot(Command &cmd, std::shared_ptr server, int version, std::string &error); std::shared_ptr createServerFromSnapshot(std::shared_ptr old, std::string, uint16_t, const ts::Command &, std::string &); - bool deploy_snapshot(std::string& /* error */, ServerId /* target server id */, const command_parser& /* source */); + + //Dotn use shared_ptr references to keep sure that they be hold in memory + SnapshotDeployResult deploy_snapshot(std::string& /* error */, std::shared_ptr /* target server */, const command_parser& /* source */); udp::PuzzleManager* rsaPuzzles() { return this->puzzles; } @@ -74,6 +86,7 @@ namespace ts::server { } io::VoiceIOManager* ioManager(){ return this->_ioManager; } + /* This must be recursive */ threads::Mutex server_create_lock; State getState() { return this->state; } @@ -97,12 +110,9 @@ namespace ts::server { void tickHandshakeClients(); - void delete_server_in_db(ServerId /* server id */); + void delete_server_in_db(ServerId /* server id */, bool /* data only */); + void change_server_id_in_db(ServerId /* old id */, ServerId /* new id */); - /* methods used to preprocess a snapshot */ - bool deploy_ts3_snapshot(std::string& /* error */, ServerId /* target server id */, const command_parser& /* source */); - bool deploy_teaspeak_snapshot(std::string& /* error */, ServerId /* target server id */, const command_parser& /* source */); - /* actual deploy method */ - bool deploy_raw_snapshot(std::string& /* error */, ServerId /* target server id */, const command_parser& /* source */, const std::string& /* hash */, size_t /* offset */, snapshots::type /* type */, snapshots::version_t /* version */); + bool try_deploy_snapshot(std::string& /* error */, ServerId /* target server id */, ServerId /* logging server id */, const command_parser& /* source */); }; } \ No newline at end of file diff --git a/server/src/client/ConnectedClientTextCommandHandler.cpp b/server/src/client/ConnectedClientTextCommandHandler.cpp index ff6a2c7..7e9546a 100644 --- a/server/src/client/ConnectedClientTextCommandHandler.cpp +++ b/server/src/client/ConnectedClientTextCommandHandler.cpp @@ -391,8 +391,8 @@ bool ConnectedClient::handle_text_command( for(const auto& song : bot_playlist->list_songs()) { string invoker = "unknown"; for(const auto& e : dbinfo) { - if(e->cldbid == song->invoker) { - invoker = "[URL=client://0/" + e->uniqueId + "~" + e->lastName + "]" + e->lastName + "[/URL]"; + if(e->client_database_id == song->invoker) { + invoker = "[URL=client://0/" + e->client_unique_id + "~" + e->client_nickname + "]" + e->client_nickname + "[/URL]"; break; } } diff --git a/server/src/client/DataClient.cpp b/server/src/client/DataClient.cpp index 59fc330..ab8599e 100644 --- a/server/src/client/DataClient.cpp +++ b/server/src/client/DataClient.cpp @@ -1,11 +1,10 @@ #include -#include #include #include -#include "DataClient.h" -#include "ConnectedClient.h" -#include "src/InstanceHandler.h" -#include "misc/base64.h" +#include + +#include "./DataClient.h" +#include "../InstanceHandler.h" using namespace std; using namespace ts; @@ -22,52 +21,78 @@ DataClient::~DataClient() { this->_properties = nullptr; } -bool DataClient::loadDataForCurrentServer() { //TODO for query - if(this->getUid().empty()) return false; +constexpr static std::string_view kClientLoadCommand{R"( + SELECT + `client_database_id`, + `client_created`, + + `client_last_connected`, + `client_total_connections`, + + `client_month_upload`, + `client_month_download`, + + `client_total_upload`, + `client_total_download` + FROM `clients_server` WHERE `server_id` = :sid AND `client_unique_id` = :uid +)"}; + +bool DataClient::loadDataForCurrentServer() { + auto uniqueId = this->getUid(); + if(uniqueId.empty()) + return false; + auto ref_server = this->server; auto server_id = ref_server ? ref_server->getServerId() : 0; properties()[property::CLIENT_DATABASE_ID] = 0; - properties()[property::CLIENT_CREATED] = 0; - properties()[property::CLIENT_TOTALCONNECTIONS] = 0; + this->clientPermissions = std::make_shared(); + auto properties = this->properties(); + sql::command{this->sql, std::string{kClientLoadCommand}, variable{":uid", uniqueId}, variable{":sid", server_id}}.query([&](int length, std::string* values, std::string* names) { + auto index{0}; - ClientDbId client_db_id = 0; - sql::command(this->sql, "SELECT `cldbid`,`firstConnect`,`connections` FROM `clients` WHERE `serverId` = :sid AND `clientUid` = :uid LIMIT 1", - variable{":sid", server_id}, - variable{":uid", this->getUid()} - ).query([&](DataClient* cl, int length, string* values, string* names){ - for (int index = 0; index < length; index++) { - try { - if (names[index] == "cldbid") { - client_db_id = stoull(values[index]); - } else if (names[index] == "firstConnect") { - cl->properties()[property::CLIENT_CREATED] = values[index]; - } else if (names[index] == "connections") { - cl->properties()[property::CLIENT_TOTALCONNECTIONS] = values[index]; - } else { - logWarning(LOG_INSTANCE, "Received unknown column with name {} within client list", names[index]); - } - } catch(const std::exception& ex) { - logError(server_id, "Failed to load client {} base properties from database. Colum parsing for column {} failed. Value: {}. Message: {}", - this->getUid(), - names[index], - values[index], - ex.what() - ); - return 0; - } + try { + assert(names[index] == "client_database_id"); + properties[property::CLIENT_DATABASE_ID] = std::stoull(values[index++]); + + assert(names[index] == "client_created"); + properties[property::CLIENT_CREATED] = std::stoull(values[index++]); + + assert(names[index] == "client_last_connected"); + properties[property::CLIENT_LASTCONNECTED] = std::stoull(values[index++]); + + assert(names[index] == "client_total_connections"); + properties[property::CLIENT_TOTALCONNECTIONS] = std::stoull(values[index++]); + + assert(names[index] == "client_month_upload"); + properties[property::CLIENT_MONTH_BYTES_UPLOADED] = std::stoull(values[index++]); + + assert(names[index] == "client_month_download"); + properties[property::CLIENT_MONTH_BYTES_DOWNLOADED] = std::stoull(values[index++]); + + assert(names[index] == "client_total_upload"); + properties[property::CLIENT_TOTAL_BYTES_UPLOADED] = std::stoull(values[index++]); + + assert(names[index] == "client_total_download"); + properties[property::CLIENT_TOTAL_BYTES_DOWNLOADED] = std::stoull(values[index++]); + + assert(index == length); + } catch (std::exception& ex) { + logError(server_id, "Failed to load client {} base properties from database: {} (Index {})", + this->getUid(), + ex.what(), + index - 1 + ); + properties[property::CLIENT_DATABASE_ID] = 0; } - return 0; - }, this); + }); - if(client_db_id == 0) + if(this->properties()[property::CLIENT_DATABASE_ID].as() == 0) return false; - this->properties()[property::CLIENT_DATABASE_ID] = client_db_id; /* do this before the property saving (it saved the cldbid as well!)*/ - //Load general properties - deque copied; + std::deque copied; for(const auto& prop : this->_properties->list_properties()){ if((prop.type().flags & property::FLAG_GLOBAL) == 0) continue; if(prop.type().default_value == prop.value()) continue; @@ -80,28 +105,7 @@ bool DataClient::loadDataForCurrentServer() { //TODO for query return false; } - auto client_type = this->getType(); - if(client_type == CLIENT_TEAMSPEAK || ref_server) { - this->_properties = serverInstance->databaseHelper()->loadClientProperties(ref_server, this->getClientDatabaseId(), client_type); - } else { - this->_properties = DatabaseHelper::default_properties_client(nullptr, client_type); - - this->_properties->registerNotifyHandler([&, server_id, client_db_id](Property& prop){ - std::string query; - if(prop.type() == property::CLIENT_TOTALCONNECTIONS) - query = "UPDATE `clients` SET `connections` = :value WHERE `serverId` = :sid AND `cldbid` = :cldbid"; - else if(prop.type() == property::CLIENT_NICKNAME) - query = "UPDATE `clients` SET `lastName` = :value WHERE `serverId` = :sid AND `cldbid` = :cldbid"; - else if(prop.type() == property::CLIENT_LASTCONNECTED) - query = "UPDATE `clients` SET `lastConnect` = :value WHERE `serverId` = :sid AND `cldbid` = :cldbid"; - else - return; - - debugMessage(server_id, "[Property] Updating general client table property for client {}. Key: {} Value: {}", client_db_id, prop.type().name, prop.value()); - sql::command(this->sql, query, variable{":sid", 0}, variable{":cldbid", client_db_id}, variable{":value", prop.value()}).executeLater() - .waitAndGetLater(LOG_SQL_CMD, {1, "failed to update general client properties"}); - }); - } + this->_properties = serverInstance->databaseHelper()->loadClientProperties(ref_server, this->getClientDatabaseId(), this->getType()); this->_properties->toggleSave(false); for(const auto& e : copied) { diff --git a/server/src/client/command_handler/client.cpp b/server/src/client/command_handler/client.cpp index 74c6975..51a6173 100644 --- a/server/src/client/command_handler/client.cpp +++ b/server/src/client/command_handler/client.cpp @@ -367,18 +367,22 @@ command_result ConnectedClient::handleCommandClientChatClosed(Command &cmd) { return command_result{error::ok}; } -//start=0 duration=10 -//pattern=%asd% - -struct ClientDbArgs { - shared_ptr server; - int index = 0; - int offset = 0; - int resultIndex = 0; - bool showIp = false; - bool largeInfo = false; - Command *result = nullptr; -}; +constexpr static auto kDBListQuery{R"( +SELECT `clients`.*, `properties`.`value` as `client_description` FROM ( + SELECT + `clients_server`.`client_database_id`, + `clients_server`.`client_unique_id`, + `clients_server`.`client_nickname`, + `clients_server`.`client_ip`, + `clients_server`.`client_created`, + `clients_server`.`client_last_connected`, + `clients_server`.`client_total_connections`, + `clients`.`client_login_name` FROM `clients_server` + INNER JOIN `clients` ON `clients`.`client_database_id` = `clients_server`.`client_database_id` + WHERE `server_id` = :serverId LIMIT :offset, :limit +) AS `clients` +LEFT JOIN `properties` ON `properties`.serverId = :serverId AND `properties`.key = 'client_description' AND `properties`.`id` = `clients`.`client_database_id` +)"}; command_result ConnectedClient::handleCommandClientDbList(Command &cmd) { CMD_REQ_SERVER; @@ -386,86 +390,79 @@ command_result ConnectedClient::handleCommandClientDbList(Command &cmd) { CMD_CHK_AND_INC_FLOOD_POINTS(25); ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_virtualserver_client_dblist, 1); - Command notify(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifyclientdblist" : ""); + size_t offset = cmd[0].has("start") ? cmd["start"].as() : 0; + size_t limit = cmd[0].has("duration") ? cmd["duration"].as() : 0; + if(limit > 2000 || limit < 1) + limit = 2000; - if (!cmd[0].has("start")) - cmd["start"] = 0; - if (!cmd[0].has("duration")) - cmd["duration"] = 20; - if (cmd[0]["duration"].as() > 2000) cmd["duration"] = 2000; - if (cmd[0]["duration"].as() < 1) cmd["duration"] = 1; + ts::command_builder result{this->notify_response_command("notifyclientdblist")}; + result.reserve_bulks(limit); - auto maxIndex = cmd["start"].as() + cmd["duration"].as(); - ClientDbArgs args; - args.server = this->server; - args.offset = cmd["start"].as(); - args.result = ¬ify; - args.resultIndex = 0; - args.index = 0; - args.showIp = permission::v2::permission_granted(1, this->calculate_permission(permission::b_client_remoteaddress_view, 0)); - args.largeInfo = cmd.hasParm("details"); + size_t command_index{0}, set_index{0}; - (LOG_SQL_CMD)(sql::command(this->server->getSql(), "SELECT * FROM `clients` WHERE `serverId` = :sid ORDER BY `cldbid` ASC" + (maxIndex > 0 ? " LIMIT " + to_string(maxIndex) : ""), variable{":sid", this->server->getServerId()}).query( - [](ClientDbArgs *pArgs, int length, char **values, char **column) { - pArgs->index++; - if (pArgs->offset < pArgs->index) { - ClientDbId id = 0; - string uid, name, ip; - string created = "0", lastConnected = "0", connections = "0"; - for (int index = 0; index < length; index++) { - string key = column[index]; - if (key == "cldbid") - id = stoll(values[index]); - else if (key == "clientUid") - uid = values[index]; - else if (key == "firstConnect") - created = values[index]; - else if (key == "lastConnect") - lastConnected = values[index]; - else if (key == "connections") - connections = values[index]; - else if (key == "lastName") - name = values[index]; - } + auto show_ip = permission::v2::permission_granted(1, this->calculate_permission(permission::b_client_remoteaddress_view, 0)); - pArgs->result->operator[](pArgs->resultIndex)["cldbid"] = id; - pArgs->result->operator[](pArgs->resultIndex)["client_unique_identifier"] = uid; - pArgs->result->operator[](pArgs->resultIndex)["client_nickname"] = name; - pArgs->result->operator[](pArgs->resultIndex)["client_created"] = created; - pArgs->result->operator[](pArgs->resultIndex)["client_lastconnected"] = lastConnected; - pArgs->result->operator[](pArgs->resultIndex)["client_totalconnections"] = connections; - pArgs->result->operator[](pArgs->resultIndex)["client_description"] = ""; + auto sqlResult = sql::command{this->server->getSql(), kDBListQuery, variable{":serverId", this->getServerId()}, variable{":offset", offset}, variable{":limit", limit}} + .query([&](int length, std::string* values, std::string* names) { + set_index++; + auto bulk = result.bulk(command_index++); + bulk.reserve(300); - auto props = serverInstance->databaseHelper()->loadClientProperties(pArgs->server, id, ClientType::CLIENT_TEAMSPEAK); - if (props) { - pArgs->result->operator[](pArgs->resultIndex)["client_lastip"] = (*props)[property::CONNECTION_CLIENT_IP].as(); - pArgs->result->operator[](pArgs->resultIndex)["client_description"] = (*props)[property::CLIENT_DESCRIPTION].as(); + auto index{0}; + try { + assert(names[index] == "client_database_id"); + bulk.put_unchecked("cldbid", values[index++]); - if (pArgs->largeInfo) { - pArgs->result->operator[](pArgs->resultIndex)["client_badges"] = (*props)[property::CLIENT_BADGES].as(); - pArgs->result->operator[](pArgs->resultIndex)["client_version"] = (*props)[property::CLIENT_VERSION].as(); - pArgs->result->operator[](pArgs->resultIndex)["client_platform"] = (*props)[property::CLIENT_PLATFORM].as(); - pArgs->result->operator[](pArgs->resultIndex)["client_hwid"] = (*props)[property::CLIENT_HARDWARE_ID].as(); - } - } - if (!pArgs->showIp) - pArgs->result->operator[](pArgs->resultIndex)["client_lastip"] = "hidden"; - pArgs->resultIndex++; - } - return 0; - }, &args)); + assert(names[index] == "client_unique_id"); + bulk.put_unchecked("client_unique_identifier", values[index++]); + + assert(names[index] == "client_nickname"); + bulk.put_unchecked("client_nickname", values[index++]); + + assert(names[index] == "client_ip"); + bulk.put_unchecked("client_lastip", show_ip ? values[index++] : "hidden"); + + assert(names[index] == "client_created"); + bulk.put_unchecked("client_lastconnected", values[index++]); + + assert(names[index] == "client_last_connected"); + bulk.put_unchecked("client_created", values[index++]); + + assert(names[index] == "client_total_connections"); + bulk.put_unchecked("client_totalconnections", values[index++]); + + assert(names[index] == "client_login_name"); + bulk.put_unchecked("client_login_name", values[index++]); + + assert(names[index] == "client_description"); + bulk.put_unchecked("client_description", values[index++]); + + assert(index == length); + } catch (std::exception& ex) { + command_index--; + logError(this->getServerId(), "Failed to parse client base properties at index {}: {}. Offset: {} Limit: {} Set: {}", + index - 1, + ex.what(), + offset, + limit, + set_index - 1 + ); + } + }); + + if (command_index == 0) + return command_result{error::database_empty_result}; - if (args.resultIndex == 0) return command_result{error::database_empty_result}; if (cmd.hasParm("count")) { - size_t result = 0; - sql::command(this->server->getSql(), "SELECT COUNT(*) AS `count` FROM `clients` WHERE `serverId` = :sid", variable{":sid", this->server->getServerId()}).query([](size_t *ptr, int, char **v, char **) { - *ptr = static_cast(stoll(v[0])); - return 0; - }, &result); - notify[0]["count"] = result; + size_t count{0}; + sql::command(this->server->getSql(), "SELECT COUNT(`client_database_id`) AS `count` FROM `clients_server` WHERE `server_id` = :sid", variable{":sid", this->server->getServerId()}) + .query([&](int, std::string* v, std::string*) { + count = stoll(v[0]); + }); + result.put_unchecked(0, "count", count); } - this->sendCommand(notify); + this->sendCommand(result); return command_result{error::ok}; } @@ -869,8 +866,9 @@ command_result ConnectedClient::handleCommandClientGetDBIDfromUID(Command &cmd) CMD_RESET_IDLE; deque unique_ids; - for(int index = 0; index < cmd.bulkCount(); index++) - unique_ids.push_back(cmd[index]["cluid"].as()); + for(int index = 0; index < cmd.bulkCount(); index++) { + unique_ids.push_back(cmd[index]["cluid"]); + } auto res = serverInstance->databaseHelper()->queryDatabaseInfoByUid(this->server, unique_ids); if (res.empty()) return command_result{error::database_empty_result}; @@ -878,8 +876,8 @@ command_result ConnectedClient::handleCommandClientGetDBIDfromUID(Command &cmd) Command result(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifyclientdbidfromuid" : ""); int result_index = 0; for(auto& info : res) { - result[result_index]["cluid"] = info->uniqueId; - result[result_index]["cldbid"] = info->cldbid; + result[result_index]["cluid"] = info->client_unique_id; + result[result_index]["cldbid"] = info->client_database_id; result_index++; } this->sendCommand(result); @@ -900,10 +898,10 @@ command_result ConnectedClient::handleCommandClientGetNameFromDBID(Command &cmd) Command result(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifyclientgetnamefromdbid" : ""); int result_index = 0; for(auto& info : res) { - result[result_index]["cluid"] = info->uniqueId; - result[result_index]["cldbid"] = info->cldbid; - result[result_index]["name"] = info->lastName; - result[result_index]["clname"] = info->lastName; + result[result_index]["cluid"] = info->client_unique_id; + result[result_index]["cldbid"] = info->client_database_id; + result[result_index]["name"] = info->client_nickname; + result[result_index]["clname"] = info->client_nickname; result_index++; } this->sendCommand(result); @@ -924,10 +922,10 @@ command_result ConnectedClient::handleCommandClientGetNameFromUid(Command &cmd) Command result(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifyclientnamefromuid" : ""); int result_index = 0; for(auto& info : res) { - result[result_index]["cluid"] = info->uniqueId; - result[result_index]["cldbid"] = info->cldbid; - result[result_index]["name"] = info->lastName; - result[result_index]["clname"] = info->lastName; + result[result_index]["cluid"] = info->client_unique_id; + result[result_index]["cldbid"] = info->client_database_id; + result[result_index]["name"] = info->client_nickname; + result[result_index]["clname"] = info->client_nickname; result_index++; } this->sendCommand(result); @@ -1086,16 +1084,16 @@ command_result ConnectedClient::handleCommandClientDbInfo(Command &cmd) { size_t index = 0; for(const auto& info : basic) { - res[index]["client_base64HashClientUID"] = hex::hex(base64::validate(info->uniqueId) ? base64::decode(info->uniqueId) : info->uniqueId, 'a', 'q'); - res[index]["client_unique_identifier"] = info->uniqueId; - res[index]["client_nickname"] = info->lastName; - res[index]["client_database_id"] = info->cldbid; - res[index]["client_created"] = chrono::duration_cast(info->created.time_since_epoch()).count(); - res[index]["client_lastconnected"] = chrono::duration_cast(info->lastjoin.time_since_epoch()).count(); - res[index]["client_totalconnections"] = info->connections; - res[index]["client_database_id"] = info->cldbid; + res[index]["client_base64HashClientUID"] = hex::hex(base64::validate(info->client_unique_id) ? base64::decode(info->client_unique_id) : info->client_unique_id, 'a', 'q'); + res[index]["client_unique_identifier"] = info->client_unique_id; + res[index]["client_nickname"] = info->client_nickname; + res[index]["client_database_id"] = info->client_database_id; + res[index]["client_created"] = chrono::duration_cast(info->client_created.time_since_epoch()).count(); + res[index]["client_lastconnected"] = chrono::duration_cast(info->client_last_connected.time_since_epoch()).count(); + res[index]["client_totalconnections"] = info->client_total_connections; + res[index]["client_database_id"] = info->client_database_id; - auto props = serverInstance->databaseHelper()->loadClientProperties(this->server, info->cldbid, ClientType::CLIENT_TEAMSPEAK); + auto props = serverInstance->databaseHelper()->loadClientProperties(this->server, info->client_database_id, ClientType::CLIENT_TEAMSPEAK); if (allow_ip) res[index]["client_lastip"] = (*props)[property::CONNECTION_CLIENT_IP].as(); else @@ -1136,13 +1134,6 @@ command_result ConnectedClient::handleCommandClientDBDelete(Command &cmd) { return command_result{error::ok}; } -struct DBFindArgs { - int index = 0; - bool full = false; - bool ip = false; - Command cmd{""}; -}; - command_result ConnectedClient::handleCommandClientDBFind(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; @@ -1152,48 +1143,73 @@ command_result ConnectedClient::handleCommandClientDBFind(Command &cmd) { bool uid = cmd.hasParm("uid"); string pattern = cmd["pattern"]; - DBFindArgs args{}; - args.cmd = Command(this->getType() == CLIENT_QUERY ? "" : "notifyclientdbfind"); - args.full = cmd.hasParm("details"); - args.ip = permission::v2::permission_granted(1, this->calculate_permission(permission::b_client_remoteaddress_view, 0)); - auto res = sql::command(this->sql, "SELECT * FROM `clients` WHERE `serverId` = :sid AND `" + std::string{uid ? "clientUid" : "lastName"} + "` LIKE :pattern LIMIT 50", - variable{":sid", this->server->getServerId()}, - variable{":pattern", pattern}).query( - [&](DBFindArgs *ptr, int len, char **values, char **names) { - for (int index = 0; index < len; index++) - if (strcmp(names[index], "cldbid") == 0) - ptr->cmd[ptr->index]["cldbid"] = values[index]; - else if (strcmp(names[index], "clientUid") == 0 && ptr->full) - ptr->cmd[ptr->index]["client_unique_identifier"] = values[index]; - else if (strcmp(names[index], "lastConnect") == 0 && ptr->full) - ptr->cmd[ptr->index]["client_lastconnected"] = values[index]; - else if (strcmp(names[index], "connections") == 0 && ptr->full) - ptr->cmd[ptr->index]["client_totalconnections"] = values[index]; - else if (strcmp(names[index], "lastName") == 0 && ptr->full) - ptr->cmd[ptr->index]["client_nickname"] = values[index]; - if (ptr->full) { - auto props = serverInstance->databaseHelper()->loadClientProperties(this->server, ptr->cmd[ptr->index]["cldbid"], ClientType::CLIENT_TEAMSPEAK); - if (props) { - if (ptr->ip) { - ptr->cmd[ptr->index]["client_lastip"] = (*props)[property::CONNECTION_CLIENT_IP].as(); - } else { - ptr->cmd[ptr->index]["client_lastip"] = "hidden"; - } - ptr->cmd[ptr->index]["client_badges"] = (*props)[property::CLIENT_BADGES].as(); - ptr->cmd[ptr->index]["client_version"] = (*props)[property::CLIENT_VERSION].as(); - ptr->cmd[ptr->index]["client_platform"] = (*props)[property::CLIENT_PLATFORM].as(); - ptr->cmd[ptr->index]["client_hwid"] = (*props)[property::CLIENT_HARDWARE_ID].as(); - } + const auto detailed = cmd.hasParm("details"); + const auto show_ip = permission::v2::permission_granted(1, this->calculate_permission(permission::b_client_remoteaddress_view, 0)); + + size_t command_index{0}; + ts::command_builder result{this->notify_response_command("notifyclientdbfind")}; + result.reserve_bulks(50); + + constexpr static auto kBaseCommand{"SELECT `client_database_id`, `client_unique_id`, `client_nickname`, `client_ip`, `client_last_connected`, `client_total_connections` FROM `clients_server` WHERE "}; + + auto sql_result = sql::command{this->sql, std::string{kBaseCommand} + "`server_id` = :sid AND " + (uid ? "`client_unique_id`" : "`client_nickname`") + " LIKE :pattern LIMIT 50", variable{":sid", this->getServerId()}, variable{":pattern", pattern}} + .query([&](int length, std::string* values, std::string* names) { + auto bulk = result.bulk(command_index++); + bulk.reserve(300); + + auto index{0}; + ClientDbId client_database_id; + try { + assert(names[index] == "client_database_id"); + client_database_id = std::stoull(values[index]); + bulk.put_unchecked("cldbid", values[index++]); + + assert(names[index] == "client_unique_id"); + bulk.put_unchecked("client_unique_identifier", values[index++]); + + assert(names[index] == "client_nickname"); + bulk.put_unchecked("client_nickname", values[index++]); + + assert(names[index] == "client_ip"); + if(detailed) { + bulk.put_unchecked("client_lastip", show_ip ? values[index++] : "hidden"); + } else { + index++; } - ptr->index++; - return 0; - }, &args); - auto pf = LOG_SQL_CMD; - pf(res); - if (args.index == 0) return command_result{error::database_empty_result}; - this->sendCommand(args.cmd); + assert(names[index] == "client_last_connected"); + bulk.put_unchecked("client_lastconnected", values[index++]); + assert(names[index] == "client_total_connections"); + bulk.put_unchecked("client_totalconnections", values[index++]); + + assert(index == length); + } catch (std::exception& ex) { + command_index--; + logError(this->getServerId(), "Failed to parse client base properties at index {}: {}. Search pattern: {}", + index - 1, + ex.what(), + pattern + ); + return; + } + + if(detailed) { + auto props = serverInstance->databaseHelper()->loadClientProperties(this->server, client_database_id, ClientType::CLIENT_TEAMSPEAK); + if (props) { + auto& properties = *props; + bulk.put_unchecked("client_badges", properties[property::CLIENT_BADGES].as()); + bulk.put_unchecked("client_version", properties[property::CLIENT_VERSION].as()); + bulk.put_unchecked("client_platform", properties[property::CLIENT_PLATFORM].as()); + bulk.put_unchecked("client_hwid", properties[property::CLIENT_HARDWARE_ID].as()); + } + } + }); + + if(command_index == 0) + return command_result{error::database_empty_result}; + + this->sendCommand(result); return command_result{error::ok}; } diff --git a/server/src/client/command_handler/misc.cpp b/server/src/client/command_handler/misc.cpp index 0d7f7e4..99b0559 100644 --- a/server/src/client/command_handler/misc.cpp +++ b/server/src/client/command_handler/misc.cpp @@ -714,7 +714,7 @@ command_result ConnectedClient::handleCommandBanAdd(Command &cmd) { return command_result{permission::b_client_ban_create}; } - auto max_ban_time = server->calculate_permission(permission::i_client_ban_max_bantime, this->getClientDatabaseId(), this->getType(), 0); + auto max_ban_time = this->calculate_permission(permission::i_client_ban_max_bantime, this->getClientDatabaseId(), this->getType(), 0); if(!max_ban_time.has_value) return command_result{permission::i_client_ban_max_bantime}; if (!max_ban_time.has_infinite_power()) { if (max_ban_time.value < time) @@ -1884,10 +1884,10 @@ command_result ConnectedClient::handleCommandComplainList(Command &cmd) { result[index]["timestamp"] = chrono::duration_cast(elm->created.time_since_epoch()).count(); for (const auto &e : dbInfo) { - if (e->cldbid == elm->target) - result[index]["tname"] = e->lastName; - if (e->cldbid == elm->invoker) - result[index]["fname"] = e->lastName; + if (e->client_database_id == elm->target) + result[index]["tname"] = e->client_nickname; + if (e->client_database_id == elm->invoker) + result[index]["fname"] = e->client_nickname; } index++; } @@ -2351,7 +2351,7 @@ command_result ConnectedClient::handleCommandQueryCreate(ts::Command &cmd) { auto info = serverInstance->databaseHelper()->queryDatabaseInfo(server, {cmd["cldbid"].as()}); if(info.empty()) return command_result{error::database_empty_result}; - uid = info[0]->uniqueId; + uid = info[0]->client_unique_id; } else { if(server) { if(!permission::v2::permission_granted(1, server->calculate_permission(permission::b_client_query_create_own, this->getClientDatabaseId(), this->getType(), 0))) diff --git a/server/src/client/music/MusicClientPlayer.cpp b/server/src/client/music/MusicClientPlayer.cpp index c0257d6..ad9e47a 100644 --- a/server/src/client/music/MusicClientPlayer.cpp +++ b/server/src/client/music/MusicClientPlayer.cpp @@ -98,7 +98,7 @@ void MusicClient::replay_song(const shared_ptr &entry, cons auto info_list = serverInstance->databaseHelper()->queryDatabaseInfo(self->getServer(), {entry->getInvoker()}); if(!info_list.empty()) { auto info = info_list.front(); - invoker = "[URL=client://0/" + info->uniqueId + "~WolverinDEV]" + info->lastName + "[/URL]"; + invoker = "[URL=client://0/" + info->client_unique_id + "~WolverinDEV]" + info->client_nickname + "[/URL]"; } } diff --git a/server/src/client/query/QueryClientCommands.cpp b/server/src/client/query/QueryClientCommands.cpp index f0b32eb..0807629 100644 --- a/server/src/client/query/QueryClientCommands.cpp +++ b/server/src/client/query/QueryClientCommands.cpp @@ -102,7 +102,7 @@ command_result QueryClient::handleCommand(Command& cmd) { case string_hash("bindinglist"): return this->handleCommandBindingList(cmd); case string_hash("serversnapshotdeploy"): { -#if 1 +#if 0 return this->handleCommandServerSnapshotDeploy(cmd); #else auto cmd_str = cmd.build(); @@ -911,33 +911,30 @@ command_result QueryClient::handleCommandServerSnapshotDeployNew(const ts::comma if(this->server) { return command_result{error::not_implemented}; ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_virtualserver_snapshot_deploy, 1); - //host = this->server->properties()[property::VIRTUALSERVER_HOST].as(); - //port = this->server->properties()[property::VIRTUALSERVER_PORT].as(); } else { ACTION_REQUIRES_INSTANCE_PERMISSION(permission::b_virtualserver_snapshot_deploy, 1); } std::string error{}; - unique_lock server_create_lock(serverInstance->getVoiceServerManager()->server_create_lock); - //TODO: Create a server if no exists + auto result = serverInstance->getVoiceServerManager()->deploy_snapshot(error, this->server, command); - { /* TODO: Only check if we're creating a new server */ - auto instances = serverInstance->getVoiceServerManager()->serverInstances(); - if(config::server::max_virtual_server != -1 && instances.size() > config::server::max_virtual_server) + using SnapshotDeployResult = VirtualServerManager::SnapshotDeployResult; + switch (result) { + case SnapshotDeployResult::SUCCESS: + break; + + case SnapshotDeployResult::REACHED_SERVER_ID_LIMIT: + return command_result{error::server_max_vs_reached, "You cant create anymore virtual servers. (Server ID limit reached)"}; + + case SnapshotDeployResult::REACHED_SOFTWARE_SERVER_LIMIT: + return command_result{error::server_max_vs_reached, "You cant create anymore virtual servers. (Software limit reached)"}; + + case SnapshotDeployResult::REACHED_CONFIG_SERVER_LIMIT: return command_result{error::server_max_vs_reached, "You reached the via config.yml enabled virtual server limit."}; - if(instances.size() >= 65535) - return command_result{error::server_max_vs_reached, "You cant create anymore virtual servers. (Software limit reached)"}; + case SnapshotDeployResult::CUSTOM_ERROR: + return command_result{error::vs_critical, error}; } - - server_create_lock.unlock(); - - //TODO: Stop the server completely - if(!serverInstance->getVoiceServerManager()->deploy_snapshot(error, 111, command)) { - //TODO: Delete server is it was new - return command_result{error::vs_critical, error}; - } - return command_result{error::ok}; } diff --git a/server/src/manager/SqlDataManager.cpp b/server/src/manager/SqlDataManager.cpp index d798d20..fbe5eb8 100644 --- a/server/src/manager/SqlDataManager.cpp +++ b/server/src/manager/SqlDataManager.cpp @@ -44,7 +44,7 @@ if(!result && result.msg().find(ignore) == string::npos){ #define RESIZE_COLUMN(tblName, rowName, size) up vote EXECUTE("Could not change column size", "ALTER TABLE " tblName " ALTER COLUMN " rowName " varchar(" size ")"); -#define CURRENT_DATABASE_VERSION 12 +#define CURRENT_DATABASE_VERSION 14 #define CURRENT_PERMISSION_VERSION 4 #define CLIENT_UID_LENGTH "64" @@ -79,6 +79,25 @@ do { \ } \ } while(0) +template +inline bool execute_commands(sql::SqlManager* sql, std::string& error, const std::array& commands) { + std::string insert_or_ignore{sql->getType() == sql::TYPE_SQLITE ? "INSERT OR IGNORE" : "INSERT IGNORE"}; + for(const auto& cmd : commands) { + std::string command{cmd}; + + if(command.starts_with("[INSERT_OR_IGNORE]")) + command = insert_or_ignore + command.substr(18); + + auto result = sql::command(sql, command).execute(); + if(!result) { + error = result.fmtStr(); + return false; + } + } + + return true; +} + bool SqlDataManager::initialize(std::string& error) { if(ts::config::database::url.find("sqlite://") == 0) this->manager = new sql::sqlite::SqliteManager(); @@ -333,10 +352,8 @@ bool SqlDataManager::update_database(std::string &error) { return false; } - if(manager->getType() == sql::TYPE_SQLITE) { - DROP_INDEX("permissions", "serverId"); - DROP_INDEX2R("permissions", "serverId", "channelId"); - } + DROP_INDEX("permissions", "serverId"); + DROP_INDEX2R("permissions", "serverId", "channelId"); CREATE_INDEX("permissions", "serverId"); CREATE_INDEX2R("permissions", "serverId", "channelId"); @@ -362,10 +379,9 @@ bool SqlDataManager::update_database(std::string &error) { error = "Failed to insert unique properties (" + result.fmtStr() + ")"; return false; } - if(manager->getType() == sql::TYPE_SQLITE) { - DROP_INDEX("properties", "serverId"); - DROP_INDEX2R("properties", "serverId", "id"); - } + + DROP_INDEX("properties", "serverId"); + DROP_INDEX2R("properties", "serverId", "id"); CREATE_INDEX("properties", "serverId"); CREATE_INDEX2R("properties", "serverId", "id"); @@ -412,71 +428,6 @@ bool SqlDataManager::update_database(std::string &error) { CREATE_INDEX2R("conversation_blocks", "server_id", "conversation_id"); db_version(11); -#if 0 /* not yet needed */ - case 11: - /* update the group table */ - { - result = sql::command(this->sql(), "CREATE TABLE `groups_v2` (`serverId` INT NOT NULL, `groupId` INTEGER AUTO_INCREMENT NOT NULL PRIMARY KEY, `target` INT, `type` INT, `displayName` VARCHAR(128));").execute(); - if(!result) { - error = "failed to create new groups table (" + result.fmtStr() + ")"; - return false; - } - result = sql::command(this->sql(), "INSERT INTO `groups_v2`(`serverId`, `groupId`, `target`, `type`, `displayName`) SELECT `serverId`, `groupId`, `target`, `type`, `displayName` FROM `groups`;").execute(); - if(!result) { - sql::command(this->sql(), "DROP TABLE `groups_v2`;").execute(); - error = "failed to insert data into the new groups table (" + result.fmtStr() + ")"; - return false; - } - result = sql::command(this->sql(), "DROP TABLE `groups`;").execute(); - if(!result) { - error = "failed to delete old groups table (" + result.fmtStr() + ")"; - return false; - } - result = sql::command(this->sql(), "ALTER TABLE `groups_v2` RENAME TO groups;").execute(); - if(!result) { - error = "failed to rename new groups table to the old groups table (" + result.fmtStr() + ")"; - return false; - } - - CREATE_INDEX2R("groups", "serverId", "groupId"); - CREATE_INDEX("groups", "serverId"); - result = sql::command(this->sql(), "ALTER TABLE `groups` ADD COLUMN `original_id` INTEGER DEFAULT 0;").execute(); - if(!result) { - error = "Failed to alter groups table"; - return false; - } - } - - /* update the client table */ - { - result = sql::command(this->sql(), "CREATE TABLE `clients_v2` (`serverId` INT NOT NULL, `cldbid` INTEGER, `original_client_id` INTEGER DEFAULT 0, `clientUid` VARCHAR(64) NOT NULL, `firstConnect` BIGINT DEFAULT 0, `lastConnect` BIGINT DEFAULT 0, `connections` INT DEFAULT 0, `lastName` VARCHAR(128) DEFAULT '', UNIQUE(`serverId`, `clientUid`));").execute(); - if(!result) { - error = "failed to create new clients table (" + result.fmtStr() + ")"; - return false; - } - result = sql::command(this->sql(), "INSERT INTO `clients_v2` (`serverId`, `cldbid`, `clientUid`, `firstConnect`, `lastConnect`, `connections`, `lastName`) SELECT `serverId`, `cldbid`, `clientUid`, `firstConnect`, `lastConnect`, `connections`, `lastName` FROM `clients`;").execute(); - if(!result) { - sql::command(this->sql(), "DROP TABLE `groups_v2`;").execute(); - error = "failed to insert data into the new clients table (" + result.fmtStr() + ")"; - return false; - } - result = sql::command(this->sql(), "DROP TABLE `clients`;").execute(); - if(!result) { - error = "failed to delete old clients table (" + result.fmtStr() + ")"; - return false; - } - result = sql::command(this->sql(), "ALTER TABLE `clients_v2` RENAME TO clients;").execute(); - if(!result) { - error = "failed to rename new clients table to the old clients table (" + result.fmtStr() + ")"; - return false; - } - - CREATE_INDEX("clients", "serverId"); - CREATE_INDEX2R("clients", "serverId", "clientUid"); - CREATE_INDEX2R("clients", "serverId", "cldbid"); - } - db_version(12); -#endif case 11: result = sql::command(this->sql(), "UPDATE `letters` SET `created` = 0 WHERE 1;").execute(); if(!result) { @@ -485,6 +436,87 @@ bool SqlDataManager::update_database(std::string &error) { } db_version(12); + + case 12: { + constexpr static std::string_view kCreateClientsV2{R"( + CREATE TABLE `clients_v2` ( + `client_database_id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + `client_unique_id` VARCHAR(40) UNIQUE, + `client_created` BIGINT, + `client_login_name` VARCHAR(20) UNIQUE + ); + )"}; + + constexpr static std::string_view kCreateClientsServer{R"( + CREATE TABLE `clients_server` ( + `server_id` INTEGER, + `client_unique_id` VARCHAR(40) NOT NULL, + `client_database_id` INTEGER NOT NULL, + + `client_nickname` VARCHAR(128), + `client_ip` VARCHAR(64), + + `client_login_password` VARCHAR(40), + + `client_last_connected` BIGINT DEFAULT 0, + `client_created` BIGINT, + `client_total_connections` BIGINT DEFAULT 0, + + `client_month_upload` BIGINT DEFAULT 0, + `client_month_download` BIGINT DEFAULT 0, + `client_total_upload` BIGINT DEFAULT 0, + `client_total_download` BIGINT DEFAULT 0, + + `original_client_id` BIGINT DEFAULT 0, + PRIMARY KEY(`server_id`, `client_database_id`) + ); + )"}; + constexpr static std::string_view kInsertClientsGlobal{"[INSERT_OR_IGNORE] INTO `clients_v2` (`client_database_id`, `client_unique_id`, `client_created`) SELECT `cldbid`, `clientUid`, `firstConnect` FROM `clients` WHERE `serverId` = 0;"}; + constexpr static std::string_view kInsertClientsServer{R"( + [INSERT_OR_IGNORE] INTO `clients_server` (`server_id`, `client_unique_id`, `client_database_id`, `client_nickname`, `client_created`, `client_last_connected`, `client_total_connections`) + SELECT `serverId`, `clientUid`, `cldbid`, `lastName`, `firstConnect`, `lastConnect`, `connections` FROM `clients`; + )"}; + + constexpr static std::array kUpdateCommands{ + kCreateClientsV2, + kCreateClientsServer, + kInsertClientsGlobal, + kInsertClientsServer, + "CREATE INDEX `idx_clients_unique_id` ON `clients_v2` (`client_unique_id`);", + "CREATE INDEX `idx_clients_database_id` ON `clients_v2` (`client_database_id`);", + "CREATE INDEX `idx_clients_login` ON `clients_v2` (`client_login_name`);", + + "CREATE INDEX `idx_clients_server_server_unique` ON `clients_server` (`server_id`, `client_unique_id`);", + "CREATE INDEX `idx_clients_server_server_database` ON `clients_server` (`server_id`, `client_database_id`);", + + "DROP TABLE `clients`;", + "ALTER TABLE `clients_v2` RENAME TO `clients`;", + }; + + if(!execute_commands(this->sql(), error, kUpdateCommands)) + return false; + + db_version(13); + } + + case 13: { + constexpr static std::array kUpdateCommands{ + "CREATE INDEX `idx_properties_serverid_id_value` ON `properties` (`serverId`, `id`, `key`);", + + "CREATE TABLE `groups_v2` (`serverId` INT NOT NULL, `groupId` INTEGER AUTO_INCREMENT NOT NULL PRIMARY KEY, `target` INT, `type` INT, `displayName` VARCHAR(128), `original_group_id` INTEGER);", + "INSERT INTO `groups_v2` (`serverId`, `groupId`, `target`, `type`, `displayName`) SELECT `serverId`, `groupId`, `target`, `type`, `displayName` FROM `groups`;", + "DROP TABLE `groups`;", + "ALTER TABLE `groups_v2` RENAME TO groups;", + + "CREATE INDEX `idx_groups_serverId_groupId` ON `groups` (`server_id`);", + "CREATE INDEX `idx_groups_serverid` ON `groups` (`server_id`, `groupId`);" + }; + + if(!execute_commands(this->sql(), error, kUpdateCommands)) + return false; + + db_version(14); + } default: break; } diff --git a/server/src/snapshots/deploy.cpp b/server/src/snapshots/deploy.cpp index ee8c426..e02b529 100644 --- a/server/src/snapshots/deploy.cpp +++ b/server/src/snapshots/deploy.cpp @@ -7,6 +7,8 @@ #include "./permission.h" #include "./client.h" #include "./groups.h" +#include "./music.h" +#include "./snapshot_data.h" #include "../VirtualServerManager.h" #include "../InstanceHandler.h" #include @@ -17,294 +19,83 @@ using namespace ts::server; using SnapshotType = ts::server::snapshots::type; using SnapshotVersion = ts::server::snapshots::version_t; -//FIXME: Music bots & Playlists +constexpr static ServerId kSnapshotServerId{0xFFFF}; +VirtualServerManager::SnapshotDeployResult VirtualServerManager::deploy_snapshot(std::string &error, std::shared_ptr server, const command_parser &command) { + if(!server) { + auto instances = this->serverInstances(); + if(config::server::max_virtual_server != -1 && instances.size() > config::server::max_virtual_server) + return SnapshotDeployResult::REACHED_CONFIG_SERVER_LIMIT; -bool VirtualServerManager::deploy_snapshot(std::string &error, ServerId server_id, const command_parser &data) { - if(data.bulk(0).has_key("version")) { - return this->deploy_ts3_snapshot(error, server_id, data); - } else if(data.bulk(1).has_key("snapshot_version")) { - /* teaspeak snapshot */ - return this->deploy_teaspeak_snapshot(error, server_id, data); + if(instances.size() >= 65534) + return SnapshotDeployResult::REACHED_SOFTWARE_SERVER_LIMIT; + } + + bool success{true}; + auto target_server_id = server ? server->getServerId() : this->next_available_server_id(success); + if(!success) { + return SnapshotDeployResult::REACHED_SERVER_ID_LIMIT; + } + + this->delete_server_in_db(kSnapshotServerId, true); + auto result = sql::command{this->handle->getSql(), "INSERT INTO `servers` (`serverId`, `host`, `port`) VALUES (:sid, :host, :port)"} + .value(":sid", kSnapshotServerId) + .value(":host", server ? server->properties()[property::VIRTUALSERVER_HOST].value() : config::binding::DefaultVoiceHost) + .value(":port", server ? server->properties()[property::VIRTUALSERVER_PORT].as() : config::voice::default_voice_port) + .execute(); + + if(!result) { + error = "failed to register the server (" + result.fmtStr() + ")"; + return SnapshotDeployResult::CUSTOM_ERROR; + } + + if(!this->try_deploy_snapshot(error, kSnapshotServerId, target_server_id, command)) { + return SnapshotDeployResult::CUSTOM_ERROR; + } + + if(!server) { + this->delete_server_in_db(target_server_id, true); + this->change_server_id_in_db(kSnapshotServerId, target_server_id); } else { - /* old TS3 snapshot format */ - return this->deploy_ts3_snapshot(error, server_id, data); + this->deleteServer(server); + this->change_server_id_in_db(kSnapshotServerId, target_server_id); } + + server = std::make_shared(target_server_id, this->handle->getSql()); + server->self = server; + if(!server->initialize(true)) { + //FIXME error handling + } + + { + threads::MutexLock l(this->instanceLock); + this->instances.push_back(server); + } + this->adjust_executor_threads(); + return SnapshotDeployResult::SUCCESS; } -bool VirtualServerManager::deploy_teaspeak_snapshot(std::string &error, ts::ServerId server_id, const ts::command_parser &data) { - if(!data.bulk(1).has_key("snapshot_version")) { - error = "Missing snapshot version"; - return false; - } - auto version = data.bulk(1).value_as("snapshot_version"); - auto hash = data.bulk(0).value("hash"); - - if(version < 1) { - error = "snapshot version too old"; - return false; - } else if(version > 2) { - error = "snapshot version is too new"; - return false; - } - - /* the actual snapshot begins at index 2 */ - return this->deploy_raw_snapshot(error, server_id, data, hash, 2, SnapshotType::TEASPEAK, version); +#if 0 +bool VirtualServerManager::deploy_snapshot(std::string &error, ts::ServerId server_id, const ts::command_parser &command) { + auto result = this->try_deploy_snapshot(error, server_id, command); + if(!result) this->delete_server_in_db(server_id, true); + return result; } +#endif -bool VirtualServerManager::deploy_ts3_snapshot(std::string &error, ts::ServerId server_id, const ts::command_parser &data) { - snapshots::version_t version{0}; - if(data.bulk(0).has_key("version")) - version = data.bulk(0).value_as("version"); +bool VirtualServerManager::try_deploy_snapshot(std::string &error, ts::ServerId target_server_id, ts::ServerId logging_server_id, const ts::command_parser &command) { + snapshots::snapshot_data snapshot_data{}; - auto hash = data.bulk(0).value("hash"); - if(data.bulk(0).has_key("salt")) { - error = "TeaSpeak dosn't support encrypted snapshots yet"; + if(!snapshots::parse_snapshot(snapshot_data, error, logging_server_id, command)) return false; - } - if(version == 0) { - return this->deploy_raw_snapshot(error, server_id, data, hash, 1, SnapshotType::TEAMSPEAK, version); - } else if(version == 1) { - error = "version 1 is an invalid version"; - return false; - } else if(version == 2) { - /* compressed data */ - error = "version 2 isn't currently supported"; - return false; - } else if(version == 3) { - error = "version 3 isn't currently supported"; - return false; - } else { - error = "snapshots with version 1-3 are currently supported"; - return false; - } -} - -struct parse_client_entry { - snapshots::client_entry parsed_data{}; -}; - -struct parsed_group_entry { - snapshots::group_entry parsed_data{}; -}; - -bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId server_id, const ts::command_parser &command, const std::string& /* hash */, size_t command_offset, - snapshots::type type, snapshots::version_t version) { - snapshots::server_entry parsed_server{}; - //TODO: Verify hash - - /* all snapshots start with the virtual server properties */ - { - snapshots::server_parser parser{type, version, command}; - if(!parser.parse(error, parsed_server, command_offset)) - return false; - } - - std::vector parsed_channels{}; - /* afterwards all channels */ - { - snapshots::channel_parser parser{type, version, command}; - auto data = command.bulk(command_offset); - if(!data.has_key("begin_channels")) { - error = "missing begin channels token at " + std::to_string(data.command_character_index()); - return false; - } - - auto end_bulk = command.next_bulk_containing("end_channels", command_offset); - if(!end_bulk.has_value()) { - error = "missing end channels token"; - return false; - } else if(*end_bulk == command_offset) { - error = "snapshot contains no channels"; - return false; - } - parsed_channels.reserve(*end_bulk - command_offset); - debugMessage(server_id, "Snapshot contains {} channels", *end_bulk - command_offset); - - while(!command.bulk(command_offset).has_key("end_channels")) { - auto& entry = parsed_channels.emplace_back(); - if(!parser.parse(error, entry, command_offset)) - return false; - } - command_offset++; /* the "end_channels" token */ - } - - std::vector parsed_clients{}; - /* after channels all clients */ - { - snapshots::client_parser parser{type, version, command}; - auto data = command.bulk(command_offset); - if(!data.has_key("begin_clients")) { - error = "missing begin clients token at " + std::to_string(data.command_character_index()); - return false; - } - - auto end_bulk = command.next_bulk_containing("end_clients", command_offset); - if(!end_bulk.has_value()) { - error = "missing end clients token"; - return false; - } - parsed_channels.reserve(*end_bulk - command_offset); - debugMessage(server_id, "Snapshot contains {} clients", *end_bulk - command_offset); - - while(!command.bulk(command_offset).has_key("end_clients")) { - auto& entry = parsed_clients.emplace_back(); - if(!parser.parse(error, entry.parsed_data, command_offset)) - return false; - } - command_offset++; /* the "end_clients" token */ - } - - bool server_groups_parsed{false}, - channel_groups_parsed{false}, - client_permissions_parsed{false}, - channel_permissions_parsed{false}, - client_channel_permissions_parsed{false}; - - std::vector parsed_server_groups{}; - snapshots::group_relations parsed_server_group_relations{}; - - std::vector parsed_channel_groups{}; - snapshots::group_relations parsed_channel_group_relations{}; - - std::deque client_permissions{}; - std::deque channel_permissions{}; - std::deque client_channel_permissions{}; - - /* permissions */ - { - if(!command.bulk(command_offset++).has_key("begin_permissions")) { - error = "missing begin permissions key"; - return false; - } - - snapshots::relation_parser relation_parser{type, version, command}; - while(!command.bulk(command_offset).has_key("end_permissions")) { - if(command.bulk(command_offset).has_key("server_groups")) { - if(server_groups_parsed) { - error = "duplicated server group list"; - return false; - } else server_groups_parsed = true; - snapshots::group_parser group_parser{type, version, command, "id", permission::teamspeak::GroupType::SERVER}; - - /* parse all groups */ - while(!command.bulk(command_offset).has_key("end_groups")){ - auto& group = parsed_server_groups.emplace_back(); - if(!group_parser.parse(error, group.parsed_data, command_offset)) /* will consume the end group token */ - return false; - command_offset++; /* for the "end_group" token */ - } - command_offset++; /* for the "end_groups" token */ - - /* parse relations */ - if(!relation_parser.parse(error, parsed_server_group_relations, command_offset)) - return false; - command_offset++; /* for the "end_relations" token */ - - if(parsed_server_group_relations.size() > 1) { - error = "all group relations should be for channel id 0 but received more than one different channel."; - return false; - } else if(!parsed_server_group_relations.empty() && parsed_server_group_relations.begin()->first != 0) { - error = "all group relations should be for channel id 0 but received it for " + std::to_string(parsed_server_group_relations.begin()->first); - return false; - } - } else if(command.bulk(command_offset).has_key("channel_groups")) { - if(channel_groups_parsed) { - error = "duplicated channel group list"; - return false; - } else channel_groups_parsed = true; - snapshots::group_parser group_parser{type, version, command, "id", permission::teamspeak::GroupType::CHANNEL}; - - /* parse all groups */ - while(!command.bulk(command_offset).has_key("end_groups")){ - auto& group = parsed_channel_groups.emplace_back(); - if(!group_parser.parse(error, group.parsed_data, command_offset)) - return false; - command_offset++; /* for the "end_group" token */ - } - command_offset++; /* for the "end_groups" token */ - - /* parse relations */ - if(!relation_parser.parse(error, parsed_channel_group_relations, command_offset)) - return false; - command_offset++; /* for the "end_relations" token */ - } else if(command.bulk(command_offset).has_key("client_flat")) { - /* client permissions */ - if(client_permissions_parsed) { - error = "duplicated client permissions list"; - return false; - } else client_permissions_parsed = true; - snapshots::flat_parser flat_parser{type, version, command, permission::teamspeak::GroupType::CLIENT}; - if(!flat_parser.parse(error, client_permissions, command_offset)) - return false; - command_offset++; /* for the "end_flat" token */ - } else if(command.bulk(command_offset).has_key("channel_flat")) { - /* channel permissions */ - if(channel_permissions_parsed) { - error = "duplicated channel permissions list"; - return false; - } else channel_permissions_parsed = true; - snapshots::flat_parser flat_parser{type, version, command, permission::teamspeak::GroupType::CHANNEL}; - if(!flat_parser.parse(error, channel_permissions, command_offset)) - return false; - - command_offset++; /* for the "end_flat" token */ - } else if(command.bulk(command_offset).has_key("channel_client_flat")) { - /* channel client permissions */ - if(client_channel_permissions_parsed) { - error = "duplicated client channel permissions list"; - return false; - } else client_channel_permissions_parsed = true; - snapshots::flat_parser flat_parser{type, version, command, permission::teamspeak::GroupType::CLIENT}; - if(!flat_parser.parse(error, client_channel_permissions, command_offset)) - return false; - - command_offset++; /* for the "end_flat" token */ - } else { - command_offset++; - } - } - } - - /* check if everything has been parsed */ - { - /* basic stuff */ - if(!server_groups_parsed) { - error = "missing server groups"; - return false; - } - - if(!channel_groups_parsed) { - error = "missing channel groups"; - return false; - } - - if(!client_permissions_parsed) { - error = "missing client permissions"; - return false; - } - - if(!channel_permissions_parsed) { - error = "missing channel permissions"; - return false; - } - - if(!client_channel_permissions_parsed) { - error = "missing client channel permissions"; - return false; - } - } - - std::map client_id_mapping{}; + std::map client_id_mapping{}; std::map channel_id_mapping{}; std::map channel_group_id_mapping{}; std::map server_group_id_mapping{}; + std::map playlist_id_mapping{}; /* lets start inserting data to the database */ { - /* cleanup all old data */ - this->delete_server_in_db(server_id); - /* register server & properties */ { @@ -316,11 +107,11 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId sql::Column("value") }; - for(const auto& property : parsed_server.properties.list_properties(property::FLAG_SAVE)) { + for(const auto& property : snapshot_data.parsed_server.properties.list_properties(property::FLAG_SAVE)) { if(!property.isModified()) continue; insert_property_query.add_entry( - server_id, + target_server_id, property::PROP_TYPE_SERVER, 0, std::string{property.type().name}, @@ -330,118 +121,79 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId { auto result = insert_property_query.execute(this->handle->getSql(), true); - if(result.failed_entries.size() > 0) - logWarning(server_id, "Failed to insert all server properties into the database. Failed property count: {}", result.failed_entries.size()); - } - - //FIXME: Override host & port - auto result = sql::command{this->handle->getSql(), "INSERT INTO `servers` (`serverId`, `host`, `port`) VALUES (:sid, :host, :port)"} - .value(":sid", server_id) - .value(":host", parsed_server.properties[property::VIRTUALSERVER_HOST].value()) - .value(":port", parsed_server.properties[property::VIRTUALSERVER_PORT].value()) - .execute(); - if(!result) { - error = "failed to register the server (" + result.fmtStr() + ")"; - return false; + if(!result.failed_entries.empty()) + logWarning(logging_server_id, "Failed to insert all server properties into the database. Failed property count: {}", result.failed_entries.size()); } } - /* register clients */ + /* register clients and bots */ { sql::InsertQuery insert_general_query{"clients", - sql::Column("serverId"), - sql::Column("cldbid"), - sql::Column("clientUid") + sql::Column("client_unique_id"), + sql::Column("client_created") }; - sql::InsertQuery insert_server_query{"clients", - sql::Column("serverId"), - sql::Column("cldbid"), - sql::Column("original_client_id"), - sql::Column("clientUid"), - sql::Column("firstConnect"), - sql::Column("lastConnect"), - sql::Column("connections"), - sql::Column("lastName") - }; - - { - auto update_result = sql::command(this->handle->getSql(), "UPDATE `clients` SET `original_client_id` = 0 WHERE `original_client_id` = :sid AND `serverId` = 0", variable{":sid", server_id}).execute(); - if(!update_result) - logWarning(server_id, "Failed to reset client id mapping for this server: {}", update_result.fmtStr()); - } - - std::unique_lock db_id_lock{DatabaseHelper::database_id_mutex}; - ClientDbId new_client_database_id{0}; - { - auto query_result = sql::command(this->handle->getSql(), "SELECT `cldbid` FROM `clients` WHERE `serverId` = 0 ORDER BY `cldbid` DESC LIMIT 1").query([&](int length, std::string* values, std::string* names) { - assert(length == 1); - new_client_database_id = (ClientDbId) stoll(values[0]); - }); - if(!query_result) { - error = "failed to query the current client database index (" + query_result.fmtStr() + ")"; - return false; - } - } - - for(const auto& client : parsed_clients) { + auto current_time_seconds = std::chrono::floor(std::chrono::system_clock::now().time_since_epoch()).count(); + for(const auto& client : snapshot_data.parsed_clients) { insert_general_query.add_entry( - 0, - ++new_client_database_id, /* this might, or might not be used as new id */ - client.parsed_data.unique_id + client.unique_id, + std::chrono::floor(client.timestamp_created.time_since_epoch()).count() ); - - /* we're updating the database id afterwards */ - insert_server_query.add_entry( - server_id, - client.parsed_data.database_id, /* to keep stuff unique */ - client.parsed_data.database_id, - client.parsed_data.unique_id, - std::chrono::floor(client.parsed_data.timestamp_created.time_since_epoch()).count(), - std::chrono::floor(client.parsed_data.timestamp_last_connected.time_since_epoch()).count(), - client.parsed_data.client_total_connections, - client.parsed_data.nickname + } + for(const auto& client : snapshot_data.music_bots) { + insert_general_query.add_entry( + client.unique_id, + current_time_seconds ); } - { - auto result = insert_general_query.execute(this->handle->getSql(), true); - for(const auto& fail : result.failed_entries) - logWarning(server_id, "Failed to insert client {} into the general database: {}", parsed_clients[std::get<0>(fail)].parsed_data.database_id, std::get<1>(fail).fmtStr()); - } - db_id_lock.unlock(); + auto result = insert_general_query.execute(this->handle->getSql(), true); + for(const auto& fail : result.failed_entries) + logWarning(logging_server_id, "Failed to insert client {} into the general client database: {} (This should not happen!)", snapshot_data.parsed_clients[std::get<0>(fail)].database_id, std::get<1>(fail).fmtStr()); - { - auto result = insert_server_query.execute(this->handle->getSql(), true); - for(const auto& fail : result.failed_entries) - logWarning(server_id, "Failed to insert client {} into the server database: {}", parsed_clients[std::get<0>(fail)].parsed_data.database_id, std::get<1>(fail).fmtStr()); + + sql::model insert_command{this->handle->getSql(), + "INSERT INTO `clients_server` (`server_id`, `client_unique_id`, `client_database_id`, `client_created`, `original_client_id`) SELECT :serverId, :uniqueId, `client_database_id`, :timestamp, :org_id FROM `clients` WHERE `client_unique_id` = :uniqueId;"}; + + insert_command.value(":serverId", target_server_id); + + for(const auto& client : snapshot_data.parsed_clients) { + auto sqlResult = insert_command.command().values( + variable{":uniqueId", client.unique_id}, + variable{":timestamp", std::chrono::floor(client.timestamp_created.time_since_epoch()).count()}, + variable{":org_id", client.database_id} + ).execute(); + + if(!sqlResult) + logWarning(logging_server_id, "Failed to insert client {} ({}) into the database: {}", client.unique_id, client.nickname, sqlResult.fmtStr()); } - { - auto update_result = sql::command{this->handle->getSql(), "UPDATE `clients` SET `cldbid` = (SELECT `cldbid` from `clients` AS `t` WHERE `t`.`clientUid` = `clients`.`clientUid` LIMIT 1) WHERE `serverId` = :sid;"} - .value(":sid", server_id) - .execute(); - if(!update_result) { - error = "failed to update client database ids (" + update_result.fmtStr() + ")"; - return false; - } + for(const auto& client : snapshot_data.music_bots) { + auto sqlResult = insert_command.command().values( + variable{":uniqueId", client.unique_id}, + variable{":timestamp", current_time_seconds}, + variable{":org_id", client.database_id} + ).execute(); + + if(!sqlResult) + logWarning(logging_server_id, "Failed to insert music bot {} into the database: {}", client.unique_id, sqlResult.fmtStr()); } - auto map_query_result = sql::command{this->handle->getSql(), "SELECT `original_client_id`,`cldbid` FROM `clients` WHERE `serverId` = :sid;"} - .value(":serverId", server_id) + auto map_query_result = sql::command{this->handle->getSql(), "SELECT `original_client_id`,`client_database_id` FROM `clients_server` WHERE `server_id` = :sid;"} + .value(":serverId", target_server_id) .query([&](int length, std::string* values, std::string* names) { ClientDbId original_id{0}, new_id{0}; try { original_id = std::stoull(values[0]); new_id = std::stoull(values[1]); } catch (std::exception& ex) { - logWarning(server_id, "Failed to parse client database entry mapping for group id {} (New ID: {})", values[1], values[0]); + logWarning(logging_server_id, "Failed to parse client database entry mapping for group id {} (New ID: {})", values[1], values[0]); return; } client_id_mapping[original_id] = new_id; }); if(!map_query_result) { - error = "failed to query client dabase id mappings (" + map_query_result.fmtStr() + ")"; + error = "failed to query client database id mappings (" + map_query_result.fmtStr() + ")"; return false; } } @@ -450,14 +202,14 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId { /* Assign each channel a new id */ ChannelId current_id{0}; - for(auto& channel : parsed_channels) { + for(auto& channel : snapshot_data.parsed_channels) { const auto new_id = current_id++; channel_id_mapping[channel.properties[property::CHANNEL_ID]] = new_id; channel.properties[property::CHANNEL_ID] = new_id; } /* Update channel parents */ - for(auto& channel : parsed_channels) { + for(auto& channel : snapshot_data.parsed_channels) { auto pid = channel.properties[property::CHANNEL_PID].as(); if(pid > 0) { auto new_id = channel_id_mapping.find(pid); @@ -484,10 +236,10 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId sql::Column("value") }; - for(auto& channel : parsed_channels) { + for(auto& channel : snapshot_data.parsed_channels) { auto channel_id = channel.properties[property::CHANNEL_ID].as(); insert_query.add_entry( - server_id, + target_server_id, channel_id, channel.properties[property::CHANNEL_PID].as() ); @@ -496,7 +248,7 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId if(!property.isModified()) continue; insert_property_query.add_entry( - server_id, + target_server_id, property::PROP_TYPE_CHANNEL, channel_id, std::string{property.type().name}, @@ -508,13 +260,13 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId { auto result = insert_query.execute(this->handle->getSql(), true); for(const auto& fail : result.failed_entries) - logWarning(server_id, "Failed to insert channel {} into the server database: {}", parsed_channels[std::get<0>(fail)].properties[property::CHANNEL_NAME].value(), std::get<1>(fail).fmtStr()); + logWarning(logging_server_id, "Failed to insert channel {} into the server database: {}", snapshot_data.parsed_channels[std::get<0>(fail)].properties[property::CHANNEL_NAME].value(), std::get<1>(fail).fmtStr()); } { auto result = insert_property_query.execute(this->handle->getSql(), true); - if(result.failed_entries.size() > 0) - logWarning(server_id, "Failed to insert all channel properties into the database. Failed property cound: {}", result.failed_entries.size()); + if(!result.failed_entries.empty()) + logWarning(logging_server_id, "Failed to insert some channel properties into the database. Failed property count: {}", result.failed_entries.size()); } } @@ -533,11 +285,11 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId sql::Column("flag_negate") }; - for(auto& entry : channel_permissions) { + for(auto& entry : snapshot_data.channel_permissions) { { auto new_id = channel_id_mapping.find(entry.id1); if(new_id == channel_id_mapping.end()) { - logWarning(server_id, "Missing channel id mapping for channel permission entry (channel id: {}). Skipping permission insert.", entry.id1); + logWarning(logging_server_id, "Missing channel id mapping for channel permission entry (channel id: {}). Skipping permission insert.", entry.id1); continue; } entry.id1 = new_id->second; @@ -545,7 +297,7 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId for(const auto& permission : entry.permissions) { insert_query.add_entry( - server_id, + target_server_id, permission::SQL_PERM_CHANNEL, entry.id1, 0, @@ -560,29 +312,29 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId auto result = insert_query.execute(this->handle->getSql(), true); if(!result.failed_entries.empty()) - logWarning(server_id, "Failed to insert all channel permissions into the database. Failed permission count: {}", result.failed_entries.size()); + logWarning(logging_server_id, "Failed to insert all channel permissions into the database. Failed permission count: {}", result.failed_entries.size()); } /* server groups */ { - sql::model insert_model{this->handle->getSql(), "INSERT INTO `groups` (`serverId`, `target`, `type`, `displayName`, `original_id`) VALUES (:serverId, :target, :type, :name, :id)"}; - insert_model.value(":serverId", server_id).value(":target", GroupTarget::GROUPTARGET_SERVER).value(":type", GroupType::GROUP_TYPE_NORMAL); + sql::model insert_model{this->handle->getSql(), "INSERT INTO `groups` (`serverId`, `target`, `type`, `displayName`, `original_group_id`) VALUES (:serverId, :target, :type, :name, :id)"}; + insert_model.value(":serverId", target_server_id).value(":target", GroupTarget::GROUPTARGET_SERVER).value(":type", GroupType::GROUP_TYPE_NORMAL); - for(auto& group : parsed_server_groups) { - auto result = insert_model.command().value(":name", group.parsed_data.name).value(":id", group.parsed_data.group_id).execute(); + for(auto& group : snapshot_data.parsed_server_groups) { + auto result = insert_model.command().value(":name", group.name).value(":id", group.group_id).execute(); if(!result) - logWarning(server_id, "Failed to insert server group \"{}\" into the database", group.parsed_data.name); + logWarning(logging_server_id, "Failed to insert server group \"{}\" into the database", group.name); } - sql::command{this->handle->getSql(), "SELECT `original_id`,`groupId` FROM `groups` WHERE `serverId` = :sid AND `target` = :target AND `type` = :type"} - .value(":serverId", server_id).value(":target", GroupTarget::GROUPTARGET_SERVER).value(":type", GroupType::GROUP_TYPE_NORMAL) + sql::command{this->handle->getSql(), "SELECT `original_group_id`,`groupId` FROM `groups` WHERE `serverId` = :sid AND `target` = :target AND `type` = :type"} + .value(":serverId", target_server_id).value(":target", GroupTarget::GROUPTARGET_SERVER).value(":type", GroupType::GROUP_TYPE_NORMAL) .query([&](int length, std::string* values, std::string* names) { GroupId original_id{0}, new_id{0}; try { original_id = std::stoull(values[0]); new_id = std::stoull(values[1]); } catch (std::exception& ex) { - logWarning(server_id, "Failed to parse server group mapping for group id {} (New ID: {})", values[1], values[0]); + logWarning(logging_server_id, "Failed to parse server group mapping for group id {} (New ID: {})", values[1], values[0]); return; } server_group_id_mapping[original_id] = new_id; @@ -598,20 +350,20 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId sql::Column("channelId") }; - for(auto& relation : parsed_server_group_relations) { + for(auto& relation : snapshot_data.parsed_server_group_relations) { for(auto& entry : relation.second) { ClientId client_id{}; { auto new_id = client_id_mapping.find(entry.client_id); if(new_id == client_id_mapping.end()) { - logWarning(server_id, "Missing client id mapping for channel group relation permission entry (client id: {}). Skipping relation insert.", entry.client_id); + logWarning(logging_server_id, "Missing client id mapping for channel group relation permission entry (client id: {}). Skipping relation insert.", entry.client_id); continue; } client_id = new_id->second; } insert_query.add_entry( - server_id, + target_server_id, client_id, entry.group_id, 0 @@ -621,29 +373,29 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId auto result = insert_query.execute(this->handle->getSql(), true); if(!result.failed_entries.empty()) - logWarning(server_id, "Failed to insert all server group relations into the database. Failed insert count: {}", result.failed_entries.size()); + logWarning(logging_server_id, "Failed to insert all server group relations into the database. Failed insert count: {}", result.failed_entries.size()); } /* channel groups */ { - sql::model insert_model{this->handle->getSql(), "INSERT INTO `groups` (`serverId`, `target`, `type`, `displayName`, `original_id`) VALUES (:serverId, :target, :type, :name, :id)"}; - insert_model.value(":serverId", server_id).value(":target", GroupTarget::GROUPTARGET_CHANNEL).value(":type", GroupType::GROUP_TYPE_NORMAL); + sql::model insert_model{this->handle->getSql(), "INSERT INTO `groups` (`serverId`, `target`, `type`, `displayName`, `original_group_id`) VALUES (:serverId, :target, :type, :name, :id)"}; + insert_model.value(":serverId", target_server_id).value(":target", GroupTarget::GROUPTARGET_CHANNEL).value(":type", GroupType::GROUP_TYPE_NORMAL); - for(auto& group : parsed_server_groups) { - auto result = insert_model.command().value(":name", group.parsed_data.name).value(":id", group.parsed_data.group_id).execute(); + for(auto& group : snapshot_data.parsed_server_groups) { + auto result = insert_model.command().value(":name", group.name).value(":id", group.group_id).execute(); if(!result) - logWarning(server_id, "Failed to insert channel group \"{}\" into the database", group.parsed_data.name); + logWarning(logging_server_id, "Failed to insert channel group \"{}\" into the database", group.name); } - sql::command{this->handle->getSql(), "SELECT `original_id`,`groupId` FROM `groups` WHERE `serverId` = :sid AND `target` = :target AND `type` = :type"} - .value(":serverId", server_id).value(":target", GroupTarget::GROUPTARGET_CHANNEL).value(":type", GroupType::GROUP_TYPE_NORMAL) + sql::command{this->handle->getSql(), "SELECT `original_group_id`,`groupId` FROM `groups` WHERE `serverId` = :sid AND `target` = :target AND `type` = :type"} + .value(":serverId", target_server_id).value(":target", GroupTarget::GROUPTARGET_CHANNEL).value(":type", GroupType::GROUP_TYPE_NORMAL) .query([&](int length, std::string* values, std::string* names) { GroupId original_id{0}, new_id{0}; try { original_id = std::stoull(values[0]); new_id = std::stoull(values[1]); } catch (std::exception& ex) { - logWarning(server_id, "Failed to parse channel group mapping for group id {} (New ID: {})", values[1], values[0]); + logWarning(logging_server_id, "Failed to parse channel group mapping for group id {} (New ID: {})", values[1], values[0]); return; } channel_group_id_mapping[original_id] = new_id; @@ -659,12 +411,12 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId sql::Column("channelId") }; - for(auto& relation : parsed_channel_group_relations) { + for(auto& relation : snapshot_data.parsed_channel_group_relations) { ChannelId channel_id{}; { auto new_id = channel_id_mapping.find(relation.first); if(new_id == channel_id_mapping.end()) { - logWarning(server_id, "Missing channel id mapping for channel group relation entry (channel id: {}). Skipping relation insert.", relation.first); + logWarning(logging_server_id, "Missing channel id mapping for channel group relation entry (channel id: {}). Skipping relation insert.", relation.first); continue; } channel_id = new_id->second; @@ -675,14 +427,14 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId { auto new_id = client_id_mapping.find(entry.client_id); if(new_id == client_id_mapping.end()) { - logWarning(server_id, "Missing client id mapping for channel group relation permission entry (client id: {}, channel id: {}). Skipping relation insert.", entry.client_id, relation.first); + logWarning(logging_server_id, "Missing client id mapping for channel group relation permission entry (client id: {}, channel id: {}). Skipping relation insert.", entry.client_id, relation.first); continue; } client_id = new_id->second; } insert_query.add_entry( - server_id, + target_server_id, client_id, entry.group_id, channel_id @@ -692,7 +444,7 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId auto result = insert_query.execute(this->handle->getSql(), true); if(!result.failed_entries.empty()) - logWarning(server_id, "Failed to insert all channel group relations into the database. Failed insert count: {}", result.failed_entries.size()); + logWarning(logging_server_id, "Failed to insert all channel group relations into the database. Failed insert count: {}", result.failed_entries.size()); } /* client permissions */ @@ -710,11 +462,11 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId sql::Column("flag_negate"), }; - for(auto& entry : client_permissions) { + for(auto& entry : snapshot_data.client_permissions) { { auto new_id = client_id_mapping.find(entry.id1); if(new_id == client_id_mapping.end()) { - logWarning(server_id, "Missing client id mapping for client permission entry (client id: {}). Skipping permission insert.", entry.id1); + logWarning(logging_server_id, "Missing client id mapping for client permission entry (client id: {}). Skipping permission insert.", entry.id1); continue; } entry.id1 = new_id->second; @@ -722,7 +474,7 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId for(const auto& permission : entry.permissions) { insert_query.add_entry( - server_id, + target_server_id, permission::SQL_PERM_USER, entry.id1, 0, @@ -737,7 +489,7 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId auto result = insert_query.execute(this->handle->getSql(), true); if(!result.failed_entries.empty()) - logWarning(server_id, "Failed to insert all client permissions into the database. Failed permission count: {}", result.failed_entries.size()); + logWarning(logging_server_id, "Failed to insert all client permissions into the database. Failed permission count: {}", result.failed_entries.size()); } /* client channel permissions */ @@ -755,11 +507,11 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId sql::Column("flag_negate"), }; - for(auto& entry : client_channel_permissions) { + for(auto& entry : snapshot_data.client_channel_permissions) { { auto new_id = channel_id_mapping.find(entry.id1); if(new_id == channel_id_mapping.end()) { - logWarning(server_id, "Missing channel id mapping for client channel permission entry (client id: {}, channel id: {}). Skipping permission insert.", entry.id2, entry.id1); + logWarning(logging_server_id, "Missing channel id mapping for client channel permission entry (client id: {}, channel id: {}). Skipping permission insert.", entry.id2, entry.id1); continue; } entry.id1 = new_id->second; @@ -768,7 +520,7 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId { auto new_id = client_id_mapping.find(entry.id2); if(new_id == client_id_mapping.end()) { - logWarning(server_id, "Missing client id mapping for client channel permission entry (client id: {}, channel id: {}). Skipping permission insert.", entry.id2, entry.id1); + logWarning(logging_server_id, "Missing client id mapping for client channel permission entry (client id: {}, channel id: {}). Skipping permission insert.", entry.id2, entry.id1); continue; } entry.id2 = new_id->second; @@ -776,7 +528,7 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId for(const auto& permission : entry.permissions) { insert_query.add_entry( - server_id, + target_server_id, permission::SQL_PERM_USER, entry.id2, (ChannelId) entry.id1, @@ -791,9 +543,146 @@ bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId auto result = insert_query.execute(this->handle->getSql(), true); if(!result.failed_entries.empty()) - logWarning(server_id, "Failed to insert all client channel permissions into the database. Failed permission count: {}", result.failed_entries.size()); + logWarning(logging_server_id, "Failed to insert all client channel permissions into the database. Failed permission count: {}", result.failed_entries.size()); + } + + /* music bot attributes */ + { + + sql::InsertQuery insert_property_query{"properties", + sql::Column("serverId"), + sql::Column("type"), + sql::Column("id"), + sql::Column("key"), + sql::Column("value") + }; + + for(auto& bot : snapshot_data.music_bots) { + auto new_id = client_id_mapping.find(bot.database_id); + if(new_id == client_id_mapping.end()) { + logWarning(logging_server_id, "Missing client id mapping for music bot (Unique ID: {}). Skipping permission insert.", bot.unique_id); + continue; + } + + for(const auto& property : bot.properties.list_properties(property::FLAG_SAVE | property::FLAG_SAVE_MUSIC)) { + if(!property.isModified()) continue; + + auto value = property.value(); + if(property.type() == property::CLIENT_LAST_CHANNEL) { + auto channel_id = property.as(); + + auto new_channel_id = channel_id_mapping.find(channel_id); + if(new_channel_id == channel_id_mapping.end()) { + logWarning(logging_server_id, "Missing channel id mapping for channel {} for music bot {}.", channel_id, bot.unique_id); + continue; + } + value = std::to_string(new_channel_id->second); + } + insert_property_query.add_entry( + target_server_id, + property::PROP_TYPE_CLIENT, + new_id->second, + std::string{property.type().name}, + value + ); + } + } + + auto result = insert_property_query.execute(this->handle->getSql(), true); + if(!result.failed_entries.empty()) + logWarning(logging_server_id, "Failed to insert some music bot properties into the database. Failed property count: {}", result.failed_entries.size()); + } + + /* playlists */ + { + + sql::InsertQuery insert_playlist_query{"playlists", + sql::Column("serverId"), + sql::Column("playlist_id") + }; + + sql::InsertQuery insert_property_query{"properties", + sql::Column("serverId"), + sql::Column("type"), + sql::Column("id"), + sql::Column("key"), + sql::Column("value") + }; + + for(auto& playlist : snapshot_data.playlists) { + insert_playlist_query.add_entry(target_server_id, playlist.playlist_id); + + for(const auto& property : playlist.properties.list_properties(property::FLAG_SAVE | property::FLAG_SAVE_MUSIC)) { + if(!property.isModified()) continue; + + insert_property_query.add_entry( + target_server_id, + property::PROP_TYPE_PLAYLIST, + playlist.playlist_id, + std::string{property.type().name}, + property.value() + ); + } + } + + { + auto result = insert_playlist_query.execute(this->handle->getSql(), true); + if(!result.failed_entries.empty()) + logWarning(logging_server_id, "Failed to insert some playlists into the database. Failed playlist count: {}", result.failed_entries.size()); + } + + { + auto result = insert_property_query.execute(this->handle->getSql(), true); + if(!result.failed_entries.empty()) + logWarning(logging_server_id, "Failed to insert some playlist properties into the database. Failed playlist count: {}", result.failed_entries.size()); + } + } + + /* playlist songs */ + { + sql::InsertQuery insert_song_query{"playlist_songs", + sql::Column("serverId"), + sql::Column("playlist_id"), + sql::Column("song_id"), + sql::Column("order_id"), + sql::Column("invoker_dbid"), + sql::Column("url"), + sql::Column("url_loader"), + sql::Column("loaded"), + sql::Column("metadata") + }; + + for(auto& song : snapshot_data.playlist_songs) { + { + auto new_id = client_id_mapping.find(song.invoker_id); + if(new_id == client_id_mapping.end()) { + logWarning(logging_server_id, "Missing client id mapping for playlist song invoker (Invoker ID: {}). Changing it to zero.", song.invoker_id); + song.invoker_id = 0; + } else { + song.invoker_id = new_id->second; + } + } + + insert_song_query.add_entry( + target_server_id, + song.playlist_id, + song.song_id, + song.order_id, + song.invoker_id, + song.url, + song.loader, + song.loaded, + song.metadata + ); + } + + { + auto result = insert_song_query.execute(this->handle->getSql(), true); + if(!result.failed_entries.empty()) + logWarning(logging_server_id, "Failed to insert some playlist songs into the database. Failed song count: {}", result.failed_entries.size()); + } } } - error = "not implemented"; - return false; + + return true; } \ No newline at end of file diff --git a/server/src/snapshots/music.cpp b/server/src/snapshots/music.cpp new file mode 100644 index 0000000..957e0c2 --- /dev/null +++ b/server/src/snapshots/music.cpp @@ -0,0 +1,102 @@ +// +// Created by WolverinDEV on 30/07/2020. +// + +#include "music.h" + +using namespace ts::server::snapshots; + +bool music_bot_parser::parse(std::string &error, music_bot_entry &entry, size_t &index) { + auto data = this->command[index]; + + size_t entry_index{0}; + std::string_view key{}; + std::string value{}; + while(data.next_entry(entry_index, key, value)) { + if(key == "bot_id") { + char* end_ptr{nullptr}; + entry.database_id = strtoull(value.c_str(), &end_ptr, 10); + if (*end_ptr) { + error = "failed to parse bot database id at character " + std::to_string(data.key_command_character_index(key) + key.length()); + return false; + } + } else if(key == "bot_unique_id") { + entry.unique_id = value; + } else if(key == "bot_owner_id") { + entry.bot_owner_id = value; + } else if(key == "end_bots") { + continue; + } else { + const auto& property = property::find(key); + if(property.is_undefined()) { + //TODO: Issue a warning + continue; + } + + //TODO: Validate value + entry.properties[property] = value; + } + } + + if(entry.database_id == 0) { + error = "missing database id at entry " + std::to_string(data.command_character_index()); + return false; + } else if(entry.unique_id.empty()) { + error = "missing unique id at entry " + std::to_string(data.command_character_index()); + return false; + } + + return true; +} + +bool playlist_parser::parse(std::string &error, playlist_entry &entry, size_t &index) { + auto data = this->command[index]; + + size_t entry_index{0}; + std::string_view key{}; + std::string value{}; + while(data.next_entry(entry_index, key, value)) { + if(key == "playlist_id") { + char* end_ptr{nullptr}; + entry.playlist_id = strtoull(value.c_str(), &end_ptr, 10); + if (*end_ptr) { + error = "failed to parse playlist id at character " + std::to_string(data.key_command_character_index(key) + key.length()); + return false; + } + } else if(key == "end_playlist") { + continue; + } else { + const auto& property = property::find(key); + if(property.is_undefined()) { + //TODO: Issue a warning + continue; + } + + //TODO: Validate value + entry.properties[property] = value; + } + } + + if(entry.playlist_id == 0) { + error = "missing playlist id at entry " + std::to_string(data.command_character_index()); + return false; + } + + return false; +} + +bool playlist_song_parser::parse(std::string &error, playlist_song_entry &entry, size_t &index) { + auto data = this->command[index]; + + entry.song_id = data.value_as("song_id"); + entry.order_id = data.value_as("song_order"); + entry.invoker_id = data.value_as("song_invoker"); + + entry.url = data.value("song_url"); + entry.loader = data.value("song_url_loader"); + entry.loaded = data.value_as("song_loaded"); + + entry.metadata = data.value("song_metadata"); + + return true; +} \ No newline at end of file diff --git a/server/src/snapshots/music.h b/server/src/snapshots/music.h new file mode 100644 index 0000000..3f4b6ad --- /dev/null +++ b/server/src/snapshots/music.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include +#include "./snapshot.h" + +namespace ts::server::snapshots { + struct music_bot_entry { + ClientDbId database_id{0}; + + std::string unique_id{}; + std::string bot_owner_id{}; + + Properties properties{}; + }; + + struct playlist_entry { + PlaylistId playlist_id{0}; + Properties properties{}; + }; + + struct playlist_song_entry { + PlaylistId playlist_id{0}; /* Attention: This will not be written into the snapshot directly! */ + SongId song_id{0}; + SongId order_id{0}; + + ClientDbId invoker_id{0}; + + std::string url{}; + std::string loader{}; + bool loaded{false}; + + std::string metadata{}; + }; + + class music_bot_parser : public parser { + public: + music_bot_parser(type type_, version_t version, const command_parser &command) : parser{type_, version, + command} {} + + bool parse( + std::string & /* error */, + music_bot_entry & /* result */, + size_t & /* offset */) override; + }; + + class playlist_parser : public parser { + public: + playlist_parser(type type_, version_t version, const command_parser &command) : parser{type_, version, + command} {} + + bool parse( + std::string & /* error */, + playlist_entry & /* result */, + size_t & /* offset */) override; + }; + + class playlist_song_parser : public parser { + public: + playlist_song_parser(type type_, version_t version, const command_parser &command) : parser{type_, version, + command} {} + + bool parse( + std::string & /* error */, + playlist_song_entry & /* result */, + size_t & /* offset */) override; + }; +} \ No newline at end of file diff --git a/server/src/snapshots/parser.cpp b/server/src/snapshots/parser.cpp new file mode 100644 index 0000000..cd6c968 --- /dev/null +++ b/server/src/snapshots/parser.cpp @@ -0,0 +1,355 @@ +// +// Created by WolverinDEV on 30/07/2020. +// + +#include +#include + +#include "./snapshot.h" +#include "./server.h" +#include "./client.h" +#include "./channel.h" +#include "./music.h" +#include "./groups.h" +#include "./snapshot_data.h" + +using namespace ts::server; + +bool snapshots::parse_snapshot(snapshot_data &result, std::string &error, ServerId server_id, const command_parser &data) { + if(data.bulk(0).has_key("version")) { + return snapshots::parse_snapshot_ts3(result, error, server_id, data); + } else if(data.bulk(1).has_key("snapshot_version")) { + /* teaspeak snapshot */ + return snapshots::parse_snapshot_teaspeak(result, error, server_id, data); + } else { + /* old TS3 snapshot format */ + return snapshots::parse_snapshot_ts3(result, error, server_id, data); + } +} + +bool snapshots::parse_snapshot_teaspeak(snapshot_data &result, std::string &error, ts::ServerId server_id, const ts::command_parser &data) { + if(!data.bulk(1).has_key("snapshot_version")) { + error = "Missing snapshot version"; + return false; + } + auto version = data.bulk(1).value_as("snapshot_version"); + auto hash = data.bulk(0).value("hash"); + + if(version < 1) { + error = "snapshot version too old"; + return false; + } else if(version > 2) { + error = "snapshot version is too new"; + return false; + } + + /* the actual snapshot begins at index 2 */ + return snapshots::parse_snapshot_raw(result, error, server_id, data, hash, 2, type::TEASPEAK, version); +} + +bool snapshots::parse_snapshot_ts3(snapshot_data &result, std::string &error, ts::ServerId server_id, const ts::command_parser &data) { + snapshots::version_t version{0}; + if(data.bulk(0).has_key("version")) + version = data.bulk(0).value_as("version"); + + auto hash = data.bulk(0).value("hash"); + if(data.bulk(0).has_key("salt")) { + error = "TeaSpeak dosn't support encrypted snapshots yet"; + return false; + } + + if(version == 0) { + return snapshots::parse_snapshot_raw(result, error, server_id, data, hash, 1, type::TEAMSPEAK, version); + } else if(version == 1) { + error = "version 1 is an invalid version"; + return false; + } else if(version == 2) { + /* compressed data */ + error = "version 2 isn't currently supported"; + return false; + } else if(version == 3) { + error = "version 3 isn't currently supported"; + return false; + } else { + error = "snapshots with version 1-3 are currently supported"; + return false; + } +} + +bool snapshots::parse_snapshot_raw(snapshot_data &snapshot_data, std::string &error, ServerId server_id, const command_parser &command, + const std::string &hash, size_t command_offset, snapshots::type type, snapshots::version_t version) { + //TODO: Verify hash + + /* all snapshots start with the virtual server properties */ + { + snapshots::server_parser parser{type, version, command}; + if(!parser.parse(error, snapshot_data.parsed_server, command_offset)) + return false; + } + + /* afterwards all channels */ + { + snapshots::channel_parser parser{type, version, command}; + auto data = command.bulk(command_offset); + if(!data.has_key("begin_channels")) { + error = "missing begin channels token at " + std::to_string(data.command_character_index()); + return false; + } + + auto end_bulk = command.next_bulk_containing("end_channels", command_offset); + if(!end_bulk.has_value()) { + error = "missing end channels token"; + return false; + } else if(*end_bulk == command_offset) { + error = "snapshot contains no channels"; + return false; + } + snapshot_data.parsed_channels.reserve(*end_bulk - command_offset); + debugMessage(server_id, "Snapshot contains {} channels", *end_bulk - command_offset); + + while(!command.bulk(command_offset).has_key("end_channels")) { + auto& entry = snapshot_data.parsed_channels.emplace_back(); + if(!parser.parse(error, entry, command_offset)) + return false; + } + command_offset++; /* the "end_channels" token */ + } + + /* after channels all clients */ + { + snapshots::client_parser parser{type, version, command}; + auto data = command.bulk(command_offset); + if(!data.has_key("begin_clients")) { + error = "missing begin clients token at " + std::to_string(data.command_character_index()); + return false; + } + + auto end_bulk = command.next_bulk_containing("end_clients", command_offset); + if(!end_bulk.has_value()) { + error = "missing end clients token"; + return false; + } + snapshot_data.parsed_channels.reserve(*end_bulk - command_offset); + debugMessage(server_id, "Snapshot contains {} clients", *end_bulk - command_offset); + + while(!command.bulk(command_offset).has_key("end_clients")) { + auto& entry = snapshot_data.parsed_clients.emplace_back(); + if(!parser.parse(error, entry, command_offset)) + return false; + } + command_offset++; /* the "end_clients" token */ + } + + /* permissions */ + { + bool server_groups_parsed{false}, + channel_groups_parsed{false}, + client_permissions_parsed{false}, + channel_permissions_parsed{false}, + client_channel_permissions_parsed{false}; + + if(!command.bulk(command_offset++).has_key("begin_permissions")) { + error = "missing begin permissions key"; + return false; + } + + snapshots::relation_parser relation_parser{type, version, command}; + while(!command.bulk(command_offset).has_key("end_permissions")) { + if(command.bulk(command_offset).has_key("server_groups")) { + if(std::exchange(server_groups_parsed, true)) { + error = "duplicated server group list"; + return false; + } + + snapshots::group_parser group_parser{type, version, command, "id", permission::teamspeak::GroupType::SERVER}; + + /* parse all groups */ + while(!command.bulk(command_offset).has_key("end_groups")){ + auto& group = snapshot_data.parsed_server_groups.emplace_back(); + if(!group_parser.parse(error, group, command_offset)) /* will consume the end group token */ + return false; + command_offset++; /* for the "end_group" token */ + } + command_offset++; /* for the "end_groups" token */ + + /* parse relations */ + if(!relation_parser.parse(error, snapshot_data.parsed_server_group_relations, command_offset)) + return false; + command_offset++; /* for the "end_relations" token */ + + if(snapshot_data.parsed_server_group_relations.size() > 1) { + error = "all group relations should be for channel id 0 but received more than one different channel."; + return false; + } else if(!snapshot_data.parsed_server_group_relations.empty() && snapshot_data.parsed_server_group_relations.begin()->first != 0) { + error = "all group relations should be for channel id 0 but received it for " + std::to_string(snapshot_data.parsed_server_group_relations.begin()->first); + return false; + } + } else if(command.bulk(command_offset).has_key("channel_groups")) { + if(std::exchange(channel_groups_parsed, true)) { + error = "duplicated channel group list"; + return false; + } + + snapshots::group_parser group_parser{type, version, command, "id", permission::teamspeak::GroupType::CHANNEL}; + + /* parse all groups */ + while(!command.bulk(command_offset).has_key("end_groups")){ + auto& group = snapshot_data.parsed_channel_groups.emplace_back(); + if(!group_parser.parse(error, group, command_offset)) + return false; + command_offset++; /* for the "end_group" token */ + } + command_offset++; /* for the "end_groups" token */ + + /* parse relations */ + if(!relation_parser.parse(error, snapshot_data.parsed_channel_group_relations, command_offset)) + return false; + command_offset++; /* for the "end_relations" token */ + } else if(command.bulk(command_offset).has_key("client_flat")) { + /* client permissions */ + if(std::exchange(client_permissions_parsed, true)) { + error = "duplicated client permissions list"; + return false; + } + + snapshots::flat_parser flat_parser{type, version, command, permission::teamspeak::GroupType::CLIENT}; + if(!flat_parser.parse(error, snapshot_data.client_permissions, command_offset)) + return false; + command_offset++; /* for the "end_flat" token */ + } else if(command.bulk(command_offset).has_key("channel_flat")) { + /* channel permissions */ + if(std::exchange(channel_permissions_parsed, true)) { + error = "duplicated channel permissions list"; + return false; + } + + snapshots::flat_parser flat_parser{type, version, command, permission::teamspeak::GroupType::CHANNEL}; + if(!flat_parser.parse(error, snapshot_data.channel_permissions, command_offset)) + return false; + + command_offset++; /* for the "end_flat" token */ + } else if(command.bulk(command_offset).has_key("channel_client_flat")) { + /* channel client permissions */ + if(std::exchange(client_channel_permissions_parsed, true)) { + error = "duplicated client channel permissions list"; + return false; + } + + snapshots::flat_parser flat_parser{type, version, command, permission::teamspeak::GroupType::CLIENT}; + if(!flat_parser.parse(error, snapshot_data.client_channel_permissions, command_offset)) + return false; + + command_offset++; /* for the "end_flat" token */ + } else { + command_offset++; + } + } + + /* check if everything has been parsed */ + { + if(!server_groups_parsed) { + error = "missing server groups"; + return false; + } + + if(!channel_groups_parsed) { + error = "missing channel groups"; + return false; + } + + if(!client_permissions_parsed) { + error = "missing client permissions"; + return false; + } + + if(!channel_permissions_parsed) { + error = "missing channel permissions"; + return false; + } + + if(!client_channel_permissions_parsed) { + error = "missing client channel permissions"; + return false; + } + } + } + + if(type == snapshots::type::TEASPEAK && version >= 2) { + + bool music_bots_parsed{false}, + playlist_parsed{false}, + playlist_songs_parsed{false}; + + if(!command.bulk(command_offset++).has_key("begin_music")) { + error = "missing begin music key"; + return false; + } + + while(!command.bulk(command_offset).has_key("end_music")) { + if(command.bulk(command_offset).has_key("begin_bots")) { + if(std::exchange(music_bots_parsed, true)) { + error = "duplicated music bot list"; + return false; + } + + snapshots::music_bot_parser parser{type, version, command}; + while(!command.bulk(command_offset).has_key("end_bots")) { + auto& bot = snapshot_data.music_bots.emplace_back(); + + if(!parser.parse(error, bot, command_offset)) + return false; + } + } else if(command.bulk(command_offset).has_key("begin_playlist")) { + if(std::exchange(playlist_parsed, true)) { + error = "duplicated playlist list"; + return false; + } + + snapshots::playlist_parser parser{type, version, command}; + while(!command.bulk(command_offset).has_key("end_playlist")) { + auto& playlist = snapshot_data.playlists.emplace_back(); + + if(!parser.parse(error, playlist, command_offset)) + return false; + } + } else if(command.bulk(command_offset).has_key("begin_playlist_songs")) { + if(std::exchange(playlist_songs_parsed, true)) { + error = "duplicated playlist songs list"; + return false; + } + + snapshots::playlist_song_parser parser{type, version, command}; + + PlaylistId current_playlist_id{0}; + while(!command.bulk(command_offset).has_key("end_playlist_songs")) { + if(snapshot_data.playlist_songs.empty() || command.bulk(command_offset).has_key("song_playlist_id")) + current_playlist_id = command.bulk(command_offset).value_as("song_playlist_id"); + + auto& playlist_song = snapshot_data.playlist_songs.emplace_back(); + playlist_song.playlist_id = current_playlist_id; + + if(!parser.parse(error, playlist_song, command_offset)) + return false; + } + } + } + + /* check if everything has been parsed */ + if(!music_bots_parsed) { + error = "missing music bots"; + return false; + } + + if(!playlist_parsed) { + error = "missing playlists"; + return false; + } + + if(!playlist_songs_parsed) { + error = "missing playlist songs"; + return false; + } + } + + return true; +} \ No newline at end of file diff --git a/server/src/snapshots/snapshot.h b/server/src/snapshots/snapshot.h index c1d5460..ed41642 100644 --- a/server/src/snapshots/snapshot.h +++ b/server/src/snapshots/snapshot.h @@ -1,12 +1,15 @@ #pragma once #include +#include #include namespace ts::server::snapshots { enum struct type { TEAMSPEAK, - TEASPEAK + TEASPEAK, + + UNKNOWN }; typedef int32_t version_t; @@ -43,4 +46,14 @@ namespace ts::server::snapshots { const type type_; const version_t version_; }; + + struct snapshot_data; + + extern bool parse_snapshot(snapshot_data& /* result */, std::string& /* error */, ServerId /* target server id */, const ts::command_parser& /* command */); + + extern bool parse_snapshot_ts3(snapshot_data& /* result */, std::string& /* error */, ServerId /* target server id */, const command_parser& /* source */); + extern bool parse_snapshot_teaspeak(snapshot_data& /* result */, std::string& /* error */, ServerId /* target server id */, const command_parser& /* source */); + + extern bool parse_snapshot_raw(snapshot_data& /* result */, std::string& /* error */, ServerId /* target server id */, const command_parser& /* source */, const std::string& /* hash */, size_t /* offset */, snapshots::type /* type */, snapshots::version_t /* version */); + } \ No newline at end of file diff --git a/server/src/snapshots/snapshot_data.h b/server/src/snapshots/snapshot_data.h new file mode 100644 index 0000000..cd6db8f --- /dev/null +++ b/server/src/snapshots/snapshot_data.h @@ -0,0 +1,32 @@ +// +// Created by WolverinDEV on 30/07/2020. +// +#pragma once + +#include "./snapshot.h" + +namespace ts::server::snapshots { + struct snapshot_data { + snapshots::type type{snapshots::type::UNKNOWN}; + version_t version{-1}; + + snapshots::server_entry parsed_server{}; + + std::vector parsed_channels{}; + std::vector parsed_clients{}; + + std::vector parsed_server_groups{}; + snapshots::group_relations parsed_server_group_relations{}; + + std::vector parsed_channel_groups{}; + snapshots::group_relations parsed_channel_group_relations{}; + + std::deque client_permissions{}; + std::deque channel_permissions{}; + std::deque client_channel_permissions{}; + + std::deque music_bots{}; + std::deque playlists{}; + std::deque playlist_songs{}; + }; +} diff --git a/shared b/shared index 1c64822..a634396 160000 --- a/shared +++ b/shared @@ -1 +1 @@ -Subproject commit 1c6482262f183f99dae9f230d5fcfe632f85fe4a +Subproject commit a6343968450d4e626bcf99ec622b4a325bf8c4ba