241 lines
8.7 KiB
Python
241 lines
8.7 KiB
Python
"""
|
|
rMesh Bridge — FastAPI service exposing Reticulum (Internet backbone)
|
|
and MeshCore (LoRa mesh) as a unified REST API.
|
|
"""
|
|
|
|
import logging
|
|
from contextlib import asynccontextmanager
|
|
from typing import Optional
|
|
|
|
from fastapi import FastAPI, HTTPException, Header
|
|
from pydantic import BaseModel
|
|
from typing import Annotated
|
|
|
|
from . import reticulum_bridge, lxmf_bridge, meshcore_bridge
|
|
from .config import BRIDGE_API_KEY, MESHCORE_ENABLED
|
|
from .models import (
|
|
StatusResponse, NodesResponse, TopologyResponse,
|
|
MessageIn, MessageOut, MessagesResponse,
|
|
IdentityResponse, AnnounceResponse, HealthResponse,
|
|
)
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s: %(message)s")
|
|
logger = logging.getLogger("rmesh.api")
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
"""Initialize Reticulum and MeshCore on startup."""
|
|
logger.info("Starting rMesh bridge...")
|
|
|
|
# Reticulum (Internet backbone)
|
|
reticulum_bridge.init()
|
|
from .reticulum_bridge import _identity
|
|
if _identity:
|
|
lxmf_bridge.init(_identity)
|
|
else:
|
|
logger.error("No Reticulum identity available for LXMF")
|
|
reticulum_bridge.announce()
|
|
|
|
# MeshCore (LoRa mesh)
|
|
if MESHCORE_ENABLED:
|
|
await meshcore_bridge.init()
|
|
else:
|
|
logger.info("MeshCore disabled (set MESHCORE_ENABLED=true to activate)")
|
|
|
|
yield
|
|
|
|
logger.info("Shutting down rMesh bridge")
|
|
if MESHCORE_ENABLED:
|
|
await meshcore_bridge.disconnect()
|
|
|
|
|
|
app = FastAPI(title="rMesh Bridge", lifespan=lifespan)
|
|
|
|
|
|
def verify_api_key(x_bridge_api_key: Annotated[str, Header()] = ""):
|
|
if not BRIDGE_API_KEY:
|
|
return
|
|
if x_bridge_api_key != BRIDGE_API_KEY:
|
|
raise HTTPException(status_code=401, detail="Invalid API key")
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# Health & Combined Status
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
@app.get("/api/health", response_model=HealthResponse)
|
|
async def health():
|
|
return {"status": "ok"}
|
|
|
|
|
|
@app.get("/api/status")
|
|
async def combined_status(x_bridge_api_key: Annotated[str, Header()] = ""):
|
|
verify_api_key(x_bridge_api_key)
|
|
result = {
|
|
"reticulum": reticulum_bridge.get_status(),
|
|
"meshcore": meshcore_bridge.get_status() if MESHCORE_ENABLED else {"connected": False, "device_info": {}, "contact_count": 0, "message_count": 0},
|
|
}
|
|
return result
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# Reticulum Endpoints (Internet backbone)
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
@app.get("/api/reticulum/status", response_model=StatusResponse)
|
|
async def reticulum_status(x_bridge_api_key: Annotated[str, Header()] = ""):
|
|
verify_api_key(x_bridge_api_key)
|
|
return reticulum_bridge.get_status()
|
|
|
|
|
|
@app.get("/api/reticulum/nodes", response_model=NodesResponse)
|
|
async def reticulum_nodes(x_bridge_api_key: Annotated[str, Header()] = ""):
|
|
verify_api_key(x_bridge_api_key)
|
|
node_list = reticulum_bridge.get_nodes()
|
|
return {"nodes": node_list, "total": len(node_list)}
|
|
|
|
|
|
@app.get("/api/reticulum/topology", response_model=TopologyResponse)
|
|
async def reticulum_topology(x_bridge_api_key: Annotated[str, Header()] = ""):
|
|
verify_api_key(x_bridge_api_key)
|
|
return reticulum_bridge.get_topology()
|
|
|
|
|
|
@app.post("/api/reticulum/announce", response_model=AnnounceResponse)
|
|
async def reticulum_announce(x_bridge_api_key: Annotated[str, Header()] = ""):
|
|
verify_api_key(x_bridge_api_key)
|
|
return reticulum_bridge.announce()
|
|
|
|
|
|
@app.get("/api/reticulum/identity", response_model=IdentityResponse)
|
|
async def reticulum_identity(x_bridge_api_key: Annotated[str, Header()] = ""):
|
|
verify_api_key(x_bridge_api_key)
|
|
return reticulum_bridge.get_identity_info()
|
|
|
|
|
|
@app.get("/api/reticulum/messages", response_model=MessagesResponse)
|
|
async def reticulum_messages(
|
|
x_bridge_api_key: Annotated[str, Header()] = "",
|
|
limit: int = 100, offset: int = 0,
|
|
):
|
|
verify_api_key(x_bridge_api_key)
|
|
msgs = lxmf_bridge.get_messages(limit=limit, offset=offset)
|
|
return {"messages": msgs, "total": lxmf_bridge.get_total_count()}
|
|
|
|
|
|
@app.post("/api/reticulum/messages", response_model=MessageOut)
|
|
async def reticulum_send_message(
|
|
body: MessageIn,
|
|
x_bridge_api_key: Annotated[str, Header()] = "",
|
|
):
|
|
verify_api_key(x_bridge_api_key)
|
|
result = lxmf_bridge.send_message(
|
|
destination_hash_hex=body.destination_hash,
|
|
content=body.content,
|
|
title=body.title,
|
|
)
|
|
if not result.get("id"):
|
|
raise HTTPException(status_code=400, detail="Failed to send message")
|
|
return result
|
|
|
|
|
|
# Keep old /api/status path as alias for backwards compat
|
|
@app.get("/api/nodes", response_model=NodesResponse)
|
|
async def nodes_compat(x_bridge_api_key: Annotated[str, Header()] = ""):
|
|
verify_api_key(x_bridge_api_key)
|
|
node_list = reticulum_bridge.get_nodes()
|
|
return {"nodes": node_list, "total": len(node_list)}
|
|
|
|
|
|
@app.get("/api/topology", response_model=TopologyResponse)
|
|
async def topology_compat(x_bridge_api_key: Annotated[str, Header()] = ""):
|
|
verify_api_key(x_bridge_api_key)
|
|
return reticulum_bridge.get_topology()
|
|
|
|
|
|
@app.get("/api/identity", response_model=IdentityResponse)
|
|
async def identity_compat(x_bridge_api_key: Annotated[str, Header()] = ""):
|
|
verify_api_key(x_bridge_api_key)
|
|
return reticulum_bridge.get_identity_info()
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# MeshCore Endpoints (LoRa mesh)
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
class MCMessageIn(BaseModel):
|
|
contact_name: str
|
|
content: str
|
|
|
|
|
|
class MCChannelMessageIn(BaseModel):
|
|
channel_idx: int
|
|
content: str
|
|
|
|
|
|
@app.get("/api/meshcore/status")
|
|
async def meshcore_status(x_bridge_api_key: Annotated[str, Header()] = ""):
|
|
verify_api_key(x_bridge_api_key)
|
|
return meshcore_bridge.get_status()
|
|
|
|
|
|
@app.get("/api/meshcore/contacts")
|
|
async def meshcore_contacts(x_bridge_api_key: Annotated[str, Header()] = ""):
|
|
verify_api_key(x_bridge_api_key)
|
|
contacts = meshcore_bridge.get_contacts()
|
|
return {"contacts": contacts, "total": len(contacts)}
|
|
|
|
|
|
@app.post("/api/meshcore/contacts/refresh")
|
|
async def meshcore_refresh_contacts(x_bridge_api_key: Annotated[str, Header()] = ""):
|
|
verify_api_key(x_bridge_api_key)
|
|
contacts = await meshcore_bridge.refresh_contacts()
|
|
return {"contacts": contacts, "total": len(contacts)}
|
|
|
|
|
|
@app.get("/api/meshcore/messages")
|
|
async def meshcore_messages(
|
|
x_bridge_api_key: Annotated[str, Header()] = "",
|
|
limit: int = 100, offset: int = 0,
|
|
):
|
|
verify_api_key(x_bridge_api_key)
|
|
msgs = meshcore_bridge.get_messages(limit=limit, offset=offset)
|
|
return {"messages": msgs, "total": meshcore_bridge.get_message_count()}
|
|
|
|
|
|
@app.post("/api/meshcore/messages")
|
|
async def meshcore_send_message(
|
|
body: MCMessageIn,
|
|
x_bridge_api_key: Annotated[str, Header()] = "",
|
|
):
|
|
verify_api_key(x_bridge_api_key)
|
|
result = await meshcore_bridge.send_message(body.contact_name, body.content)
|
|
if not result.get("id"):
|
|
raise HTTPException(status_code=400, detail=result.get("error", "Failed to send"))
|
|
return result
|
|
|
|
|
|
@app.post("/api/meshcore/channels/messages")
|
|
async def meshcore_send_channel_message(
|
|
body: MCChannelMessageIn,
|
|
x_bridge_api_key: Annotated[str, Header()] = "",
|
|
):
|
|
verify_api_key(x_bridge_api_key)
|
|
result = await meshcore_bridge.send_channel_message(body.channel_idx, body.content)
|
|
if not result.get("id"):
|
|
raise HTTPException(status_code=400, detail=result.get("error", "Failed to send"))
|
|
return result
|
|
|
|
|
|
@app.post("/api/meshcore/advert")
|
|
async def meshcore_advert(x_bridge_api_key: Annotated[str, Header()] = ""):
|
|
verify_api_key(x_bridge_api_key)
|
|
return await meshcore_bridge.send_advert()
|
|
|
|
|
|
@app.get("/api/meshcore/stats")
|
|
async def meshcore_stats(x_bridge_api_key: Annotated[str, Header()] = ""):
|
|
verify_api_key(x_bridge_api_key)
|
|
return await meshcore_bridge.get_device_stats()
|