diff --git a/README.md b/README.md
index 009cdb7..2a9893b 100644
--- a/README.md
+++ b/README.md
@@ -462,13 +462,16 @@ As well as being my work, I have also gratefully received feature patches from S
The project contains GeoJSON files for CQ and ITU zones, in the `/datafiles/` directory. These are MIT-licenced and, to
my knowledge, created by HA8TKS for his CQ and ITU zone layers for Leaflet.
-The project contains a set of flag icons generated using the "Noto Color Emoji" font on a Debian system, in the `/webassets/img/flags/` directory.
+The project contains a set of flag icons generated using the "Noto Color Emoji" font on a Debian system, in the
+`/webassets/img/flags/` directory.
-The software uses a number of Python libraries as listed in `requirements.txt`, and a number of JavaScript libraries. This project would not have been possible without these libraries, so many thanks to their developers.
+The software uses a number of Python libraries as listed in `requirements.txt`, and a number of JavaScript libraries.
+This project would not have been possible without these libraries, so many thanks to their developers.
### Third Party Libraries
-A number of third-party libraries are self-hosted in the `/webassets/vendor/` directory. These files are subject to their own licences and are not covered by the overall licence declared in the `LICENSE` file.
+A number of third-party libraries are self-hosted in the `/webassets/vendor/` directory. These files are subject to
+their own licences and are not covered by the overall licence declared in the `LICENSE` file.
A number of third-party libraries are self-hosted in the `/webassets/vendor/` directory. These files are subject to
their own licences and are not covered by the overall licence declared in the `LICENSE` file.
diff --git a/server/handlers/api/addspot.py b/server/handlers/api/addspot.py
index c5ac073..0ea8af0 100644
--- a/server/handlers/api/addspot.py
+++ b/server/handlers/api/addspot.py
@@ -19,6 +19,7 @@ from core.sig_utils import get_ref_regex_for_sig
from core.utils import serialize_everything
from data.sig_ref import SIGRef
from data.spot import Spot
+from spotproviders.spot_provider import SpotProvider
RECAPTCHA_VERIFY_URL = "https://www.google.com/recaptcha/api/siteverify"
@@ -29,6 +30,7 @@ class APISpotHandler(tornado.web.RequestHandler):
def __init__(self, application: "Application", request: httputil.HTTPServerRequest, **kwargs: Any):
self._spots = None
self._web_server_metrics = None
+ self._spot_providers = None
super().__init__(application, request, **kwargs)
def initialize(self, spots, web_server_metrics, spot_providers=None):
@@ -185,7 +187,7 @@ class APISpotHandler(tornado.web.RequestHandler):
# 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, lambda: provider.force_poll()).start()
+ threading.Timer(1.0, provider.force_poll).start()
except NotImplementedError as e:
upstream_warning = str(e)
except Exception as e:
@@ -218,7 +220,7 @@ class APISpotHandler(tornado.web.RequestHandler):
self.set_header("Cache-Control", "no-store")
self.set_header("Content-Type", "application/json")
- def _find_provider(self, provider_name, sig):
+ def _find_provider(self, provider_name, sig) -> SpotProvider | None:
"""Find an enabled provider by name that can submit spots for the given SIG."""
for p in self._spot_providers:
@@ -226,7 +228,8 @@ class APISpotHandler(tornado.web.RequestHandler):
return p
return None
- def _verify_recaptcha(self, token):
+ @staticmethod
+ def _verify_recaptcha(token):
"""Verify a Google reCAPTCHA v2 token. Returns True if valid."""
try:
diff --git a/server/handlers/api/options.py b/server/handlers/api/options.py
index cfca2d4..fb9f6be 100644
--- a/server/handlers/api/options.py
+++ b/server/handlers/api/options.py
@@ -19,6 +19,7 @@ class APIOptionsHandler(tornado.web.RequestHandler):
def __init__(self, application: "Application", request: httputil.HTTPServerRequest, **kwargs: Any):
self._status_data = None
self._web_server_metrics = None
+ self._spot_providers = None
super().__init__(application, request, **kwargs)
def initialize(self, status_data, web_server_metrics, spot_providers=None):
@@ -42,23 +43,27 @@ class APIOptionsHandler(tornado.web.RequestHandler):
if provider.can_submit_spot(sig.name):
spot_submit_providers.setdefault(sig.name, []).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.
+ spot_sources: list = list(
+ map(lambda p: p["name"], filter(lambda p: p["enabled"], self._status_data["spot_providers"])))
+ alert_sources = list(
+ map(lambda p: p["name"], filter(lambda p: p["enabled"], self._status_data["alert_providers"])))
+ # If spotting to this server is enabled, "API" is another valid spot source even though it does not come from
+ # one of our providers.
+ if ALLOW_SPOTTING:
+ spot_sources.append("API")
+
options = {"bands": BANDS,
"modes": ALL_MODES,
"mode_types": MODE_TYPES,
"sigs": SIGS,
- # 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.
- "spot_sources": list(
- map(lambda p: p["name"], filter(lambda p: p["enabled"], self._status_data["spot_providers"]))),
- "alert_sources": list(
- map(lambda p: p["name"], filter(lambda p: p["enabled"], self._status_data["alert_providers"]))),
+ "spot_sources": spot_sources,
+ "alert_sources": alert_sources,
"continents": CONTINENTS,
"max_spot_age": MAX_SPOT_AGE,
"spot_allowed": ALLOW_SPOTTING,
"spot_submit_providers": spot_submit_providers}
- # If spotting to this server is enabled, "API" is another valid spot source even though it does not come from
- # one of our proviers.
- if ALLOW_SPOTTING:
- options["spot_sources"].append("API")
self.write(json.dumps(options, default=serialize_everything))
self.set_status(200)
diff --git a/server/webserver.py b/server/webserver.py
index 7645ff8..27c28f3 100644
--- a/server/webserver.py
+++ b/server/webserver.py
@@ -72,12 +72,14 @@ class WebServer:
{"sse_alert_queues": self._sse_alert_queues, **handler_opts}),
(r"/api/v1/solar", APISolarConditionsHandler, {"solar_conditions": self._solar_conditions, **handler_opts}),
(r"/api/v1/dxstats", APIDxStatsHandler, {"spots": self._spots, **handler_opts}),
- (r"/api/v1/options", APIOptionsHandler, {"status_data": self._status_data, "spot_providers": self._spot_providers, **handler_opts}),
+ (r"/api/v1/options", APIOptionsHandler,
+ {"status_data": self._status_data, "spot_providers": self._spot_providers, **handler_opts}),
(r"/api/v1/status", APIStatusHandler, {"status_data": self._status_data, **handler_opts}),
(r"/api/v1/lookup/call", APILookupCallHandler, {**handler_opts}),
(r"/api/v1/lookup/sigref", APILookupSIGRefHandler, {**handler_opts}),
(r"/api/v1/lookup/grid", APILookupGridHandler, {**handler_opts}),
- (r"/api/v1/spot", APISpotHandler, {"spots": self._spots, "spot_providers": self._spot_providers, **handler_opts}),
+ (r"/api/v1/spot", APISpotHandler,
+ {"spots": self._spots, "spot_providers": self._spot_providers, **handler_opts}),
]
# If in API-only mode, serve a basic homepage; in normal mode, serve the usual UI routes
diff --git a/spotproviders/parksnpeaks.py b/spotproviders/parksnpeaks.py
index 51bacc7..6beac64 100644
--- a/spotproviders/parksnpeaks.py
+++ b/spotproviders/parksnpeaks.py
@@ -76,7 +76,8 @@ class ParksNPeaks(HTTPSpotProvider):
user_id = credentials.get("user_id", "")
api_key = credentials.get("api_key", "")
if not user_id or not api_key:
- raise ValueError("Parks N Peaks user ID and API key are required. Get yours from your Parks N Peaks account.")
+ raise ValueError(
+ "Parks N Peaks user ID and API key are required. Get yours from your Parks N Peaks account.")
sig_ref = spot.sig_refs[0].id if spot.sig_refs else ""
body = {
"actClass": spot.sig or "",
diff --git a/spotproviders/sota.py b/spotproviders/sota.py
index b59f85d..e6c6aa4 100644
--- a/spotproviders/sota.py
+++ b/spotproviders/sota.py
@@ -93,9 +93,10 @@ class SOTA(HTTPSpotProvider):
"mode": mode or "",
"callsign": spot.de_call,
"comments": spot.comment or "",
- "type": "TEST" # todo replatce with NORMAL/QRT once testing complete
+ "type": "TEST" # todo replatce with NORMAL/QRT once testing complete
}
- headers = {**HTTP_HEADERS, "Authorization": "bearer " + access_token, "id_token": id_token, "Content-Type": "application/json"}
+ headers = {**HTTP_HEADERS, "Authorization": "bearer " + access_token, "id_token": id_token,
+ "Content-Type": "application/json"}
response = requests.post(self.SUBMIT_URL, json=body, headers=headers, timeout=(5, 30))
if not response.ok:
raise RuntimeError("SOTA API returned " + str(response.status_code) + ": " + response.text)
diff --git a/spotproviders/tiles.py b/spotproviders/tiles.py
index cfc6151..b80470b 100644
--- a/spotproviders/tiles.py
+++ b/spotproviders/tiles.py
@@ -14,7 +14,8 @@ class Tiles(HTTPSpotProvider):
POLL_INTERVAL_SEC = 120
SPOTS_URL = "https://icneuzxitdqtofutxbla.supabase.co/functions/v1/spots?active_hours=24"
SUBMIT_URL = "https://icneuzxitdqtofutxbla.supabase.co/functions/v1/self-spot"
- VALID_MODES = ["SSB", "CW", "FT8", "FT4", "FM", "DMR", "D-STAR", "M17", "AX.25", "JS8Call", "PSK31", "Olivia", "VarAC", "Other"]
+ VALID_MODES = ["SSB", "CW", "FT8", "FT4", "FM", "DMR", "D-STAR", "M17", "AX.25", "JS8Call", "PSK31", "Olivia",
+ "VarAC", "Other"]
def __init__(self, provider_config):
super().__init__(provider_config, self.SPOTS_URL, self.POLL_INTERVAL_SEC)
@@ -82,11 +83,13 @@ class Tiles(HTTPSpotProvider):
headers = {**HTTP_HEADERS, "Content-Type": "application/json"}
response = requests.post(self.SUBMIT_URL, json=body, headers=headers, timeout=(5, 30))
if not response.ok:
- raise RuntimeError("Tiles on the Air API returned " + str(response.status_code) + ": " + response.text)
+ raise RuntimeError(
+ "Tiles on the Air API returned " + str(response.status_code) + ": " + response.text)
else:
raise RuntimeError("The Tiles on the Air API requires a mode to be set.")
else:
- raise RuntimeError("The Tiles on the Air API only supports self-spots, the DX call and spotter call must match.")
+ raise RuntimeError(
+ "The Tiles on the Air API only supports self-spots, the DX call and spotter call must match.")
# Utility function to keep the first decimal point in a given string but remove any others. Used to parse Tiles'
diff --git a/webassets/apidocs/openapi.yml b/webassets/apidocs/openapi.yml
index 321b6da..0ab168a 100644
--- a/webassets/apidocs/openapi.yml
+++ b/webassets/apidocs/openapi.yml
@@ -1696,8 +1696,8 @@ components:
items:
type: string
example:
- POTA: [POTA]
- SOTA: [SOTA]
+ POTA: [ POTA ]
+ SOTA: [ SOTA, GMA, ParksNPeaks ]
CallLookup:
type: object
diff --git a/webassets/js/add-spot.js b/webassets/js/add-spot.js
index 9bf5a77..12d3586 100644
--- a/webassets/js/add-spot.js
+++ b/webassets/js/add-spot.js
@@ -1,24 +1,28 @@
// Credentials schema per provider name. Defines the fields to collect and how to label them.
-var PROVIDER_CREDENTIAL_SCHEMAS = {
+const PROVIDER_CREDENTIAL_SCHEMAS = {
// todo Figure out SOTA authentication
// see e.g. https://github.com/ham2k/app-polo/blob/main/src/extensions/activities/sota/SOTAAccount.jsx
// https://github.com/ham2k/app-polo/blob/main/src/store/apis/apiSOTA/apiSOTA.js
// Refresh token? Way to show user that they need to log in again because cached credentials aren't valid?
// todo type: text/password distinction on text boxes so API keys can be obscured
"SOTA": [
- { key: "access_token", label: "SOTA Access Token", help: "" },
- { key: "id_token", label: "SOTA ID Token", help: "TODO SOTA authentication to provide this..." }
+ {key: "access_token", label: "SOTA Access Token", help: ""},
+ {key: "id_token", label: "SOTA ID Token", help: "TODO SOTA authentication to provide this..."}
],
"ParksNPeaks": [
- { key: "user_id", label: "Parks N Peaks User ID", help: "" },
- { key: "api_key", label: "Parks N Peaks API Key", help: "Get your API key from your Parks N Peaks account." }
+ {key: "user_id", label: "Parks N Peaks User ID", help: ""},
+ {key: "api_key", label: "Parks N Peaks API Key", help: "Get your API key from your Parks N Peaks account."}
],
"ZLOTA": [
- { key: "user_id", label: "ZLOTA User ID", help: "" },
- { key: "api_key", label: "ZLOTA User PIN", help: "Get your PIN from your ZLOTA account." }
+ {key: "user_id", label: "ZLOTA User ID", help: ""},
+ {key: "api_key", label: "ZLOTA User PIN", help: "Get your PIN from your ZLOTA account."}
],
"Tiles": [
- { key: "offline_spot_gateway_pin", label: "Offline Spot Gateway PIN", help: "Get your PIN from your Tiles on the Air account profile." }
+ {
+ key: "offline_spot_gateway_pin",
+ label: "Offline Spot Gateway PIN",
+ help: "Get your PIN from your Tiles on the Air account profile."
+ }
]
};
@@ -62,7 +66,7 @@ function loadOptions() {
function loadRecaptcha(siteKey) {
window._recaptchaSiteKey = siteKey;
if (!document.getElementById('recaptcha-script')) {
- var script = document.createElement('script');
+ const script = document.createElement('script');
script.id = 'recaptcha-script';
script.src = 'https://www.google.com/recaptcha/api.js?render=explicit&onload=renderRecaptcha';
script.async = true;
@@ -87,8 +91,8 @@ function updateUpstreamArea() {
return;
}
- var sig = $("#sig").val();
- var providers = (sig && options["spot_submit_providers"][sig]) ? options["spot_submit_providers"][sig] : [];
+ const sig = $("#sig").val();
+ const providers = (sig && options["spot_submit_providers"][sig]) ? options["spot_submit_providers"][sig] : [];
if (providers.length === 0) {
$("#upstream-area").hide();
@@ -99,8 +103,8 @@ function updateUpstreamArea() {
// Update the provider selector
$("#upstream-provider-select").empty();
- $.each(providers, function(i, name) {
- $("#upstream-provider-select").append($('