332 lines
8.4 KiB
Go
332 lines
8.4 KiB
Go
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)
|
|
}
|