#include "ChatService.h" #include "RedisMessageStore.h" #include "ModerationService.h" #include "StickerService.h" #include "CensorService.h" #include "../controllers/ChatWebSocketController.h" #include #include #include namespace services { ChatService& ChatService::getInstance() { static ChatService instance; return instance; } void ChatService::initialize() { LOG_INFO << "ChatService initialized"; startCleanupTask(); } SendMessageResult ChatService::sendMessage(const std::string& realmId, const std::string& userId, const std::string& username, const std::string& userColor, const std::string& avatarUrl, const std::string& content, bool isGuest, bool isModerator, bool isStreamer, models::ChatMessage& outMessage, int selfDestructSeconds, bool isBot) { auto& redis = RedisMessageStore::getInstance(); auto& modService = ModerationService::getInstance(); // SECURITY FIX: Bot rate limiting (1 message per second) if (isBot) { if (!canBotSendMessage(userId)) { return SendMessageResult::BOT_RATE_LIMITED; } } // Check if user can chat (not banned or muted) if (!isGuest && !modService.canUserChat(realmId, userId)) { if (redis.isBanned(realmId, userId)) { return SendMessageResult::BANNED; } if (redis.isMuted(realmId, userId)) { return SendMessageResult::MUTED; } } // Validate content if (!isContentValid(content)) { return SendMessageResult::INVALID_CONTENT; } // Check message length auto config = drogon::app().getCustomConfig().get("chat", Json::Value::null); int maxLength = config.get("max_message_length", 500).asInt(); if (content.length() > static_cast(maxLength)) { return SendMessageResult::MESSAGE_TOO_LONG; } // Get realm settings auto settings = redis.getRealmSettings(realmId); // Check if guests are allowed (global setting overrides realm setting) if (isGuest) { auto globalSettings = getGlobalSettings(); // Global setting takes priority - if disabled site-wide, no guests can chat if (!globalSettings.guestsAllowedSiteWide) { return SendMessageResult::GUESTS_NOT_ALLOWED; } // Per-realm setting - if disabled for this realm, guests can't chat here if (!settings.chatGuestsAllowed) { return SendMessageResult::GUESTS_NOT_ALLOWED; } } // Check if links are allowed (unless moderator or streamer) if (!isModerator && !isStreamer && !settings.linksAllowed && containsLinks(content)) { return SendMessageResult::LINKS_NOT_ALLOWED; } // Check slow mode (unless moderator or streamer) // Guests are also subject to slow mode to prevent spam if (!isModerator && !isStreamer) { if (!redis.canSendMessage(realmId, userId, settings.slowModeSeconds)) { return SendMessageResult::SLOW_MODE; } } // Process special stickers (:roll: and :rtd:) auto& stickerService = StickerService::getInstance(); auto stickerResult = stickerService.processSpecialStickers(content); std::string processedContent = stickerResult.content; // Track sticker usage (async, fire-and-forget) auto usedStickers = stickerService.extractStickerNames(processedContent); if (!usedStickers.empty()) { stickerService.trackStickerUsage(usedStickers); } // Apply censoring to remove banned words auto& censorService = CensorService::getInstance(); processedContent = censorService.censor(processedContent); // If message is empty after censoring, reject it if (processedContent.empty()) { return SendMessageResult::INVALID_CONTENT; } // Create message with optional self-destruct timer and roll/rtd flags models::ChatMessage message(realmId, userId, username, userColor, avatarUrl, processedContent, isGuest, isModerator, isStreamer, selfDestructSeconds, stickerResult.usedRoll, stickerResult.usedRtd); // Store in Redis if (!redis.addMessage(message)) { return SendMessageResult::ERROR; } // Record message sent for slow mode (all users except mods/streamers) if (!isModerator && !isStreamer) { redis.recordMessageSent(realmId, userId); } // Record user activity redis.recordUserActivity(realmId, userId); // Schedule self-destruct if timer is set if (selfDestructSeconds > 0) { scheduleSelfDestruct(realmId, message.messageId, selfDestructSeconds); } outMessage = message; return SendMessageResult::SUCCESS; } void ChatService::scheduleSelfDestruct(const std::string& realmId, const std::string& messageId, int delaySeconds) { LOG_DEBUG << "Scheduling self-destruct for message " << messageId << " in " << delaySeconds << " seconds"; // Use Drogon's timer to schedule deletion drogon::app().getLoop()->runAfter(delaySeconds, [realmId, messageId]() { LOG_DEBUG << "Self-destructing message: " << messageId; auto& modService = ModerationService::getInstance(); if (modService.deleteMessage(realmId, messageId, "system:self-destruct")) { // Broadcast deletion to all connected clients in the realm Json::Value broadcast; broadcast["type"] = "message_deleted"; broadcast["messageId"] = messageId; ChatWebSocketController::broadcastToRealm(realmId, broadcast); LOG_DEBUG << "Broadcasted self-destruct deletion for message: " << messageId; } }); } std::vector ChatService::getRealmMessages(const std::string& realmId, int limit, int64_t beforeTimestamp) { auto& redis = RedisMessageStore::getInstance(); return redis.getMessages(realmId, limit, beforeTimestamp); } bool ChatService::deleteMessage(const std::string& realmId, const std::string& messageId, const std::string& moderatorId) { auto& modService = ModerationService::getInstance(); return modService.deleteMessage(realmId, messageId, moderatorId); } models::ChatSettings ChatService::getRealmSettings(const std::string& realmId) { auto& redis = RedisMessageStore::getInstance(); return redis.getRealmSettings(realmId); } bool ChatService::updateRealmSettings(const std::string& realmId, const models::ChatSettings& settings) { auto& redis = RedisMessageStore::getInstance(); redis.setRealmSettings(realmId, settings); return true; } models::GlobalChatSettings ChatService::getGlobalSettings() { auto& redis = RedisMessageStore::getInstance(); return redis.getGlobalSettings(); } bool ChatService::updateGlobalSettings(const models::GlobalChatSettings& settings) { auto& redis = RedisMessageStore::getInstance(); redis.setGlobalSettings(settings); return true; } std::string ChatService::generateGuestUsername() { auto& redis = RedisMessageStore::getInstance(); return redis.generateGuestId(""); } std::string ChatService::getRandomDefaultAvatar() { try { // Synchronous HTTP request to backend for random default avatar auto client = drogon::HttpClient::newHttpClient("http://drogon-backend:8080"); auto req = drogon::HttpRequest::newHttpRequest(); req->setPath("/api/default-avatar/random"); req->setMethod(drogon::Get); auto [result, resp] = client->sendRequest(req, 2.0); // 2 second timeout if (result == drogon::ReqResult::Ok && resp) { auto json = resp->getJsonObject(); if (json && (*json)["success"].asBool()) { auto avatarUrl = (*json)["avatarUrl"]; if (!avatarUrl.isNull()) { return avatarUrl.asString(); } } } } catch (const std::exception& e) { LOG_WARN << "Failed to get random default avatar: " << e.what(); } return ""; // Return empty string if no default avatars available } void ChatService::startCleanupTask() { auto config = drogon::app().getCustomConfig().get("chat", Json::Value::null); int intervalSeconds = config.get("cleanup_interval_seconds", 300).asInt(); // Schedule periodic cleanup drogon::app().getLoop()->runEvery(intervalSeconds, [this]() { cleanupMessages(); }); LOG_INFO << "Chat cleanup task started (interval: " << intervalSeconds << "s)"; } bool ChatService::containsLinks(const std::string& content) { // Simple regex to detect URLs std::regex urlPattern(R"((https?://|www\.)[^\s]+)", std::regex::icase); return std::regex_search(content, urlPattern); } bool ChatService::isContentValid(const std::string& content) { // Check if content is empty or only whitespace if (content.empty() || content.find_first_not_of(" \t\n\r") == std::string::npos) { return false; } return true; } void ChatService::cleanupMessages() { LOG_DEBUG << "Running message cleanup task"; auto& redis = RedisMessageStore::getInstance(); if (!redis.isInitialized()) { LOG_WARN << "Redis not initialized - skipping cleanup"; return; } // Get list of active realms from Redis auto activeRealms = redis.getActiveRealms(); LOG_DEBUG << "Cleaning up messages for " << activeRealms.size() << " active realms"; for (const auto& realmId : activeRealms) { auto settings = redis.getRealmSettings(realmId); if (settings.retentionHours > 0) { redis.cleanupOldMessages(realmId, settings.retentionHours); LOG_DEBUG << "Cleaned up old messages for realm: " << realmId << " (retention: " << settings.retentionHours << "h)"; } // Clean up any expired self-destruct messages that weren't deleted by their timers // (e.g., due to server restart or timer failures) redis.cleanupExpiredSelfDestruct(realmId); } } // SECURITY FIX: Bot rate limiting implementation bool ChatService::canBotSendMessage(const std::string& botUserId) { std::lock_guard lock(botRateLimitMutex_); auto now = std::chrono::steady_clock::now(); auto it = botLastMessage_.find(botUserId); if (it != botLastMessage_.end()) { auto elapsed = std::chrono::duration_cast(now - it->second).count(); if (elapsed < BOT_RATE_LIMIT_MS) { LOG_DEBUG << "Bot " << botUserId << " rate limited (only " << elapsed << "ms since last message)"; return false; } } botLastMessage_[botUserId] = now; return true; } } // namespace services