mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-28 18:22:05 +00:00
Allow spotting upstream to cluster. #39
This commit is contained in:
@@ -6,6 +6,7 @@ from typing import Any
|
||||
import requests
|
||||
import tornado
|
||||
from tornado import httputil
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.web import Application
|
||||
|
||||
from core.activity_utils import get_ref_regex_for_activity
|
||||
@@ -13,7 +14,9 @@ from core.config import ALLOW_SPOTTING, ALLOW_UPSTREAM_SPOTTING, RECAPTCHA_SECRE
|
||||
from core.constants import UNKNOWN_BAND
|
||||
from core.utils import infer_band_from_freq, safe_json_dumps
|
||||
from data.spot import Spot
|
||||
from providers.spot.spot_provider import SpotProvider
|
||||
from providers.spot.dxcluster import DXCluster
|
||||
from providers.spot.spot_provider import SpotProvider, SpotSubmissionError
|
||||
from providers.spot.tiles import Tiles
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,7 +40,10 @@ class APISpotHandler(tornado.web.RequestHandler):
|
||||
self._spots = spots
|
||||
self._spot_providers = spot_providers or []
|
||||
|
||||
def post(self):
|
||||
async def post(self):
|
||||
"""Handle the post request with spot data. This is async because it could go on to submit spots to providers
|
||||
which could be slow, so we need to stop it blocking the whole web server"""
|
||||
|
||||
try:
|
||||
# Reject if not allowed
|
||||
if not ALLOW_SPOTTING:
|
||||
@@ -77,10 +83,6 @@ class APISpotHandler(tornado.web.RequestHandler):
|
||||
upstream_credentials = handling.get("upstream_credentials", {})
|
||||
captcha_token = handling.get("captcha_token", None)
|
||||
|
||||
# Spothole v2.0 release only: deny upstream spotting. Spothole API breaking changes were in v2.0 but
|
||||
# functionality is not ready yet. TODO
|
||||
submit_upstream = False
|
||||
|
||||
# Verify CAPTCHA if required
|
||||
if RECAPTCHA_SECRET_KEY:
|
||||
if not captcha_token:
|
||||
@@ -89,7 +91,7 @@ class APISpotHandler(tornado.web.RequestHandler):
|
||||
self.set_header("Cache-Control", "no-store")
|
||||
self.set_header("Content-Type", "application/json")
|
||||
return
|
||||
if not self._verify_recaptcha(captcha_token):
|
||||
if not await IOLoop.current().run_in_executor(None, self._verify_recaptcha, captcha_token):
|
||||
self.set_status(422)
|
||||
self.write(safe_json_dumps("Error - CAPTCHA verification failed."))
|
||||
self.set_header("Cache-Control", "no-store")
|
||||
@@ -166,20 +168,15 @@ class APISpotHandler(tornado.web.RequestHandler):
|
||||
|
||||
# Validate upstream submission requirements
|
||||
if submit_upstream and upstream_provider_name:
|
||||
if not spot.activities:
|
||||
# TODO when we allow spotting to cluster upstream, we need to remove this restriction
|
||||
self.set_status(422)
|
||||
self.write(safe_json_dumps("Error - an activity must be selected to submit upstream."))
|
||||
self.set_header("Cache-Control", "no-store")
|
||||
self.set_header("Content-Type", "application/json")
|
||||
return
|
||||
if not spot.activity_refs and upstream_provider_name != "Tiles":
|
||||
is_cluster = isinstance(self._find_provider(upstream_provider_name, spot.activities), DXCluster)
|
||||
is_tiles = isinstance(self._find_provider(upstream_provider_name, spot.activities), Tiles)
|
||||
if not spot.activity_refs and not is_tiles and not is_cluster:
|
||||
self.set_status(422)
|
||||
self.write(safe_json_dumps("Error - an activity reference is required to submit upstream."))
|
||||
self.set_header("Cache-Control", "no-store")
|
||||
self.set_header("Content-Type", "application/json")
|
||||
return
|
||||
if not spot.dx_grid and upstream_provider_name == "Tiles":
|
||||
if not spot.dx_grid and is_tiles:
|
||||
self.set_status(422)
|
||||
self.write(
|
||||
safe_json_dumps("Error - a grid reference is required to submit upstream to Tiles on the Air.")
|
||||
@@ -187,7 +184,7 @@ class APISpotHandler(tornado.web.RequestHandler):
|
||||
self.set_header("Cache-Control", "no-store")
|
||||
self.set_header("Content-Type", "application/json")
|
||||
return
|
||||
if not spot.mode and upstream_provider_name == "Tiles":
|
||||
if not spot.mode and is_tiles:
|
||||
self.set_status(422)
|
||||
self.write(safe_json_dumps("Error - a mode is required to submit upstream to Tiles on the Air."))
|
||||
self.set_header("Cache-Control", "no-store")
|
||||
@@ -200,19 +197,29 @@ class APISpotHandler(tornado.web.RequestHandler):
|
||||
provider = self._find_provider(upstream_provider_name, spot.activities)
|
||||
if provider:
|
||||
try:
|
||||
# Submit spot to the upstream provider
|
||||
provider.submit_spot(spot, upstream_credentials)
|
||||
# Trigger a re-poll after 1 second so the spot appears quickly
|
||||
threading.Timer(1.0, provider.force_poll).start()
|
||||
# Submit spot to the upstream provider. Run in a separate thread otherwise this blocks the whole
|
||||
# web server for everyone!
|
||||
await IOLoop.current().run_in_executor(None, provider.submit_spot, spot, upstream_credentials)
|
||||
# Trigger a re-poll after 3 second so the spot appears quickly. (Submitting to a cluster node is
|
||||
# slower than this, but we get data as a live stream from cluster anyway, so force_poll does
|
||||
# nothing in that case. This is really just for the HTTP providers when we submit a spot to them)
|
||||
threading.Timer(3.0, provider.force_poll).start()
|
||||
except NotImplementedError as e:
|
||||
upstream_warning = str(e)
|
||||
except SpotSubmissionError as e:
|
||||
logger.warning(f"Upstream submission to {upstream_provider_name} was not accepted: {e}")
|
||||
upstream_warning = (
|
||||
f"Spot was saved locally but upstream submission to {upstream_provider_name} failed: {e}"
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(f"Failed to submit spot upstream to {upstream_provider_name}")
|
||||
upstream_warning = (
|
||||
f"Spot was saved locally but upstream submission to {upstream_provider_name} failed."
|
||||
)
|
||||
else:
|
||||
elif spot.activities:
|
||||
upstream_warning = f"No enabled provider named '{upstream_provider_name}' supports upstream submission for {', '.join(spot.activities)} spots."
|
||||
else:
|
||||
upstream_warning = f"No enabled provider named '{upstream_provider_name}' supports upstream submission for spots with no activity."
|
||||
|
||||
# If we successfully submitted the spot upstream, don't add it direct to Spothole, otherwise it will be a
|
||||
# duplicate with what immediately comes back from the API. But if we weren't asked to send it upstream, or
|
||||
@@ -239,10 +246,11 @@ class APISpotHandler(tornado.web.RequestHandler):
|
||||
self.set_header("Content-Type", "application/json")
|
||||
|
||||
def _find_provider(self, provider_name, activities) -> SpotProvider | None:
|
||||
"""Find an enabled provider by name that can submit spots for at least one of the given activities."""
|
||||
"""Find an enabled provider by name that can submit spots for at least one of the given activities. If there
|
||||
are no activities, find one that can submit spots with no activity."""
|
||||
|
||||
for p in self._spot_providers:
|
||||
if p.enabled and p.name == provider_name and any(p.can_submit_spot(a) for a in activities):
|
||||
if p.enabled and p.name == provider_name and any(p.can_submit_spot(a) for a in activities or [None]):
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
@@ -21,16 +21,18 @@ def build_options():
|
||||
"""Build the options data that is returned by the /options API and used to populate the web UI templates. This
|
||||
doesn't change at runtime, so it is built once on first use and then cached."""
|
||||
|
||||
# Build a map of activity name -> list of provider names that can submit spots for that activity
|
||||
# Build a map of activity name -> list of provider names that can submit spots for that activity. Spots with no
|
||||
# activity use the empty string as the key as that's effectively what is in the "blank" entry in the drop-down
|
||||
# on the Add Spot page
|
||||
spot_submit_providers = {}
|
||||
|
||||
# Spothole v2.0 - disable this for now, API changes are in but this functionality is not ready yet. TODO
|
||||
# for provider in DATA_PROVIDERS.spot_providers:
|
||||
# if not provider.enabled:
|
||||
# continue
|
||||
# for activity in ACTIVITIES.values():
|
||||
# if provider.can_submit_spot(activity.name):
|
||||
# spot_submit_providers.setdefault(activity.name, []).append(provider.name)
|
||||
for provider in DATA_PROVIDERS.spot_providers:
|
||||
if not provider.enabled:
|
||||
continue
|
||||
for activity_name in [None, *(activity.name for activity in ACTIVITIES.values())]:
|
||||
if provider.can_submit_spot(activity_name):
|
||||
names = spot_submit_providers.setdefault(activity_name or "", [])
|
||||
if provider.name not in names:
|
||||
names.append(provider.name)
|
||||
|
||||
# Spot/alert sources are filtered for only ones that are enabled in config, no point letting the user toggle
|
||||
# things that aren't even available.
|
||||
|
||||
Reference in New Issue
Block a user