From 23c07a3570ad8fcdfa60948f7668c270fe8f5c76 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Sun, 31 Aug 2025 23:11:09 +0200 Subject: [PATCH] =?UTF-8?q?F=C3=BCge=20UndergroundWorker=20hinzu=20und=20i?= =?UTF-8?q?mplementiere=20Logik=20f=C3=BCr=20unterirdische=20Aufgaben.=20A?= =?UTF-8?q?ktualisiere=20CMakeLists.txt,=20um=20neue=20Quell-=20und=20Head?= =?UTF-8?q?er-Dateien=20einzuschlie=C3=9Fen.=20Verbessere=20die=20Fehlerbe?= =?UTF-8?q?handlung=20in=20der=20Datenbank=20und=20sende=20Benachrichtigun?= =?UTF-8?q?gen=20nach=20bestimmten=20Ereignissen.=20Integriere=20Hilfsfunk?= =?UTF-8?q?tionen=20zur=20sicheren=20Verarbeitung=20von=20Daten.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 4 +- CMakeLists.txt.user | 22 +- deploy.sh | 203 ++++++++++++-- install-dependencies.sh | 159 +++++++++++ src/character_creation_worker.cpp | 8 + src/database.cpp | 59 ++-- src/main.cpp | 2 + src/politics_worker.cpp | 27 ++ src/politics_worker.h | 53 ++++ src/produce_worker.cpp | 8 + src/underground_worker.cpp | 445 ++++++++++++++++++++++++++++++ src/underground_worker.h | 101 +++++++ src/usercharacterworker.cpp | 82 ++++-- src/usercharacterworker.h | 2 +- src/utils.cpp | 94 +++++++ src/utils.h | 30 ++ src/websocket_server.cpp | 5 + yourpart-daemon.service | 39 +++ 18 files changed, 1255 insertions(+), 88 deletions(-) create mode 100644 install-dependencies.sh create mode 100644 src/underground_worker.cpp create mode 100644 src/underground_worker.h create mode 100644 src/utils.cpp create mode 100644 src/utils.h create mode 100644 yourpart-daemon.service diff --git a/CMakeLists.txt b/CMakeLists.txt index 1775a1d..2878102 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,7 +63,9 @@ set(HEADERS ) # Define executable target -add_executable(yourpart-daemon ${SOURCES} ${HEADERS}) +add_executable(yourpart-daemon ${SOURCES} ${HEADERS} + src/utils.h src/utils.cpp + src/underground_worker.h src/underground_worker.cpp) # Include directories target_include_directories(yourpart-daemon PRIVATE diff --git a/CMakeLists.txt.user b/CMakeLists.txt.user index fb68610..bb3066d 100644 --- a/CMakeLists.txt.user +++ b/CMakeLists.txt.user @@ -1,6 +1,6 @@ - + EnvironmentId @@ -104,14 +104,14 @@ 2 false - -DCMAKE_BUILD_TYPE:STRING=Release + -DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} +-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON -DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} --DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} --DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} --DCMAKE_GENERATOR:STRING=Unix Makefiles -DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake -DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} --DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON +-DCMAKE_GENERATOR:STRING=Unix Makefiles +-DCMAKE_BUILD_TYPE:STRING=Release +-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} /home/torsten/Programs/yourpart-daemon/build/ @@ -229,14 +229,14 @@ 2 false - -DCMAKE_BUILD_TYPE:STRING=Debug + -DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} +-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON -DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} --DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} --DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} --DCMAKE_GENERATOR:STRING=Unix Makefiles -DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake -DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} --DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON +-DCMAKE_GENERATOR:STRING=Unix Makefiles +-DCMAKE_BUILD_TYPE:STRING=Debug +-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} /mnt/share/torsten/Programs/yourpart-daemon /home/torsten/Programs/yourpart-daemon/build diff --git a/deploy.sh b/deploy.sh index 32d25e6..9e6f149 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,36 +1,183 @@ +# YourPart Daemon Deployment Script für Ubuntu 22 +# Verwendung: ./deploy.sh [server_ip] [ssh_user] + +set -euo pipefail + +# Farben für Output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Konfiguration +SERVER_IP="${1:-your-part.de}" +SSH_USER="${2:-root}" +DAEMON_USER="yourpart" +PROJECT_NAME="yourpart-daemon" +REMOTE_DIR="/opt/yourpart" +SERVICE_NAME="yourpart-daemon" + +# Funktionen +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Prüfe ob wir im richtigen Verzeichnis sind +if [ ! -f "CMakeLists.txt" ] || [ ! -f "daemon.conf" ]; then + log_error "Bitte führen Sie dieses Script aus dem Projektverzeichnis aus!" + exit 1 +fi + +log_info "Starte Deployment für YourPart Daemon..." +log_info "Server: $SERVER_IP" +log_info "SSH User: $SSH_USER" + +# 1. Lokales Build +log_info "Baue Projekt lokal..." +if [ ! -d "build" ]; then + mkdir build +fi + +cd build +cmake .. -DCMAKE_BUILD_TYPE=Release +make -j$(nproc) +cd .. + +log_success "Lokaler Build abgeschlossen" + +# 2. Erstelle Deployment-Paket +log_info "Erstelle Deployment-Paket..." +DEPLOY_DIR="deploy_package" +rm -rf "$DEPLOY_DIR" +mkdir -p "$DEPLOY_DIR" + +# Kopiere Binärdatei +cp build/yourpart-daemon "$DEPLOY_DIR/" + +# Kopiere Konfigurationsdatei +cp daemon.conf "$DEPLOY_DIR/" + +# Kopiere Service-Datei +cp yourpart-daemon.service "$DEPLOY_DIR/" + +# Erstelle Installations-Script +cat > "$DEPLOY_DIR/install.sh" << 'EOF' #!/bin/bash +set -euo pipefail -echo "=== YourPart Deployment Script ===" -echo "" +# Farben +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' -# Prüfen ob wir im richtigen Verzeichnis sind -if [ ! -f "package.json" ]; then - echo "Error: Please run this script from the YourPart3 root directory" - exit 1 +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +DAEMON_USER="yourpart" +REMOTE_DIR="/opt/yourpart" +SERVICE_NAME="yourpart-daemon" + +log_info "Installiere YourPart Daemon..." + +# Erstelle Benutzer falls nicht vorhanden +if ! id "$DAEMON_USER" &>/dev/null; then + log_info "Erstelle Benutzer $DAEMON_USER..." + useradd --system --shell /bin/false --home-dir "$REMOTE_DIR" --create-home "$DAEMON_USER" + log_success "Benutzer $DAEMON_USER erstellt" +else + log_info "Benutzer $DAEMON_USER existiert bereits" fi -# Prüfen ob sudo verfügbar ist -if ! command -v sudo &> /dev/null; then - echo "Error: sudo is required but not installed" - exit 1 -fi +# Erstelle Verzeichnisse +log_info "Erstelle Verzeichnisse..." +mkdir -p "$REMOTE_DIR"/{logs,config} +mkdir -p /etc/yourpart +mkdir -p /var/log/yourpart -# Backend deployen -echo "" -echo "=== Deploying Backend ===" -./deploy-backend.sh +# Kopiere Dateien +log_info "Kopiere Dateien..." +cp yourpart-daemon /usr/local/bin/ +cp daemon.conf /etc/yourpart/ +cp yourpart-daemon.service /etc/systemd/system/ -# Frontend bauen und deployen -echo "" -echo "=== Building and Deploying Frontend ===" -./deploy-frontend.sh +# Setze Berechtigungen +chmod +x /usr/local/bin/yourpart-daemon +chown -R "$DAEMON_USER:$DAEMON_USER" "$REMOTE_DIR" +chown -R "$DAEMON_USER:$DAEMON_USER" /var/log/yourpart +chmod 600 /etc/yourpart/daemon.conf -echo "" -echo "=== Deployment Completed! ===" -echo "Your application should now be available at:" -echo " HTTP: http://your-part.de (redirects to HTTPS)" -echo " HTTPS: https://www.your-part.de" -echo "" -echo "To check logs:" -echo " Backend: sudo journalctl -u yourpart.service -f" -echo " Apache: sudo tail -f /var/log/apache2/yourpart.*.log" +# Lade systemd neu +log_info "Lade systemd Konfiguration neu..." +systemctl daemon-reload + +# Aktiviere Service +log_info "Aktiviere Service..." +systemctl enable "$SERVICE_NAME" + +log_success "Installation abgeschlossen!" +log_info "Verwenden Sie 'systemctl start $SERVICE_NAME' um den Service zu starten" +log_info "Verwenden Sie 'systemctl status $SERVICE_NAME' um den Status zu prüfen" +log_info "Verwenden Sie 'journalctl -u $SERVICE_NAME -f' um die Logs zu verfolgen" +EOF + +chmod +x "$DEPLOY_DIR/install.sh" + +# Erstelle Tarball +tar -czf "${PROJECT_NAME}_deploy.tar.gz" -C "$DEPLOY_DIR" . + +log_success "Deployment-Paket erstellt: ${PROJECT_NAME}_deploy.tar.gz" + +# 3. Upload zum Server +log_info "Lade Dateien zum Server hoch..." +scp "${PROJECT_NAME}_deploy.tar.gz" "$SSH_USER@$SERVER_IP:/tmp/" + +# 4. Installation auf dem Server +log_info "Installiere auf dem Server..." +ssh "$SSH_USER@$SERVER_IP" << EOF +set -euo pipefail + +# Entpacke Deployment-Paket +cd /tmp +tar -xzf "${PROJECT_NAME}_deploy.tar.gz" + +# Führe Installation aus +./install.sh + +# Starte Service +systemctl start $SERVICE_NAME + +# Prüfe Status +systemctl status $SERVICE_NAME --no-pager + +# Aufräumen +rm -f "${PROJECT_NAME}_deploy.tar.gz" +rm -rf /tmp/yourpart-daemon /tmp/daemon.conf /tmp/yourpart-daemon.service /tmp/install.sh + +echo "Deployment erfolgreich abgeschlossen!" +EOF + +# 5. Aufräumen +log_info "Räume lokale Dateien auf..." +rm -rf "$DEPLOY_DIR" +rm -f "${PROJECT_NAME}_deploy.tar.gz" + +log_success "Deployment erfolgreich abgeschlossen!" +log_info "Der YourPart Daemon läuft jetzt auf $SERVER_IP" +log_info "Verwenden Sie 'ssh $SSH_USER@$SERVER_IP systemctl status $SERVICE_NAME' um den Status zu prüfen" diff --git a/install-dependencies.sh b/install-dependencies.sh new file mode 100644 index 0000000..a5f18d6 --- /dev/null +++ b/install-dependencies.sh @@ -0,0 +1,159 @@ +#!/bin/bash + +# YourPart Daemon Dependencies Installation Script für Ubuntu 22 +# Führen Sie dieses Script auf dem Server aus, bevor Sie das Deployment durchführen + +set -euo pipefail + +# Farben für Output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +log_info "Installiere Dependencies für YourPart Daemon auf Ubuntu 22..." + +# Update Package Lists +log_info "Aktualisiere Paketlisten..." +apt update + +# Installiere Build-Tools +log_info "Installiere Build-Tools..." +apt install -y \ + build-essential \ + cmake \ + pkg-config \ + git \ + curl \ + wget + +# Installiere C++ Compiler (Ubuntu 22 hat GCC 11, aber wir brauchen GCC 15) +log_info "Installiere GCC 15..." +apt install -y software-properties-common +add-apt-repository -y ppa:ubuntu-toolchain-r/test +apt update +apt install -y gcc-15 g++-15 + +# Setze GCC 15 als Standard +update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 100 +update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-15 100 + +# Installiere PostgreSQL Development Libraries +log_info "Installiere PostgreSQL Development Libraries..." +apt install -y \ + postgresql-server-dev-14 \ + libpq-dev \ + libpqxx-dev + +# Installiere libwebsockets +log_info "Installiere libwebsockets..." +apt install -y \ + libwebsockets-dev \ + libssl-dev \ + libz-dev + +# Installiere nlohmann-json +log_info "Installiere nlohmann-json..." +apt install -y nlohmann-json3-dev + +# Installiere PostgreSQL Server (falls nicht vorhanden) +log_info "Prüfe PostgreSQL Installation..." +if ! systemctl is-active --quiet postgresql; then + log_info "Installiere PostgreSQL Server..." + apt install -y postgresql postgresql-contrib + + # Starte PostgreSQL + systemctl start postgresql + systemctl enable postgresql + + log_success "PostgreSQL installiert und gestartet" +else + log_success "PostgreSQL läuft bereits" +fi + +# Erstelle Datenbank und Benutzer +log_info "Konfiguriere PostgreSQL..." +sudo -u postgres psql << EOF +-- Erstelle Benutzer falls nicht vorhanden +DO \$\$ +BEGIN + IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'yourpart') THEN + CREATE USER yourpart WITH PASSWORD 'hitomisan'; + END IF; +END +\$\$; + +-- Erstelle Datenbank falls nicht vorhanden +SELECT 'CREATE DATABASE yp3 OWNER yourpart' +WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'yp3')\gexec + +-- Setze Berechtigungen +GRANT ALL PRIVILEGES ON DATABASE yp3 TO yourpart; +\q +EOF + +log_success "PostgreSQL konfiguriert" + +# Installiere systemd (sollte bereits vorhanden sein) +log_info "Prüfe systemd..." +if ! command -v systemctl &> /dev/null; then + log_error "systemd ist nicht installiert. Bitte installieren Sie Ubuntu 22 LTS." + exit 1 +fi + +log_success "systemd verfügbar" + +# Installiere zusätzliche Tools für Monitoring +log_info "Installiere Monitoring-Tools..." +apt install -y \ + htop \ + iotop \ + netstat-nat \ + lsof + +# Konfiguriere Firewall (falls ufw installiert ist) +if command -v ufw &> /dev/null; then + log_info "Konfiguriere Firewall..." + ufw allow 4551/tcp comment "YourPart Daemon WebSocket" + ufw allow 22/tcp comment "SSH" + log_success "Firewall konfiguriert" +fi + +# Erstelle Log-Verzeichnis +log_info "Erstelle Log-Verzeichnisse..." +mkdir -p /var/log/yourpart +chmod 755 /var/log/yourpart + +log_success "Alle Dependencies erfolgreich installiert!" +log_info "" +log_info "Nächste Schritte:" +log_info "1. Führen Sie das deploy.sh Script von Ihrem Entwicklungsrechner aus" +log_info "2. Oder kopieren Sie die Binärdatei manuell und konfigurieren Sie den Service" +log_info "" +log_info "Verfügbare Services:" +log_info "- PostgreSQL: systemctl status postgresql" +log_info "- Firewall: ufw status" +log_info "" +log_info "Datenbankverbindung:" +log_info "- Host: localhost" +log_info "- Port: 5432" +log_info "- Database: yp3" +log_info "- User: yourpart" +log_info "- Password: hitomisan" diff --git a/src/character_creation_worker.cpp b/src/character_creation_worker.cpp index 116b78d..a7b48d2 100644 --- a/src/character_creation_worker.cpp +++ b/src/character_creation_worker.cpp @@ -186,6 +186,14 @@ void CharacterCreationWorker::notifyUser(int userId, const std::string &eventTyp db.prepare("insert_notification", QUERY_INSERT_NOTIFICATION); db.execute("insert_notification", { std::to_string(userId) }); + // Sende falukantUpdateStatus nach dem Einfügen der Benachrichtigung + nlohmann::json updateMessage = { + {"event", "falukantUpdateStatus"}, + {"user_id", userId} + }; + broker.publish(updateMessage.dump()); + + // Sende auch die ursprüngliche Benachrichtigung nlohmann::json message = { {"event", eventType}, {"user_id", userId} diff --git a/src/database.cpp b/src/database.cpp index e3e7f9d..4c57be0 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -46,6 +46,14 @@ Database::query(const std::string &sql) void Database::prepare(const std::string &stmtName, const std::string &sql) { try { + // Versuche zuerst, das alte Statement zu entfernen, falls es existiert + try { + remove(stmtName); + } catch (...) { + // Ignoriere Fehler beim Entfernen - das Statement existiert möglicherweise nicht + } + + // Erstelle das neue Statement pqxx::work txn(*connection_); txn.conn().prepare(stmtName, sql); txn.commit(); @@ -55,31 +63,38 @@ void Database::prepare(const std::string &stmtName, const std::string &sql) } } -Database::FieldList Database::execute(const std::string &stmtName, const std::vector ¶ms) { - std::vector> rows; +Database::FieldList Database::execute(const std::string& stmtName, + const std::vector& params) +{ try { pqxx::work txn(*connection_); - pqxx::prepare::invocation inv = txn.prepared(stmtName); - for (auto &p : params) { - inv(p); - } - pqxx::result r = inv.exec(); - txn.commit(); - for (const auto &row : r) { - std::unordered_map mapRow; - for (pqxx::row::size_type i = 0; i < row.size(); ++i) { - std::string colName = r.column_name(i); - mapRow[colName] = row[i].c_str() ? row[i].c_str() : ""; - } - rows.push_back(std::move(mapRow)); - } - } - catch (const std::exception &ex) { - std::cerr << "[Database] execute-Fehler: " << ex.what() - << "\nStatement: " << stmtName << std::endl; - } - return rows; + pqxx::result res; + if (params.empty()) { + res = txn.exec_prepared(stmtName); + } else { + pqxx::params p; + for (const auto& v : params) p.append(v); + res = txn.exec_prepared(stmtName, p); + } + + FieldList out; + out.reserve(res.size()); + for (const auto& row : res) { + std::unordered_map m; + for (const auto& f : row) { + m.emplace(f.name(), f.is_null() ? std::string{} : std::string(f.c_str())); + } + out.emplace_back(std::move(m)); + } + + txn.commit(); + return out; + } catch (const std::exception& e) { + std::cerr << "[Database] execute-Fehler: " << e.what() + << "\n\nStatement: " << stmtName << std::endl; + return {}; + } } void Database::remove(const std::string &stmtName) { diff --git a/src/main.cpp b/src/main.cpp index 85c29a2..77fd994 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,6 +9,7 @@ #include "usercharacterworker.h" #include "houseworker.h" #include "politics_worker.h" +#include "underground_worker.h" #include "config.h" #include #include @@ -52,6 +53,7 @@ int main() { workers.push_back(std::make_unique(pool, broker)); workers.push_back(std::make_unique(pool, broker)); workers.push_back(std::make_unique(pool, broker)); + workers.push_back(std::make_unique(pool, broker)); websocketServer.setWorkers(workers); broker.start(); diff --git a/src/politics_worker.cpp b/src/politics_worker.cpp index d8188fb..0b7af48 100644 --- a/src/politics_worker.cpp +++ b/src/politics_worker.cpp @@ -180,6 +180,15 @@ void PoliticsWorker::notifyOfficeExpirations() { signalActivity(); db.execute("NOTIFY_OFFICE_EXPIRATION"); signalActivity(); + + // Sende falukantUpdateStatus an alle betroffenen Benutzer + db.prepare("GET_USERS_WITH_EXPIRING_OFFICES", QUERY_GET_USERS_WITH_EXPIRING_OFFICES); + const auto affectedUsers = db.execute("GET_USERS_WITH_EXPIRING_OFFICES"); + for (const auto &user : affectedUsers) { + int userId = std::stoi(user.at("user_id")); + nlohmann::json message = { { "event", "falukantUpdateStatus" } }; + sendMessageToFalukantUsers(userId, message); + } } void PoliticsWorker::notifyElectionCreated(const std::vector>& elections) { @@ -192,6 +201,15 @@ void PoliticsWorker::notifyElectionCreated(const std::vector> db.execute("NOTIFY_ELECTION_CREATED", { std::to_string(pr.first) }); signalActivity(); } + + // Sende falukantUpdateStatus an alle betroffenen Benutzer + db.prepare("GET_USERS_IN_REGIONS_WITH_ELECTIONS", QUERY_GET_USERS_IN_REGIONS_WITH_ELECTIONS); + const auto affectedUsers = db.execute("GET_USERS_IN_REGIONS_WITH_ELECTIONS"); + for (const auto &user : affectedUsers) { + int userId = std::stoi(user.at("user_id")); + nlohmann::json message = { { "event", "falukantUpdateStatus" } }; + sendMessageToFalukantUsers(userId, message); + } } void PoliticsWorker::notifyOfficeFilled(const std::vector>& newOffices) { @@ -205,6 +223,15 @@ void PoliticsWorker::notifyOfficeFilled(const std::vector> PoliticsWorker::processElections() { diff --git a/src/politics_worker.h b/src/politics_worker.h index 31985e6..cf35575 100644 --- a/src/politics_worker.h +++ b/src/politics_worker.h @@ -451,6 +451,59 @@ private: ($1, 'notify_office_filled', NOW(), NOW()); )"; + // ------------------------------------------------------------ + // QUERY: Hole alle Benutzer, deren Amt in 2 Tagen abläuft + // ------------------------------------------------------------ + static constexpr const char* QUERY_GET_USERS_WITH_EXPIRING_OFFICES = R"( + SELECT DISTINCT + ch.user_id + FROM + falukant_data.political_office AS po + JOIN + falukant_type.political_office_type AS pot + ON po.office_type_id = pot.id + JOIN + falukant_data."character" AS ch + ON po.character_id = ch.id + WHERE + ch.user_id IS NOT NULL + AND (po.created_at + (pot.term_length * INTERVAL '1 day')) + BETWEEN (NOW() + INTERVAL '2 days') + AND (NOW() + INTERVAL '2 days' + INTERVAL '1 second'); + )"; + + // ------------------------------------------------------------ + // QUERY: Hole alle Benutzer in Regionen mit neuen Wahlen + // ------------------------------------------------------------ + static constexpr const char* QUERY_GET_USERS_IN_REGIONS_WITH_ELECTIONS = R"( + SELECT DISTINCT + ch.user_id + FROM + falukant_data.election AS e + JOIN + falukant_data."character" AS ch + ON ch.region_id = e.region_id + WHERE + ch.user_id IS NOT NULL + AND e."date" >= NOW() - INTERVAL '1 day'; + )"; + + // ------------------------------------------------------------ + // QUERY: Hole alle Benutzer, deren Amt neu besetzt wurde + // ------------------------------------------------------------ + static constexpr const char* QUERY_GET_USERS_WITH_FILLED_OFFICES = R"( + SELECT DISTINCT + ch.user_id + FROM + falukant_data.political_office AS po + JOIN + falukant_data."character" AS ch + ON po.character_id = ch.id + WHERE + ch.user_id IS NOT NULL + AND po.created_at >= NOW() - INTERVAL '1 minute'; + )"; + static constexpr const char* QUERY_PROCESS_ELECTIONS = R"( SELECT office_id, office_type_id, character_id, region_id FROM falukant_data.process_elections(); diff --git a/src/produce_worker.cpp b/src/produce_worker.cpp index bca873f..3b611f3 100644 --- a/src/produce_worker.cpp +++ b/src/produce_worker.cpp @@ -120,6 +120,14 @@ bool ProduceWorker::addToInventory(Database &db, {"value", remainingQuantity} }; db.execute("QUERY_ADD_OVERPRODUCTION_NOTIFICATION", {std::to_string(userId), notification.dump()}); + + // Sende falukantUpdateStatus nach dem Einfügen der Benachrichtigung + nlohmann::json updateMessage = { + {"event", "falukantUpdateStatus"}, + {"user_id", userId} + }; + broker.publish(updateMessage.dump()); + return true; } catch (const std::exception &e) { std::cerr << "[ProduceWorker] Fehler in addToInventory: " << e.what() << std::endl; diff --git a/src/underground_worker.cpp b/src/underground_worker.cpp new file mode 100644 index 0000000..2fd6451 --- /dev/null +++ b/src/underground_worker.cpp @@ -0,0 +1,445 @@ +#include "underground_worker.h" +#include +#include +#include +#include +#include + +using json = nlohmann::json; + +UndergroundWorker::~UndergroundWorker() = default; + +static std::mt19937& rng() { + static thread_local std::mt19937 g{std::random_device{}()}; + return g; +} + +int UndergroundWorker::randomInt(int lo,int hi){ + std::uniform_int_distribution d(lo,hi); + return d(rng()); +} + +long long UndergroundWorker::randomLL(long long lo,long long hi){ + std::uniform_int_distribution d(lo,hi); + return d(rng()); +} + +std::vector UndergroundWorker::randomIndices(size_t n,size_t k){ + std::vector idx(n); + std::iota(idx.begin(),idx.end(),0); + std::shuffle(idx.begin(),idx.end(),rng()); + if(k UndergroundWorker::fetchPending(){ + ConnectionGuard g(pool); + auto& db=g.get(); + db.prepare("UG_SELECT_PENDING",Q_SELECT_PENDING); + return db.execute("UG_SELECT_PENDING"); +} + +nlohmann::json UndergroundWorker::executeRow(const Row& r){ + int performerId=std::stoi(r.at("performer_id")); + int victimId=std::stoi(r.at("victim_id")); + std::string type=r.at("underground_type"); + std::string params=r.at("parameters"); + return handleTask(type,performerId,victimId,params); +} + +nlohmann::json UndergroundWorker::handleTask(const std::string& type,int performerId,int victimId,const std::string& paramsJson){ + json p; try{ p=json::parse(paramsJson);} catch(...){ p=json::object(); } + if(type=="spyin") return spyIn(performerId,victimId,p); + if(type=="assassin") return assassin(performerId,victimId,p); + if(type=="sabotage") return sabotage(performerId,victimId,p); + if(type=="corrupt_politician") return corruptPolitician(performerId,victimId,p); + if(type=="rob") return rob(performerId,victimId,p); + return {{"status","unknown_type"},{"type",type}}; +} + +nlohmann::json UndergroundWorker::spyIn(int performerId,int victimId,const json& p){ + ConnectionGuard g(pool); + auto& db=g.get(); + db.prepare("UG_SELECT_BY_PERFORMER",Q_SELECT_BY_PERFORMER); + const auto rows = db.execute("UG_SELECT_BY_PERFORMER",{ std::to_string(victimId) }); + + json activities = json::array(); + for(const auto& r : rows){ + json params = json::object(); + try{ params = json::parse(r.at("parameters")); }catch(...){} + json result = nullptr; + auto it = r.find("result_text"); + if(it != r.end()){ + try{ result = json::parse(it->second); }catch(...){} + } + std::string status = "pending"; + if(result.is_object()){ + if(auto s = result.find("status"); s!=result.end() && s->is_string()) status = *s; + else status = "done"; + } + activities.push_back({ + {"id", std::stoi(r.at("id"))}, + {"type", r.at("underground_type")}, + {"performed_by", std::stoi(r.at("performer_id"))}, + {"victim_id", std::stoi(r.at("victim_id"))}, + {"created_at", r.at("created_at")}, + {"parameters", params}, + {"result", result}, + {"status", status} + }); + } + return { + {"status","success"}, + {"action","spyin"}, + {"performer_id", performerId}, + {"victim_id", victimId}, + {"details", p}, + {"victim_illegal_activity_count", activities.size()}, + {"victim_illegal_activities", activities} + }; +} + +nlohmann::json UndergroundWorker::assassin(int performerId,int victimId,const json& p){ + ConnectionGuard g(pool); + auto& db=g.get(); + db.prepare("UG_SELECT_CHAR_HEALTH",Q_SELECT_CHAR_HEALTH); + db.prepare("UG_UPDATE_CHAR_HEALTH",Q_UPDATE_CHAR_HEALTH); + const auto rows=db.execute("UG_SELECT_CHAR_HEALTH",{std::to_string(victimId)}); + if(rows.empty()) return {{"status","error"},{"action","assassin"},{"performer_id",performerId},{"victim_id",victimId},{"message","victim_not_found"},{"details",p}}; + int current=std::stoi(rows.front().at("health")); + std::uniform_int_distribution dist(0,current); + int new_health=dist(rng()); + db.execute("UG_UPDATE_CHAR_HEALTH",{std::to_string(victimId),std::to_string(new_health)}); + return {{"status","success"},{"action","assassin"},{"performer_id",performerId},{"victim_id",victimId},{"details",p},{"previous_health",current},{"new_health",new_health},{"reduced_by",current-new_health}}; +} + +nlohmann::json UndergroundWorker::sabotage(int performerId,int victimId,const json& p){ + const auto target=p.value("target",std::string{}); + if(target=="house") return sabotageHouse(performerId,victimId,p); + if(target=="storage") return sabotageStorage(performerId,victimId,p); + return {{"status","error"},{"action","sabotage"},{"message","unknown_target"},{"performer_id",performerId},{"victim_id",victimId},{"details",p}}; +} + +int UndergroundWorker::getUserIdForCharacter(int characterId){ + ConnectionGuard g(pool); + auto& db=g.get(); + db.prepare("UG_SELECT_CHAR_USER",Q_SELECT_CHAR_USER); + const auto r=db.execute("UG_SELECT_CHAR_USER",{std::to_string(characterId)}); + if(r.empty()) return -1; + return std::stoi(r.front().at("user_id")); +} + +std::optional UndergroundWorker::getHouseByUser(int userId){ + ConnectionGuard g(pool); + auto& db=g.get(); + db.prepare("UG_SELECT_HOUSE_BY_USER",Q_SELECT_HOUSE_BY_USER); + const auto r=db.execute("UG_SELECT_HOUSE_BY_USER",{std::to_string(userId)}); + if(r.empty()) return std::nullopt; + HouseConditions h{ + std::stoi(r.front().at("id")), + std::stoi(r.front().at("roof_condition")), + std::stoi(r.front().at("floor_condition")), + std::stoi(r.front().at("wall_condition")), + std::stoi(r.front().at("window_condition")) + }; + return h; +} + +void UndergroundWorker::updateHouse(const HouseConditions& h){ + ConnectionGuard g(pool); + auto& db=g.get(); + db.prepare("UG_UPDATE_HOUSE",Q_UPDATE_HOUSE); + db.execute("UG_UPDATE_HOUSE",{ + std::to_string(h.id), + std::to_string(std::clamp(h.roof,0,100)), + std::to_string(std::clamp(h.floor,0,100)), + std::to_string(std::clamp(h.wall,0,100)), + std::to_string(std::clamp(h.windowc,0,100)) + }); +} + +nlohmann::json UndergroundWorker::sabotageHouse(int performerId,int victimId,const json& p){ + int userId=getUserIdForCharacter(victimId); + if(userId<0) return {{"status","error"},{"action","sabotage"},{"target","house"},{"message","victim_not_found"},{"performer_id",performerId},{"victim_id",victimId},{"details",p}}; + auto hopt=getHouseByUser(userId); + if(!hopt) return {{"status","error"},{"action","sabotage"},{"target","house"},{"message","house_not_found"},{"performer_id",performerId},{"victim_id",victimId},{"details",p}}; + auto h=*hopt; + + std::vector allow; + if(p.contains("conditions") && p["conditions"].is_array()) + for(const auto& s:p["conditions"]) if(s.is_string()) allow.push_back(s.get()); + + std::vector> fields={ + {"roof_condition",&h.roof}, + {"floor_condition",&h.floor}, + {"wall_condition",&h.wall}, + {"window_condition",&h.windowc} + }; + std::vector> pool; + for(auto& f:fields) if(allow.empty() || std::find(allow.begin(),allow.end(),f.first)!=allow.end()) pool.push_back(f); + if(pool.empty()) return {{"status","error"},{"action","sabotage"},{"target","house"},{"message","no_conditions_selected"},{"performer_id",performerId},{"victim_id",victimId},{"details",p}}; + + size_t k=static_cast(randomInt(1,(int)pool.size())); + std::vector picks=randomIndices(pool.size(),k); + + json changed=json::array(); + for(size_t i: picks){ + int& cur=*pool[i].second; + if(cur>0){ + int red=randomInt(1,cur); + cur=std::clamp(cur-red,0,100); + } + changed.push_back(pool[i].first); + } + + updateHouse(h); + return { + {"status","success"}, + {"action","sabotage"}, + {"target","house"}, + {"performer_id",performerId}, + {"victim_id",victimId}, + {"details",p}, + {"changed_conditions",changed}, + {"new_conditions",{ + {"roof_condition",h.roof}, + {"floor_condition",h.floor}, + {"wall_condition",h.wall}, + {"window_condition",h.windowc} + }} + }; +} + +std::vector UndergroundWorker::selectStockByBranch(int branchId){ + ConnectionGuard g(pool); + auto& db=g.get(); + db.prepare("UG_SELECT_STOCK_BY_BRANCH",Q_SELECT_STOCK_BY_BRANCH); + return db.execute("UG_SELECT_STOCK_BY_BRANCH",{std::to_string(branchId)}); +} + +std::vector UndergroundWorker::filterByStockTypes(const std::vector& rows,const std::vector& allowed){ + if(allowed.empty()) return rows; + std::vector out; + out.reserve(rows.size()); + for(const auto& r:rows){ + int t=std::stoi(r.at("stock_type_id")); + if(std::find(allowed.begin(),allowed.end(),t)!=allowed.end()) out.push_back(r); + } + return out; +} + +void UndergroundWorker::updateStockQty(int id,long long qty){ + ConnectionGuard g(pool); + auto& db=g.get(); + db.prepare("UG_UPDATE_STOCK_QTY",Q_UPDATE_STOCK_QTY); + db.execute("UG_UPDATE_STOCK_QTY",{std::to_string(id),std::to_string(qty)}); +} + +nlohmann::json UndergroundWorker::sabotageStorage(int performerId,int victimId,const json& p){ + if(!p.contains("branch_id") || !p["branch_id"].is_number_integer()) + return {{"status","error"},{"action","sabotage"},{"target","storage"},{"message","branch_id_required"},{"performer_id",performerId},{"victim_id",victimId},{"details",p}}; + int branchId=p["branch_id"].get(); + + std::vector allowed; + if(p.contains("stock_type_ids") && p["stock_type_ids"].is_array()) + for(const auto& v:p["stock_type_ids"]) if(v.is_number_integer()) allowed.push_back(v.get()); + + auto rows=filterByStockTypes(selectStockByBranch(branchId),allowed); + if(rows.empty()) return {{"status","success"},{"action","sabotage"},{"target","storage"},{"performer_id",performerId},{"victim_id",victimId},{"details",p},{"removed_total",0},{"affected_rows",json::array()}}; + + long long total=0; + for(const auto& r:rows) total+=std::stoll(r.at("quantity")); + if(total<=0) return {{"status","success"},{"action","sabotage"},{"target","storage"},{"performer_id",performerId},{"victim_id",victimId},{"details",p},{"removed_total",0},{"affected_rows",json::array()}}; + + long long cap=total/4; + if(cap<=0) return {{"status","success"},{"action","sabotage"},{"target","storage"},{"performer_id",performerId},{"victim_id",victimId},{"details",p},{"removed_total",0},{"affected_rows",json::array()}}; + + long long to_remove=randomLL(1,cap); + std::shuffle(rows.begin(),rows.end(),rng()); + + json affected=json::array(); + for(const auto& r:rows){ + if(to_remove==0) break; + int id=std::stoi(r.at("id")); + long long q=std::stoll(r.at("quantity")); + if(q<=0) continue; + long long take=randomLL(1,std::min(q,to_remove)); + long long newq=q-take; + updateStockQty(id,newq); + to_remove-=take; + affected.push_back({{"id",id},{"stock_type_id",std::stoi(r.at("stock_type_id"))},{"previous_quantity",q},{"new_quantity",newq},{"removed",take}}); + } + + long long removed=0; + for(const auto& a:affected) removed+=a.at("removed").get(); + + return { + {"status","success"}, + {"action","sabotage"}, + {"target","storage"}, + {"performer_id",performerId}, + {"victim_id",victimId}, + {"details",p}, + {"removed_total",removed}, + {"affected_rows",affected} + }; +} + +nlohmann::json UndergroundWorker::corruptPolitician(int performerId,int victimId,const json& p){ + return {{"status","success"},{"action","corrupt_politician"},{"performer_id",performerId},{"victim_id",victimId},{"details",p}}; +} + +nlohmann::json UndergroundWorker::rob(int performerId,int victimId,const json& p){ + int userId=getUserIdForCharacter(victimId); + if(userId<0) return {{"status","error"},{"action","rob"},{"message","victim_not_found"},{"performer_id",performerId},{"victim_id",victimId},{"details",p}}; + + ConnectionGuard g(pool); + auto& db=g.get(); + db.prepare("UG_SELECT_FALUKANT_USER",Q_SELECT_FALUKANT_USER); + const auto fu=db.execute("UG_SELECT_FALUKANT_USER",{std::to_string(userId)}); + if(fu.empty()) return {{"status","error"},{"action","rob"},{"message","falukant_user_not_found"},{"performer_id",performerId},{"victim_id",victimId},{"details",p}}; + + int falukantUserId=std::stoi(fu.front().at("id")); + double money=std::stod(fu.front().at("money")); + int defaultBranch=std::stoi(fu.front().at("main_branch_region_id")); + + bool stealGoods = (randomInt(0,1)==1); + if(stealGoods){ + int branchId = p.contains("branch_id") && p["branch_id"].is_number_integer() + ? p["branch_id"].get() + : defaultBranch; + + if(branchId<=0){ + return {{"status","success"},{"action","rob"},{"mode","goods"},{"performer_id",performerId},{"victim_id",victimId},{"details",p},{"removed_total",0},{"affected_rows",json::array()}}; + } + + auto rows = selectStockByBranch(branchId); + if(rows.empty()){ + return {{"status","success"},{"action","rob"},{"mode","goods"},{"performer_id",performerId},{"victim_id",victimId},{"details",p},{"removed_total",0},{"affected_rows",json::array()}}; + } + + long long total=0; + for(const auto& r:rows) total+=std::stoll(r.at("quantity")); + if(total<=0){ + return {{"status","success"},{"action","rob"},{"mode","goods"},{"performer_id",performerId},{"victim_id",victimId},{"details",p},{"removed_total",0},{"affected_rows",json::array()}}; + } + + long long cap = std::max(1, total/2); + long long to_remove = randomLL(1, cap); + + std::shuffle(rows.begin(),rows.end(),rng()); + json affected = json::array(); + for(const auto& r:rows){ + if(to_remove==0) break; + int id=std::stoi(r.at("id")); + long long q=std::stoll(r.at("quantity")); + if(q<=0) continue; + long long take=randomLL(1,std::min(q,to_remove)); + long long newq=q-take; + updateStockQty(id,newq); + to_remove-=take; + affected.push_back({ + {"id",id}, + {"stock_type_id",std::stoi(r.at("stock_type_id"))}, + {"previous_quantity",q}, + {"new_quantity",newq}, + {"removed",take} + }); + } + + long long removed=0; + for(const auto& a:affected) removed+=a.at("removed").get(); + + return { + {"status","success"}, + {"action","rob"}, + {"mode","goods"}, + {"performer_id",performerId}, + {"victim_id",victimId}, + {"details",p}, + {"removed_total",removed}, + {"affected_rows",affected} + }; + } else { + if(money<=0.0){ + return {{"status","success"},{"action","rob"},{"mode","money"},{"performer_id",performerId},{"victim_id",victimId},{"details",p},{"stolen",0.0},{"balance_before",0.0},{"balance_after",0.0}}; + } + + double rate = randomDouble(0.0,0.18); + double amount = std::floor(money * rate * 100.0 + 0.5) / 100.0; + if(amount < 0.01) amount = 0.01; + if(amount > money) amount = money; + + json msg = { + {"event","money_changed"}, + {"reason","robbery"}, + {"delta",-amount}, + {"performer_id",performerId}, + {"victim_id",victimId} + }; + changeFalukantUserMoney(falukantUserId, -amount, "robbery", msg); + + double after = std::floor((money - amount) * 100.0 + 0.5)/100.0; + + return { + {"status","success"}, + {"action","rob"}, + {"mode","money"}, + {"performer_id",performerId}, + {"victim_id",victimId}, + {"details",p}, + {"stolen",amount}, + {"rate",rate}, + {"balance_before",money}, + {"balance_after",after} + }; + } +} + +void UndergroundWorker::updateResult(int id,const nlohmann::json& result){ + ConnectionGuard g(pool); + auto& db=g.get(); + db.prepare("UG_UPDATE_RESULT",Q_UPDATE_RESULT); + db.execute("UG_UPDATE_RESULT",{std::to_string(id),result.dump()}); +} + +double UndergroundWorker::randomDouble(double lo,double hi){ + std::uniform_real_distribution d(lo,hi); + return d(rng()); +} diff --git a/src/underground_worker.h b/src/underground_worker.h new file mode 100644 index 0000000..4b59c55 --- /dev/null +++ b/src/underground_worker.h @@ -0,0 +1,101 @@ +#pragma once +#include +#include +#include +#include "worker.h" + +class UndergroundWorker final: public Worker{ + using Row = std::unordered_map; + struct HouseConditions { int id; int roof; int floor; int wall; int windowc; }; +public: + UndergroundWorker(ConnectionPool& pool,MessageBroker& broker):Worker(pool,broker,"UndergroundWorker"){} + ~UndergroundWorker() override; + +protected: + void run() override; + +private: + void tick(); + std::vector fetchPending(); + nlohmann::json executeRow(const Row& r); + nlohmann::json handleTask(const std::string& type,int performerId,int victimId,const std::string& paramsJson); + nlohmann::json spyIn(int performerId,int victimId,const nlohmann::json& p); + nlohmann::json assassin(int performerId,int victimId,const nlohmann::json& p); + nlohmann::json sabotage(int performerId,int victimId,const nlohmann::json& p); + nlohmann::json corruptPolitician(int performerId,int victimId,const nlohmann::json& p); + nlohmann::json rob(int performerId,int victimId,const nlohmann::json& p); + void updateResult(int id,const nlohmann::json& result); + + nlohmann::json sabotageHouse(int performerId,int victimId,const nlohmann::json& p); + nlohmann::json sabotageStorage(int performerId,int victimId,const nlohmann::json& p); + + int getUserIdForCharacter(int characterId); + std::optional getHouseByUser(int userId); + void updateHouse(const HouseConditions& h); + std::vector selectStockByBranch(int branchId); + std::vector filterByStockTypes(const std::vector& rows,const std::vector& allowed); + void updateStockQty(int id,long long qty); + static int randomInt(int lo,int hi); + static long long randomLL(long long lo,long long hi); + static std::vector randomIndices(size_t n,size_t k); + static double randomDouble(double lo,double hi); + +private: + static constexpr const char* Q_SELECT_BY_PERFORMER=R"SQL( + SELECT u.id, t.tr AS underground_type, u.performer_id, u.victim_id, + to_char(u.created_at,'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS created_at, + COALESCE(u.parameters::text,'{}') AS parameters, + COALESCE(u.result::text,'null') AS result_text + FROM falukant_data.underground u + JOIN falukant_type.underground t ON t.tr=u.underground_type_id + WHERE u.performer_id=$1 + ORDER BY u.created_at DESC + )SQL"; + static constexpr const char* Q_SELECT_PENDING=R"SQL( + SELECT u.id,t.tr AS underground_type,u.performer_id,u.victim_id,COALESCE(u.parameters::text,'{}') AS parameters + FROM falukant_data.underground u + JOIN falukant_type.underground t ON t.tr=u.underground_type_id + WHERE u.result IS NULL AND u.created_at<=NOW()-INTERVAL '1 day' + ORDER BY u.created_at ASC + LIMIT 200 + )SQL"; + static constexpr const char* Q_UPDATE_RESULT=R"SQL( + UPDATE falukant_data.underground SET result=$2::jsonb,updated_at=NOW() WHERE id=$1 + )SQL"; + static constexpr const char* Q_SELECT_CHAR_USER=R"SQL( + SELECT user_id FROM falukant_data."character" WHERE id=$1 + )SQL"; + static constexpr const char* Q_SELECT_HOUSE_BY_USER=R"SQL( + SELECT id, roof_condition, floor_condition, wall_condition, window_condition + FROM falukant_data.user_house + WHERE user_id=$1 + LIMIT 1 + )SQL"; + static constexpr const char* Q_UPDATE_HOUSE=R"SQL( + UPDATE falukant_data.user_house + SET roof_condition=$2, floor_condition=$3, wall_condition=$4, window_condition=$5 + WHERE id=$1 + )SQL"; + static constexpr const char* Q_SELECT_STOCK_BY_BRANCH=R"SQL( + SELECT id, stock_type_id, quantity + FROM falukant_data.stock + WHERE branch_id=$1 + ORDER BY quantity DESC + )SQL"; + static constexpr const char* Q_UPDATE_STOCK_QTY=R"SQL( + UPDATE falukant_data.stock SET quantity=$2 WHERE id=$1 + )SQL"; + static constexpr const char* Q_SELECT_CHAR_HEALTH=R"SQL( + SELECT health FROM falukant_data."character" WHERE id=$1 + )SQL"; + static constexpr const char* Q_UPDATE_CHAR_HEALTH=R"SQL( + UPDATE falukant_data."character" SET health=$2, updated_at=NOW() WHERE id=$1 + )SQL"; + static constexpr const char* Q_SELECT_FALUKANT_USER=R"SQL( + SELECT id, money, COALESCE(main_branch_region_id,0) AS main_branch_region_id + FROM falukant_data.falukant_user + WHERE user_id=$1 + LIMIT 1 + )SQL"; + +}; diff --git a/src/usercharacterworker.cpp b/src/usercharacterworker.cpp index 7f8dbe1..22778bd 100644 --- a/src/usercharacterworker.cpp +++ b/src/usercharacterworker.cpp @@ -5,6 +5,7 @@ #include #include #include +#include "utils.h" UserCharacterWorker::UserCharacterWorker(ConnectionPool &pool, MessageBroker &broker) : Worker(pool, broker, "UserCharacterWorker"), @@ -17,8 +18,11 @@ void UserCharacterWorker::run() { auto lastExecutionTime = steady_clock::now(); int lastPregnancyDay = -1; + while (runningWorker) { signalActivity(); + + // 1h-Block auto nowSteady = steady_clock::now(); auto elapsed = duration_cast(nowSteady - lastExecutionTime).count(); if (elapsed >= 3600) { @@ -31,20 +35,22 @@ void UserCharacterWorker::run() { } lastExecutionTime = nowSteady; } - { - auto nowSys = system_clock::now(); - std::time_t t = system_clock::to_time_t(nowSys); - std::tm local_tm; - localtime_r(&t, &local_tm); - if (local_tm.tm_hour == 6 && local_tm.tm_yday != lastPregnancyDay) { - try { - processPregnancies(); - } catch (const std::exception &e) { - std::cerr << "[UserCharacterWorker] Fehler in processPregnancies: " << e.what() << std::endl; - } - lastPregnancyDay = local_tm.tm_yday; + + // Schwangerschaftsverarbeitung: initial oder täglich um 06:00 einmal pro Tag + auto nowSys = system_clock::now(); + std::time_t t = system_clock::to_time_t(nowSys); + std::tm local_tm; + localtime_r(&t, &local_tm); + + if (lastPregnancyDay == -1 || (local_tm.tm_hour == 6 && local_tm.tm_yday != lastPregnancyDay)) { + try { + processPregnancies(); + } catch (const std::exception &e) { + std::cerr << "[UserCharacterWorker] Fehler in processPregnancies: " << e.what() << std::endl; } + lastPregnancyDay = local_tm.tm_yday; } + std::this_thread::sleep_for(seconds(1)); recalculateKnowledge(); } @@ -225,22 +231,34 @@ void UserCharacterWorker::recalculateKnowledge() { void UserCharacterWorker::processPregnancies() { ConnectionGuard connGuard(pool); auto &db = connGuard.get(); + db.prepare("QUERY_AUTOBATISM", QUERY_AUTOBATISM); db.execute("QUERY_AUTOBATISM"); + db.prepare("get_candidates", QUERY_GET_PREGNANCY_CANDIDATES); auto rows = db.execute("get_candidates"); + const nlohmann::json message = { {"event", "children_update"}, }; - for (auto const &row : rows) { - int fatherCid = std::stoi(row.at("father_cid")); - int motherCid = std::stoi(row.at("mother_cid")); - int fatherUid = std::stoi(row.at("father_uid")); - int motherUid = std::stoi(row.at("mother_uid")); - int titleOfNobility = std::stoi(row.at("title_of_nobility")); - int lastName = std::stoi(row.at("last_name")); - int regionId = std::stoi(row.at("region_id")); + + for (const auto &row : rows) { + int fatherCid = Utils::optionalStoiOrDefault(row, "father_cid", -1); + int motherCid = Utils::optionalStoiOrDefault(row, "mother_cid", -1); + if (fatherCid < 0 || motherCid < 0) { + continue; // ungültige Daten überspringen + } + + int titleOfNobility = Utils::optionalStoiOrDefault(row, "title_of_nobility", 0); + int lastName = Utils::optionalStoiOrDefault(row, "last_name", 0); + int regionId = Utils::optionalStoiOrDefault(row, "region_id", 0); + + auto fatherUidOpt = Utils::optionalUid(row.at("father_uid")); + auto motherUidOpt = Utils::optionalUid(row.at("mother_uid")); + + // Geschlecht zufällig std::string gender = (dist(gen) < 0.5) ? "male" : "female"; + db.prepare("insert_child", QUERY_INSERT_CHILD); auto resChild = db.execute("insert_child", { std::to_string(regionId), // $1 @@ -248,16 +266,30 @@ void UserCharacterWorker::processPregnancies() { std::to_string(lastName), // $3 std::to_string(titleOfNobility) // $4 }); - int childCid = std::stoi(resChild.front().at("child_cid")); + + if (resChild.empty()) continue; + int childCid = Utils::optionalStoiOrDefault(resChild.front(), "child_cid", -1); + if (childCid < 0) continue; + db.prepare("insert_relation", QUERY_INSERT_CHILD_RELATION); - auto resRel = db.execute("insert_relation", { + db.execute("insert_relation", { std::to_string(fatherCid), std::to_string(motherCid), std::to_string(childCid) }); - const nlohmann::json message = {{"event", "children_update"}}; - sendMessageToFalukantUsers(fatherUid, message); - sendMessageToFalukantUsers(motherUid, message); + + if (fatherUidOpt) { + sendMessageToFalukantUsers(*fatherUidOpt, message); + // Sende falukantUpdateStatus nach dem Erstellen des Kindes + nlohmann::json updateMessage = { { "event", "falukantUpdateStatus" } }; + sendMessageToFalukantUsers(*fatherUidOpt, updateMessage); + } + if (motherUidOpt) { + sendMessageToFalukantUsers(*motherUidOpt, message); + // Sende falukantUpdateStatus nach dem Erstellen des Kindes + nlohmann::json updateMessage = { { "event", "falukantUpdateStatus" } }; + sendMessageToFalukantUsers(*motherUidOpt, updateMessage); + } } } diff --git a/src/usercharacterworker.h b/src/usercharacterworker.h index 392e3a6..30eb682 100644 --- a/src/usercharacterworker.h +++ b/src/usercharacterworker.h @@ -134,7 +134,7 @@ private: - 2.638267 ) ) - ); + ) / 2; -- Geburtenrate halbiert )"; static constexpr char const* QUERY_INSERT_CHILD = R"( diff --git a/src/utils.cpp b/src/utils.cpp new file mode 100644 index 0000000..e1f9bea --- /dev/null +++ b/src/utils.cpp @@ -0,0 +1,94 @@ +#include "utils.h" +#include +#include +#include + +int Utils::optionalStoiOrDefault(const std::unordered_map& row, + const std::string& key, int def) { + auto it = row.find(key); + if (it == row.end()) return def; + const std::string& val = it->second; + if (isNullOrEmpty(val)) return def; + try { + return std::stoi(val); + } catch (...) { + return def; + } +} + +double Utils::optionalStodOrDefault(const std::unordered_map& row, + const std::string& key, double def) { + auto it = row.find(key); + if (it == row.end()) return def; + const std::string& val = it->second; + if (isNullOrEmpty(val)) return def; + try { + return std::stod(val); + } catch (...) { + return def; + } +} + +bool Utils::isNullOrEmpty(const std::string& s) { + return s.empty() || s == "NULL"; +} + +std::optional Utils::parseTimestamp(const std::string& iso) { + std::istringstream ss(iso); + std::tm tm = {}; + ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S"); + if (ss.fail()) { + ss.clear(); + ss.str(iso); + ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S"); + if (ss.fail()) return std::nullopt; + } + std::time_t time_c = std::mktime(&tm); + if (time_c == -1) return std::nullopt; + return std::chrono::system_clock::from_time_t(time_c); +} + +std::optional Utils::computeAgeYears(const std::string& birthdate_iso) { + auto birth_tp = parseTimestamp(birthdate_iso); + if (!birth_tp) return std::nullopt; + auto now = std::chrono::system_clock::now(); + + std::time_t birth_time = std::chrono::system_clock::to_time_t(*birth_tp); + std::time_t now_time = std::chrono::system_clock::to_time_t(now); + + std::tm birth_tm; + std::tm now_tm; +#if defined(_WIN32) || defined(_WIN64) + localtime_s(&birth_tm, &birth_time); + localtime_s(&now_tm, &now_time); +#else + localtime_r(&birth_time, &birth_tm); + localtime_r(&now_time, &now_tm); +#endif + + int years = now_tm.tm_year - birth_tm.tm_year; + if (now_tm.tm_mon < birth_tm.tm_mon || + (now_tm.tm_mon == birth_tm.tm_mon && now_tm.tm_mday < birth_tm.tm_mday)) { + years--; + } + return years; +} + +std::string Utils::buildPgIntArrayLiteral(const std::vector& elems) { + std::string res = "{"; + for (size_t i = 0; i < elems.size(); ++i) { + res += std::to_string(elems[i]); + if (i + 1 < elems.size()) res += ","; + } + res += "}"; + return res; +} + +std::optional Utils::optionalUid(const std::string& val) { + if (isNullOrEmpty(val)) return std::nullopt; + try { + return std::stoi(val); + } catch (...) { + return std::nullopt; + } +} diff --git a/src/utils.h b/src/utils.h new file mode 100644 index 0000000..1560bc1 --- /dev/null +++ b/src/utils.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include +#include +#include + +class Utils { +public: + // Safe conversions with fallback + static int optionalStoiOrDefault(const std::unordered_map& row, + const std::string& key, int def = -1); + static double optionalStodOrDefault(const std::unordered_map& row, + const std::string& key, double def = 0.0); + + static bool isNullOrEmpty(const std::string& s); + + // Parse timestamp from common ISO / SQL formats into time_point + static std::optional parseTimestamp(const std::string& iso); + + // Compute full years age from birthdate string; returns nullopt on parse failure. + static std::optional computeAgeYears(const std::string& birthdate_iso); + + // Build Postgres integer array literal "{1,2,3}" + static std::string buildPgIntArrayLiteral(const std::vector& elems); + + // Safely parse a nullable integer-like string + static std::optional optionalUid(const std::string& val); +}; diff --git a/src/websocket_server.cpp b/src/websocket_server.cpp index cdfef85..b7f5104 100644 --- a/src/websocket_server.cpp +++ b/src/websocket_server.cpp @@ -56,6 +56,11 @@ void WebSocketServer::startServer() { memset(&info, 0, sizeof(info)); info.port = port; info.protocols = protocols; + + // Reduziere Log-Level um weniger Debug-Ausgaben zu haben + // Setze Umgebungsvariable für Log-Level + setenv("LWS_LOG_LEVEL", "0", 1); // 0 = nur Fehler + context = lws_create_context(&info); if (!context) { throw std::runtime_error("Failed to create LWS context"); diff --git a/yourpart-daemon.service b/yourpart-daemon.service new file mode 100644 index 0000000..4633f4b --- /dev/null +++ b/yourpart-daemon.service @@ -0,0 +1,39 @@ +[Unit] +Description=YourPart Daemon Service +Documentation=https://your-part.de +After=network.target postgresql.service +Wants=postgresql.service + +[Service] +Type=simple +User=yourpart +Group=yourpart +WorkingDirectory=/opt/yourpart +ExecStart=/usr/local/bin/yourpart-daemon +ExecReload=/bin/kill -HUP $MAINPID +Restart=always +RestartSec=5 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=yourpart-daemon + +# Sicherheitseinstellungen +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/opt/yourpart/logs /var/log/yourpart +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true + +# Umgebungsvariablen +Environment=NODE_ENV=production +Environment=PYTHONUNBUFFERED=1 + +# Ressourcenlimits +LimitNOFILE=65536 +LimitNPROC=4096 + +[Install] +WantedBy=multi-user.target