Füge Unterstützung für Würfelspiele hinzu und verbessere Debugging-Optionen

- Implementiere neue Funktionen in der ChatRoom-Klasse für das Starten, Rollen und Beenden von Würfelspielen.
- Füge eine Option zur Aktivierung von Debug-Logging in CMake hinzu, um die Entwicklung zu erleichtern.
- Aktualisiere die ChatUser-Klasse, um die Interaktion mit dem Würfelspiel zu ermöglichen.
- Verbessere die Socket-Verwaltung im Server, um WebSocket-Verbindungen zu unterstützen und die Handhabung von Anfragen zu optimieren.
- Aktualisiere die Konfiguration, um die neue Funktionalität zu unterstützen und die Benutzererfahrung zu verbessern.
This commit is contained in:
Torsten Schulz (local)
2025-08-16 22:43:08 +02:00
parent 864d86aa09
commit 7338f1a740
15 changed files with 1556 additions and 135 deletions

View File

@@ -8,6 +8,7 @@
#include <algorithm>
#include <utility>
#include <thread>
#include <unordered_set>
namespace Yc
{
@@ -19,7 +20,10 @@ namespace Yc
_parent(std::move(parent)),
_blocked(false),
_stop(false),
_roundRunning(false)
_roundRunning(false),
_diceGameRunning(false),
_currentRound(0),
_totalRounds(0)
{
_name = roomParams["name"].asString();
_password = roomParams["password"].asString();
@@ -37,6 +41,12 @@ namespace Yc
void ChatRoom::run()
{
// Nachrichtentypen, die nicht an den Auslöser zurückgeschickt werden
static const std::unordered_set<std::string> kSuppressToSender = {
"room_entered",
"user_entered_room",
"user_color_changed"
};
while (!_stop)
{
if (_msgQueue.size() > 0 && !_blocked)
@@ -47,6 +57,18 @@ namespace Yc
Message message = _msgQueue.front();
for (auto &user : _users)
{
// Konfigurierbare Unterdrückung für Systemmeldungen an den Sender
if (message.type == ChatUser::system) {
bool suppress = false;
// message.messageTr kann JSON (mit tr) oder plain String sein
Json::Value maybeJson = getJsonTree(message.messageTr);
if (maybeJson.isObject() && maybeJson.isMember("tr")) {
suppress = kSuppressToSender.count(maybeJson["tr"].asString()) > 0;
} else {
suppress = kSuppressToSender.count(message.messageTr) > 0;
}
if (suppress && user->name() == message.userName) continue;
}
user->sendMsg(message.type, message.messageTr, message.userName, message.color);
}
_msgQueue.pop();
@@ -69,8 +91,29 @@ namespace Yc
}
auto newUser = std::make_shared<ChatUser>(shared_from_this(), _userName, color, socket);
_users.push_back(newUser);
newUser->sendMsg(ChatUser::roomList, _parent->jsonRoomList(), "", "");
addMessage(ChatUser::system, "room_entered", newUser->name(), newUser->color());
newUser->start();
Json::Value roomList = _parent->jsonRoomList();
newUser->sendMsg(ChatUser::roomList, roomList, "", "");
// Private Rückmeldung an den User: In welchem Raum befindet er/sie sich jetzt?
{
Json::Value msg = Json::objectValue;
msg["tr"] = "room_entered";
msg["to"] = _name;
newUser->sendMsg(ChatUser::system, msg, "", "");
}
// Sende aktuelle Userliste an den neuen User
Json::Value currentUserList = userList();
newUser->sendMsg(ChatUser::userListe, currentUserList, "", "");
// Sende aktualisierte Userliste an alle anderen User im Raum
for (auto &existingUser : _users) {
if (existingUser != newUser) {
existingUser->sendMsg(ChatUser::userListe, currentUserList, "", "");
}
}
// Broadcast an andere Nutzer: Benutzer X hat den Raum betreten (mit Farbinfo)
addMessage(ChatUser::system, "user_entered_room", newUser->name(), newUser->color());
_initRound();
return true;
}
@@ -107,7 +150,7 @@ namespace Yc
{
if (!silent)
{
addMessage(ChatUser::system, (*it)->name(), (*it)->color());
addMessage(ChatUser::system, std::string("leaved_chat"), (*it)->name(), (*it)->color());
}
_users.erase(it);
break;
@@ -123,7 +166,7 @@ namespace Yc
{
if (!silent)
{
addMessage(ChatUser::system, (*it)->name(), (*it)->color());
addMessage(ChatUser::system, std::string("leaved_chat"), (*it)->name(), (*it)->color());
}
_users.erase(it);
break;
@@ -164,6 +207,10 @@ namespace Yc
}
_users.push_back(user);
user->setParent(shared_from_this());
// Sende aktuelle Userliste an den User nach Raumwechsel
Json::Value currentUserList = userList();
user->sendMsg(ChatUser::userListe, currentUserList, "", "");
}
bool ChatRoom::userToNewRoom(std::shared_ptr<ChatUser> user, std::string newRoom, std::string password)
@@ -188,6 +235,7 @@ namespace Yc
{
Json::Value jsonUser = Json::objectValue;
jsonUser["name"] = user->name();
jsonUser["color"] = user->color();
users.append(jsonUser);
}
return users;
@@ -243,6 +291,18 @@ namespace Yc
return false;
}
std::shared_ptr<ChatUser> ChatRoom::findUserByName(std::string userName)
{
for (auto &user : _users)
{
if (userName == user->name())
{
return user;
}
}
return nullptr;
}
void ChatRoom::_handleDice()
{
if (((_type & rounds) == rounds))
@@ -266,7 +326,8 @@ namespace Yc
void ChatRoom::_initRound()
{
if (_users.size() == 2)
// Nur in Räumen mit Runden & Würfeln ankündigen
if (_users.size() == 2 && ((_type & rounds) == rounds) && ((_type & dice) == dice))
{
_lastRoundEnd = time(NULL);
addMessage(ChatUser::system, "next_round_starts_soon");
@@ -293,5 +354,245 @@ namespace Yc
return _name;
}
// Neue Würfel-Funktionen
bool ChatRoom::startDiceGame(int rounds, std::shared_ptr<ChatUser> admin)
{
if (!_isUserAdmin(admin)) {
return false; // Kein Admin
}
if (_diceGameRunning) {
return false; // Spiel läuft bereits
}
if (!canDice()) {
return false; // Raum unterstützt kein Würfeln
}
_diceGameRunning = true;
_currentRound = 1;
_totalRounds = rounds;
_gameStartTime = std::chrono::steady_clock::now();
_gameResults.clear();
// Alle User für das Spiel vorbereiten
for (auto& user : _users) {
_gameResults[user->name()] = std::vector<DiceResult>();
_hasRolledThisRound[user->name()] = false;
}
// Erste Runde starten
_startDiceRound();
// Nachricht an alle senden
Json::Value msg = Json::objectValue;
msg["tr"] = "dice_game_started";
msg["rounds"] = rounds;
msg["admin"] = admin->name();
addMessage(ChatUser::system, msg, "", "");
return true;
}
bool ChatRoom::rollDice(std::shared_ptr<ChatUser> user, int diceValue)
{
if (!_diceGameRunning) {
return false; // Kein Spiel aktiv
}
if (diceValue < 1 || diceValue > 6) {
return false; // Ungültiger Würfelwert
}
std::string userName = user->name();
if (_hasRolledThisRound[userName]) {
return false; // Bereits gewürfelt
}
// Würfelwert speichern
DiceResult result;
result.userName = userName;
result.diceValue = diceValue;
result.rollTime = std::chrono::steady_clock::now();
result.valid = true;
_gameResults[userName].push_back(result);
_hasRolledThisRound[userName] = true;
// Nachricht an alle senden
Json::Value msg = Json::objectValue;
msg["tr"] = "dice_rolled";
msg["user"] = userName;
msg["value"] = diceValue;
msg["round"] = _currentRound;
addMessage(ChatUser::dice, msg, userName, user->color());
// Prüfen ob alle gewürfelt haben
bool allRolled = true;
for (auto& pair : _hasRolledThisRound) {
if (!pair.second) {
allRolled = false;
break;
}
}
if (allRolled) {
// Alle haben gewürfelt, Runde beenden
_endDiceRound();
}
return true;
}
void ChatRoom::endDiceGame()
{
if (!_diceGameRunning) {
return;
}
_diceGameRunning = false;
_showGameResults();
// Nachricht an alle senden
Json::Value msg = Json::objectValue;
msg["tr"] = "dice_game_ended";
addMessage(ChatUser::system, msg, "", "");
}
void ChatRoom::_startDiceRound()
{
if (_currentRound > _totalRounds) {
endDiceGame();
return;
}
_roundStartTime = std::chrono::steady_clock::now();
// Alle User für neue Runde zurücksetzen
for (auto& pair : _hasRolledThisRound) {
pair.second = false;
}
// Nachricht an alle senden
Json::Value msg = Json::objectValue;
msg["tr"] = "dice_round_started";
msg["round"] = _currentRound;
msg["total_rounds"] = _totalRounds;
addMessage(ChatUser::system, msg, "", "");
// Timer für 15 Sekunden starten
_startRoundTimer();
}
void ChatRoom::_endDiceRound()
{
// Ungültige Ergebnisse für User die nicht gewürfelt haben
for (auto& pair : _hasRolledThisRound) {
if (!pair.second) {
DiceResult result;
result.userName = pair.first;
result.diceValue = 0;
result.rollTime = std::chrono::steady_clock::now();
result.valid = false;
_gameResults[pair.first].push_back(result);
}
}
// Nachricht an alle senden
Json::Value msg = Json::objectValue;
msg["tr"] = "dice_round_ended";
msg["round"] = _currentRound;
addMessage(ChatUser::system, msg, "", "");
_currentRound++;
if (_currentRound <= _totalRounds) {
// Nächste Runde starten
std::this_thread::sleep_for(std::chrono::seconds(2)); // 2 Sekunden Pause
_startDiceRound();
} else {
// Spiel beenden
endDiceGame();
}
}
void ChatRoom::_startRoundTimer()
{
_roundTimerThread = std::thread([this]() {
std::this_thread::sleep_for(std::chrono::seconds(15));
// Prüfen ob Runde noch läuft
if (_diceGameRunning && _currentRound <= _totalRounds) {
_endDiceRound();
}
});
_roundTimerThread.detach();
}
void ChatRoom::_showGameResults()
{
Json::Value results = Json::objectValue;
results["tr"] = "dice_game_results";
results["total_rounds"] = _totalRounds;
Json::Value userResults = Json::arrayValue;
for (auto& pair : _gameResults) {
Json::Value userResult = Json::objectValue;
userResult["user"] = pair.first;
Json::Value rounds = Json::arrayValue;
int totalScore = 0;
int validRolls = 0;
for (auto& result : pair.second) {
Json::Value round = Json::objectValue;
round["round"] = rounds.size() + 1;
round["value"] = result.diceValue;
round["valid"] = result.valid;
rounds.append(round);
if (result.valid) {
totalScore += result.diceValue;
validRolls++;
}
}
userResult["rounds"] = rounds;
userResult["total_score"] = totalScore;
userResult["valid_rolls"] = validRolls;
userResult["average"] = validRolls > 0 ? (double)totalScore / validRolls : 0.0;
userResults.append(userResult);
}
results["user_results"] = userResults;
addMessage(ChatUser::result, results, "", "");
}
bool ChatRoom::_isUserAdmin(std::shared_ptr<ChatUser> user) const
{
// Einfache Admin-Prüfung: Erster User im Raum ist Admin
// In einer echten Implementierung würde man hier Rechte aus der Datenbank prüfen
if (_users.empty()) return false;
return _users[0] == user;
}
void ChatRoom::reloadRoomList()
{
// Neue Raumliste vom Server holen
Json::Value roomList = _parent->jsonRoomList();
// An alle User im Raum senden
for (auto& user : _users) {
user->sendMsg(ChatUser::roomList, roomList, "", "");
}
// System-Nachricht an alle senden
Json::Value msg = Json::objectValue;
msg["tr"] = "room_list_reloaded";
addMessage(ChatUser::system, msg, "", "");
}
} // namespace Lib
} // namespace Yc

View File

@@ -9,6 +9,8 @@
#include <future>
#include <utility>
#include <vector>
#include <map>
#include <chrono>
#include "lib/base.h"
#include "chat_user.h"
@@ -53,10 +55,22 @@ namespace Yc
unsigned int addDice(std::shared_ptr<ChatUser> user, int diceValue);
bool accessAllowed(std::string userName, std::string password);
bool userIsInRoom(std::string userName);
std::shared_ptr<ChatUser> findUserByName(std::string userName);
void addUserWhenQueueEmpty(std::shared_ptr<ChatUser> user);
bool userToNewRoom(std::shared_ptr<ChatUser> user, std::string newRoom, std::string password);
unsigned int flags();
Json::Value userList();
// Neue Würfel-Funktionen
bool startDiceGame(int rounds, std::shared_ptr<ChatUser> admin);
bool rollDice(std::shared_ptr<ChatUser> user, int diceValue);
void endDiceGame();
bool isDiceGameRunning() const { return _diceGameRunning; }
int getCurrentRound() const { return _currentRound; }
int getTotalRounds() const { return _totalRounds; }
// Raumliste neu laden
void reloadRoomList();
private:
struct Message
@@ -67,6 +81,14 @@ namespace Yc
std::string color;
};
struct DiceResult
{
std::string userName;
int diceValue;
std::chrono::steady_clock::time_point rollTime;
bool valid;
};
std::shared_ptr<Server> _parent;
std::string _name;
std::string _password;
@@ -83,11 +105,30 @@ namespace Yc
int _roundLength;
std::vector<std::pair<std::shared_ptr<ChatUser>, int>> _diceValues;
Yc::Object::Room _room;
// Neue Würfel-Spiel-Variablen
bool _diceGameRunning;
int _currentRound;
int _totalRounds;
std::chrono::steady_clock::time_point _roundStartTime;
std::chrono::steady_clock::time_point _gameStartTime;
std::map<std::string, std::vector<DiceResult>> _gameResults; // userName -> Runden-Ergebnisse
std::map<std::string, bool> _hasRolledThisRound; // userName -> hat in dieser Runde gewürfelt
std::thread _roundTimerThread;
void _handleDice();
void _startDiceRound();
void _endDiceRound();
void _initRound();
void _showDiceRoundResults();
// Neue Würfel-Funktionen
void _startRoundTimer();
void _endRoundTimer();
void _processRoundResults();
void _showGameResults();
void _resetRound();
bool _isUserAdmin(std::shared_ptr<ChatUser> user) const;
};
} // namespace Lib

