beeta/chat-service/src/services/ChatService.cpp
2026-01-05 22:54:27 -05:00

294 lines
11 KiB
C++

#include "ChatService.h"
#include "RedisMessageStore.h"
#include "ModerationService.h"
#include "StickerService.h"
#include "CensorService.h"
#include "../controllers/ChatWebSocketController.h"
#include <drogon/drogon.h>
#include <drogon/HttpClient.h>
#include <regex>
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<size_t>(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<models::ChatMessage> 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)";
}
}
}
// SECURITY FIX: Bot rate limiting implementation
bool ChatService::canBotSendMessage(const std::string& botUserId) {
std::lock_guard<std::mutex> lock(botRateLimitMutex_);
auto now = std::chrono::steady_clock::now();
auto it = botLastMessage_.find(botUserId);
if (it != botLastMessage_.end()) {
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(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