294 lines
11 KiB
C++
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
|