View File

@@ -3,6 +3,7 @@
#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>
@@ -12,12 +13,22 @@
#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),
@@ -79,14 +90,20 @@ namespace Yc
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);
sendMsg(token, _token, "", "");
thread = std::make_unique<std::thread>(&ChatUser::checkerTask, this);
// 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()
{
_parent->addMessage(ChatUser::system, std::string("leaved_chat"), std::string(_name), std::string(_color));
// 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
@@ -111,6 +128,9 @@ namespace Yc
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;
@@ -121,6 +141,9 @@ namespace Yc
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;
@@ -131,28 +154,44 @@ namespace Yc
void ChatUser::checkerTask()
{
while (!_stop)
{
fd_set readSd;
FD_ZERO(&readSd);
FD_SET(_socket, &readSd);
timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 500;
int selectResult = select(_socket + 1, &readSd, NULL, NULL, &tv);
if (selectResult == 1 && FD_ISSET(_socket, &readSd) == 1)
try {
while (!_stop)
{
_parent->removeUser(_token);
_stop = true;
delete this;
return;
fd_set readSd;
FD_ZERO(&readSd);
FD_SET(_socket, &readSd);
timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 500;
int selectResult = select(_socket + 1, &readSd, NULL, NULL, &tv);
if (selectResult == 1 && FD_ISSET(_socket, &readSd) == 1)
{
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->removeUser(_token);
_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);
}
std::string msg = readSocket(_socket);
if (msg == "")
{
continue;
}
handleMessage(msg);
} catch (const std::exception& ex) {
// swallow, optional hook
} catch (...) {
// swallow, optional hook
}
}
@@ -171,19 +210,29 @@ namespace Yc
_parent = std::move(parent);
}
void ChatUser::send(std::string out)
{
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)
{
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;
@@ -194,7 +243,38 @@ namespace Yc
}
else if (jsonTree["type"].asString() == "dice")
{
doDice();
// 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")
{
@@ -202,7 +282,68 @@ namespace Yc
}
else if (jsonTree["type"].asString() == "do")
{
_parent->addMessage(ChatUser::dosomething, jsonTree["message"].asString(), std::string(_name), std::string(_color));
// "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")
{
@@ -230,10 +371,90 @@ namespace Yc
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));
@@ -271,5 +492,10 @@ namespace Yc
}
}
void ChatUser::start() {
auto self = shared_from_this();
thread = std::thread([self]() { self->checkerTask(); });
}
} // namespace Lib
} // namespace Yc

