Files
yourchat/src/core/chat_user.cpp
Torsten Schulz (local) d92c40748e Implementiere Benutzerverbindungskontrolle und verbessere Nachrichtenverwaltung
- Füge die Methode `removeUserDisconnected` in der ChatRoom-Klasse hinzu, um Benutzer bei Verbindungsabbrüchen zu entfernen und entsprechende Nachrichten zu senden.
- Aktualisiere die ChatUser-Klasse, um einen Token-Getter bereitzustellen und die Verbindungsprüfung zu optimieren.
- Ändere die Server-Klasse, um die Benutzerverwaltung bei Raumwechseln zu verbessern und Debug-Informationen hinzuzufügen.
- Optimiere die Socket-Optionen für eine schnellere Fehlererkennung und verbessere die Handhabung von Anfragen.
2025-09-01 15:30:19 +02:00

549 lines
17 KiB
C++

// entfernt: #include "user.h"
#include "chat_user.h"
#include "server.h"
#include "lib/tools.h"
#include "lib/base.h"
#include <json/json.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include "chat_room.h"
#include <iostream>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sstream>
#include <cctype>
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<unsigned char>(ch))) return false;
}
return true;
}
ChatUser::ChatUser(std::shared_ptr<ChatRoom> 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<int>();
// 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<int>();
userJson["falukant_user_id"] = u["falukant_user_id"].as<int>();
userJson["display_name"] = u["display_name"].c_str();
userJson["color"] = u["color"].c_str();
userJson["show_gender"] = u["show_gender"].as<bool>();
userJson["show_age"] = u["show_age"].as<bool>();
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<int>();
userJson["falukant_user_id"] = u["falukant_user_id"].as<int>();
userJson["display_name"] = u["display_name"].c_str();
userJson["color"] = u["color"].c_str();
userJson["show_gender"] = u["show_gender"].as<bool>();
userJson["show_age"] = u["show_age"].as<bool>();
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<ChatUser> 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: <hidden>)" << 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: <hidden>)" << 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: <hidden>)" << 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<ChatRoom> 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"] = "<hidden>";
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 <runden>
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 <würfelwert>
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