fixes lol
All checks were successful
Build and Push / build-all (push) Successful in 9m17s

This commit is contained in:
doomtube 2026-01-11 14:54:44 -05:00
parent 33c20bf59d
commit a92bfc1d22
14 changed files with 2629 additions and 59 deletions

View 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);
}
}

View 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);
};