This commit is contained in:
parent
33c20bf59d
commit
a92bfc1d22
14 changed files with 2629 additions and 59 deletions
779
backend/src/controllers/PyramidController.cpp
Normal file
779
backend/src/controllers/PyramidController.cpp
Normal file
|
|
@ -0,0 +1,779 @@
|
|||
#include "PyramidController.h"
|
||||
#include "../services/DatabaseService.h"
|
||||
#include "../common/HttpHelpers.h"
|
||||
#include "../common/AuthHelpers.h"
|
||||
#include <regex>
|
||||
#include <mutex>
|
||||
#include <chrono>
|
||||
|
||||
using namespace drogon::orm;
|
||||
|
||||
// Static members for WebSocket controller
|
||||
std::unordered_map<WebSocketConnectionPtr, PyramidWebSocketController::ConnectionInfo> PyramidWebSocketController::connections_;
|
||||
std::mutex PyramidWebSocketController::connectionsMutex_;
|
||||
|
||||
// ==================== Validation Helpers ====================
|
||||
|
||||
bool PyramidController::isValidPixelPosition(int faceId, int x, int y) {
|
||||
// Face ID must be 0-4
|
||||
if (faceId < 0 || faceId > 4) return false;
|
||||
|
||||
// X and Y must be within face bounds
|
||||
if (x < 0 || x >= FACE_SIZE || y < 0 || y >= FACE_SIZE) return false;
|
||||
|
||||
// For triangular faces (1-4), validate within triangle bounds
|
||||
if (faceId > 0) {
|
||||
// Triangle with base at bottom (y=199), apex at top (y=0)
|
||||
// At y=0, only x=99,100 are valid (center)
|
||||
// At y=199, x=0-199 are valid (full width)
|
||||
int halfWidth = (y + 1) / 2;
|
||||
int center = FACE_SIZE / 2;
|
||||
int minX = center - halfWidth;
|
||||
int maxX = center + halfWidth;
|
||||
if (x < minX || x > maxX) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PyramidController::isValidColor(const std::string &color) {
|
||||
// Must be a valid hex color like #RRGGBB
|
||||
if (color.length() != 7 || color[0] != '#') return false;
|
||||
|
||||
for (int i = 1; i < 7; i++) {
|
||||
char c = std::toupper(color[i]);
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F'))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ==================== Canvas State Methods ====================
|
||||
|
||||
void PyramidController::getState(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
try {
|
||||
auto dbClient = app().getDbClient();
|
||||
|
||||
// Get all pixels grouped by face
|
||||
*dbClient << "SELECT face_id, x, y, color FROM pyramid_pixels ORDER BY face_id, y, x"
|
||||
>> [callback](const Result& r) {
|
||||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
resp["pixels"] = Json::Value(Json::arrayValue);
|
||||
|
||||
for (const auto& row : r) {
|
||||
Json::Value pixel;
|
||||
pixel["faceId"] = row["face_id"].as<int>();
|
||||
pixel["x"] = row["x"].as<int>();
|
||||
pixel["y"] = row["y"].as<int>();
|
||||
pixel["color"] = row["color"].as<std::string>();
|
||||
resp["pixels"].append(pixel);
|
||||
}
|
||||
|
||||
resp["totalPixels"] = static_cast<Json::UInt>(r.size());
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
>> DB_ERROR(callback, "get pyramid state");
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR << "Exception in getState: " << e.what();
|
||||
callback(jsonError("Internal server error"));
|
||||
}
|
||||
}
|
||||
|
||||
void PyramidController::getFaceState(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &faceIdStr) {
|
||||
try {
|
||||
PARSE_ID(faceId, faceIdStr, callback);
|
||||
|
||||
if (faceId < 0 || faceId > 4) {
|
||||
callback(jsonError("Invalid face ID (must be 0-4)"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto dbClient = app().getDbClient();
|
||||
*dbClient << "SELECT x, y, color FROM pyramid_pixels WHERE face_id = $1 ORDER BY y, x"
|
||||
<< static_cast<int>(faceId)
|
||||
>> [callback, faceId](const Result& r) {
|
||||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
resp["faceId"] = static_cast<int>(faceId);
|
||||
resp["pixels"] = Json::Value(Json::arrayValue);
|
||||
|
||||
for (const auto& row : r) {
|
||||
Json::Value pixel;
|
||||
pixel["x"] = row["x"].as<int>();
|
||||
pixel["y"] = row["y"].as<int>();
|
||||
pixel["color"] = row["color"].as<std::string>();
|
||||
resp["pixels"].append(pixel);
|
||||
}
|
||||
|
||||
resp["totalPixels"] = static_cast<Json::UInt>(r.size());
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
>> DB_ERROR(callback, "get face state");
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR << "Exception in getFaceState: " << e.what();
|
||||
callback(jsonError("Internal server error"));
|
||||
}
|
||||
}
|
||||
|
||||
void PyramidController::getColors(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
try {
|
||||
auto dbClient = app().getDbClient();
|
||||
*dbClient << "SELECT color, name FROM pyramid_colors WHERE is_active = true ORDER BY sort_order"
|
||||
>> [callback](const Result& r) {
|
||||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
resp["colors"] = Json::Value(Json::arrayValue);
|
||||
|
||||
for (const auto& row : r) {
|
||||
Json::Value color;
|
||||
color["color"] = row["color"].as<std::string>();
|
||||
color["name"] = row["name"].isNull() ? "" : row["name"].as<std::string>();
|
||||
resp["colors"].append(color);
|
||||
}
|
||||
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
>> DB_ERROR(callback, "get colors");
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR << "Exception in getColors: " << e.what();
|
||||
callback(jsonError("Internal server error"));
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Pixel Operation Methods ====================
|
||||
|
||||
void PyramidController::placePixel(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
try {
|
||||
UserInfo user = getUserFromRequest(req);
|
||||
CHECK_AUTH(user, callback);
|
||||
|
||||
auto json = req->getJsonObject();
|
||||
if (!json) {
|
||||
callback(jsonError("Invalid JSON body"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!json->isMember("faceId") || !json->isMember("x") ||
|
||||
!json->isMember("y") || !json->isMember("color")) {
|
||||
callback(jsonError("Missing required fields: faceId, x, y, color"));
|
||||
return;
|
||||
}
|
||||
|
||||
int faceId = (*json)["faceId"].asInt();
|
||||
int x = (*json)["x"].asInt();
|
||||
int y = (*json)["y"].asInt();
|
||||
std::string color = (*json)["color"].asString();
|
||||
|
||||
// Validate pixel position
|
||||
if (!isValidPixelPosition(faceId, x, y)) {
|
||||
callback(jsonError("Invalid pixel position"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate color format
|
||||
if (!isValidColor(color)) {
|
||||
callback(jsonError("Invalid color format (use #RRGGBB)"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Uppercase the color for consistency
|
||||
std::transform(color.begin(), color.end(), color.begin(), ::toupper);
|
||||
|
||||
auto dbClient = app().getDbClient();
|
||||
int64_t userId = user.id;
|
||||
std::string username = user.username;
|
||||
|
||||
// Check daily limit using upsert pattern
|
||||
*dbClient << R"(
|
||||
INSERT INTO pyramid_daily_limits (user_id, date, pixels_placed)
|
||||
VALUES ($1, CURRENT_DATE, 1)
|
||||
ON CONFLICT (user_id, date)
|
||||
DO UPDATE SET pixels_placed = pyramid_daily_limits.pixels_placed + 1
|
||||
WHERE pyramid_daily_limits.pixels_placed < $2
|
||||
RETURNING pixels_placed
|
||||
)"
|
||||
<< userId << DAILY_PIXEL_LIMIT
|
||||
>> [callback, dbClient, userId, username, faceId, x, y, color](const Result& r) {
|
||||
if (r.empty()) {
|
||||
// Daily limit reached
|
||||
callback(jsonError("Daily pixel limit reached (1000/day)"));
|
||||
return;
|
||||
}
|
||||
|
||||
int pixelsPlaced = r[0]["pixels_placed"].as<int>();
|
||||
int remaining = PyramidController::DAILY_PIXEL_LIMIT - pixelsPlaced;
|
||||
|
||||
// Insert/update the pixel
|
||||
*dbClient << R"(
|
||||
INSERT INTO pyramid_pixels (face_id, x, y, color, placed_by, placed_at)
|
||||
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (face_id, x, y)
|
||||
DO UPDATE SET color = $4, placed_by = $5, placed_at = CURRENT_TIMESTAMP
|
||||
)"
|
||||
<< faceId << x << y << color << userId
|
||||
>> [callback, dbClient, userId, username, faceId, x, y, color, remaining](const Result&) {
|
||||
// Record in history
|
||||
*dbClient << R"(
|
||||
INSERT INTO pyramid_pixel_history (face_id, x, y, color, placed_by, placed_at)
|
||||
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)
|
||||
)"
|
||||
<< faceId << x << y << color << userId
|
||||
>> [callback, userId, username, faceId, x, y, color, remaining](const Result&) {
|
||||
// Broadcast to WebSocket clients
|
||||
PyramidWebSocketController::broadcastPixelUpdate(
|
||||
faceId, x, y, color, userId, username);
|
||||
|
||||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
resp["remainingToday"] = remaining;
|
||||
resp["pixel"]["faceId"] = faceId;
|
||||
resp["pixel"]["x"] = x;
|
||||
resp["pixel"]["y"] = y;
|
||||
resp["pixel"]["color"] = color;
|
||||
resp["pixel"]["placedBy"] = username;
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
>> DB_ERROR(callback, "record pixel history");
|
||||
}
|
||||
>> DB_ERROR(callback, "place pixel");
|
||||
}
|
||||
>> DB_ERROR(callback, "check daily limit");
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR << "Exception in placePixel: " << e.what();
|
||||
callback(jsonError("Internal server error"));
|
||||
}
|
||||
}
|
||||
|
||||
void PyramidController::getPixelInfo(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &faceIdStr,
|
||||
const std::string &xStr,
|
||||
const std::string &yStr) {
|
||||
try {
|
||||
PARSE_ID(faceId, faceIdStr, callback);
|
||||
PARSE_ID(x, xStr, callback);
|
||||
PARSE_ID(y, yStr, callback);
|
||||
|
||||
if (!isValidPixelPosition(static_cast<int>(faceId), static_cast<int>(x), static_cast<int>(y))) {
|
||||
callback(jsonError("Invalid pixel position"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto dbClient = app().getDbClient();
|
||||
*dbClient << R"(
|
||||
SELECT p.color, p.placed_at, u.id as user_id, u.username, u.user_color, u.avatar_url
|
||||
FROM pyramid_pixels p
|
||||
JOIN users u ON p.placed_by = u.id
|
||||
WHERE p.face_id = $1 AND p.x = $2 AND p.y = $3
|
||||
)"
|
||||
<< static_cast<int>(faceId) << static_cast<int>(x) << static_cast<int>(y)
|
||||
>> [callback, faceId, x, y](const Result& r) {
|
||||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
resp["faceId"] = static_cast<int>(faceId);
|
||||
resp["x"] = static_cast<int>(x);
|
||||
resp["y"] = static_cast<int>(y);
|
||||
|
||||
if (r.empty()) {
|
||||
resp["isEmpty"] = true;
|
||||
} else {
|
||||
const auto& row = r[0];
|
||||
resp["isEmpty"] = false;
|
||||
resp["color"] = row["color"].as<std::string>();
|
||||
resp["placedAt"] = row["placed_at"].as<std::string>();
|
||||
resp["user"]["id"] = static_cast<Json::Int64>(row["user_id"].as<int64_t>());
|
||||
resp["user"]["username"] = row["username"].as<std::string>();
|
||||
resp["user"]["userColor"] = row["user_color"].isNull() ? "#561D5E" : row["user_color"].as<std::string>();
|
||||
resp["user"]["avatarUrl"] = row["avatar_url"].isNull() ? "" : row["avatar_url"].as<std::string>();
|
||||
}
|
||||
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
>> DB_ERROR(callback, "get pixel info");
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR << "Exception in getPixelInfo: " << e.what();
|
||||
callback(jsonError("Internal server error"));
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== User Limits Methods ====================
|
||||
|
||||
void PyramidController::getLimits(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
try {
|
||||
UserInfo user = getUserFromRequest(req);
|
||||
CHECK_AUTH(user, callback);
|
||||
|
||||
auto dbClient = app().getDbClient();
|
||||
*dbClient << "SELECT pixels_placed FROM pyramid_daily_limits WHERE user_id = $1 AND date = CURRENT_DATE"
|
||||
<< user.id
|
||||
>> [callback](const Result& r) {
|
||||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
resp["dailyLimit"] = PyramidController::DAILY_PIXEL_LIMIT;
|
||||
|
||||
if (r.empty()) {
|
||||
resp["pixelsPlacedToday"] = 0;
|
||||
resp["remainingToday"] = PyramidController::DAILY_PIXEL_LIMIT;
|
||||
} else {
|
||||
int placed = r[0]["pixels_placed"].as<int>();
|
||||
resp["pixelsPlacedToday"] = placed;
|
||||
resp["remainingToday"] = PyramidController::DAILY_PIXEL_LIMIT - placed;
|
||||
}
|
||||
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
>> DB_ERROR(callback, "get limits");
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR << "Exception in getLimits: " << e.what();
|
||||
callback(jsonError("Internal server error"));
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Moderation Methods ====================
|
||||
|
||||
void PyramidController::rollbackPixel(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
try {
|
||||
UserInfo user = getUserFromRequest(req);
|
||||
CHECK_AUTH(user, callback);
|
||||
|
||||
// Only admins and moderators can rollback
|
||||
if (!user.isAdmin && !user.isModerator) {
|
||||
callback(jsonError("Moderator access required", k403Forbidden));
|
||||
return;
|
||||
}
|
||||
|
||||
auto json = req->getJsonObject();
|
||||
if (!json) {
|
||||
callback(jsonError("Invalid JSON body"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json->isMember("faceId") || !json->isMember("x") || !json->isMember("y")) {
|
||||
callback(jsonError("Missing required fields: faceId, x, y"));
|
||||
return;
|
||||
}
|
||||
|
||||
int faceId = (*json)["faceId"].asInt();
|
||||
int x = (*json)["x"].asInt();
|
||||
int y = (*json)["y"].asInt();
|
||||
|
||||
if (!isValidPixelPosition(faceId, x, y)) {
|
||||
callback(jsonError("Invalid pixel position"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto dbClient = app().getDbClient();
|
||||
int64_t moderatorId = user.id;
|
||||
std::string moderatorName = user.username;
|
||||
|
||||
// Find the previous state (second most recent non-rolled-back entry)
|
||||
*dbClient << R"(
|
||||
SELECT id, color, placed_by FROM pyramid_pixel_history
|
||||
WHERE face_id = $1 AND x = $2 AND y = $3 AND rolled_back = false
|
||||
ORDER BY placed_at DESC
|
||||
LIMIT 2
|
||||
)"
|
||||
<< faceId << x << y
|
||||
>> [callback, dbClient, moderatorId, moderatorName, faceId, x, y](const Result& r) {
|
||||
if (r.size() < 1) {
|
||||
callback(jsonError("No pixel found at this position"));
|
||||
return;
|
||||
}
|
||||
|
||||
int64_t currentHistoryId = r[0]["id"].as<int64_t>();
|
||||
|
||||
if (r.size() < 2) {
|
||||
// No previous state, delete the pixel entirely
|
||||
*dbClient << "DELETE FROM pyramid_pixels WHERE face_id = $1 AND x = $2 AND y = $3"
|
||||
<< faceId << x << y
|
||||
>> [callback, dbClient, currentHistoryId, moderatorId, moderatorName, faceId, x, y](const Result&) {
|
||||
// Mark as rolled back
|
||||
*dbClient << R"(
|
||||
UPDATE pyramid_pixel_history
|
||||
SET rolled_back = true, rolled_back_by = $1, rolled_back_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $2
|
||||
)"
|
||||
<< moderatorId << currentHistoryId
|
||||
>> [callback, moderatorName, faceId, x, y](const Result&) {
|
||||
// Broadcast rollback (empty means deleted)
|
||||
PyramidWebSocketController::broadcastRollback(
|
||||
faceId, x, y, "", moderatorName);
|
||||
|
||||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
resp["message"] = "Pixel deleted (no previous state)";
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
>> DB_ERROR(callback, "mark rolled back");
|
||||
}
|
||||
>> DB_ERROR(callback, "delete pixel");
|
||||
} else {
|
||||
// Restore previous state
|
||||
std::string prevColor = r[1]["color"].as<std::string>();
|
||||
int64_t prevUserId = r[1]["placed_by"].as<int64_t>();
|
||||
|
||||
*dbClient << R"(
|
||||
UPDATE pyramid_pixels
|
||||
SET color = $1, placed_by = $2, placed_at = CURRENT_TIMESTAMP
|
||||
WHERE face_id = $3 AND x = $4 AND y = $5
|
||||
)"
|
||||
<< prevColor << prevUserId << faceId << x << y
|
||||
>> [callback, dbClient, currentHistoryId, moderatorId, moderatorName, faceId, x, y, prevColor](const Result&) {
|
||||
// Mark current as rolled back
|
||||
*dbClient << R"(
|
||||
UPDATE pyramid_pixel_history
|
||||
SET rolled_back = true, rolled_back_by = $1, rolled_back_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $2
|
||||
)"
|
||||
<< moderatorId << currentHistoryId
|
||||
>> [callback, moderatorName, faceId, x, y, prevColor](const Result&) {
|
||||
// Broadcast rollback
|
||||
PyramidWebSocketController::broadcastRollback(
|
||||
faceId, x, y, prevColor, moderatorName);
|
||||
|
||||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
resp["message"] = "Pixel rolled back to previous state";
|
||||
resp["newColor"] = prevColor;
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
>> DB_ERROR(callback, "mark rolled back");
|
||||
}
|
||||
>> DB_ERROR(callback, "restore pixel");
|
||||
}
|
||||
}
|
||||
>> DB_ERROR(callback, "find pixel history");
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR << "Exception in rollbackPixel: " << e.what();
|
||||
callback(jsonError("Internal server error"));
|
||||
}
|
||||
}
|
||||
|
||||
void PyramidController::getPixelHistory(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &faceIdStr,
|
||||
const std::string &xStr,
|
||||
const std::string &yStr) {
|
||||
try {
|
||||
UserInfo user = getUserFromRequest(req);
|
||||
CHECK_AUTH(user, callback);
|
||||
|
||||
// Only admins and moderators can view full history
|
||||
if (!user.isAdmin && !user.isModerator) {
|
||||
callback(jsonError("Moderator access required", k403Forbidden));
|
||||
return;
|
||||
}
|
||||
|
||||
PARSE_ID(faceId, faceIdStr, callback);
|
||||
PARSE_ID(x, xStr, callback);
|
||||
PARSE_ID(y, yStr, callback);
|
||||
|
||||
if (!isValidPixelPosition(static_cast<int>(faceId), static_cast<int>(x), static_cast<int>(y))) {
|
||||
callback(jsonError("Invalid pixel position"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto dbClient = app().getDbClient();
|
||||
*dbClient << R"(
|
||||
SELECT h.id, h.color, h.placed_at, h.rolled_back, h.rolled_back_at,
|
||||
u.id as user_id, u.username, u.user_color,
|
||||
rb.username as rolled_back_by_username
|
||||
FROM pyramid_pixel_history h
|
||||
JOIN users u ON h.placed_by = u.id
|
||||
LEFT JOIN users rb ON h.rolled_back_by = rb.id
|
||||
WHERE h.face_id = $1 AND h.x = $2 AND h.y = $3
|
||||
ORDER BY h.placed_at DESC
|
||||
LIMIT 50
|
||||
)"
|
||||
<< static_cast<int>(faceId) << static_cast<int>(x) << static_cast<int>(y)
|
||||
>> [callback, faceId, x, y](const Result& r) {
|
||||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
resp["faceId"] = static_cast<int>(faceId);
|
||||
resp["x"] = static_cast<int>(x);
|
||||
resp["y"] = static_cast<int>(y);
|
||||
resp["history"] = Json::Value(Json::arrayValue);
|
||||
|
||||
for (const auto& row : r) {
|
||||
Json::Value entry;
|
||||
entry["id"] = static_cast<Json::Int64>(row["id"].as<int64_t>());
|
||||
entry["color"] = row["color"].as<std::string>();
|
||||
entry["placedAt"] = row["placed_at"].as<std::string>();
|
||||
entry["rolledBack"] = row["rolled_back"].as<bool>();
|
||||
entry["userId"] = static_cast<Json::Int64>(row["user_id"].as<int64_t>());
|
||||
entry["username"] = row["username"].as<std::string>();
|
||||
entry["userColor"] = row["user_color"].isNull() ? "#561D5E" : row["user_color"].as<std::string>();
|
||||
|
||||
if (row["rolled_back"].as<bool>()) {
|
||||
entry["rolledBackAt"] = row["rolled_back_at"].as<std::string>();
|
||||
entry["rolledBackBy"] = row["rolled_back_by_username"].isNull() ? "" : row["rolled_back_by_username"].as<std::string>();
|
||||
}
|
||||
|
||||
resp["history"].append(entry);
|
||||
}
|
||||
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
>> DB_ERROR(callback, "get pixel history");
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR << "Exception in getPixelHistory: " << e.what();
|
||||
callback(jsonError("Internal server error"));
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== WebSocket Controller ====================
|
||||
|
||||
void PyramidWebSocketController::handleNewConnection(const HttpRequestPtr &req,
|
||||
const WebSocketConnectionPtr &wsConnPtr) {
|
||||
std::lock_guard<std::mutex> lock(connectionsMutex_);
|
||||
connections_[wsConnPtr] = ConnectionInfo{};
|
||||
|
||||
// Send welcome message
|
||||
Json::Value welcome;
|
||||
welcome["type"] = "welcome";
|
||||
welcome["message"] = "Connected to Pyramid World";
|
||||
|
||||
Json::StreamWriterBuilder builder;
|
||||
wsConnPtr->send(Json::writeString(builder, welcome));
|
||||
|
||||
LOG_INFO << "Pyramid WebSocket connection established";
|
||||
}
|
||||
|
||||
void PyramidWebSocketController::handleConnectionClosed(const WebSocketConnectionPtr &wsConnPtr) {
|
||||
std::lock_guard<std::mutex> lock(connectionsMutex_);
|
||||
connections_.erase(wsConnPtr);
|
||||
LOG_INFO << "Pyramid WebSocket connection closed";
|
||||
}
|
||||
|
||||
void PyramidWebSocketController::handleNewMessage(const WebSocketConnectionPtr &wsConnPtr,
|
||||
std::string &&message,
|
||||
const WebSocketMessageType &type) {
|
||||
if (type != WebSocketMessageType::Text) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Json::Value msg;
|
||||
Json::CharReaderBuilder builder;
|
||||
std::istringstream stream(message);
|
||||
std::string errors;
|
||||
|
||||
if (!Json::parseFromStream(builder, stream, &msg, &errors)) {
|
||||
LOG_ERROR << "Failed to parse WebSocket message: " << errors;
|
||||
return;
|
||||
}
|
||||
|
||||
std::string msgType = msg["type"].asString();
|
||||
|
||||
if (msgType == "auth") {
|
||||
handleAuth(wsConnPtr, msg);
|
||||
} else if (msgType == "place_pixel") {
|
||||
handlePlacePixel(wsConnPtr, msg);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR << "Error handling WebSocket message: " << e.what();
|
||||
}
|
||||
}
|
||||
|
||||
void PyramidWebSocketController::handleAuth(const WebSocketConnectionPtr &wsConnPtr, const Json::Value &msg) {
|
||||
std::string token = msg["token"].asString();
|
||||
|
||||
if (token.empty()) {
|
||||
Json::Value error;
|
||||
error["type"] = "error";
|
||||
error["message"] = "No token provided";
|
||||
Json::StreamWriterBuilder builder;
|
||||
wsConnPtr->send(Json::writeString(builder, error));
|
||||
return;
|
||||
}
|
||||
|
||||
UserInfo user;
|
||||
if (!AuthService::getInstance().validateToken(token, user) || user.id == 0) {
|
||||
Json::Value error;
|
||||
error["type"] = "error";
|
||||
error["message"] = "Invalid token";
|
||||
Json::StreamWriterBuilder builder;
|
||||
wsConnPtr->send(Json::writeString(builder, error));
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(connectionsMutex_);
|
||||
auto it = connections_.find(wsConnPtr);
|
||||
if (it != connections_.end()) {
|
||||
it->second.userId = user.id;
|
||||
it->second.username = user.username;
|
||||
it->second.isAuthenticated = true;
|
||||
it->second.isModerator = user.isModerator;
|
||||
it->second.isAdmin = user.isAdmin;
|
||||
}
|
||||
}
|
||||
|
||||
// Get remaining pixels for today
|
||||
auto dbClient = drogon::app().getDbClient();
|
||||
*dbClient << "SELECT pixels_placed FROM pyramid_daily_limits WHERE user_id = $1 AND date = CURRENT_DATE"
|
||||
<< user.id
|
||||
>> [wsConnPtr](const Result& r) {
|
||||
int remaining = 1000;
|
||||
if (!r.empty()) {
|
||||
remaining = 1000 - r[0]["pixels_placed"].as<int>();
|
||||
}
|
||||
|
||||
Json::Value authSuccess;
|
||||
authSuccess["type"] = "auth_success";
|
||||
authSuccess["remainingPixels"] = remaining;
|
||||
Json::StreamWriterBuilder builder;
|
||||
wsConnPtr->send(Json::writeString(builder, authSuccess));
|
||||
}
|
||||
>> [](const DrogonDbException& e) {
|
||||
LOG_ERROR << "Failed to get daily limit: " << e.base().what();
|
||||
};
|
||||
}
|
||||
|
||||
void PyramidWebSocketController::handlePlacePixel(const WebSocketConnectionPtr &wsConnPtr, const Json::Value &msg) {
|
||||
ConnectionInfo connInfo;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(connectionsMutex_);
|
||||
auto it = connections_.find(wsConnPtr);
|
||||
if (it == connections_.end() || !it->second.isAuthenticated) {
|
||||
Json::Value error;
|
||||
error["type"] = "error";
|
||||
error["message"] = "Not authenticated";
|
||||
Json::StreamWriterBuilder builder;
|
||||
wsConnPtr->send(Json::writeString(builder, error));
|
||||
return;
|
||||
}
|
||||
connInfo = it->second;
|
||||
}
|
||||
|
||||
int faceId = msg["faceId"].asInt();
|
||||
int x = msg["x"].asInt();
|
||||
int y = msg["y"].asInt();
|
||||
std::string color = msg["color"].asString();
|
||||
|
||||
// Validate and uppercase color
|
||||
std::transform(color.begin(), color.end(), color.begin(), ::toupper);
|
||||
|
||||
// Use the REST endpoint logic for actual placement
|
||||
// This is a simplified version - in production, consider calling the same code
|
||||
auto dbClient = drogon::app().getDbClient();
|
||||
|
||||
*dbClient << R"(
|
||||
INSERT INTO pyramid_daily_limits (user_id, date, pixels_placed)
|
||||
VALUES ($1, CURRENT_DATE, 1)
|
||||
ON CONFLICT (user_id, date)
|
||||
DO UPDATE SET pixels_placed = pyramid_daily_limits.pixels_placed + 1
|
||||
WHERE pyramid_daily_limits.pixels_placed < 1000
|
||||
RETURNING pixels_placed
|
||||
)"
|
||||
<< connInfo.userId << 1000
|
||||
>> [wsConnPtr, dbClient, connInfo, faceId, x, y, color](const Result& r) {
|
||||
if (r.empty()) {
|
||||
Json::Value error;
|
||||
error["type"] = "error";
|
||||
error["message"] = "Daily limit reached";
|
||||
Json::StreamWriterBuilder builder;
|
||||
wsConnPtr->send(Json::writeString(builder, error));
|
||||
return;
|
||||
}
|
||||
|
||||
int remaining = 1000 - r[0]["pixels_placed"].as<int>();
|
||||
|
||||
*dbClient << R"(
|
||||
INSERT INTO pyramid_pixels (face_id, x, y, color, placed_by, placed_at)
|
||||
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (face_id, x, y)
|
||||
DO UPDATE SET color = $4, placed_by = $5, placed_at = CURRENT_TIMESTAMP
|
||||
)"
|
||||
<< faceId << x << y << color << connInfo.userId
|
||||
>> [wsConnPtr, dbClient, connInfo, faceId, x, y, color, remaining](const Result&) {
|
||||
*dbClient << R"(
|
||||
INSERT INTO pyramid_pixel_history (face_id, x, y, color, placed_by, placed_at)
|
||||
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)
|
||||
)"
|
||||
<< faceId << x << y << color << connInfo.userId
|
||||
>> [wsConnPtr, connInfo, faceId, x, y, color, remaining](const Result&) {
|
||||
// Broadcast to all clients
|
||||
broadcastPixelUpdate(faceId, x, y, color, connInfo.userId, connInfo.username);
|
||||
|
||||
// Send confirmation to the placing client
|
||||
Json::Value confirm;
|
||||
confirm["type"] = "pixel_placed";
|
||||
confirm["remainingPixels"] = remaining;
|
||||
Json::StreamWriterBuilder builder;
|
||||
wsConnPtr->send(Json::writeString(builder, confirm));
|
||||
}
|
||||
>> [](const DrogonDbException& e) {
|
||||
LOG_ERROR << "Failed to record history: " << e.base().what();
|
||||
};
|
||||
}
|
||||
>> [wsConnPtr](const DrogonDbException& e) {
|
||||
LOG_ERROR << "Failed to place pixel: " << e.base().what();
|
||||
Json::Value error;
|
||||
error["type"] = "error";
|
||||
error["message"] = "Failed to place pixel";
|
||||
Json::StreamWriterBuilder builder;
|
||||
wsConnPtr->send(Json::writeString(builder, error));
|
||||
};
|
||||
}
|
||||
>> [wsConnPtr](const DrogonDbException& e) {
|
||||
LOG_ERROR << "Failed to check limit: " << e.base().what();
|
||||
Json::Value error;
|
||||
error["type"] = "error";
|
||||
error["message"] = "Database error";
|
||||
Json::StreamWriterBuilder builder;
|
||||
wsConnPtr->send(Json::writeString(builder, error));
|
||||
};
|
||||
}
|
||||
|
||||
void PyramidWebSocketController::broadcastPixelUpdate(int faceId, int x, int y,
|
||||
const std::string &color,
|
||||
int64_t userId,
|
||||
const std::string &username) {
|
||||
Json::Value msg;
|
||||
msg["type"] = "pixel_update";
|
||||
msg["faceId"] = faceId;
|
||||
msg["x"] = x;
|
||||
msg["y"] = y;
|
||||
msg["color"] = color;
|
||||
msg["userId"] = static_cast<Json::Int64>(userId);
|
||||
msg["username"] = username;
|
||||
|
||||
Json::StreamWriterBuilder builder;
|
||||
std::string msgStr = Json::writeString(builder, msg);
|
||||
|
||||
std::lock_guard<std::mutex> lock(connectionsMutex_);
|
||||
for (const auto& [conn, info] : connections_) {
|
||||
conn->send(msgStr);
|
||||
}
|
||||
}
|
||||
|
||||
void PyramidWebSocketController::broadcastRollback(int faceId, int x, int y,
|
||||
const std::string &newColor,
|
||||
const std::string &moderator) {
|
||||
Json::Value msg;
|
||||
msg["type"] = "rollback";
|
||||
msg["faceId"] = faceId;
|
||||
msg["x"] = x;
|
||||
msg["y"] = y;
|
||||
msg["newColor"] = newColor;
|
||||
msg["moderator"] = moderator;
|
||||
|
||||
Json::StreamWriterBuilder builder;
|
||||
std::string msgStr = Json::writeString(builder, msg);
|
||||
|
||||
std::lock_guard<std::mutex> lock(connectionsMutex_);
|
||||
for (const auto& [conn, info] : connections_) {
|
||||
conn->send(msgStr);
|
||||
}
|
||||
}
|
||||
106
backend/src/controllers/PyramidController.h
Normal file
106
backend/src/controllers/PyramidController.h
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
#pragma once
|
||||
#include <drogon/HttpController.h>
|
||||
#include <drogon/WebSocketController.h>
|
||||
#include "../services/AuthService.h"
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
class PyramidController : public HttpController<PyramidController> {
|
||||
public:
|
||||
METHOD_LIST_BEGIN
|
||||
// Canvas state
|
||||
ADD_METHOD_TO(PyramidController::getState, "/api/pyramid/state", Get);
|
||||
ADD_METHOD_TO(PyramidController::getFaceState, "/api/pyramid/face/{1}", Get);
|
||||
ADD_METHOD_TO(PyramidController::getColors, "/api/pyramid/colors", Get);
|
||||
|
||||
// Pixel operations
|
||||
ADD_METHOD_TO(PyramidController::placePixel, "/api/pyramid/pixel", Post);
|
||||
ADD_METHOD_TO(PyramidController::getPixelInfo, "/api/pyramid/pixel/{1}/{2}/{3}", Get);
|
||||
|
||||
// User limits
|
||||
ADD_METHOD_TO(PyramidController::getLimits, "/api/pyramid/limits", Get);
|
||||
|
||||
// Moderation (admin/moderator only)
|
||||
ADD_METHOD_TO(PyramidController::rollbackPixel, "/api/pyramid/rollback", Post);
|
||||
ADD_METHOD_TO(PyramidController::getPixelHistory, "/api/pyramid/history/{1}/{2}/{3}", Get);
|
||||
METHOD_LIST_END
|
||||
|
||||
// Canvas state methods
|
||||
void getState(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
void getFaceState(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &faceId);
|
||||
void getColors(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
// Pixel operation methods
|
||||
void placePixel(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
void getPixelInfo(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &faceId,
|
||||
const std::string &x,
|
||||
const std::string &y);
|
||||
|
||||
// User limits methods
|
||||
void getLimits(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
// Moderation methods
|
||||
void rollbackPixel(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
void getPixelHistory(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &faceId,
|
||||
const std::string &x,
|
||||
const std::string &y);
|
||||
|
||||
private:
|
||||
static const int DAILY_PIXEL_LIMIT = 1000;
|
||||
static const int FACE_SIZE = 200;
|
||||
|
||||
bool isValidPixelPosition(int faceId, int x, int y);
|
||||
bool isValidColor(const std::string &color);
|
||||
};
|
||||
|
||||
// WebSocket controller for real-time pixel updates
|
||||
class PyramidWebSocketController : public WebSocketController<PyramidWebSocketController> {
|
||||
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;
|
||||
|
||||
// Static method to broadcast pixel updates to all connected clients
|
||||
static void broadcastPixelUpdate(int faceId, int x, int y,
|
||||
const std::string &color,
|
||||
int64_t userId,
|
||||
const std::string &username);
|
||||
|
||||
// Static method to broadcast rollback to all connected clients
|
||||
static void broadcastRollback(int faceId, int x, int y,
|
||||
const std::string &newColor,
|
||||
const std::string &moderator);
|
||||
|
||||
WS_PATH_LIST_BEGIN
|
||||
WS_PATH_ADD("/ws/pyramid");
|
||||
WS_PATH_LIST_END
|
||||
|
||||
private:
|
||||
struct ConnectionInfo {
|
||||
int64_t userId = 0;
|
||||
std::string username;
|
||||
bool isAuthenticated = false;
|
||||
bool isModerator = false;
|
||||
bool isAdmin = false;
|
||||
};
|
||||
|
||||
static std::unordered_map<WebSocketConnectionPtr, ConnectionInfo> connections_;
|
||||
static std::mutex connectionsMutex_;
|
||||
|
||||
void handleAuth(const WebSocketConnectionPtr &wsConnPtr, const Json::Value &msg);
|
||||
void handlePlacePixel(const WebSocketConnectionPtr &wsConnPtr, const Json::Value &msg);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue