Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/add validator to pixel routes #64

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 94 additions & 35 deletions backend/routes/indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,86 +58,145 @@ func InitIndexerRoutes() {
// TODO: User might miss some messages between loading canvas and connecting to websocket?
// TODO: Check thread safety of these things
func consumeIndexerMsg(w http.ResponseWriter, r *http.Request) {
// Read request body
requestBody, err := io.ReadAll(r.Body)
if err != nil {
fmt.Println("Error reading request body: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

// Unmarshal JSON body
// TODO: Parse message fully, check block status, number, ...
reqBody := map[string]interface{}{}
err = json.Unmarshal(requestBody, &reqBody)
if err != nil {
var reqBody map[string]interface{}
if err := json.Unmarshal(requestBody, &reqBody); err != nil {
fmt.Println("Error unmarshalling request body: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

address := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["keys"].([]interface{})[1]
address = address.(string)[2:]
posHex := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["keys"].([]interface{})[2]
dayIdxHex := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["keys"].([]interface{})[3]
colorHex := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["data"].([]interface{})[0]
// Extract necessary fields from the request body
data, ok := reqBody["data"].(map[string]interface{})
aweolumidedavid marked this conversation as resolved.
Show resolved Hide resolved
if !ok {
handleError(w, "Invalid request format (missing 'data' field)", http.StatusBadRequest)
return
}

batch, ok := data["batch"].([]interface{})
if !ok || len(batch) == 0 {
handleError(w, "Invalid request format (missing or empty 'batch' array)", http.StatusBadRequest)
return
}

event, ok := batch[0].(map[string]interface{})["events"].([]interface{})
if !ok || len(event) == 0 {
handleError(w, "Invalid request format (missing or empty 'events' array)", http.StatusBadRequest)
return
}

eventData, ok := event[0].(map[string]interface{})
if !ok {
handleError(w, "Invalid event format (missing 'event' object)", http.StatusBadRequest)
return
}

keys, ok := eventData["keys"].([]interface{})
if !ok || len(keys) < 4 {
handleError(w, "Invalid event format (missing or incomplete 'keys' array)", http.StatusBadRequest)
return
}

// Convert hex to int
position, err := strconv.ParseInt(posHex.(string), 0, 64)
// Extract and validate address, position, day index, and color
address := getStringValue(keys, 1)
posHex := getStringValue(keys, 2)
dayIdxHex := getStringValue(keys, 3)
colorHex := getStringValue(eventData["data"], 0)

position, err := strconv.ParseInt(posHex, 0, 64)
if err != nil {
fmt.Println("Error converting position hex to int: ", err)
w.WriteHeader(http.StatusInternalServerError)
handleError(w, "Error converting position hex to int", http.StatusBadRequest)
return
}
dayIdx, err := strconv.ParseInt(dayIdxHex.(string), 0, 64)

dayIdx, err := strconv.ParseInt(dayIdxHex, 0, 64)
if err != nil {
fmt.Println("Error converting day index hex to int: ", err)
w.WriteHeader(http.StatusInternalServerError)
handleError(w, "Error converting day index hex to int", http.StatusBadRequest)
return
}
color, err := strconv.ParseInt(colorHex.(string), 0, 64)

color, err := strconv.ParseInt(colorHex, 0, 64)
if err != nil {
fmt.Println("Error converting color hex to int: ", err)
w.WriteHeader(http.StatusInternalServerError)
handleError(w, "Error converting color hex to int", http.StatusBadRequest)
return
}

// Validate position and color
if position < 0 || position >= int64(core.ArtPeaceBackend.CanvasConfig.Canvas.Width*core.ArtPeaceBackend.CanvasConfig.Canvas.Height) {
handleError(w, "Invalid position value", http.StatusBadRequest)
return
}

if color < 0 || color >= (1 << core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth) {
aweolumidedavid marked this conversation as resolved.
Show resolved Hide resolved
handleError(w, "Invalid color value", http.StatusBadRequest)
return
}

bitfieldType := "u" + strconv.Itoa(int(core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth))
pos := uint(position) * core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth

fmt.Println("Pixel indexed with position: ", position, " and color: ", color)

// Set pixel in redis
// Set pixel in Redis
ctx := context.Background()
err = core.ArtPeaceBackend.Databases.Redis.BitField(ctx, "canvas", "SET", bitfieldType, pos, color).Err()
if err != nil {
panic(err)
if err := core.ArtPeaceBackend.Databases.Redis.BitField(ctx, "canvas", "SET", bitfieldType, pos, color).Err(); err != nil {
handleError(w, "Failed to set pixel in Redis", http.StatusInternalServerError)
return
}

// Set pixel in postgres
_, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO Pixels (address, position, day, color) VALUES ($1, $2, $3, $4)", address, position, dayIdx, color)
// Set pixel in PostgreSQL
_, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(),
"INSERT INTO Pixels (address, position, day, color) VALUES ($1, $2, $3, $4)",
address, position, dayIdx, color)
if err != nil {
fmt.Println("Error inserting pixel into postgres: ", err)
w.WriteHeader(http.StatusInternalServerError)
handleError(w, "Failed to insert pixel into PostgreSQL", http.StatusInternalServerError)
return
}

// Send message to all connected clients
var message = map[string]interface{}{
// Send message to all connected WebSocket clients
message := map[string]interface{}{
"position": position,
"color": color,
}
messageBytes, err := json.Marshal(message)
if err != nil {
fmt.Println("Error marshalling message: ", err)
w.WriteHeader(http.StatusInternalServerError)
handleError(w, "Failed to marshal message", http.StatusInternalServerError)
return
}

for idx, conn := range core.ArtPeaceBackend.WSConnections {
if err := conn.WriteMessage(websocket.TextMessage, messageBytes); err != nil {
fmt.Println(err)
// TODO: Should we always remove connection?
// Remove connection
fmt.Println("Error sending message to WebSocket client:", err)
// Remove disconnected WebSocket client from connections slice
conn.Close()
core.ArtPeaceBackend.WSConnections = append(core.ArtPeaceBackend.WSConnections[:idx], core.ArtPeaceBackend.WSConnections[idx+1:]...)
}
}

// Respond with success
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message": "Pixel placed"}`))
}

func getStringValue(arr interface{}, idx int) string {
if slice, ok := arr.([]interface{}); ok && len(slice) > idx {
if str, ok := slice[idx].(string); ok {
return str
}
}
return ""
}

func handleError(w http.ResponseWriter, errMsg string, statusCode int) {
fmt.Println(errMsg)
w.WriteHeader(statusCode)
w.Write([]byte(`{"error": "` + errMsg + `"}`))
}
168 changes: 112 additions & 56 deletions backend/routes/pixel.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,31 @@ func InitPixelRoutes() {
}

func getPixel(w http.ResponseWriter, r *http.Request) {
position, err := strconv.Atoi(r.URL.Query().Get("position"))
if err != nil {
// TODO: panic or return error?
panic(err)
}
bitfieldType := "u" + strconv.Itoa(int(core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth))
pos := uint(position) * core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth

ctx := context.Background()
val, err := core.ArtPeaceBackend.Databases.Redis.BitField(ctx, "canvas", "GET", bitfieldType, pos).Result()
if err != nil {
panic(err)
}

w.Header().Set("Access-Control-Allow-Origin", "*")
positionStr := r.URL.Query().Get("position")
position, err := strconv.Atoi(positionStr)
if err != nil {
http.Error(w, "Invalid position parameter", http.StatusBadRequest)
return
}

// Check if position is within canvas bounds
if position < 0 || position >= (int(core.ArtPeaceBackend.CanvasConfig.Canvas.Width) * int(core.ArtPeaceBackend.CanvasConfig.Canvas.Height)) {
http.Error(w, "Position out of range", http.StatusBadRequest)
return
}

bitfieldType := "u" + strconv.Itoa(int(core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth))
pos := uint(position) * core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth

ctx := context.Background()
val, err := core.ArtPeaceBackend.Databases.Redis.BitField(ctx, "canvas", "GET", bitfieldType, pos).Result()
if err != nil {
panic(err)
}

w.Header().Set("Access-Control-Allow-Origin", "*")
// TODO: Check this
w.Write([]byte(strconv.Itoa(int(val[0]))))
w.Write([]byte(strconv.Itoa(int(val[0]))))
}

func getPixelInfo(w http.ResponseWriter, r *http.Request) {
Expand All @@ -57,60 +65,108 @@ func getPixelInfo(w http.ResponseWriter, r *http.Request) {
}

func placePixelDevnet(w http.ResponseWriter, r *http.Request) {
reqBody, err := io.ReadAll(r.Body)
aweolumidedavid marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
return
}
// Disable this in production
if core.ArtPeaceBackend.BackendConfig.Production {
http.Error(w, "Not available in production", http.StatusNotImplemented)
return
}

reqBody, err := io.ReadAll(r.Body)
if err != nil {
panic(err)
}
var jsonBody map[string]string
err = json.Unmarshal(reqBody, &jsonBody)
if err != nil {
panic(err)
}

position, err := strconv.Atoi(jsonBody["position"])
if err != nil {
panic(err)
}

shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.PlacePixelDevnet
contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS")

cmd := exec.Command(shellCmd, contract, "place_pixel", strconv.Itoa(position), jsonBody["color"])
_, err = cmd.Output()
if err != nil {
fmt.Println("Error executing shell command: ", err)
panic(err)
}

w.Header().Set("Access-Control-Allow-Origin", "*")
w.Write([]byte("Pixel placed"))
positionStr := jsonBody["position"]
position, err := strconv.Atoi(positionStr)
if err != nil {
http.Error(w, "Invalid position parameter", http.StatusBadRequest)
return
}

color := jsonBody["color"]

// Validate position range
if position < 0 || position >= int(core.ArtPeaceBackend.CanvasConfig.Canvas.Width*core.ArtPeaceBackend.CanvasConfig.Canvas.Height) {
http.Error(w, "Position out of range", http.StatusBadRequest)
return
}

// Validate color format (e.g., validate against allowed colors)
// Example: check if color is a valid hex color code
if !isValidHexColor(color) {
aweolumidedavid marked this conversation as resolved.
Show resolved Hide resolved
http.Error(w, "Invalid color format", http.StatusBadRequest)
return
}

shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.PlacePixelDevnet
contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS")

cmd := exec.Command(shellCmd, contract, "place_pixel", strconv.Itoa(position), color)
_, err = cmd.Output()
if err != nil {
fmt.Println("Error executing shell command: ", err)
http.Error(w, "Failed to place pixel", http.StatusInternalServerError)
return
}

w.Header().Set("Access-Control-Allow-Origin", "*")
w.Write([]byte("Pixel placed"))
}

func placePixelRedis(w http.ResponseWriter, r *http.Request) {
// TODO: Only allow mods to place pixels on redis instance
reqBody, err := io.ReadAll(r.Body)
if err != nil {
panic(err)
}
var jsonBody map[string]uint
err = json.Unmarshal(reqBody, &jsonBody)
if err != nil {
panic(err)
}
position := jsonBody["position"]
color := jsonBody["color"]
bitfieldType := "u" + strconv.Itoa(int(core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth))
pos := position * core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth

ctx := context.Background()
err = core.ArtPeaceBackend.Databases.Redis.BitField(ctx, "canvas", "SET", bitfieldType, pos, color).Err()
if err != nil {
panic(err)
}
reqBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
return
}

var jsonBody map[string]uint
if err := json.Unmarshal(reqBody, &jsonBody); err != nil {
http.Error(w, "Invalid JSON format", http.StatusBadRequest)
return
}

position := jsonBody["position"]
color := jsonBody["color"]

canvasWidth := core.ArtPeaceBackend.CanvasConfig.Canvas.Width
canvasHeight := core.ArtPeaceBackend.CanvasConfig.Canvas.Height

// Validate position range
if position >= canvasWidth*canvasHeight {
http.Error(w, "Position out of range", http.StatusBadRequest)
return
}

// Validate color range (e.g., ensure color value fits within bit width)
if color >= (1 << core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth) {
aweolumidedavid marked this conversation as resolved.
Show resolved Hide resolved
http.Error(w, "Color value exceeds bit width", http.StatusBadRequest)
return
}

bitfieldType := "u" + strconv.Itoa(int(core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth))
pos := position * core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth

ctx := context.Background()
err = core.ArtPeaceBackend.Databases.Redis.BitField(ctx, "canvas", "SET", bitfieldType, pos, color).Err()
if err != nil {
http.Error(w, "Failed to set pixel in Redis", http.StatusInternalServerError)
return
}

w.Header().Set("Access-Control-Allow-Origin", "*")
w.Write([]byte("Pixel placed"))
}

// Helper function to validate hex color format
func isValidHexColor(color string) bool {
_, err := strconv.ParseUint(color[1:], 16, 32) // Skip '#' character
return err == nil && len(color) == 7 // Check if it's a valid hex color (#RRGGBB)
}