rmesh-reticulum/app/main.py

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()