diff --git a/backend/src/main.cpp b/backend/src/main.cpp index ca62e7f..205f4e8 100644 --- a/backend/src/main.cpp +++ b/backend/src/main.cpp @@ -4,6 +4,7 @@ #include "controllers/UserController.h" #include "controllers/AdminController.h" #include "controllers/RealmController.h" +#include "controllers/RestreamController.h" #include "services/DatabaseService.h" #include "services/StatsService.h" #include "services/AuthService.h" diff --git a/chat-service/src/controllers/ChatWebSocketController.cpp b/chat-service/src/controllers/ChatWebSocketController.cpp index 0771143..c3db017 100644 --- a/chat-service/src/controllers/ChatWebSocketController.cpp +++ b/chat-service/src/controllers/ChatWebSocketController.cpp @@ -424,6 +424,9 @@ void ChatWebSocketController::handleNewMessage(const WebSocketConnectionPtr& wsC } else if (msgType == "auth") { // SECURITY FIX #9: Handle auth token sent as message (not in URL) handleAuthMessage(wsConnPtr, data); + } else if (msgType == "get_global_history") { + // Global chat: fetch messages from ALL active realms + handleGetGlobalHistory(wsConnPtr, info); } else { sendError(wsConnPtr, "Unknown message type"); } @@ -918,6 +921,24 @@ void ChatWebSocketController::handleGetParticipants(const WebSocketConnectionPtr wsConnPtr->send(Json::writeString(Json::StreamWriterBuilder(), response)); } +void ChatWebSocketController::handleGetGlobalHistory(const WebSocketConnectionPtr& wsConnPtr, + const ConnectionInfo& info) { + // Get messages from ALL active realms for global chat feature + auto& chatService = services::ChatService::getInstance(); + auto messages = chatService.getGlobalMessages(100); + + Json::Value response; + response["type"] = "global_history"; + response["messages"] = Json::arrayValue; + + for (const auto& msg : messages) { + response["messages"].append(msg.toJson()); + } + + LOG_DEBUG << "Sending global history with " << messages.size() << " messages"; + wsConnPtr->send(Json::writeString(Json::StreamWriterBuilder(), response)); +} + void ChatWebSocketController::handleRename(const WebSocketConnectionPtr& wsConnPtr, ConnectionInfo& info, const Json::Value& data) { diff --git a/chat-service/src/controllers/ChatWebSocketController.h b/chat-service/src/controllers/ChatWebSocketController.h index f9a67ce..5142cdc 100644 --- a/chat-service/src/controllers/ChatWebSocketController.h +++ b/chat-service/src/controllers/ChatWebSocketController.h @@ -92,6 +92,10 @@ private: void handleBotApiKeyAuth(const WebSocketConnectionPtr& wsConnPtr, const std::string& apiKey); + // Global chat: fetch messages from ALL active realms + void handleGetGlobalHistory(const WebSocketConnectionPtr& wsConnPtr, + const ConnectionInfo& info); + void sendError(const WebSocketConnectionPtr& wsConnPtr, const std::string& error); void sendSuccess(const WebSocketConnectionPtr& wsConnPtr, const Json::Value& data); diff --git a/chat-service/src/services/ChatService.cpp b/chat-service/src/services/ChatService.cpp index 1c0fd65..22f1983 100644 --- a/chat-service/src/services/ChatService.cpp +++ b/chat-service/src/services/ChatService.cpp @@ -166,6 +166,11 @@ std::vector ChatService::getRealmMessages(const std::string return redis.getMessages(realmId, limit, beforeTimestamp); } +std::vector ChatService::getGlobalMessages(int limit) { + auto& redis = RedisMessageStore::getInstance(); + return redis.getGlobalMessages(limit); +} + bool ChatService::deleteMessage(const std::string& realmId, const std::string& messageId, const std::string& moderatorId) { auto& modService = ModerationService::getInstance(); diff --git a/chat-service/src/services/ChatService.h b/chat-service/src/services/ChatService.h index 9763446..40c70cf 100644 --- a/chat-service/src/services/ChatService.h +++ b/chat-service/src/services/ChatService.h @@ -49,6 +49,9 @@ public: int limit = 100, int64_t beforeTimestamp = 0); + // Get messages from ALL active realms (for global chat history) + std::vector getGlobalMessages(int limit = 100); + bool deleteMessage(const std::string& realmId, const std::string& messageId, const std::string& moderatorId); diff --git a/chat-service/src/services/RedisMessageStore.cpp b/chat-service/src/services/RedisMessageStore.cpp index 58c391c..e7719b2 100644 --- a/chat-service/src/services/RedisMessageStore.cpp +++ b/chat-service/src/services/RedisMessageStore.cpp @@ -131,6 +131,55 @@ std::vector RedisMessageStore::getMessages(const std::string& realm return messages; } +std::vector RedisMessageStore::getGlobalMessages(int limit) { + std::vector allMessages; + if (!redis_) { + LOG_ERROR << "Redis not initialized - cannot get global messages"; + return allMessages; + } + + try { + // Get all active realms + auto activeRealms = getActiveRealms(); + LOG_DEBUG << "Getting global messages from " << activeRealms.size() << " active realms"; + + auto now = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() + ).count(); + + // Fetch messages from each realm (limit per realm to avoid overwhelming) + int perRealmLimit = std::max(20, limit / 2); // At least 20, or half of total limit + for (const auto& realmId : activeRealms) { + auto realmMessages = getMessages(realmId, perRealmLimit, 0); + for (auto& msg : realmMessages) { + // Filter out expired self-destruct messages + if (msg.selfDestructAt > 0 && msg.selfDestructAt <= now) { + continue; + } + allMessages.push_back(std::move(msg)); + } + } + + // Sort all messages by timestamp + std::sort(allMessages.begin(), allMessages.end(), + [](const ChatMessage& a, const ChatMessage& b) { + return a.timestamp < b.timestamp; + }); + + // Keep only the most recent 'limit' messages + if (static_cast(allMessages.size()) > limit) { + allMessages.erase(allMessages.begin(), allMessages.begin() + (allMessages.size() - limit)); + } + + LOG_DEBUG << "Returning " << allMessages.size() << " global messages"; + + } catch (const Error& e) { + LOG_ERROR << "Redis error getting global messages: " << e.what(); + } + + return allMessages; +} + bool RedisMessageStore::deleteMessage(const std::string& realmId, const std::string& messageId) { if (!redis_) { LOG_ERROR << "Redis not initialized - cannot delete message"; diff --git a/chat-service/src/services/RedisMessageStore.h b/chat-service/src/services/RedisMessageStore.h index 613dfab..e82ee8b 100644 --- a/chat-service/src/services/RedisMessageStore.h +++ b/chat-service/src/services/RedisMessageStore.h @@ -24,6 +24,8 @@ public: std::vector getMessages(const std::string& realmId, int limit = 100, int64_t beforeTimestamp = 0); + // Get messages from ALL active realms (for global chat history) + std::vector getGlobalMessages(int limit = 100); bool deleteMessage(const std::string& realmId, const std::string& messageId); void cleanupOldMessages(const std::string& realmId, int retentionHours); void cleanupExpiredSelfDestruct(const std::string& realmId); diff --git a/frontend/src/lib/chat/chatWebSocket.js b/frontend/src/lib/chat/chatWebSocket.js index 3b55a27..b2814cf 100644 --- a/frontend/src/lib/chat/chatWebSocket.js +++ b/frontend/src/lib/chat/chatWebSocket.js @@ -239,6 +239,12 @@ class ChatWebSocket { console.log(`Participant left: ${data.username} (${data.participantCount} total)`); break; + case 'global_history': + // Global chat: merge messages from ALL realms into existing messages + console.log(`Received global history with ${(data.messages || []).length} messages`); + setMessageHistory(data.messages || [], true); // Always merge global history + break; + default: console.log('Unknown message type:', data.type); } @@ -368,6 +374,21 @@ class ChatWebSocket { return true; } + /** + * Request global chat history (messages from ALL active realms) + * Used for the global chat feature where users can see messages across realms + */ + requestGlobalHistory() { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + console.error('WebSocket not connected'); + return false; + } + + console.log('[ChatWebSocket] Requesting global history'); + this.ws.send(JSON.stringify({ type: 'get_global_history' })); + return true; + } + sendRename(newName) { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { console.error('WebSocket not connected'); diff --git a/frontend/src/lib/components/chat/ChatPanel.svelte b/frontend/src/lib/components/chat/ChatPanel.svelte index 10fa7e2..66732a4 100644 --- a/frontend/src/lib/components/chat/ChatPanel.svelte +++ b/frontend/src/lib/components/chat/ChatPanel.svelte @@ -61,9 +61,12 @@ $: isConnected = $connectionStatus === 'connected'; - // Auto-load participants when connected + // Auto-load participants and global history when connected $: if (isConnected) { chatWebSocket.getParticipants(); + // Request global history for global chat feature (messages from ALL realms) + // Small delay to ensure connection is fully established + setTimeout(() => chatWebSocket.requestGlobalHistory(), 500); } // Reconnect WebSocket when user logs in or registers while already connected as guest @@ -769,6 +772,7 @@ .chat-panel { display: flex; flex-direction: column; + flex: 1 1 0; /* Fill available space in flex parent */ height: 100%; max-height: 100%; min-height: 0; @@ -852,7 +856,7 @@ } .messages-container { - flex: 1 1 0; /* Grow, shrink, base 0 */ + flex: 1 1 auto; /* Grow and shrink, but with natural base size */ background: #000; overflow-y: auto; overflow-x: hidden; @@ -860,7 +864,7 @@ display: flex; flex-direction: column; gap: 0.125rem; - min-height: 0; /* Allow flex shrinking */ + min-height: 100px; /* Ensure minimum visibility */ max-height: 100%; /* Don't exceed container */ width: 100%; /* Ensure full width */ } @@ -872,6 +876,7 @@ justify-content: center; color: #666; font-style: italic; + min-height: 100px; /* Ensure visibility even if flex calculations fail */ } .chat-disabled-message {