View File

@@ -42,6 +42,7 @@ namespace Yc
void sendMsg(MsgType type, const char *message, std::string userName, std::string color);
void sendMsg(MsgType type, Json::Value message, std::string userName, std::string color);
void checkerTask();
void start();
void stop();
std::string color() const;
void setParent(std::shared_ptr<ChatRoom> parent);
@@ -54,7 +55,7 @@ namespace Yc
int _socket;
std::string _token;
bool _stop;
std::unique_ptr<std::thread> thread;
std::thread thread;
void send(std::string out);
void send(Json::Value out);

View File

@@ -12,8 +12,29 @@ namespace Yc {
}
void Config::loadConfig() {
std::ifstream configStream("/etc/yourpart/chatconfig.json", std::ifstream::binary);
configStream >> jsonConfig;
// Try system-wide path first
{
std::ifstream configStream("/etc/yourpart/chatconfig.json", std::ifstream::binary);
if (configStream.good()) {
configStream >> jsonConfig;
}
}
// Fallback to relative project config path
if (jsonConfig.isNull() || jsonConfig.empty()) {
std::ifstream localStream("config/chatconfig.json", std::ifstream::binary);
if (localStream.good()) {
localStream >> jsonConfig;
}
}
// Graceful defaults if still empty
if (jsonConfig.isNull() || jsonConfig.empty()) {
Json::Value def;
def["server"]["port"] = 1235;
Json::Value rooms(Json::arrayValue);
Json::Value r1; r1["name"] = "Halle"; r1["password"] = ""; r1["allowed"] = Json::arrayValue; r1["type"] = 0; r1["roundlength"] = 0; rooms.append(r1);
def["rooms"] = rooms;
jsonConfig = def;
}
}
Json::Value Config::value(std::string groupName, std::string field) {

View File

@@ -10,7 +10,9 @@
#include <json/json.h>
#include <iostream>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <sstream>
#include <regex>
namespace Yc {
namespace Lib {
@@ -19,18 +21,45 @@ namespace Yc {
_config(std::move(config)),
_database(std::move(database)),
_stop(false) {
struct sockaddr_in serverAddr;
int opt = true;
_socket = socket(AF_INET, SOCK_STREAM, 0);
setsockopt(_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(opt));
int flags = 1;
setsockopt(_socket, IPPROTO_TCP, TCP_NODELAY, (void *)&flags, sizeof(flags));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(1235);
if (bind(_socket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
std::cout << "bind not possible" << std::endl;
exit(-1);
int port = 1235;
try {
Json::Value p = _config->value("server", "port");
if (p.isInt()) port = p.asInt();
} catch (...) {}
int opt = 1;
// Try IPv6 dual-stack (accepts IPv4 via v4-mapped if v6only=0)
_socket = socket(AF_INET6, SOCK_STREAM, 0);
if (_socket >= 0) {
setsockopt(_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(opt));
int v6only = 0; setsockopt(_socket, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only));
int flags = 1; setsockopt(_socket, IPPROTO_TCP, TCP_NODELAY, (void *)&flags, sizeof(flags));
int ka = 1; setsockopt(_socket, SOL_SOCKET, SO_KEEPALIVE, &ka, sizeof(ka));
struct sockaddr_in6 addr6{}; addr6.sin6_family = AF_INET6; addr6.sin6_addr = in6addr_any; addr6.sin6_port = htons(port);
if (bind(_socket, (struct sockaddr *)&addr6, sizeof(addr6)) == 0) {
std::cout << "[YourChat] Server gestartet. Lausche auf Port " << port << " (IPv6 dual-stack)" << std::endl;
} else {
// Fallback to IPv4
close(_socket);
_socket = socket(AF_INET, SOCK_STREAM, 0);
setsockopt(_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(opt));
flags = 1; setsockopt(_socket, IPPROTO_TCP, TCP_NODELAY, (void *)&flags, sizeof(flags));
ka = 1; setsockopt(_socket, SOL_SOCKET, SO_KEEPALIVE, &ka, sizeof(ka));
struct sockaddr_in addr4{}; addr4.sin_family = AF_INET; addr4.sin_addr.s_addr = INADDR_ANY; addr4.sin_port = htons(port);
if (bind(_socket, (struct sockaddr *)&addr4, sizeof(addr4)) != 0) {
std::cout << "bind not possible" << std::endl; exit(-1);
}
std::cout << "[YourChat] Server gestartet. Lausche auf Port " << port << " (IPv4)" << std::endl;
}
} else {
// Fallback to IPv4 directly
_socket = socket(AF_INET, SOCK_STREAM, 0);
if (_socket < 0) { std::cout << "socket create failed" << std::endl; exit(-1);}
setsockopt(_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(opt));
int flags = 1; setsockopt(_socket, IPPROTO_TCP, TCP_NODELAY, (void *)&flags, sizeof(flags));
int ka = 1; setsockopt(_socket, SOL_SOCKET, SO_KEEPALIVE, &ka, sizeof(ka));
struct sockaddr_in addr4{}; addr4.sin_family = AF_INET; addr4.sin_addr.s_addr = INADDR_ANY; addr4.sin_port = htons(port);
if (bind(_socket, (struct sockaddr *)&addr4, sizeof(addr4)) != 0) { std::cout << "bind not possible" << std::endl; exit(-1);}
std::cout << "[YourChat] Server gestartet. Lausche auf Port " << port << " (IPv4)" << std::endl;
}
}
@@ -42,14 +71,58 @@ namespace Yc {
timeval origTv;
origTv.tv_sec = 5;
origTv.tv_usec = 0;
int _maxSd = _socket;
int _maxSd = _socket;
std::set<int> activeSockets;
std::mutex socketMutex;
while (!_stop) {
timeval tv(origTv);
fd_set fd;
FD_ZERO(&fd);
FD_SET(_socket, &fd);
// Alle aktiven Client-Sockets hinzufügen
{
std::lock_guard<std::mutex> lock(socketMutex);
for (int clientSock : activeSockets) {
FD_SET(clientSock, &fd);
if (clientSock > _maxSd) _maxSd = clientSock;
}
}
if (select(_maxSd + 1, &fd, NULL, NULL, &tv) > 0) {
std::thread(&Server::handleRequest, this).detach();
// Neue Verbindung?
if (FD_ISSET(_socket, &fd)) {
std::thread(&Server::handleRequest, this).detach();
}
// Client-Socket-Aktivität?
{
std::lock_guard<std::mutex> lock(socketMutex);
auto it = activeSockets.begin();
while (it != activeSockets.end()) {
if (FD_ISSET(*it, &fd)) {
// Socket ist aktiv, aber handleRequest läuft bereits in separatem Thread
// Hier könnten wir Heartbeat/Keepalive prüfen
++it;
} else {
++it;
}
}
}
}
// Aufgeräumte Sockets entfernen
{
std::lock_guard<std::mutex> lock(socketMutex);
auto it = activeSockets.begin();
while (it != activeSockets.end()) {
if (activeSockets.count(*it) == 0) {
it = activeSockets.erase(it);
} else {
++it;
}
}
}
}
}
@@ -73,16 +146,16 @@ namespace Yc {
return list;
}
bool Server::roomAllowed(std::string roomName, std::string userName, std::string password){
bool Server::roomAllowed(std::string roomName, std::string userName, std::string password){
for (auto &room: _rooms) {
if (room->name() == roomName && room->accessAllowed(userName, password)) {
if (room->name() == roomName && room->accessAllowed(userName, password)) {
return true;
}
}
return false;
}
bool Server::changeRoom(std::shared_ptr<ChatUser> user, std::string newRoom, std::string password) {
bool Server::changeRoom(std::shared_ptr<ChatUser> user, std::string newRoom, std::string password) {
if (!roomAllowed(newRoom, user->name(), password)) {
return false;
}
@@ -113,56 +186,247 @@ namespace Yc {
}
void Server::createRooms(Json::Value roomList) {
// Ignoriere roomList, lade stattdessen aus der Datenbank
auto self = shared_from_this();
std::string query = R"(
SELECT r.id, r.title, r.password_hash, r.room_type_id, r.is_public, r.owner_id, r.min_age, r.max_age, r.created_at, r.updated_at, rt.tr as room_type
FROM chat.room r
LEFT JOIN chat.room_type rt ON r.room_type_id = rt.id
)";
auto result = _database->exec(query);
for (const auto& row : result) {
Json::Value room;
room["id"] = row["id"].as<int>();
room["name"] = row["title"].c_str();
room["password"] = row["password_hash"].is_null() ? "" : row["password_hash"].c_str();
room["type"] = row["room_type_id"].is_null() ? 0 : row["room_type_id"].as<int>();
room["is_public"] = row["is_public"].as<bool>();
room["owner_id"] = row["owner_id"].is_null() ? 0 : row["owner_id"].as<int>();
room["min_age"] = row["min_age"].is_null() ? 0 : row["min_age"].as<int>();
room["max_age"] = row["max_age"].is_null() ? 0 : row["max_age"].as<int>();
room["created_at"] = row["created_at"].c_str();
room["updated_at"] = row["updated_at"].c_str();
room["room_type"] = row["room_type"].is_null() ? "" : row["room_type"].c_str();
// Platzhalter für Felder, die im Konstruktor benötigt werden
room["allowed"] = Json::arrayValue; // ggf. später befüllen
room["roundlength"] = 60; // Default-Wert
auto newRoom = std::make_shared<ChatRoom>(self, room);
_rooms.push_back(newRoom);
bool created = false;
try {
std::string query = R"(
SELECT r.id, r.title, r.password_hash, r.room_type_id, r.is_public, r.owner_id, r.min_age, r.max_age, r.created_at, r.updated_at, rt.tr as room_type
FROM chat.room r
LEFT JOIN chat.room_type rt ON r.room_type_id = rt.id
)";
auto result = _database->exec(query);
for (const auto& row : result) {
Json::Value room;
room["id"] = row["id"].as<int>();
room["name"] = row["title"].c_str();
room["password"] = row["password_hash"].is_null() ? "" : row["password_hash"].c_str();
room["type"] = row["room_type_id"].is_null() ? 0 : row["room_type_id"].as<int>();
room["is_public"] = row["is_public"].as<bool>();
room["owner_id"] = row["owner_id"].is_null() ? 0 : row["owner_id"].as<int>();
room["min_age"] = row["min_age"].is_null() ? 0 : row["min_age"].as<int>();
room["max_age"] = row["max_age"].is_null() ? 0 : row["max_age"].as<int>();
room["created_at"] = row["created_at"].c_str();
room["updated_at"] = row["updated_at"].c_str();
room["room_type"] = row["room_type"].is_null() ? "" : row["room_type"].c_str();
// Platzhalter für Felder, die im Konstruktor benötigt werden
room["allowed"] = Json::arrayValue; // ggf. später befüllen
room["roundlength"] = 60; // Default-Wert
auto newRoom = std::make_shared<ChatRoom>(self, room);
_rooms.push_back(newRoom);
created = true;
}
} catch (...) {
// ignore DB errors, fallback below
}
if (!created) {
// fallback to provided JSON room list (if any)
if (roomList.isArray() && roomList.size() > 0) {
for (const auto& room : roomList) {
auto newRoom = std::make_shared<ChatRoom>(self, room);
_rooms.push_back(newRoom);
created = true;
}
} else {
// final fallback: builtin default room
Json::Value room;
room["name"] = "Halle";
room["password"] = "";
room["allowed"] = Json::arrayValue;
room["type"] = 0;
room["roundlength"] = 0;
auto newRoom = std::make_shared<ChatRoom>(self, room);
_rooms.push_back(newRoom);
created = true;
}
}
}
void Server::handleRequest() {
void Server::handleRequest() {
struct sockaddr_in sockAddr;
socklen_t sockAddrLen = sizeof(sockAddr);
int userSock = accept(_socket, (struct sockaddr *)&sockAddr, &sockAddrLen);
if (userSock < 0) {
return;
}
// Neuen Socket zur Überwachung hinzufügen
{
std::lock_guard<std::mutex> lock(socketMutex);
activeSockets.insert(userSock);
}
// Log jede akzeptierte Verbindung
char clientIP[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(sockAddr.sin_addr), clientIP, INET_ADDRSTRLEN);
std::cout << "[YourChat] Verbindung akzeptiert von " << clientIP << ":" << ntohs(sockAddr.sin_port) << " (fd=" << userSock << ")" << std::endl;
int flags = 1;
setsockopt(userSock, IPPROTO_TCP, TCP_NODELAY, (void *)&flags, sizeof(flags));
int ka2 = 1; setsockopt(userSock, SOL_SOCKET, SO_KEEPALIVE, &ka2, sizeof(ka2));
// Begrenze Blockierzeit beim Senden, um langsame Clients nicht alle zu verzögern
timeval sendTimeout; sendTimeout.tv_sec = 0; sendTimeout.tv_usec = 500000; // 500ms
setsockopt(userSock, SOL_SOCKET, SO_SNDTIMEO, &sendTimeout, sizeof(sendTimeout));
std::string msg = readSocket(userSock);
#ifdef YC_DEBUG
std::cout << "[Debug] Neue Anfrage erhalten: " << msg << std::endl;
#endif
if (msg == "") {
return;
}
inputSwitcher(userSock, msg);
// OPTIONS Request (CORS Preflight)
if (msg.rfind("OPTIONS ", 0) == 0) {
std::ostringstream resp;
resp << "HTTP/1.1 200 OK\r\n"
<< "Access-Control-Allow-Origin: *\r\n"
<< "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n"
<< "Access-Control-Allow-Headers: Upgrade, Connection, Sec-WebSocket-Key, Sec-WebSocket-Version, Sec-WebSocket-Protocol, Origin\r\n"
<< "Access-Control-Allow-Credentials: true\r\n"
<< "Access-Control-Max-Age: 86400\r\n"
<< "Content-Length: 0\r\n"
<< "\r\n";
::send(userSock, resp.str().c_str(), resp.str().size(), 0);
close(userSock);
return;
}
// WebSocket Upgrade?
if (msg.rfind("GET ", 0) == 0 && msg.find("Upgrade: websocket") != std::string::npos) {
// sehr einfacher Header-Parser
std::string key;
std::string subprotocol;
std::string origin;
std::string version;
std::string extensions;
std::istringstream iss(msg);
std::string line;
#ifdef YC_DEBUG
std::cout << "[Debug] === WebSocket Headers ===" << std::endl;
#endif
while (std::getline(iss, line)) {
if (!line.empty() && (line.back() == '\r' || line.back() == '\n')) line.pop_back();
auto pos = line.find(":");
if (pos != std::string::npos) {
std::string h = line.substr(0, pos);
std::string v = line.substr(pos+1);
// trim
auto ltrim = [](std::string &s){ s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch){return !std::isspace(ch);}));};
auto rtrim = [](std::string &s){ s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch){return !std::isspace(ch);}).base(), s.end());};
ltrim(h); rtrim(h); ltrim(v); rtrim(v);
#ifdef YC_DEBUG
std::cout << "[Debug] Header: '" << h << "' = '" << v << "'" << std::endl;
#endif
if (strcasecmp(h.c_str(), "Sec-WebSocket-Key") == 0) {
key = v;
} else if (strcasecmp(h.c_str(), "Sec-WebSocket-Protocol") == 0) {
subprotocol = v;
} else if (strcasecmp(h.c_str(), "Origin") == 0) {
origin = v;
} else if (strcasecmp(h.c_str(), "Sec-WebSocket-Version") == 0) {
version = v;
} else if (strcasecmp(h.c_str(), "Sec-WebSocket-Extensions") == 0) {
extensions = v;
}
}
if (line.empty()) break;
}
#ifdef YC_DEBUG
std::cout << "[Debug] === Parsed Values ===" << std::endl;
std::cout << "[Debug] Key: " << (key.empty() ? "MISSING" : key) << std::endl;
std::cout << "[Debug] Protocol: " << (subprotocol.empty() ? "NONE" : subprotocol) << std::endl;
std::cout << "[Debug] Origin: " << (origin.empty() ? "MISSING" : origin) << std::endl;
std::cout << "[Debug] Version: " << (version.empty() ? "MISSING" : version) << std::endl;
std::cout << "[Debug] Extensions: " << (extensions.empty() ? "NONE" : extensions) << std::endl;
std::cout << "[Debug] ======================" << std::endl;
#endif
if (!key.empty()) {
std::string accept = Base::webSocketAcceptKey(key);
std::ostringstream resp;
resp << "HTTP/1.1 101 Switching Protocols\r\n"
<< "Upgrade: websocket\r\n"
<< "Connection: Upgrade\r\n"
<< "Sec-WebSocket-Accept: " << accept << "\r\n";
// CORS-Header hinzufügen
resp << "Access-Control-Allow-Origin: *\r\n"
<< "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n"
<< "Access-Control-Allow-Headers: Upgrade, Connection, Sec-WebSocket-Key, Sec-WebSocket-Version, Sec-WebSocket-Protocol, Origin\r\n"
<< "Access-Control-Allow-Credentials: true\r\n";
// Subprotokoll-Unterstützung
if (!subprotocol.empty()) {
resp << "Sec-WebSocket-Protocol: " << subprotocol << "\r\n";
} else {
// Fallback: Standard-Subprotokoll anbieten
resp << "Sec-WebSocket-Protocol: chat\r\n";
}
resp << "\r\n";
#ifdef YC_DEBUG
std::cout << "[Debug] === WebSocket Response ===" << std::endl;
std::cout << "[Debug] " << resp.str() << std::endl;
std::cout << "[Debug] =========================" << std::endl;
#endif
::send(userSock, resp.str().c_str(), resp.str().size(), 0);
Base::markWebSocket(userSock);
// Sofort eine Willkommen-Nachricht senden, um Firefox's Timing-Problem zu lösen
// Verwende korrektes WebSocket-Framing
std::string welcomeMsg = "{\"type\":\"welcome\",\"message\":\"WebSocket-Verbindung hergestellt\"}";
Base::sendWebSocketMessage(userSock, welcomeMsg);
#ifdef YC_DEBUG
std::cout << "[Debug] WS upgrade ok, welcome message sent, waiting for init... fd=" << userSock << std::endl;
#endif
// Jetzt WebSocket Nachrichten lesen und an inputSwitcher weitergeben
bool ownedByUser = false;
while (true) {
std::string wmsg = readSocket(userSock);
if (wmsg.empty()) break;
ownedByUser = inputSwitcher(userSock, wmsg);
if (ownedByUser) break;
}
if (ownedByUser) {
#ifdef YC_DEBUG
std::cout << "[Debug] Ownership transferred to ChatUser, stop reading in Server for fd=" << userSock << std::endl;
#endif
}
if (!ownedByUser) {
#ifdef YC_DEBUG
std::cout << "[Debug] WS closing (no ownership) fd=" << userSock << std::endl;
#endif
Base::unmarkWebSocket(userSock);
close(userSock);
}
return;
}
}
// Fallback: Plain JSON
bool owned = inputSwitcher(userSock, msg);
if (!owned) {
#ifdef YC_DEBUG
std::cout << "[Debug] Plain JSON path without ownership, closing fd=" << userSock << std::endl;
#endif
close(userSock);
}
}
void Server::inputSwitcher(int userSocket, std::string input) {
bool Server::inputSwitcher(int userSocket, std::string input) {
Json::Value inputTree = getJsonTree(input);
#ifdef YC_DEBUG
std::cout << "[Debug] inputSwitcher: type=" << inputTree["type"].asString() << std::endl;
#endif
if (inputTree["type"] == "init") {
initUser(userSocket, inputTree);
return true; // ChatUser übernimmt nun den Socket
} else {
}
return false;
}
bool Server::userExists(std::string userName) {
@@ -175,7 +439,23 @@ namespace Yc {
}
void Server::initUser(int userSocket, Json::Value data) {
if (userExists(data["name"].asString())) {
std::string name = data.isMember("name") ? data["name"].asString() : "";
std::string room = data.isMember("room") ? data["room"].asString() : "";
std::string color = data.isMember("color") ? data["color"].asString() : "#000000";
std::string password = data.isMember("password") ? data["password"].asString() : "";
#ifdef YC_DEBUG
std::cout << "[Debug] initUser: name=" << name << ", room=" << room << ", color=" << color << std::endl;
#endif
if (name.empty() || room.empty()) {
Json::Value errorJson;
errorJson["type"] = ChatUser::error;
errorJson["message"] = "missing_fields";
errorJson["detail"] = "'name' und 'room' müssen gesetzt sein.";
send(userSocket, errorJson);
close(userSocket);
return;
}
if (userExists(name)) {
Json::Value errorJson;
errorJson["type"] = ChatUser::error;
errorJson["message"] = "loggedin";
@@ -183,17 +463,20 @@ namespace Yc {
close(userSocket);
return;
}
std::string roomName = data["room"].asString();
bool added(false);
for (auto &room: _rooms) {
if (room->name() == roomName) {
if (room->addUser(data["name"].asString(), data["color"].asString(), data["password"].asString(), userSocket)) {
for (auto &roomObj: _rooms) {
if (roomObj->name() == room) {
if (roomObj->addUser(name, color, password, userSocket)) {
added = true;
break;
}
}
}
if (!added) {
Json::Value errorJson;
errorJson["type"] = ChatUser::error;
errorJson["message"] = "room_not_found_or_join_failed";
send(userSocket, errorJson);
close(userSocket);
}
}

View File

@@ -28,9 +28,14 @@ namespace Yc {
void createRooms(Json::Value roomList);
private:
void handleRequest();
void inputSwitcher(int userSocket, std::string input);
// returns true if the connection is now owned by a ChatUser thread
bool inputSwitcher(int userSocket, std::string input);
bool userExists(std::string userName);
void initUser(int userSocket, Json::Value data);
// Socket-Überwachung
std::set<int> activeSockets;
std::mutex socketMutex;
};
} // namespace Lib