mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-20 06:17:41 +00:00
142 lines
5.9 KiB
Python
142 lines
5.9 KiB
Python
import logging
|
|
import re
|
|
from typing import Any
|
|
|
|
import tornado
|
|
from tornado import httputil
|
|
from tornado.web import Application
|
|
|
|
from core.config import ALLOW_SPOTTING
|
|
from core.constants import UNKNOWN_BAND
|
|
from core.sig_utils import get_ref_regex_for_sig
|
|
from core.utils import infer_band_from_freq, safe_json_dumps
|
|
from data.spot import Spot
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class V1APISpotHandler(tornado.web.RequestHandler):
|
|
"""API request handler for /api/v1/spot (POST). Included in early Spothole v2 for backwards compatibility."""
|
|
|
|
def __init__(
|
|
self,
|
|
application: "Application",
|
|
request: httputil.HTTPServerRequest,
|
|
**kwargs: Any,
|
|
):
|
|
self._spots = None
|
|
super().__init__(application, request, **kwargs)
|
|
|
|
def initialize(self, spots):
|
|
self._spots = spots
|
|
|
|
def post(self):
|
|
try:
|
|
# Reject if not allowed
|
|
if not ALLOW_SPOTTING:
|
|
self.set_status(401)
|
|
self.write(safe_json_dumps("Error - this server does not allow new spots to be added via the API."))
|
|
self.set_header("Cache-Control", "no-store")
|
|
self.set_header("Content-Type", "application/json")
|
|
return
|
|
|
|
# Reject if format not json
|
|
if not self.request.headers.get("Content-Type", "").startswith("application/json"):
|
|
self.set_status(415)
|
|
self.write(safe_json_dumps("Error - request Content-Type must be application/json"))
|
|
self.set_header("Cache-Control", "no-store")
|
|
self.set_header("Content-Type", "application/json")
|
|
return
|
|
|
|
# Reject if request body is empty
|
|
post_data = self.request.body
|
|
if not post_data:
|
|
self.set_status(422)
|
|
self.write(safe_json_dumps("Error - request body is empty"))
|
|
self.set_header("Cache-Control", "no-store")
|
|
self.set_header("Content-Type", "application/json")
|
|
return
|
|
|
|
# Read in the request body as JSON then convert to a Spot object
|
|
json_spot = tornado.escape.json_decode(post_data)
|
|
spot = Spot(**json_spot)
|
|
|
|
# Reject if no timestamp, frequency, dx_call or de_call
|
|
if not spot.time or not spot.dx_call or not spot.freq or not spot.de_call:
|
|
self.set_status(422)
|
|
self.write(
|
|
safe_json_dumps("Error - 'time', 'dx_call', 'freq' and 'de_call' must be provided as a minimum.")
|
|
)
|
|
self.set_header("Cache-Control", "no-store")
|
|
self.set_header("Content-Type", "application/json")
|
|
return
|
|
|
|
# Reject invalid-looking callsigns
|
|
if not re.match(r"^[A-Za-z0-9/\-]*$", spot.dx_call):
|
|
self.set_status(422)
|
|
self.write(safe_json_dumps(f"Error - '{spot.dx_call}' does not look like a valid callsign."))
|
|
self.set_header("Cache-Control", "no-store")
|
|
self.set_header("Content-Type", "application/json")
|
|
return
|
|
if not re.match(r"^[A-Za-z0-9/\-]*$", spot.de_call):
|
|
self.set_status(422)
|
|
self.write(safe_json_dumps(f"Error - '{spot.de_call}' does not look like a valid callsign."))
|
|
self.set_header("Cache-Control", "no-store")
|
|
self.set_header("Content-Type", "application/json")
|
|
return
|
|
|
|
# Reject if frequency not in a known band
|
|
if infer_band_from_freq(spot.freq) == UNKNOWN_BAND:
|
|
self.set_status(422)
|
|
self.write(safe_json_dumps(f"Error - Frequency of {spot.freq / 1000.0!s}kHz is not in a known band."))
|
|
self.set_header("Cache-Control", "no-store")
|
|
self.set_header("Content-Type", "application/json")
|
|
return
|
|
|
|
# Reject if grid formatting incorrect
|
|
if spot.dx_grid and not re.match(
|
|
r"^([A-R]{2}[0-9]{2}[A-X]{2}[0-9]{2}[A-X]{2}|[A-R]{2}[0-9]{2}[A-X]{2}[0-9]{2}|[A-R]{2}[0-9]{2}[A-X]{2}|[A-R]{2}[0-9]{2})$",
|
|
spot.dx_grid.upper(),
|
|
):
|
|
self.set_status(422)
|
|
self.write(safe_json_dumps(f"Error - '{spot.dx_grid}' does not look like a valid Maidenhead grid."))
|
|
self.set_header("Cache-Control", "no-store")
|
|
self.set_header("Content-Type", "application/json")
|
|
return
|
|
|
|
# Reject if sig_ref format incorrect for sig
|
|
if (
|
|
spot.sig
|
|
and spot.sig_refs
|
|
and len(spot.sig_refs) > 0
|
|
and spot.sig_refs[0].id
|
|
and get_ref_regex_for_sig(spot.sig)
|
|
and not re.match(get_ref_regex_for_sig(spot.sig), spot.sig_refs[0].id)
|
|
):
|
|
self.set_status(422)
|
|
self.write(
|
|
safe_json_dumps(
|
|
f"Error - '{spot.sig_refs[0].id}' does not look like a valid reference for {spot.sig}."
|
|
)
|
|
)
|
|
self.set_header("Cache-Control", "no-store")
|
|
self.set_header("Content-Type", "application/json")
|
|
return
|
|
|
|
# infer missing data, and add it to our database.
|
|
spot.source = "API"
|
|
spot.infer_missing()
|
|
self._spots.set(spot.id, spot)
|
|
|
|
self.write(safe_json_dumps("OK"))
|
|
self.set_status(201)
|
|
self.set_header("Cache-Control", "no-store")
|
|
self.set_header("Content-Type", "application/json")
|
|
|
|
except Exception:
|
|
logger.exception("Exception when handling client request to add spot API")
|
|
self.write(safe_json_dumps("Error - an internal server error occurred."))
|
|
self.set_status(500)
|
|
self.set_header("Cache-Control", "no-store")
|
|
self.set_header("Content-Type", "application/json")
|