FreeDATA/freedata_server/api/freedata.py

583 lines
23 KiB
Python

from fastapi import APIRouter, Depends
import asyncio
from freedata_server.api.common import api_response, api_abort
from freedata_server.api.command_helpers import enqueue_tx_command
from freedata_server.message_system_db_messages import DatabaseManagerMessages
from freedata_server.message_system_db_attachments import DatabaseManagerAttachments
from freedata_server.message_system_db_beacon import DatabaseManagerBeacon
from freedata_server.message_system_db_station import DatabaseManagerStations
from freedata_server.message_system_db_broadcasts import DatabaseManagerBroadcasts
from freedata_server import command_message_send
from freedata_server import adif_udp_logger
from freedata_server import wavelog_api_logger
from freedata_server import command_norm
from freedata_server.context import AppContext, get_ctx
from freedata_server.norm.norm_transmission_iss import NormTransmissionISS
router = APIRouter()
def _mgr_msgs(ctx: AppContext):
return DatabaseManagerMessages(ctx)
def _mgr_attach(ctx: AppContext):
return DatabaseManagerAttachments(ctx)
def _mgr_beacon(ctx: AppContext):
return DatabaseManagerBeacon(ctx)
def _mgr_stations(ctx: AppContext):
return DatabaseManagerStations(ctx)
def _mgr_broadcasts(ctx: AppContext):
return DatabaseManagerBroadcasts(ctx)
@router.get(
"/messages/{message_id}",
summary="Get Message by ID",
tags=["FreeDATA"],
responses={
200: {"description": "Message found and returned."},
404: {"description": "Message not found."},
},
)
async def get_freedata_message(message_id: str, ctx: AppContext = Depends(get_ctx)):
message = _mgr_msgs(ctx).get_message_by_id_json(message_id)
if message is None:
api_abort("Message not found", 404)
return api_response(message)
@router.post(
"/messages",
summary="Transmit Message",
tags=["FreeDATA"],
responses={
200: {
"description": "Message transmitted successfully.",
"content": {"application/json": {"example": {"destination": "XX1XXX-6", "body": "Hello FreeDATA"}}},
},
404: {
"description": "The requested resource was not found.",
"content": {"application/json": {"example": {"error": "Resource not found."}}},
},
},
)
async def post_freedata_message(payload: dict, ctx: AppContext = Depends(get_ctx)):
# Transmit FreeDATA message
await enqueue_tx_command(ctx, command_message_send.SendMessageCommand, payload)
return api_response(payload)
@router.post(
"/messages/{message_id}/adif",
summary="Send Message ADIF Log",
tags=["FreeDATA"],
responses={
200: {
"description": "ADIF log sent successfully.",
"content": {"application/json": {"example": {"adif_output": "ADIF data..."}}},
},
400: {
"description": "Bad Request: The request was malformed or missing required parameters.",
"content": {"application/json": {"example": {"error": "Invalid message ID."}}},
},
404: {
"description": "The requested resource was not found.",
"content": {"application/json": {"example": {"error": "Message not found."}}},
},
503: {
"description": "Modem not running.",
"content": {"application/json": {"example": {"error": "Modem not running."}}},
},
},
)
async def post_freedata_message_adif_log(message_id: str, ctx: AppContext = Depends(get_ctx)):
adif = _mgr_msgs(ctx).get_message_by_id_adif(message_id)
if not adif:
api_abort("Message not found or ADIF unavailable", 404)
# send logs
adif_udp_logger.send_adif_qso_data(ctx, adif)
wavelog_api_logger.send_wavelog_qso_data(ctx, adif)
return api_response({"adif_output": adif})
@router.patch(
"/messages/{message_id}",
summary="Update Message by ID",
tags=["FreeDATA"],
responses={
200: {
"description": "Message updated successfully.",
"content": {"application/json": {"example": {"is_read": True}}},
},
400: {
"description": "Bad Request: The request was malformed or missing required parameters.",
"content": {"application/json": {"example": {"error": "Invalid parameters."}}},
},
404: {
"description": "Message not found.",
"content": {"application/json": {"example": {"error": "Message not found."}}},
},
},
)
async def patch_freedata_message(message_id: str, payload: dict, ctx: AppContext = Depends(get_ctx)):
if payload.get("action") == "retransmit":
_mgr_msgs(ctx).update_message(message_id, {"status": "queued"})
_mgr_msgs(ctx).increment_message_attempts(message_id)
result = {"message_id": message_id, "status": "queued"}
else:
result = _mgr_msgs(ctx).update_message(message_id, update_data=payload)
if result is None:
api_abort("Message not found", 404)
return api_response(result)
@router.get(
"/messages",
summary="Get All Messages",
tags=["FreeDATA"],
responses={
200: {
"description": "List of all messages.",
"content": {
"application/json": {
"example": {
"total_messages": 1,
"messages": [
{
"id": "DXCALL-6_MYCALL-0_2024-04-12T20:39:05.302479",
"timestamp": "2024-04-12T20:39:05.302479",
"origin": "DXCALL-6",
"via": None,
"destination": "MYCALL-0",
"direction": "receive",
"body": "Hello !",
"attachments": [],
"status": "received",
"priority": 10,
"is_read": False,
"statistics": {
"total_bytes": 120,
"duration": 29.76698660850525,
"bytes_per_minute": 241,
"time_histogram": {
"0": "2024-04-12T20:39:23.423169",
"1": "2024-04-12T20:39:30.504638",
"2": "2024-04-12T20:39:37.745075",
},
"snr_histogram": {"0": -6, "1": -6, "2": -6},
"bpm_histogram": {"0": 198, "1": 265, "2": 252},
},
}
],
}
}
},
},
404: {
"description": "The requested resource was not found.",
"content": {"application/json": {"example": {"error": "Resource not found."}}},
},
},
)
async def get_freedata_messages(ctx: AppContext = Depends(get_ctx)):
filters = {k: v for k, v in ctx.config_manager.read().get("FILTERS", {}).items()}
# use query params if needed
# filters = dict(ctx.request.query_params)
result = _mgr_msgs(ctx).get_all_messages_json(filters=filters)
return api_response(result)
@router.delete(
"/messages/{message_id}",
summary="Delete Message by ID",
tags=["FreeDATA"],
responses={
200: {
"description": "Message deleted successfully.",
"content": {
"application/json": {
"example": {
"message": "DXCALL-0_MYCALL-5_2024-04-04T17:22:14.002502 deleted",
"status": "success",
}
}
},
},
404: {
"description": "Message not found.",
"content": {"application/json": {"example": {"message": "Message not found", "status": "failure"}}},
},
},
)
async def delete_freedata_message(message_id: str, ctx: AppContext = Depends(get_ctx)):
ok = _mgr_msgs(ctx).delete_message(message_id)
if not ok:
api_abort("Message not found", 404)
return api_response({"message": f"{message_id} deleted", "status": "success"})
@router.get(
"/messages/{message_id}/attachments",
summary="Get Attachments by Message ID",
tags=["FreeDATA"],
responses={
200: {
"description": "List of attachments for the specified message.",
"content": {
"application/json": {
"example": {
"attachments": [
{
"id": "attachment1",
"filename": "file1.txt",
"file_size": 1024,
"file_type": "text/plain",
"data_sha512": "abcdef1234567890...",
},
{
"id": "attachment2",
"filename": "image.png",
"file_size": 2048,
"file_type": "image/png",
"data_sha512": "123456abcdef7890...",
},
]
}
}
},
},
404: {
"description": "The requested resource was not found.",
"content": {"application/json": {"example": {"error": "Resource not found."}}},
},
503: {
"description": "Modem not running.",
"content": {"application/json": {"example": {"error": "Modem not running."}}},
},
},
)
async def get_message_attachments(message_id: str, ctx: AppContext = Depends(get_ctx)):
attachments = _mgr_attach(ctx).get_attachments_by_message_id_json(message_id)
return api_response({"attachments": attachments})
@router.get(
"/messages/attachment/{data_sha512}",
summary="Get Attachment by SHA512",
tags=["FreeDATA"],
responses={
200: {
"description": "Retrieve a specific attachment by its SHA512 hash.",
"content": {
"application/json": {
"example": {
"id": "attachment1",
"filename": "file1.txt",
"file_size": 1024,
"file_type": "text/plain",
"data_sha512": "abcdef1234567890...",
}
}
},
},
404: {
"description": "The requested attachment was not found.",
"content": {"application/json": {"example": {"error": "Attachment not found."}}},
},
503: {
"description": "Modem not running.",
"content": {"application/json": {"example": {"error": "Modem not running."}}},
},
},
)
async def get_message_attachment(data_sha512: str, ctx: AppContext = Depends(get_ctx)):
attachment = _mgr_attach(ctx).get_attachment_by_sha512(data_sha512)
if attachment is None:
api_abort("Attachment not found", 404)
return api_response(attachment)
@router.get(
"/beacons",
summary="Get Received Beacons",
tags=["FreeDATA"],
responses={
200: {
"description": "List of received beacons.",
"content": {
"application/json": {
"example": {
"total_beacons": 2,
"beacons": [
{
"id": "DXCALL-0_MYCALL-5_2024-04-04T17:22:14.002502",
"timestamp": "2024-04-04T17:22:14.002502",
"origin": "DXCALL-0",
"via": None,
"destination": "MYCALL-5",
"direction": "receive",
"body": "Hello FreeDATA",
"attachments": [],
"status": "received",
"priority": 10,
"is_read": False,
"statistics": {
"total_bytes": 120,
"duration": 29.77,
"bytes_per_minute": 241,
"time_histogram": {
"0": "2024-04-04T17:22:23.423169",
"1": "2024-04-04T17:22:30.504638",
"2": "2024-04-04T17:22:37.745075",
},
"snr_histogram": {"0": -6, "1": -6, "2": -6},
"bpm_histogram": {"0": 198, "1": 265, "2": 252},
},
}
],
}
}
},
},
400: {
"description": "Bad Request: The request was malformed or missing required parameters.",
"content": {"application/json": {"example": {"error": "Invalid request."}}},
},
404: {
"description": "The requested resource was not found.",
"content": {"application/json": {"example": {"error": "Resource not found."}}},
},
500: {
"description": "Internal Server Error: An unexpected error occurred on the server.",
"content": {"application/json": {"example": {"error": "Internal server error."}}},
},
503: {
"description": "Modem not running.",
"content": {"application/json": {"example": {"error": "Modem not running."}}},
},
},
)
async def get_all_beacons(ctx: AppContext = Depends(get_ctx)):
beacons = _mgr_beacon(ctx).get_all_beacons()
return api_response(beacons)
@router.get(
"/beacons/{callsign}",
summary="Get Beacon by Callsign",
tags=["FreeDATA"],
responses={
200: {
"description": "List of beacons from the specified callsign.",
"content": {
"application/json": {
"example": {
"beacons": [
{
"id": "DXCALL-0_MYCALL-5_2024-04-04T17:22:14.002502",
"timestamp": "2024-04-04T17:22:14.002502",
"origin": "DXCALL-0",
"via": None,
"destination": "MYCALL-5",
"direction": "receive",
"body": "Hello FreeDATA",
"attachments": [],
"status": "received",
"priority": 10,
"is_read": False,
"statistics": {
"total_bytes": 120,
"duration": 29.77,
"bytes_per_minute": 241,
"time_histogram": {
"0": "2024-04-04T17:22:23.423169",
"1": "2024-04-04T17:22:30.504638",
"2": "2024-04-04T17:22:37.745075",
},
"snr_histogram": {"0": -6, "1": -6, "2": -6},
"bpm_histogram": {"0": 198, "1": 265, "2": 252},
},
}
]
}
}
},
},
400: {
"description": "Bad Request: The request was malformed or missing required parameters.",
"content": {"application/json": {"example": {"error": "Invalid request."}}},
},
404: {
"description": "The requested resource was not found.",
"content": {"application/json": {"example": {"error": "Resource not found."}}},
},
500: {
"description": "Internal Server Error: An unexpected error occurred on the server.",
"content": {"application/json": {"example": {"error": "Internal server error."}}},
},
503: {
"description": "Modem not running.",
"content": {"application/json": {"example": {"error": "Modem not running."}}},
},
},
)
async def get_beacons_by_callsign(callsign: str, ctx: AppContext = Depends(get_ctx)):
beacons = _mgr_beacon(ctx).get_beacons_by_callsign(callsign)
return api_response(beacons)
@router.get(
"/station/{callsign}",
summary="Get Station Info",
tags=["FreeDATA"],
responses={
200: {
"description": "Retrieve station information by callsign.",
"content": {
"application/json": {
"example": {
"callsign": "MYCALL-0",
"location": "Springfield",
"frequency": "14093000",
"mode": "PKTUSB",
"status": "active",
"additional_info": "Station details here.",
}
}
},
},
400: {
"description": "Bad Request: The request was malformed or missing required parameters.",
"content": {"application/json": {"example": {"error": "Invalid callsign parameter."}}},
},
404: {
"description": "The requested station was not found.",
"content": {"application/json": {"example": {"error": "Station not found."}}},
},
500: {
"description": "Internal Server Error: An unexpected error occurred on the server.",
"content": {"application/json": {"example": {"error": "Internal server error."}}},
},
503: {
"description": "Modem not running.",
"content": {"application/json": {"example": {"error": "Modem not running."}}},
},
},
)
async def get_station_info(callsign: str, ctx: AppContext = Depends(get_ctx)):
station = _mgr_stations(ctx).get_station(callsign)
if station is None:
api_abort("Station not found", 404)
return api_response(station)
@router.post(
"/station/{callsign}",
summary="Set Station Info",
tags=["FreeDATA"],
responses={
200: {
"description": "Station information updated successfully.",
"content": {
"application/json": {
"example": {
"callsign": "MYCALL-0",
"location": "Springfield",
"frequency": "14093000",
"mode": "PKTUSB",
"status": "active",
"additional_info": "Updated station details.",
}
}
},
},
400: {
"description": "Bad Request: The request was malformed or missing required parameters.",
"content": {"application/json": {"example": {"error": "Invalid input data."}}},
},
404: {
"description": "The requested station was not found.",
"content": {"application/json": {"example": {"error": "Station not found."}}},
},
500: {
"description": "Internal Server Error: An unexpected error occurred on the server.",
"content": {"application/json": {"example": {"error": "Internal server error."}}},
},
503: {
"description": "Modem not running.",
"content": {"application/json": {"example": {"error": "Modem not running."}}},
},
},
)
async def set_station_info(callsign: str, payload: dict, ctx: AppContext = Depends(get_ctx)):
info = payload.get("info")
if not isinstance(info, dict):
api_abort("Invalid input data", 400)
result = _mgr_stations(ctx).update_station_info(callsign, new_info=info)
if result is None:
api_abort("Station not found", 404)
return api_response(result)
@router.get("/broadcasts", summary="Get All Broadcast Messages", tags=["FreeDATA"], responses={})
async def get_freedata_broadcasts(ctx: AppContext = Depends(get_ctx)):
# filters = {k: v for k, v in ctx.config_manager.read().get('FILTERS', {}).items()}
# use query params if needed
# filters = dict(ctx.request.query_params)
result = _mgr_broadcasts(ctx).get_all_broadcasts_json()
return api_response(result)
@router.get("/broadcasts/{domain}/", summary="Get Broadcats per Domain", tags=["FreeDATA"], responses={})
async def get_freedata_broadcasts_per_domain(domain: str, ctx: AppContext = Depends(get_ctx)):
result = _mgr_broadcasts(ctx).get_broadcasts_per_domain_json(domain)
return api_response(result)
@router.get("/broadcasts/domains", summary="Get All Broadcast Messages", tags=["FreeDATA"], responses={})
async def get_freedata_broadcasts(ctx: AppContext = Depends(get_ctx)): # noqa: F811 # same as line 534, but different path
# filters = {k: v for k, v in ctx.config_manager.read().get('FILTERS', {}).items()}
# use query params if needed
# filters = dict(ctx.request.query_params)
result = _mgr_broadcasts(ctx).get_broadcast_domains_json()
return api_response(result)
@router.delete("/broadcasts/{id}", summary="Delete Message or Broadcast by ID", tags=["FreeDATA"], responses={})
async def delete_freedata_broadcast_domain(id: str, ctx: AppContext = Depends(get_ctx)):
ok = _mgr_broadcasts(ctx).delete_broadcast_message_or_domain(id)
if not ok:
api_abort("Message not found", 404)
return api_response({"message": f"{id} deleted", "status": "success"})
@router.patch("/broadcasts/{id}", summary="Retransmit Broadcast by ID", tags=["FreeDATA"], responses={})
async def patch_freedata_broadcast_domain(id: str, payload: dict, ctx: AppContext = Depends(get_ctx)):
if payload.get("action") == "retransmit":
_mgr_broadcasts(ctx).increment_attempts(id)
msg = _mgr_broadcasts(ctx).get_broadcast_per_id(id, get_object=True)
if msg:
loop = asyncio.get_running_loop()
loop.run_in_executor(None, NormTransmissionISS(ctx).retransmit_data, msg)
return api_response({"message_id": id, "status": "retransmit started"})
else:
api_abort("Message not found", 404)
api_abort("Invalid action", 400)
@router.post("/broadcasts", summary="Transmit Broadcast", tags=["FreeDATA"], responses={})
async def post_freedata_broadcast(payload: dict, ctx: AppContext = Depends(get_ctx)):
await enqueue_tx_command(ctx, command_norm.Norm, payload)
return api_response({"message": "broadcast transmitted", "status": "success"})