mirror of https://github.com/DJ2LS/FreeDATA.git
583 lines
23 KiB
Python
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"})
|