package server import ( "net/http" "strconv" "github.com/labstack/echo/v4" "git.maze.io/go/ham/protocol/meshcore" "git.maze.io/ham/hamview/schema" ) // handleGetMeshCore returns aggregated statistics for the MeshCore network. // // Endpoint: GET /api/v1/meshcore // // Response: 200 OK // // MeshCoreStats - Network statistics including message counts and packet timeline // // Response: 500 Internal Server Error // // ErrorResponse - Error retrieving statistics // // Example Request: // // GET /api/v1/meshcore // // Example Response: // // { // "messages": 150234, // "nodes": 127, // "receivers": 8, // "packets": { // "timestamps": [1709650800, 1709654400, 1709658000], // "packets": [142, 203, 178] // } // } func (s *Server) handleGetMeshCore(c echo.Context) error { stats, err := schema.GetMeshCoreStats(c.Request().Context()) if err != nil { return s.apiError(c, err) } return c.JSON(http.StatusOK, stats) } // handleGetMeshCoreGroups returns a list of public MeshCore groups/channels. // // Endpoint: GET /api/v1/meshcore/groups // // Response: 200 OK // // []MeshCoreGroup - Array of public group objects // // Response: 500 Internal Server Error // // ErrorResponse - Error retrieving groups // // Example Request: // // GET /api/v1/meshcore/groups // // Example Response: // // [ // { // "id": 5, // "name": "General Chat", // "secret": "0123456789abcdef0123456789abcdef" // }, // { // "id": 7, // "name": "Emergency", // "secret": "fedcba9876543210fedcba9876543210" // } // ] func (s *Server) handleGetMeshCoreGroups(c echo.Context) error { groups, err := schema.GetMeshCoreGroups(c.Request().Context()) if err != nil { return s.apiError(c, err) } return c.JSON(http.StatusOK, groups) } // handleGetMeshCoreNodes returns a list of MeshCore network nodes. // // Endpoint: GET /api/v1/meshcore/nodes // // Query Parameters: // - type (optional): Filter by node type // - "chat" - Chat nodes // - "room" - Room nodes // - "sensor" - Sensor nodes // - "repeater" - Repeater nodes (default) // // Response: 200 OK // // []MeshCoreNode - Array of node objects, sorted by last_heard_at (descending) // // Response: 500 Internal Server Error // // ErrorResponse - Error retrieving nodes // // Example Request: // // GET /api/v1/meshcore/nodes // GET /api/v1/meshcore/nodes?type=chat // // Example Response: // // [ // { // "id": 42, // "packet": [], // "name": "NODE-CHARLIE", // "type": 0, // "prefix": "mc", // "public_key": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", // "first_heard_at": "2026-01-15T08:00:00Z", // "last_heard_at": "2026-03-05T14:25:00Z", // "last_latitude": 52.3667, // "last_longitude": 4.8945 // } // ] func (s *Server) handleGetMeshCoreNodes(c echo.Context) error { var ( nodes []*schema.MeshCoreNode err error ) if kind := c.QueryParam("type"); kind != "" { switch kind { case "chat": nodes, err = schema.GetMeshCoreNodesByType(c.Request().Context(), meshcore.Chat) case "room": nodes, err = schema.GetMeshCoreNodesByType(c.Request().Context(), meshcore.Room) case "sensor": nodes, err = schema.GetMeshCoreNodesByType(c.Request().Context(), meshcore.Sensor) case "repeater": fallthrough default: nodes, err = schema.GetMeshCoreNodesByType(c.Request().Context(), meshcore.Repeater) } } else { nodes, err = schema.GetMeshCoreNodes(c.Request().Context()) } if err != nil { return s.apiError(c, err) } return c.JSON(http.StatusOK, nodes) } // handleGetMeshCoreNodesCloseTo returns nodes within a specified radius of a reference node. // // Endpoint: GET /api/v1/meshcore/nodes/close-to/:publickey // // Path Parameters: // - publickey: Hex-encoded Ed25519 public key (64 characters) of the reference node // // Query Parameters: // - radius (optional): Search radius in meters (default: 25000 = 25 km) // // Response: 200 OK // // NodesCloseToResponse - Object containing the reference node and nearby nodes // { // "node": MeshCoreNode, // The reference node // "nodes": []MeshCoreNode // Nearby nodes, sorted by distance (ascending) // // Each node has the "distance" field populated (in meters) // } // // Response: 404 Not Found // // null - Node not found or has no location data // // Response: 400 Bad Request // // ErrorResponse - Invalid radius parameter // // Response: 500 Internal Server Error // // ErrorResponse - Error performing proximity query // // Example Request: // // GET /api/v1/meshcore/nodes/close-to/1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef // GET /api/v1/meshcore/nodes/close-to/1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef?radius=50000 // // Example Response: // // { // "node": { // "id": 42, // "name": "NODE-CHARLIE", // "type": 0, // "prefix": "mc", // "public_key": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", // "first_heard_at": "2026-01-15T08:00:00Z", // "last_heard_at": "2026-03-05T14:25:00Z", // "last_latitude": 52.3667, // "last_longitude": 4.8945, // "distance": 0 // }, // "nodes": [ // { // "id": 43, // "name": "NODE-DELTA", // "type": 0, // "prefix": "mc", // "public_key": "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", // "first_heard_at": "2026-02-01T10:00:00Z", // "last_heard_at": "2026-03-05T14:20:00Z", // "last_latitude": 52.3700, // "last_longitude": 4.9000, // "distance": 450.5 // } // ] // } func (s *Server) handleGetMeshCoreNodesCloseTo(c echo.Context) error { var radius float64 if value := c.QueryParam("radius"); value != "" { var err error if radius, err = strconv.ParseFloat(value, 64); err != nil { return s.apiError(c, err) } } if radius <= 0 { radius = 25000.0 // 25 km } node, nodes, err := schema.GetMeshCoreNodesCloseTo(c.Request().Context(), c.Param("publickey"), radius) if err != nil { return s.apiError(c, err) } return c.JSON(http.StatusOK, map[string]any{ "node": node, "nodes": nodes, }) } // handleGetMeshCorePackets returns MeshCore packets based on various filter criteria. // // Endpoint: GET /api/v1/meshcore/packets // // Query Parameters (mutually exclusive, evaluated in order): // - hash: 16-character hex hash - Returns all packets with this hash // - type: Integer payload type - Returns packets of this type // - Can be combined with channel_hash for group messages // - channel_hash: 2-character channel hash (requires type parameter) // - (no parameters): Returns the 100 most recent packets // // Response: 200 OK // // []MeshCorePacket - Array of packet objects // // Response: 400 Bad Request // // ErrorResponse - Invalid hash format or payload type // // Response: 500 Internal Server Error // // ErrorResponse - Error retrieving packets // // Example Requests: // // GET /api/v1/meshcore/packets // GET /api/v1/meshcore/packets?hash=a1b2c3d4e5f67890 // GET /api/v1/meshcore/packets?type=3 // GET /api/v1/meshcore/packets?type=3&channel_hash=ab // // Example Response: // // [ // { // "id": 12345, // "radio_id": 1, // "radio": null, // "snr": 8.5, // "rssi": -95, // "version": 1, // "route_type": 0, // "payload_type": 3, // "hash": "a1b2c3d4e5f67890", // "path": "AQIDBA==", // "payload": "SGVsbG8gV29ybGQ=", // "raw": "AQIDBAUGBwg=", // "parsed": {"text": "Hello World"}, // "channel_hash": "ab", // "received_at": "2026-03-05T14:30:00Z" // } // ] // // Payload Types: // - 0: Ping // - 1: Node announcement // - 2: Direct text message // - 3: Group text message // - 4: Position update // - (other types may be defined by the protocol) func (s *Server) handleGetMeshCorePackets(c echo.Context) error { var ( ctx = c.Request().Context() packets []*schema.MeshCorePacket err error ) if hash := c.QueryParam("hash"); hash != "" { packets, err = schema.GetMeshCorePacketsByHash(ctx, hash) } else if kind := c.QueryParam("type"); kind != "" { var payloadType int if payloadType, err = strconv.Atoi(kind); err == nil { if hash := c.QueryParam("channel_hash"); hash != "" { packets, err = schema.GetMeshCorePacketsByChannelHash(ctx, hash) } else { packets, err = schema.GetMeshCorePacketsByPayloadType(ctx, meshcore.PayloadType(payloadType)) } } } else { packets, err = schema.GetMeshCorePackets(ctx, 100) } if err != nil { return s.apiError(c, err) } return c.JSON(http.StatusOK, packets) }