Initial commit - realms platform
This commit is contained in:
parent
c590ab6d18
commit
c717c3751c
234 changed files with 74103 additions and 15231 deletions
129
chat-service/src/controllers/ChatAdminController.cpp
Normal file
129
chat-service/src/controllers/ChatAdminController.cpp
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
#include "ChatAdminController.h"
|
||||
#include "../services/ChatService.h"
|
||||
#include "../services/AuthService.h"
|
||||
#include "../services/RedisMessageStore.h"
|
||||
#include "../services/StickerService.h"
|
||||
#include "../services/CensorService.h"
|
||||
#include <json/json.h>
|
||||
|
||||
void ChatAdminController::getGlobalSettings(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback) {
|
||||
// Verify admin
|
||||
auto& authService = services::AuthService::getInstance();
|
||||
auto token = req->getHeader("Authorization");
|
||||
if (token.find("Bearer ") == 0) token = token.substr(7);
|
||||
|
||||
auto claims = authService.verifyToken(token);
|
||||
if (!claims.has_value() || !claims->isAdmin) {
|
||||
Json::Value error;
|
||||
error["error"] = "Unauthorized";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k401Unauthorized);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& chatService = services::ChatService::getInstance();
|
||||
auto settings = chatService.getGlobalSettings();
|
||||
|
||||
auto resp = HttpResponse::newHttpJsonResponse(settings.toJson());
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
void ChatAdminController::updateGlobalSettings(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback) {
|
||||
auto jsonPtr = req->getJsonObject();
|
||||
if (!jsonPtr) {
|
||||
Json::Value error;
|
||||
error["error"] = "Invalid JSON";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify admin
|
||||
auto& authService = services::AuthService::getInstance();
|
||||
auto token = req->getHeader("Authorization");
|
||||
if (token.find("Bearer ") == 0) token = token.substr(7);
|
||||
|
||||
auto claims = authService.verifyToken(token);
|
||||
if (!claims.has_value() || !claims->isAdmin) {
|
||||
Json::Value error;
|
||||
error["error"] = "Unauthorized";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k401Unauthorized);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto settings = models::GlobalChatSettings::fromJson(*jsonPtr);
|
||||
|
||||
auto& chatService = services::ChatService::getInstance();
|
||||
bool success = chatService.updateGlobalSettings(settings);
|
||||
|
||||
Json::Value response;
|
||||
response["success"] = success;
|
||||
response["settings"] = settings.toJson();
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
void ChatAdminController::getStats(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback) {
|
||||
// Verify admin
|
||||
auto& authService = services::AuthService::getInstance();
|
||||
auto token = req->getHeader("Authorization");
|
||||
if (token.find("Bearer ") == 0) token = token.substr(7);
|
||||
|
||||
auto claims = authService.verifyToken(token);
|
||||
if (!claims.has_value() || !claims->isAdmin) {
|
||||
Json::Value error;
|
||||
error["error"] = "Unauthorized";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k401Unauthorized);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Collect comprehensive chat statistics
|
||||
Json::Value stats;
|
||||
stats["totalMessages"] = 0;
|
||||
stats["activeConnections"] = 0;
|
||||
stats["totalRealms"] = 0;
|
||||
|
||||
auto resp = HttpResponse::newHttpJsonResponse(stats);
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
void ChatAdminController::refreshStickers(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback) {
|
||||
// Internal endpoint called by backend after sticker modifications
|
||||
// No auth required - only accessible within Docker network
|
||||
LOG_INFO << "Sticker cache refresh requested";
|
||||
|
||||
auto& stickerService = services::StickerService::getInstance();
|
||||
stickerService.refreshCache();
|
||||
|
||||
Json::Value response;
|
||||
response["success"] = true;
|
||||
response["message"] = "Sticker cache refresh initiated";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
void ChatAdminController::refreshCensoredWords(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback) {
|
||||
// Internal endpoint called by backend after censored words modifications
|
||||
// No auth required - only accessible within Docker network
|
||||
LOG_INFO << "Censored words cache refresh requested";
|
||||
|
||||
auto& censorService = services::CensorService::getInstance();
|
||||
censorService.invalidateCache();
|
||||
|
||||
Json::Value response;
|
||||
response["success"] = true;
|
||||
response["message"] = "Censored words cache refresh initiated";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
callback(resp);
|
||||
}
|
||||
21
chat-service/src/controllers/ChatAdminController.h
Normal file
21
chat-service/src/controllers/ChatAdminController.h
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#pragma once
|
||||
#include <drogon/HttpController.h>
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
class ChatAdminController : public drogon::HttpController<ChatAdminController> {
|
||||
public:
|
||||
METHOD_LIST_BEGIN
|
||||
ADD_METHOD_TO(ChatAdminController::getGlobalSettings, "/api/chat/admin/settings", Get);
|
||||
ADD_METHOD_TO(ChatAdminController::updateGlobalSettings, "/api/chat/admin/settings", Put);
|
||||
ADD_METHOD_TO(ChatAdminController::getStats, "/api/chat/admin/stats", Get);
|
||||
ADD_METHOD_TO(ChatAdminController::refreshStickers, "/api/chat/admin/stickers/refresh", Post);
|
||||
ADD_METHOD_TO(ChatAdminController::refreshCensoredWords, "/api/chat/admin/censored-words/refresh", Post);
|
||||
METHOD_LIST_END
|
||||
|
||||
void getGlobalSettings(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
void updateGlobalSettings(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
void getStats(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
void refreshStickers(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
void refreshCensoredWords(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
};
|
||||
184
chat-service/src/controllers/ChatController.cpp
Normal file
184
chat-service/src/controllers/ChatController.cpp
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
#include "ChatController.h"
|
||||
#include "ChatWebSocketController.h"
|
||||
#include "../services/ChatService.h"
|
||||
#include "../services/AuthService.h"
|
||||
#include <json/json.h>
|
||||
|
||||
void ChatController::getMessages(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback,
|
||||
const std::string& realmId) {
|
||||
auto& chatService = services::ChatService::getInstance();
|
||||
|
||||
auto limitParam = req->getParameter("limit");
|
||||
int limit = limitParam.empty() ? 100 : std::stoi(limitParam);
|
||||
|
||||
auto beforeParam = req->getParameter("before");
|
||||
int64_t before = beforeParam.empty() ? 0 : std::stoll(beforeParam);
|
||||
|
||||
auto messages = chatService.getRealmMessages(realmId, limit, before);
|
||||
|
||||
Json::Value response;
|
||||
response["messages"] = Json::arrayValue;
|
||||
for (const auto& msg : messages) {
|
||||
response["messages"].append(msg.toJson());
|
||||
}
|
||||
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
void ChatController::sendMessage(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback) {
|
||||
// This endpoint is mainly for testing; WebSocket is preferred
|
||||
auto jsonPtr = req->getJsonObject();
|
||||
if (!jsonPtr) {
|
||||
Json::Value error;
|
||||
error["error"] = "Invalid JSON";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& json = *jsonPtr;
|
||||
std::string realmId = json.get("realmId", "").asString();
|
||||
std::string content = json.get("content", "").asString();
|
||||
|
||||
if (realmId.empty() || content.empty()) {
|
||||
Json::Value error;
|
||||
error["error"] = "Missing required fields";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get user from token
|
||||
auto& authService = services::AuthService::getInstance();
|
||||
auto token = req->getHeader("Authorization");
|
||||
if (token.find("Bearer ") == 0) {
|
||||
token = token.substr(7);
|
||||
}
|
||||
|
||||
auto claims = authService.verifyToken(token);
|
||||
if (!claims.has_value()) {
|
||||
Json::Value error;
|
||||
error["error"] = "Unauthorized";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k401Unauthorized);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& chatService = services::ChatService::getInstance();
|
||||
models::ChatMessage message;
|
||||
|
||||
auto result = chatService.sendMessage(
|
||||
realmId, claims->userId, claims->username, claims->userColor, claims->avatarUrl, content,
|
||||
false, false, false, message
|
||||
);
|
||||
|
||||
if (result == services::SendMessageResult::SUCCESS) {
|
||||
auto resp = HttpResponse::newHttpJsonResponse(message.toJson());
|
||||
callback(resp);
|
||||
} else {
|
||||
Json::Value error;
|
||||
error["error"] = "Failed to send message";
|
||||
error["reason"] = static_cast<int>(result);
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatController::deleteMessage(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback,
|
||||
const std::string& messageId) {
|
||||
// Verify user is moderator
|
||||
auto& authService = services::AuthService::getInstance();
|
||||
auto token = req->getHeader("Authorization");
|
||||
if (token.find("Bearer ") == 0) {
|
||||
token = token.substr(7);
|
||||
}
|
||||
|
||||
auto claims = authService.verifyToken(token);
|
||||
if (!claims.has_value()) {
|
||||
Json::Value error;
|
||||
error["error"] = "Unauthorized";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k401Unauthorized);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
std::string realmId = req->getParameter("realmId");
|
||||
auto& chatService = services::ChatService::getInstance();
|
||||
|
||||
bool success = chatService.deleteMessage(realmId, messageId, claims->userId);
|
||||
|
||||
Json::Value response;
|
||||
response["success"] = success;
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
void ChatController::getSettings(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback,
|
||||
const std::string& realmId) {
|
||||
auto& chatService = services::ChatService::getInstance();
|
||||
auto settings = chatService.getRealmSettings(realmId);
|
||||
|
||||
auto resp = HttpResponse::newHttpJsonResponse(settings.toJson());
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
void ChatController::updateSettings(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback,
|
||||
const std::string& realmId) {
|
||||
auto jsonPtr = req->getJsonObject();
|
||||
if (!jsonPtr) {
|
||||
Json::Value error;
|
||||
error["error"] = "Invalid JSON";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify user is realm owner or moderator
|
||||
auto& authService = services::AuthService::getInstance();
|
||||
auto token = req->getHeader("Authorization");
|
||||
if (token.find("Bearer ") == 0) {
|
||||
token = token.substr(7);
|
||||
}
|
||||
|
||||
auto claims = authService.verifyToken(token);
|
||||
if (!claims.has_value()) {
|
||||
Json::Value error;
|
||||
error["error"] = "Unauthorized";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k401Unauthorized);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto settings = models::ChatSettings::fromJson(*jsonPtr);
|
||||
settings.realmId = realmId;
|
||||
|
||||
auto& chatService = services::ChatService::getInstance();
|
||||
bool success = chatService.updateRealmSettings(realmId, settings);
|
||||
|
||||
Json::Value response;
|
||||
response["success"] = success;
|
||||
response["settings"] = settings.toJson();
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
void ChatController::getRealmStats(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback) {
|
||||
// Get realm stats from WebSocket controller (active connections per realm)
|
||||
auto stats = ChatWebSocketController::getRealmStats();
|
||||
auto resp = HttpResponse::newHttpJsonResponse(stats);
|
||||
callback(resp);
|
||||
}
|
||||
38
chat-service/src/controllers/ChatController.h
Normal file
38
chat-service/src/controllers/ChatController.h
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#pragma once
|
||||
#include <drogon/HttpController.h>
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
class ChatController : public drogon::HttpController<ChatController> {
|
||||
public:
|
||||
METHOD_LIST_BEGIN
|
||||
ADD_METHOD_TO(ChatController::getMessages, "/api/chat/messages/{realmId}", Get);
|
||||
ADD_METHOD_TO(ChatController::sendMessage, "/api/chat/send", Post);
|
||||
ADD_METHOD_TO(ChatController::deleteMessage, "/api/chat/message/{messageId}", Delete);
|
||||
ADD_METHOD_TO(ChatController::getSettings, "/api/chat/settings/{realmId}", Get);
|
||||
ADD_METHOD_TO(ChatController::updateSettings, "/api/chat/settings/{realmId}", Put);
|
||||
ADD_METHOD_TO(ChatController::getRealmStats, "/api/chat/realms/stats", Get);
|
||||
METHOD_LIST_END
|
||||
|
||||
void getMessages(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback,
|
||||
const std::string& realmId);
|
||||
|
||||
void sendMessage(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
|
||||
void deleteMessage(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback,
|
||||
const std::string& messageId);
|
||||
|
||||
void getSettings(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback,
|
||||
const std::string& realmId);
|
||||
|
||||
void updateSettings(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback,
|
||||
const std::string& realmId);
|
||||
|
||||
void getRealmStats(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
};
|
||||
1340
chat-service/src/controllers/ChatWebSocketController.cpp
Normal file
1340
chat-service/src/controllers/ChatWebSocketController.cpp
Normal file
File diff suppressed because it is too large
Load diff
103
chat-service/src/controllers/ChatWebSocketController.h
Normal file
103
chat-service/src/controllers/ChatWebSocketController.h
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#pragma once
|
||||
#include <drogon/WebSocketController.h>
|
||||
#include <drogon/PubSubService.h>
|
||||
#include <unordered_map>
|
||||
#include <memory>
|
||||
#include <chrono>
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
class ChatWebSocketController : public drogon::WebSocketController<ChatWebSocketController> {
|
||||
public:
|
||||
void handleNewMessage(const WebSocketConnectionPtr& wsConnPtr, std::string&& message,
|
||||
const WebSocketMessageType& type) override;
|
||||
|
||||
void handleNewConnection(const HttpRequestPtr& req, const WebSocketConnectionPtr& wsConnPtr) override;
|
||||
|
||||
void handleConnectionClosed(const WebSocketConnectionPtr& wsConnPtr) override;
|
||||
|
||||
WS_PATH_LIST_BEGIN
|
||||
WS_PATH_ADD("/chat/ws");
|
||||
WS_PATH_ADD("/chat/stream/{1}");
|
||||
WS_PATH_LIST_END
|
||||
|
||||
static void broadcastMessage(const std::string& realmId, const Json::Value& message);
|
||||
static void broadcastToRealm(const std::string& realmId, const Json::Value& message);
|
||||
static void broadcastToUser(const std::string& userId, const Json::Value& message);
|
||||
|
||||
// Get stats for all active realms (realmId -> participant count)
|
||||
static Json::Value getRealmStats();
|
||||
|
||||
// Force linker to include this object file
|
||||
static void ensureLoaded();
|
||||
|
||||
// Check and disconnect guests that have exceeded their session timeout
|
||||
static void checkGuestTimeouts();
|
||||
|
||||
private:
|
||||
struct ConnectionInfo {
|
||||
std::string realmId;
|
||||
std::string userId;
|
||||
std::string username;
|
||||
std::string userColor;
|
||||
std::string avatarUrl;
|
||||
std::string fingerprint; // Browser fingerprint for guests (empty for registered users)
|
||||
bool isGuest = false;
|
||||
bool isAdmin = false; // Site admin (from JWT)
|
||||
bool isSiteModerator = false; // Site-wide moderator role (from JWT is_moderator claim)
|
||||
bool isModerator = false; // Has mod powers in current realm (computed: admin/siteMod/realmOwner/realmMod)
|
||||
bool isStreamer = false;
|
||||
bool isRestreamer = false; // SECURITY FIX #9: Added for auth message handling
|
||||
bool isApiKeyConnection = false; // Bot API key connections (can only send/receive, no mod actions)
|
||||
int64_t apiKeyId = 0; // API key ID for bot connections (for connection limit tracking)
|
||||
std::string botScopes; // Scopes for bot connections (e.g., "chat:rw")
|
||||
std::chrono::system_clock::time_point connectionTime; // When this connection was established
|
||||
int sessionTimeoutMinutes = 0; // Random timeout for guests (0 = no timeout)
|
||||
};
|
||||
|
||||
static std::unordered_map<WebSocketConnectionPtr, ConnectionInfo> connections_;
|
||||
static std::unordered_map<WebSocketConnectionPtr, std::chrono::steady_clock::time_point> pendingConnections_; // SECURITY FIX: Track pending API key validations
|
||||
static std::unordered_map<int64_t, WebSocketConnectionPtr> apiKeyConnections_; // SECURITY FIX: Track 1 connection per API key
|
||||
static std::unordered_map<WebSocketConnectionPtr, std::chrono::steady_clock::time_point> lastRenameTime_; // SECURITY FIX #24: Rate limit guest renames
|
||||
static std::unordered_map<std::string, WebSocketConnectionPtr> usernameToConnection_; // SECURITY FIX #25: O(1) username collision lookup
|
||||
static std::mutex connectionsMutex_;
|
||||
|
||||
void handleChatMessage(const WebSocketConnectionPtr& wsConnPtr,
|
||||
const ConnectionInfo& info,
|
||||
const Json::Value& data);
|
||||
|
||||
void handleJoinRealm(const WebSocketConnectionPtr& wsConnPtr,
|
||||
ConnectionInfo& info,
|
||||
const Json::Value& data);
|
||||
|
||||
void handleModAction(const WebSocketConnectionPtr& wsConnPtr,
|
||||
const ConnectionInfo& info,
|
||||
const Json::Value& data);
|
||||
|
||||
void handleGetParticipants(const WebSocketConnectionPtr& wsConnPtr,
|
||||
const ConnectionInfo& info);
|
||||
|
||||
void handleRename(const WebSocketConnectionPtr& wsConnPtr,
|
||||
ConnectionInfo& info,
|
||||
const Json::Value& data);
|
||||
|
||||
// SECURITY FIX #9: Handle auth token sent as message instead of URL param
|
||||
void handleAuthMessage(const WebSocketConnectionPtr& wsConnPtr,
|
||||
const Json::Value& data);
|
||||
|
||||
// SECURITY FIX: Handle bot API key authentication via message (not URL)
|
||||
void handleBotApiKeyAuth(const WebSocketConnectionPtr& wsConnPtr,
|
||||
const std::string& apiKey);
|
||||
|
||||
void sendError(const WebSocketConnectionPtr& wsConnPtr, const std::string& error);
|
||||
void sendSuccess(const WebSocketConnectionPtr& wsConnPtr, const Json::Value& data);
|
||||
|
||||
// Broadcast participant events (caller must hold connectionsMutex_)
|
||||
static void broadcastParticipantJoined(const std::string& realmId, const ConnectionInfo& joinedUser);
|
||||
static void broadcastParticipantLeft(const std::string& realmId, const std::string& userId, const std::string& username);
|
||||
|
||||
public:
|
||||
// Internal API: Try to uberban a user by ID (used by backend admin endpoint)
|
||||
// Returns: fingerprint if user was connected and banned, empty string if not connected
|
||||
static std::string tryUberbanConnectedUser(const std::string& userId);
|
||||
};
|
||||
392
chat-service/src/controllers/ModerationController.cpp
Normal file
392
chat-service/src/controllers/ModerationController.cpp
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
#include "ModerationController.h"
|
||||
#include "ChatWebSocketController.h"
|
||||
#include "../services/ModerationService.h"
|
||||
#include "../services/AuthService.h"
|
||||
#include <json/json.h>
|
||||
|
||||
// Helper to check if user can perform uberban (admins + site moderators only)
|
||||
static bool canUberban(const services::UserClaims& claims) {
|
||||
return claims.isAdmin || claims.isModerator;
|
||||
}
|
||||
|
||||
// Helper to check if user can perform realm moderation (admin, site mod, or has mod flag)
|
||||
static bool canModerate(const services::UserClaims& claims) {
|
||||
return claims.isAdmin || claims.isModerator;
|
||||
// TODO: Add realm owner and per-realm moderator checks via backend API
|
||||
}
|
||||
|
||||
void ModerationController::uberbanUser(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback) {
|
||||
auto jsonPtr = req->getJsonObject();
|
||||
if (!jsonPtr) {
|
||||
Json::Value error;
|
||||
error["error"] = "Invalid JSON";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& json = *jsonPtr;
|
||||
std::string fingerprint = json.get("fingerprint", "").asString();
|
||||
std::string reason = json.get("reason", "").asString();
|
||||
|
||||
if (fingerprint.empty()) {
|
||||
Json::Value error;
|
||||
error["error"] = "Fingerprint required";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify admin or site moderator
|
||||
auto& authService = services::AuthService::getInstance();
|
||||
auto token = req->getHeader("Authorization");
|
||||
if (token.find("Bearer ") == 0) token = token.substr(7);
|
||||
|
||||
auto claims = authService.verifyToken(token);
|
||||
if (!claims.has_value() || !canUberban(*claims)) {
|
||||
Json::Value error;
|
||||
error["error"] = "Unauthorized - only admins and site moderators can uberban";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k401Unauthorized);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& modService = services::ModerationService::getInstance();
|
||||
bool success = modService.uberbanUser(fingerprint, claims->userId, reason);
|
||||
|
||||
Json::Value response;
|
||||
response["success"] = success;
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
void ModerationController::unUberbanUser(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback) {
|
||||
auto jsonPtr = req->getJsonObject();
|
||||
if (!jsonPtr) {
|
||||
Json::Value error;
|
||||
error["error"] = "Invalid JSON";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& json = *jsonPtr;
|
||||
std::string fingerprint = json.get("fingerprint", "").asString();
|
||||
|
||||
if (fingerprint.empty()) {
|
||||
Json::Value error;
|
||||
error["error"] = "Fingerprint required";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& authService = services::AuthService::getInstance();
|
||||
auto token = req->getHeader("Authorization");
|
||||
if (token.find("Bearer ") == 0) token = token.substr(7);
|
||||
|
||||
auto claims = authService.verifyToken(token);
|
||||
if (!claims.has_value() || !canUberban(*claims)) {
|
||||
Json::Value error;
|
||||
error["error"] = "Unauthorized - only admins and site moderators can remove uberbans";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k401Unauthorized);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& modService = services::ModerationService::getInstance();
|
||||
bool success = modService.unUberbanUser(fingerprint, claims->userId);
|
||||
|
||||
Json::Value response;
|
||||
response["success"] = success;
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
void ModerationController::getUberbannedUsers(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback) {
|
||||
auto& authService = services::AuthService::getInstance();
|
||||
auto token = req->getHeader("Authorization");
|
||||
if (token.find("Bearer ") == 0) token = token.substr(7);
|
||||
|
||||
auto claims = authService.verifyToken(token);
|
||||
if (!claims.has_value() || !canUberban(*claims)) {
|
||||
Json::Value error;
|
||||
error["error"] = "Unauthorized";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k401Unauthorized);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& modService = services::ModerationService::getInstance();
|
||||
auto uberbannedFingerprints = modService.getUberbannedFingerprints();
|
||||
|
||||
Json::Value response;
|
||||
response["uberbannedFingerprints"] = Json::arrayValue;
|
||||
for (const auto& fp : uberbannedFingerprints) {
|
||||
response["uberbannedFingerprints"].append(fp);
|
||||
}
|
||||
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
void ModerationController::banUser(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback) {
|
||||
auto jsonPtr = req->getJsonObject();
|
||||
if (!jsonPtr) {
|
||||
Json::Value error;
|
||||
error["error"] = "Invalid JSON";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& json = *jsonPtr;
|
||||
std::string realmId = json.get("realmId", "").asString();
|
||||
std::string targetUserId = json.get("targetUserId", "").asString();
|
||||
std::string guestFingerprint = json.get("fingerprint", "").asString();
|
||||
std::string reason = json.get("reason", "").asString();
|
||||
|
||||
// Verify moderator
|
||||
auto& authService = services::AuthService::getInstance();
|
||||
auto token = req->getHeader("Authorization");
|
||||
if (token.find("Bearer ") == 0) token = token.substr(7);
|
||||
|
||||
auto claims = authService.verifyToken(token);
|
||||
if (!claims.has_value() || !canModerate(*claims)) {
|
||||
Json::Value error;
|
||||
error["error"] = "Unauthorized";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k401Unauthorized);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& modService = services::ModerationService::getInstance();
|
||||
bool success = modService.banUserFromRealm(realmId, targetUserId, claims->userId, reason, guestFingerprint);
|
||||
|
||||
Json::Value response;
|
||||
response["success"] = success;
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
void ModerationController::unbanUser(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback) {
|
||||
auto jsonPtr = req->getJsonObject();
|
||||
if (!jsonPtr) {
|
||||
Json::Value error;
|
||||
error["error"] = "Invalid JSON";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& json = *jsonPtr;
|
||||
std::string realmId = json.get("realmId", "").asString();
|
||||
std::string targetUserId = json.get("targetUserId", "").asString();
|
||||
std::string guestFingerprint = json.get("fingerprint", "").asString();
|
||||
|
||||
auto& authService = services::AuthService::getInstance();
|
||||
auto token = req->getHeader("Authorization");
|
||||
if (token.find("Bearer ") == 0) token = token.substr(7);
|
||||
|
||||
auto claims = authService.verifyToken(token);
|
||||
if (!claims.has_value() || !canModerate(*claims)) {
|
||||
Json::Value error;
|
||||
error["error"] = "Unauthorized";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k401Unauthorized);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& modService = services::ModerationService::getInstance();
|
||||
bool success = modService.unbanUserFromRealm(realmId, targetUserId, claims->userId, guestFingerprint);
|
||||
|
||||
Json::Value response;
|
||||
response["success"] = success;
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
void ModerationController::getBannedUsers(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback,
|
||||
const std::string& realmId) {
|
||||
auto& modService = services::ModerationService::getInstance();
|
||||
auto bannedIdentifiers = modService.getRealmBannedIdentifiers(realmId);
|
||||
|
||||
Json::Value response;
|
||||
response["bannedUsers"] = Json::arrayValue;
|
||||
for (const auto& identifier : bannedIdentifiers) {
|
||||
response["bannedUsers"].append(identifier);
|
||||
}
|
||||
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
void ModerationController::kickUser(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback) {
|
||||
auto jsonPtr = req->getJsonObject();
|
||||
if (!jsonPtr) {
|
||||
Json::Value error;
|
||||
error["error"] = "Invalid JSON";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& json = *jsonPtr;
|
||||
std::string realmId = json.get("realmId", "").asString();
|
||||
std::string targetUserId = json.get("targetUserId", "").asString();
|
||||
std::string reason = json.get("reason", "").asString();
|
||||
int duration = json.get("duration", 60).asInt(); // Default 1 minute
|
||||
|
||||
auto& authService = services::AuthService::getInstance();
|
||||
auto token = req->getHeader("Authorization");
|
||||
if (token.find("Bearer ") == 0) token = token.substr(7);
|
||||
|
||||
auto claims = authService.verifyToken(token);
|
||||
if (!claims.has_value() || !canModerate(*claims)) {
|
||||
Json::Value error;
|
||||
error["error"] = "Unauthorized";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k401Unauthorized);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& modService = services::ModerationService::getInstance();
|
||||
bool success = modService.kickUser(realmId, targetUserId, claims->userId, reason, duration);
|
||||
|
||||
Json::Value response;
|
||||
response["success"] = success;
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
void ModerationController::muteUser(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback) {
|
||||
auto jsonPtr = req->getJsonObject();
|
||||
if (!jsonPtr) {
|
||||
Json::Value error;
|
||||
error["error"] = "Invalid JSON";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& json = *jsonPtr;
|
||||
std::string realmId = json.get("realmId", "").asString();
|
||||
std::string targetUserId = json.get("targetUserId", "").asString();
|
||||
int duration = json.get("duration", 0).asInt(); // 0 = permanent (default)
|
||||
std::string reason = json.get("reason", "").asString();
|
||||
|
||||
auto& authService = services::AuthService::getInstance();
|
||||
auto token = req->getHeader("Authorization");
|
||||
if (token.find("Bearer ") == 0) token = token.substr(7);
|
||||
|
||||
auto claims = authService.verifyToken(token);
|
||||
if (!claims.has_value() || !canModerate(*claims)) {
|
||||
Json::Value error;
|
||||
error["error"] = "Unauthorized";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k401Unauthorized);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& modService = services::ModerationService::getInstance();
|
||||
bool success = modService.muteUser(realmId, targetUserId, claims->userId, duration, reason);
|
||||
|
||||
Json::Value response;
|
||||
response["success"] = success;
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
void ModerationController::unmuteUser(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback) {
|
||||
auto jsonPtr = req->getJsonObject();
|
||||
if (!jsonPtr) {
|
||||
Json::Value error;
|
||||
error["error"] = "Invalid JSON";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& json = *jsonPtr;
|
||||
std::string realmId = json.get("realmId", "").asString();
|
||||
std::string targetUserId = json.get("targetUserId", "").asString();
|
||||
|
||||
auto& authService = services::AuthService::getInstance();
|
||||
auto token = req->getHeader("Authorization");
|
||||
if (token.find("Bearer ") == 0) token = token.substr(7);
|
||||
|
||||
auto claims = authService.verifyToken(token);
|
||||
if (!claims.has_value() || !canModerate(*claims)) {
|
||||
Json::Value error;
|
||||
error["error"] = "Unauthorized";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k401Unauthorized);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& modService = services::ModerationService::getInstance();
|
||||
bool success = modService.unmuteUser(realmId, targetUserId, claims->userId);
|
||||
|
||||
Json::Value response;
|
||||
response["success"] = success;
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
// Internal API: Called by backend to uberban a user
|
||||
// Option C: All registered user uberbans are deferred - fingerprint captured on reconnect
|
||||
// If user is connected: disconnect them + set pending_uberban (fingerprint captured on reconnect)
|
||||
// If not connected: backend should set pending_uberban
|
||||
// Returns { disconnected: true/false }
|
||||
void ModerationController::internalUberbanUser(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback,
|
||||
const std::string& userId) {
|
||||
// This is an internal API - no auth check needed (only accessible from within Docker network)
|
||||
// The backend AdminController handles authentication before calling this
|
||||
|
||||
// Try to disconnect the user if they're connected (sets pending_uberban + disconnects)
|
||||
std::string result = ChatWebSocketController::tryUberbanConnectedUser(userId);
|
||||
|
||||
Json::Value response;
|
||||
if (result == "disconnected") {
|
||||
// User was connected - disconnected and pending_uberban set
|
||||
// Fingerprint will be captured on reconnect
|
||||
response["disconnected"] = true;
|
||||
response["immediate"] = false; // Fingerprint captured on reconnect
|
||||
LOG_INFO << "Internal uberban: User " << userId << " was connected, disconnected (pending uberban)";
|
||||
} else {
|
||||
// User not connected - caller should set pending_uberban
|
||||
response["disconnected"] = false;
|
||||
response["immediate"] = false;
|
||||
LOG_INFO << "Internal uberban: User " << userId << " not connected, pending uberban needed";
|
||||
}
|
||||
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
callback(resp);
|
||||
}
|
||||
46
chat-service/src/controllers/ModerationController.h
Normal file
46
chat-service/src/controllers/ModerationController.h
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#pragma once
|
||||
#include <drogon/HttpController.h>
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
class ModerationController : public drogon::HttpController<ModerationController> {
|
||||
public:
|
||||
METHOD_LIST_BEGIN
|
||||
// Site-wide uberban (admins + site moderators only)
|
||||
ADD_METHOD_TO(ModerationController::uberbanUser, "/api/chat/uberban", Post);
|
||||
ADD_METHOD_TO(ModerationController::unUberbanUser, "/api/chat/unuberban", Post);
|
||||
ADD_METHOD_TO(ModerationController::getUberbannedUsers, "/api/chat/uberbanned", Get);
|
||||
|
||||
// Per-realm ban (admins, site mods, realm owners, realm mods)
|
||||
ADD_METHOD_TO(ModerationController::banUser, "/api/chat/ban", Post);
|
||||
ADD_METHOD_TO(ModerationController::unbanUser, "/api/chat/unban", Post);
|
||||
ADD_METHOD_TO(ModerationController::getBannedUsers, "/api/chat/banned/{realmId}", Get);
|
||||
|
||||
// Kick (admins, site mods, realm owners, realm mods)
|
||||
ADD_METHOD_TO(ModerationController::kickUser, "/api/chat/kick", Post);
|
||||
|
||||
// Mute (admins, site mods, realm owners, realm mods)
|
||||
ADD_METHOD_TO(ModerationController::muteUser, "/api/chat/mute", Post);
|
||||
ADD_METHOD_TO(ModerationController::unmuteUser, "/api/chat/unmute", Post);
|
||||
|
||||
// Internal endpoint for backend to uberban a connected user (gets fingerprint from active connection)
|
||||
ADD_METHOD_TO(ModerationController::internalUberbanUser, "/api/internal/user/{userId}/uberban", Post);
|
||||
METHOD_LIST_END
|
||||
|
||||
void uberbanUser(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
void unUberbanUser(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
void getUberbannedUsers(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
|
||||
void banUser(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
void unbanUser(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
void getBannedUsers(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback,
|
||||
const std::string& realmId);
|
||||
|
||||
void kickUser(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
|
||||
void muteUser(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
void unmuteUser(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
|
||||
void internalUberbanUser(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback,
|
||||
const std::string& userId);
|
||||
};
|
||||
1470
chat-service/src/controllers/WatchSyncController.cpp
Normal file
1470
chat-service/src/controllers/WatchSyncController.cpp
Normal file
File diff suppressed because it is too large
Load diff
152
chat-service/src/controllers/WatchSyncController.h
Normal file
152
chat-service/src/controllers/WatchSyncController.h
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
#pragma once
|
||||
#include <drogon/WebSocketController.h>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
class WatchSyncController : public drogon::WebSocketController<WatchSyncController> {
|
||||
public:
|
||||
void handleNewMessage(const WebSocketConnectionPtr& wsConnPtr, std::string&& message,
|
||||
const WebSocketMessageType& type) override;
|
||||
|
||||
void handleNewConnection(const HttpRequestPtr& req, const WebSocketConnectionPtr& wsConnPtr) override;
|
||||
|
||||
void handleConnectionClosed(const WebSocketConnectionPtr& wsConnPtr) override;
|
||||
|
||||
WS_PATH_LIST_BEGIN
|
||||
WS_PATH_ADD("/watch/ws");
|
||||
WS_PATH_LIST_END
|
||||
|
||||
// Broadcast sync state to all viewers in a watch room
|
||||
static void broadcastToRoom(const std::string& realmId, const Json::Value& message);
|
||||
|
||||
// Get viewer count for a room
|
||||
static int getViewerCount(const std::string& realmId);
|
||||
|
||||
// Force linker to include this object file
|
||||
static void ensureLoaded();
|
||||
|
||||
// Start/stop the sync loop (called on first connection and when last viewer leaves)
|
||||
static void startSyncLoop();
|
||||
static void stopSyncLoop();
|
||||
|
||||
private:
|
||||
struct ViewerInfo {
|
||||
std::string realmId;
|
||||
std::string userId;
|
||||
std::string username;
|
||||
std::string authToken; // Store auth token for backend API calls
|
||||
bool canAddToPlaylist = false;
|
||||
bool canControlPlayback = false;
|
||||
bool isGuest = false;
|
||||
std::chrono::system_clock::time_point connectionTime;
|
||||
|
||||
// Rate limiting fields
|
||||
int64_t lastMessageMs = 0; // Timestamp of last message
|
||||
int messageCount = 0; // Message count in current window
|
||||
int64_t windowStartMs = 0; // Start of rate limit window
|
||||
};
|
||||
|
||||
// In-memory room state for accurate time tracking (CyTube-style)
|
||||
struct RoomState {
|
||||
std::string playbackState = "paused"; // "playing", "paused", "ended", "buffering"
|
||||
double currentTime = 0.0; // Current playback position in seconds
|
||||
int64_t lastUpdateMs = 0; // Timestamp of last update (milliseconds)
|
||||
std::string currentVideoId; // YouTube video ID
|
||||
int64_t currentPlaylistItemId = 0; // Playlist item ID
|
||||
std::string currentVideoTitle;
|
||||
int durationSeconds = 0;
|
||||
bool leadInActive = false; // True during initial buffering period
|
||||
int64_t leadInStartMs = 0; // When lead-in started
|
||||
int repeatCount = 0; // Current repeat count for last video (max 3)
|
||||
bool isRepeating = false; // True when in repeat mode (last video looping)
|
||||
bool currentVideoLocked = false; // True if current video is locked (loops forever)
|
||||
|
||||
// Skip idempotency tracking
|
||||
int64_t lastSkipMs = 0; // Timestamp of last skip (prevents double-skip)
|
||||
uint64_t stateVersion = 0; // State version for sync validation
|
||||
|
||||
// State freshness tracking
|
||||
int64_t lastDbSyncMs = 0; // Last time state was synced from database
|
||||
};
|
||||
|
||||
static std::unordered_map<WebSocketConnectionPtr, ViewerInfo> viewers_;
|
||||
static std::mutex viewersMutex_;
|
||||
|
||||
// In-memory room states (keyed by realmId)
|
||||
static std::unordered_map<std::string, RoomState> roomStates_;
|
||||
static std::mutex roomStatesMutex_;
|
||||
|
||||
// Sync loop thread
|
||||
static std::thread syncLoopThread_;
|
||||
static std::atomic<bool> syncLoopRunning_;
|
||||
static std::mutex syncLoopMutex_; // Protects thread start/stop operations
|
||||
|
||||
// Sync loop - runs every second to update time and broadcast
|
||||
static void syncLoop();
|
||||
|
||||
// Update room state from database (called when joining or on state change)
|
||||
static void updateRoomStateFromDb(const std::string& realmId);
|
||||
|
||||
// Broadcast current state to all viewers in a room
|
||||
static void broadcastRoomSync(const std::string& realmId);
|
||||
|
||||
// Auto-advance to next video when current video ends (server-side, no owner required)
|
||||
static void autoAdvanceToNextVideo(const std::string& realmId);
|
||||
|
||||
// Get current expected playback time for a room
|
||||
static double getExpectedTime(const RoomState& state);
|
||||
|
||||
void handleJoinRoom(const WebSocketConnectionPtr& wsConnPtr,
|
||||
ViewerInfo& info,
|
||||
const Json::Value& data);
|
||||
|
||||
void handleSyncRequest(const WebSocketConnectionPtr& wsConnPtr,
|
||||
const ViewerInfo& info);
|
||||
|
||||
void handlePlaybackControl(const WebSocketConnectionPtr& wsConnPtr,
|
||||
const ViewerInfo& info,
|
||||
const Json::Value& data);
|
||||
|
||||
void handleSkipWithRepeat(const WebSocketConnectionPtr& wsConnPtr,
|
||||
const ViewerInfo& info,
|
||||
const Json::Value& data);
|
||||
|
||||
void performSkip(const WebSocketConnectionPtr& wsConnPtr,
|
||||
const ViewerInfo& info,
|
||||
const Json::Value& data);
|
||||
|
||||
void handleUpdateDuration(const WebSocketConnectionPtr& wsConnPtr,
|
||||
const ViewerInfo& info,
|
||||
const Json::Value& data);
|
||||
|
||||
void sendError(const WebSocketConnectionPtr& wsConnPtr, const std::string& error);
|
||||
void sendSuccess(const WebSocketConnectionPtr& wsConnPtr, const Json::Value& data);
|
||||
|
||||
// Helper to check if connection still exists (for async callback safety)
|
||||
static bool isConnectionValid(const WebSocketConnectionPtr& wsConnPtr);
|
||||
|
||||
// Helper to safely send to a connection (validates first)
|
||||
static void safeSend(const WebSocketConnectionPtr& wsConnPtr, const std::string& message);
|
||||
|
||||
// Broadcast viewer count update
|
||||
static void broadcastViewerCount(const std::string& realmId);
|
||||
|
||||
// Rate limiting check (returns true if message should be processed)
|
||||
bool checkRateLimit(const WebSocketConnectionPtr& wsConnPtr);
|
||||
|
||||
// Constants for rate limiting
|
||||
static constexpr int RATE_LIMIT_MESSAGES = 30; // Max messages per window
|
||||
static constexpr int64_t RATE_LIMIT_WINDOW_MS = 10000; // 10 second window
|
||||
static constexpr int64_t MIN_MESSAGE_INTERVAL_MS = 100; // Min 100ms between messages
|
||||
|
||||
// Skip debounce interval (prevent double-skips within this window)
|
||||
static constexpr int64_t SKIP_DEBOUNCE_MS = 1000;
|
||||
|
||||
// Database sync freshness threshold (refresh from DB if older than this)
|
||||
static constexpr int64_t DB_SYNC_STALE_MS = 5000;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue