// entfernt: #include "user.h" #include "chat_user.h" #include "server.h" #include "lib/tools.h" #include "lib/base.h" #include #include #include #include #include "chat_room.h" #include #include #include #include #include namespace Yc { namespace Lib { static bool _isValidHexColor(const std::string &c) { if (c.size() != 7 || c[0] != '#') return false; for (size_t i = 1; i < c.size(); ++i) { char ch = c[i]; if (!std::isxdigit(static_cast(ch))) return false; } return true; } ChatUser::ChatUser(std::shared_ptr parent, std::string name, std::string color, int socket) : _parent(std::move(parent)), _name(name), _color(color), _socket(socket), _stop(false) { // Hole DB-Connection auto server = _parent->getServer(); auto db = server->_database; // Suche Community-User std::string query = "SELECT * FROM community.\"user\" WHERE username = '" + name + "' LIMIT 1;"; auto result = db->exec(query); Json::Value userJson; if (result.empty()) { // Kein Community-User, lege Dummy an userJson["display_name"] = name; userJson["color"] = "#000000"; } else { const auto& row = result[0]; int falukant_user_id = row["id"].as(); // Suche Chat-User std::string chatUserQuery = "SELECT * FROM chat.\"user\" WHERE falukant_user_id = " + std::to_string(falukant_user_id) + " LIMIT 1;"; auto chatUserResult = db->exec(chatUserQuery); if (chatUserResult.empty()) { // Chat-User anlegen std::string insert = "INSERT INTO chat.\"user\" (falukant_user_id, display_name, color, show_gender, show_age, created_at, updated_at) VALUES (" + std::to_string(falukant_user_id) + ", '" + name + "', '#000000', true, true, NOW(), NOW()) RETURNING *;"; auto newUser = db->exec(insert); if (!newUser.empty()) { const auto& u = newUser[0]; userJson["id"] = u["id"].as(); userJson["falukant_user_id"] = u["falukant_user_id"].as(); userJson["display_name"] = u["display_name"].c_str(); userJson["color"] = u["color"].c_str(); userJson["show_gender"] = u["show_gender"].as(); userJson["show_age"] = u["show_age"].as(); userJson["created_at"] = u["created_at"].c_str(); userJson["updated_at"] = u["updated_at"].c_str(); } } else { const auto& u = chatUserResult[0]; userJson["id"] = u["id"].as(); userJson["falukant_user_id"] = u["falukant_user_id"].as(); userJson["display_name"] = u["display_name"].c_str(); userJson["color"] = u["color"].c_str(); userJson["show_gender"] = u["show_gender"].as(); userJson["show_age"] = u["show_age"].as(); userJson["created_at"] = u["created_at"].c_str(); userJson["updated_at"] = u["updated_at"].c_str(); } // Rechte laden std::string rightsQuery = "SELECT r.tr FROM chat.user_rights ur JOIN chat.rights r ON ur.chat_right_id = r.id WHERE ur.chat_user_id = " + std::to_string(userJson["id"].asInt()) + ";"; auto rightsResult = db->exec(rightsQuery); Json::Value rights(Json::arrayValue); for (const auto& r : rightsResult) { rights.append(r["tr"].c_str()); } userJson["rights"] = rights; } _user = Yc::Object::User(userJson); // Prefer DB color if available if (_user.id() != 0 && !_user.color().empty()) { _color = _user.color(); } _token = Yc::Lib::Tools::generateRandomString(32); // Beim Initial-Token direkt Name und aktuelle Farbe mitsenden, damit der Client "ich" korrekt färben kann sendMsg(token, _token, _name, _color); // Thread-Start erfolgt jetzt explizit per start(), nicht im Konstruktor } ChatUser::~ChatUser() { // Hinweis: Thread wird nicht im Destruktor gejoint, um Deadlocks zu vermeiden! // Der Thread muss explizit von außen gestoppt und gejoint werden (z.B. im ChatRoom beim Entfernen des Users). } std::string ChatUser::name() const { return _name; } std::string ChatUser::getToken() const { return _token; } bool ChatUser::validateToken(std::string token) { return (token == _token); } bool ChatUser::isUser(std::shared_ptr toValidate) { return (toValidate.get() == this); } void ChatUser::sendMsg(MsgType type, const char *message, std::string userName, std::string color) { sendMsg(type, std::string(message), userName, color); } void ChatUser::sendMsg(MsgType type, std::string message, std::string userName, std::string color) { // Standardwerte für leere Felder setzen if (userName.empty()) userName = _name; if (color.empty()) color = _color; Json::Value sendMessage; sendMessage["type"] = type; sendMessage["message"] = message; sendMessage["userName"] = userName; sendMessage["color"] = color; send(sendMessage); } void ChatUser::sendMsg(MsgType type, Json::Value message, std::string userName, std::string color) { // Standardwerte für leere Felder setzen if (userName.empty()) userName = _name; if (color.empty()) color = _color; Json::Value sendMessage; sendMessage["type"] = type; sendMessage["message"] = message; sendMessage["userName"] = userName; sendMessage["color"] = color; send(sendMessage); } void ChatUser::checkerTask() { try { // Heartbeat-Intervall: Alle 10 Sekunden Verbindung prüfen const int HEARTBEAT_INTERVAL = 2; int heartbeatCounter = 0; while (!_stop) { std::this_thread::sleep_for(std::chrono::seconds(1)); // Heartbeat-Check alle 10 Sekunden heartbeatCounter++; if (heartbeatCounter >= HEARTBEAT_INTERVAL) { heartbeatCounter = 0; // Prüfe Verbindung mit MSG_PEEK (nicht-blockierend) char peek; ssize_t r = recv(_socket, &peek, 1, MSG_PEEK | MSG_DONTWAIT); if (r == 0) { #ifdef YC_DEBUG std::cout << "[Debug] Verbindung zum Client abgebrochen (Token: )" << std::endl; #endif _parent->removeUserDisconnected(shared_from_this()); _stop = true; if (thread.joinable() && std::this_thread::get_id() == thread.get_id()) { thread.detach(); } return; } else if (r < 0) { // EINTR = Interrupted system call (normal), EAGAIN/EWOULDBLOCK = No data available (normal) // Andere Fehler bedeuten Verbindungsabbruch if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) { #ifdef YC_DEBUG std::cout << "[Debug] Socket-Fehler: " << strerror(errno) << " (Token: )" << std::endl; #endif _parent->removeUserDisconnected(shared_from_this()); _stop = true; if (thread.joinable() && std::this_thread::get_id() == thread.get_id()) { thread.detach(); } return; } } // Optional: Sende Heartbeat-Ping an Client // (kann helfen, NAT/Firewall-Verbindungen aktiv zu halten) #ifdef YC_DEBUG std::cout << "[Debug] Heartbeat check passed for user: " << _name << std::endl; #endif } // Ursprüngliche Verbindungsprüfung (alle 1 Sekunde) { char peek; ssize_t r = recv(_socket, &peek, 1, MSG_PEEK); if (r == 0) { #ifdef YC_DEBUG std::cout << "[Debug] Verbindung zum Client abgebrochen (Token: )" << std::endl; #endif _parent->removeUserDisconnected(shared_from_this()); _stop = true; if (thread.joinable() && std::this_thread::get_id() == thread.get_id()) { thread.detach(); } return; } } std::string msg = readSocket(_socket); if (msg == "") { // Kein Inhalt gelesen; kann Idle/Keepalive sein continue; } handleMessage(msg); } } catch (const std::exception& ex) { // swallow, optional hook } catch (...) { // swallow, optional hook } } void ChatUser::stop() { _stop = true; } std::string ChatUser::color() const { return _color; } void ChatUser::setParent(std::shared_ptr parent) { _parent = std::move(parent); } void ChatUser::send(std::string out) { // Entferne ggf. Token-Felder aus JSON-Strings und sende über Socket/WebSocket Base::sanitizeTokensInString(out); Base::send(_socket, out); } void ChatUser::send(Json::Value out) { // Entferne rekursiv alle Token-Felder und sende über Socket/WebSocket Base::sanitizeTokens(out); Base::send(_socket, out); } void ChatUser::handleMessage(std::string message) { Json::Value jsonTree = getJsonTree(message); // Eingehende Nachricht (nur im Debug-Mode) #ifdef YC_DEBUG { Json::Value toLog = jsonTree; if (toLog.isMember("token")) toLog["token"] = ""; std::cout << "[Debug] <- Client(fd=" << _socket << ", user='" << _name << "'): " << getJsonString(toLog) << std::endl; } #endif if (jsonTree["token"].asString() != _token) { return; } if (jsonTree["type"].asString() == "message") { checkString(jsonTree["message"].asString()); } else if (jsonTree["type"].asString() == "dice") { // Neue Würfel-Behandlung für das Würfelspiel if (jsonTree.isMember("value")) { int diceValue = jsonTree["value"].asInt(); if (!_parent->rollDice(shared_from_this(), diceValue)) { sendMsg(ChatUser::error, "dice_roll_failed", "", ""); } } else { // Fallback: Einfacher Würfelwurf doDice(); } } else if (jsonTree["type"].asString() == "start_dice_game") { // Würfelspiel starten (nur Admin) if (jsonTree.isMember("rounds")) { int rounds = jsonTree["rounds"].asInt(); if (rounds < 1 || rounds > 10) { sendMsg(ChatUser::error, "invalid_rounds", "", ""); return; } if (!_parent->startDiceGame(rounds, shared_from_this())) { sendMsg(ChatUser::error, "dice_game_start_failed", "", ""); } } else { sendMsg(ChatUser::error, "missing_rounds", "", ""); } } else if (jsonTree["type"].asString() == "end_dice_game") { // Würfelspiel beenden (nur Admin) _parent->endDiceGame(); } else if (jsonTree["type"].asString() == "scream") { _parent->addMessage(ChatUser::scream, jsonTree["message"].asString(), std::string(_name), std::string(_color)); } else if (jsonTree["type"].asString() == "do") { // "do" erwartet "value" (Aktion) und optional "to" (Ziel-User) std::string action = jsonTree.isMember("value") ? jsonTree["value"].asString() : ""; std::string targetUser = jsonTree.isMember("to") ? jsonTree["to"].asString() : ""; if (action.empty()) { sendMsg(ChatUser::error, "missing_action", "", ""); return; } // Erstelle strukturierte Nachricht mit Ziel-User und Aktion Json::Value doMsg = Json::objectValue; doMsg["tr"] = "user_action"; doMsg["action"] = action; if (!targetUser.empty()) { doMsg["to"] = targetUser; // Suche den Ziel-User und füge dessen Informationen hinzu auto targetUserObj = _parent->findUserByName(targetUser); if (targetUserObj) { doMsg["targetName"] = targetUserObj->name(); doMsg["targetColor"] = targetUserObj->color(); } } // Debug-Ausgabe für "do"-Nachrichten #ifdef YC_DEBUG std::cout << "[Debug] Sending do message: type=" << ChatUser::dosomething << ", action=" << action << ", to=" << (targetUser.empty() ? "all" : targetUser) << std::endl; #endif _parent->addMessage(ChatUser::dosomething, doMsg, std::string(_name), std::string(_color)); } else if (jsonTree["type"].asString() == "color") { std::string newColor = jsonTree.isMember("value") ? jsonTree["value"].asString() : ""; std::string oldColor = _color; if (!_isValidHexColor(newColor)) { sendMsg(ChatUser::error, "invalid_color", "", ""); return; } _color = newColor; _user.set_color(newColor); // Persistieren, falls DB-ID vorhanden try { if (_user.id() != 0) { auto server = _parent->getServer(); auto db = server->_database; std::string query = "UPDATE chat.\"user\" SET color = '" + newColor + "', updated_at = NOW() WHERE id = " + std::to_string(_user.id()) + ";"; (void)db->exec(query); } } catch (...) { // Ignoriere DB-Fehler still } // Bestätigung an User sendMsg(ChatUser::system, "color_changed", "", newColor); // Broadcast an andere: alte und neue Farbe mitsenden { Json::Value msg = Json::objectValue; msg["tr"] = "user_color_changed"; msg["from"] = oldColor; msg["to"] = newColor; _parent->addMessage(ChatUser::system, msg, _name, newColor); } } else if (jsonTree["type"].asString() == "join") { changeRoom(jsonTree["room"].asString(), jsonTree["password"].asString()); } else if (jsonTree["type"].asString() == "userlist") { sendUserList(); } } void ChatUser::checkString(std::string message) { if (message.substr(0, 6) == "/join ") { message = message.substr(6); if (message.find(" ") == std::string::npos) { changeRoom(message, ""); } else { std::string room = message.substr(0, message.find(" ")); std::string password = message.substr(message.find(" ") + 1); changeRoom(room, password); } } else if (message.substr(0, 7) == "/color ") { std::string newColor = message.substr(7); std::string oldColor = _color; if (!_isValidHexColor(newColor)) { sendMsg(ChatUser::error, "invalid_color", "", ""); return; } _color = newColor; _user.set_color(newColor); try { if (_user.id() != 0) { auto server = _parent->getServer(); auto db = server->_database; std::string query = "UPDATE chat.\"user\" SET color = '" + newColor + "', updated_at = NOW() WHERE id = " + std::to_string(_user.id()) + ";"; (void)db->exec(query); } } catch (...) { // ignore } sendMsg(ChatUser::system, "color_changed", "", newColor); { Json::Value msg = Json::objectValue; msg["tr"] = "user_color_changed"; msg["from"] = oldColor; msg["to"] = newColor; _parent->addMessage(ChatUser::system, msg, _name, newColor); } } else if (message == "/dice") { doDice(); } else if (message.substr(0, 15) == "/start_dice_game") { // Format: /start_dice_game if (message.length() > 16) { std::string roundsStr = message.substr(16); try { int rounds = std::stoi(roundsStr); if (rounds >= 1 && rounds <= 10) { if (!_parent->startDiceGame(rounds, shared_from_this())) { sendMsg(ChatUser::error, "dice_game_start_failed", "", ""); } } else { sendMsg(ChatUser::error, "invalid_rounds", "", ""); } } catch (...) { sendMsg(ChatUser::error, "invalid_rounds", "", ""); } } else { sendMsg(ChatUser::error, "missing_rounds", "", ""); } } else if (message == "/end_dice_game") { _parent->endDiceGame(); } else if (message == "/reload_rooms") { // Raumliste neu laden _parent->reloadRoomList(); } else if (message.substr(0, 5) == "/roll ") { // Format: /roll if (message.length() > 6) { std::string diceStr = message.substr(6); try { int diceValue = std::stoi(diceStr); if (diceValue >= 1 && diceValue <= 6) { if (!_parent->rollDice(shared_from_this(), diceValue)) { sendMsg(ChatUser::error, "dice_roll_failed", "", ""); } } else { sendMsg(ChatUser::error, "invalid_dice_value", "", ""); } } catch (...) { sendMsg(ChatUser::error, "invalid_dice_value", "", ""); } } else { sendMsg(ChatUser::error, "missing_dice_value", "", ""); } } else { _parent->addMessage(ChatUser::message, std::string(message), std::string(_name), std::string(_color)); } } void ChatUser::sendUserList() { Json::Value userList = _parent->userList(); Json::Value msg = Json::objectValue; msg["userlist"] = userList; sendMsg(userListe, msg, "", ""); } void ChatUser::doDice() { switch (_parent->addDice(shared_from_this(), (rand() % 6) + 1)) { case 1: sendMsg(system, "dice_not_possible", "", ""); break; case 2: sendMsg(system, "dice_allready_done", "", ""); break; default: break; } } void ChatUser::changeRoom(std::string newRoom, std::string password) { if (!_parent->userToNewRoom(shared_from_this(), newRoom, password)) { sendMsg(ChatUser::system, "room_not_possible", "", ""); } } void ChatUser::start() { auto self = shared_from_this(); thread = std::thread([self]() { self->checkerTask(); }); } } // namespace Lib } // namespace Yc