This commit is contained in:
parent
48f62c8c02
commit
33624d3b02
9 changed files with 114 additions and 3 deletions
|
|
@ -4,6 +4,7 @@
|
||||||
#include "controllers/UserController.h"
|
#include "controllers/UserController.h"
|
||||||
#include "controllers/AdminController.h"
|
#include "controllers/AdminController.h"
|
||||||
#include "controllers/RealmController.h"
|
#include "controllers/RealmController.h"
|
||||||
|
#include "controllers/RestreamController.h"
|
||||||
#include "services/DatabaseService.h"
|
#include "services/DatabaseService.h"
|
||||||
#include "services/StatsService.h"
|
#include "services/StatsService.h"
|
||||||
#include "services/AuthService.h"
|
#include "services/AuthService.h"
|
||||||
|
|
|
||||||
|
|
@ -424,6 +424,9 @@ void ChatWebSocketController::handleNewMessage(const WebSocketConnectionPtr& wsC
|
||||||
} else if (msgType == "auth") {
|
} else if (msgType == "auth") {
|
||||||
// SECURITY FIX #9: Handle auth token sent as message (not in URL)
|
// SECURITY FIX #9: Handle auth token sent as message (not in URL)
|
||||||
handleAuthMessage(wsConnPtr, data);
|
handleAuthMessage(wsConnPtr, data);
|
||||||
|
} else if (msgType == "get_global_history") {
|
||||||
|
// Global chat: fetch messages from ALL active realms
|
||||||
|
handleGetGlobalHistory(wsConnPtr, info);
|
||||||
} else {
|
} else {
|
||||||
sendError(wsConnPtr, "Unknown message type");
|
sendError(wsConnPtr, "Unknown message type");
|
||||||
}
|
}
|
||||||
|
|
@ -918,6 +921,24 @@ void ChatWebSocketController::handleGetParticipants(const WebSocketConnectionPtr
|
||||||
wsConnPtr->send(Json::writeString(Json::StreamWriterBuilder(), response));
|
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,
|
void ChatWebSocketController::handleRename(const WebSocketConnectionPtr& wsConnPtr,
|
||||||
ConnectionInfo& info,
|
ConnectionInfo& info,
|
||||||
const Json::Value& data) {
|
const Json::Value& data) {
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,10 @@ private:
|
||||||
void handleBotApiKeyAuth(const WebSocketConnectionPtr& wsConnPtr,
|
void handleBotApiKeyAuth(const WebSocketConnectionPtr& wsConnPtr,
|
||||||
const std::string& apiKey);
|
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 sendError(const WebSocketConnectionPtr& wsConnPtr, const std::string& error);
|
||||||
void sendSuccess(const WebSocketConnectionPtr& wsConnPtr, const Json::Value& data);
|
void sendSuccess(const WebSocketConnectionPtr& wsConnPtr, const Json::Value& data);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,11 @@ std::vector<models::ChatMessage> ChatService::getRealmMessages(const std::string
|
||||||
return redis.getMessages(realmId, limit, beforeTimestamp);
|
return redis.getMessages(realmId, limit, beforeTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<models::ChatMessage> ChatService::getGlobalMessages(int limit) {
|
||||||
|
auto& redis = RedisMessageStore::getInstance();
|
||||||
|
return redis.getGlobalMessages(limit);
|
||||||
|
}
|
||||||
|
|
||||||
bool ChatService::deleteMessage(const std::string& realmId, const std::string& messageId,
|
bool ChatService::deleteMessage(const std::string& realmId, const std::string& messageId,
|
||||||
const std::string& moderatorId) {
|
const std::string& moderatorId) {
|
||||||
auto& modService = ModerationService::getInstance();
|
auto& modService = ModerationService::getInstance();
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,9 @@ public:
|
||||||
int limit = 100,
|
int limit = 100,
|
||||||
int64_t beforeTimestamp = 0);
|
int64_t beforeTimestamp = 0);
|
||||||
|
|
||||||
|
// Get messages from ALL active realms (for global chat history)
|
||||||
|
std::vector<models::ChatMessage> getGlobalMessages(int limit = 100);
|
||||||
|
|
||||||
bool deleteMessage(const std::string& realmId, const std::string& messageId,
|
bool deleteMessage(const std::string& realmId, const std::string& messageId,
|
||||||
const std::string& moderatorId);
|
const std::string& moderatorId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,55 @@ std::vector<ChatMessage> RedisMessageStore::getMessages(const std::string& realm
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<ChatMessage> RedisMessageStore::getGlobalMessages(int limit) {
|
||||||
|
std::vector<ChatMessage> 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::milliseconds>(
|
||||||
|
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<int>(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) {
|
bool RedisMessageStore::deleteMessage(const std::string& realmId, const std::string& messageId) {
|
||||||
if (!redis_) {
|
if (!redis_) {
|
||||||
LOG_ERROR << "Redis not initialized - cannot delete message";
|
LOG_ERROR << "Redis not initialized - cannot delete message";
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ public:
|
||||||
std::vector<models::ChatMessage> getMessages(const std::string& realmId,
|
std::vector<models::ChatMessage> getMessages(const std::string& realmId,
|
||||||
int limit = 100,
|
int limit = 100,
|
||||||
int64_t beforeTimestamp = 0);
|
int64_t beforeTimestamp = 0);
|
||||||
|
// Get messages from ALL active realms (for global chat history)
|
||||||
|
std::vector<models::ChatMessage> getGlobalMessages(int limit = 100);
|
||||||
bool deleteMessage(const std::string& realmId, const std::string& messageId);
|
bool deleteMessage(const std::string& realmId, const std::string& messageId);
|
||||||
void cleanupOldMessages(const std::string& realmId, int retentionHours);
|
void cleanupOldMessages(const std::string& realmId, int retentionHours);
|
||||||
void cleanupExpiredSelfDestruct(const std::string& realmId);
|
void cleanupExpiredSelfDestruct(const std::string& realmId);
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,12 @@ class ChatWebSocket {
|
||||||
console.log(`Participant left: ${data.username} (${data.participantCount} total)`);
|
console.log(`Participant left: ${data.username} (${data.participantCount} total)`);
|
||||||
break;
|
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:
|
default:
|
||||||
console.log('Unknown message type:', data.type);
|
console.log('Unknown message type:', data.type);
|
||||||
}
|
}
|
||||||
|
|
@ -368,6 +374,21 @@ class ChatWebSocket {
|
||||||
return true;
|
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) {
|
sendRename(newName) {
|
||||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||||
console.error('WebSocket not connected');
|
console.error('WebSocket not connected');
|
||||||
|
|
|
||||||
|
|
@ -61,9 +61,12 @@
|
||||||
|
|
||||||
$: isConnected = $connectionStatus === 'connected';
|
$: isConnected = $connectionStatus === 'connected';
|
||||||
|
|
||||||
// Auto-load participants when connected
|
// Auto-load participants and global history when connected
|
||||||
$: if (isConnected) {
|
$: if (isConnected) {
|
||||||
chatWebSocket.getParticipants();
|
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
|
// Reconnect WebSocket when user logs in or registers while already connected as guest
|
||||||
|
|
@ -769,6 +772,7 @@
|
||||||
.chat-panel {
|
.chat-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1 1 0; /* Fill available space in flex parent */
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|
@ -852,7 +856,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages-container {
|
.messages-container {
|
||||||
flex: 1 1 0; /* Grow, shrink, base 0 */
|
flex: 1 1 auto; /* Grow and shrink, but with natural base size */
|
||||||
background: #000;
|
background: #000;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
@ -860,7 +864,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.125rem;
|
gap: 0.125rem;
|
||||||
min-height: 0; /* Allow flex shrinking */
|
min-height: 100px; /* Ensure minimum visibility */
|
||||||
max-height: 100%; /* Don't exceed container */
|
max-height: 100%; /* Don't exceed container */
|
||||||
width: 100%; /* Ensure full width */
|
width: 100%; /* Ensure full width */
|
||||||
}
|
}
|
||||||
|
|
@ -872,6 +876,7 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #666;
|
color: #666;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
min-height: 100px; /* Ensure visibility even if flex calculations fail */
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-disabled-message {
|
.chat-disabled-message {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue