mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-05-30 17:35:11 +00:00
Modify the backend so that instead of using the server owner's QRZ & HamQTH credentials, it instead requires them to be provided by the client (if none are provided, the lookups do not occur.)
This commit is contained in:
@@ -192,15 +192,6 @@ web-server-port: 8080
|
|||||||
max-spot-age-sec: 3600
|
max-spot-age-sec: 3600
|
||||||
max-alert-age-sec: 604800
|
max-alert-age-sec: 604800
|
||||||
|
|
||||||
# Login for QRZ.com to look up information. Optional. You will need an "XML Subscriber" (paid) package to retrieve all
|
|
||||||
# the data for a callsign via their system.
|
|
||||||
qrz-username: ""
|
|
||||||
qrz-password: ""
|
|
||||||
|
|
||||||
# Login for HamQTH to look up information. Optional.
|
|
||||||
hamqth-username: ""
|
|
||||||
hamqth-password: ""
|
|
||||||
|
|
||||||
# API key for Clublog to look up information. Optional. You sill need to request one via their helpdesk portal if you
|
# API key for Clublog to look up information. Optional. You sill need to request one via their helpdesk portal if you
|
||||||
# want to use callsign lookups from Clublog.
|
# want to use callsign lookups from Clublog.
|
||||||
clublog-api-key: ""
|
clublog-api-key: ""
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import re
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import requests
|
||||||
import xmltodict
|
import xmltodict
|
||||||
from diskcache import Cache
|
from diskcache import Cache
|
||||||
from pyhamtools import LookupLib, Callinfo, callinfo
|
from pyhamtools import LookupLib, Callinfo, callinfo
|
||||||
@@ -17,6 +18,38 @@ from core.cache_utils import SEMI_STATIC_URL_DATA_CACHE
|
|||||||
from core.config import config
|
from core.config import config
|
||||||
from core.constants import BANDS, UNKNOWN_BAND, CW_MODES, PHONE_MODES, DATA_MODES, ALL_MODES, \
|
from core.constants import BANDS, UNKNOWN_BAND, CW_MODES, PHONE_MODES, DATA_MODES, ALL_MODES, \
|
||||||
HTTP_HEADERS, HAMQTH_PRG, MODE_ALIASES
|
HTTP_HEADERS, HAMQTH_PRG, MODE_ALIASES
|
||||||
|
from data.lookup_credentials import LookupCredentials
|
||||||
|
|
||||||
|
# QRZ XML field names differ from pyhamtools' normalised names; map them here.
|
||||||
|
_QRZ_FIELD_MAP = {
|
||||||
|
"lat": "latitude",
|
||||||
|
"lon": "longitude",
|
||||||
|
"grid": "locator",
|
||||||
|
"ituzone": "ituz",
|
||||||
|
"cqzone": "cqz",
|
||||||
|
}
|
||||||
|
_QRZ_INT_FIELDS = {"adif", "cqz", "ituz"}
|
||||||
|
_QRZ_FLOAT_FIELDS = {"latitude", "longitude"}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_qrz_data(raw):
|
||||||
|
data = {}
|
||||||
|
for k, v in raw.items():
|
||||||
|
if v is None:
|
||||||
|
continue
|
||||||
|
mapped_key = _QRZ_FIELD_MAP.get(k, k)
|
||||||
|
if mapped_key in _QRZ_INT_FIELDS:
|
||||||
|
try:
|
||||||
|
v = int(v)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
elif mapped_key in _QRZ_FLOAT_FIELDS:
|
||||||
|
try:
|
||||||
|
v = float(v)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
data[mapped_key] = v
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class LookupHelper:
|
class LookupHelper:
|
||||||
@@ -36,9 +69,10 @@ class LookupHelper:
|
|||||||
self._clublog_cty_xml_cache = None
|
self._clublog_cty_xml_cache = None
|
||||||
self._clublog_api_key = None
|
self._clublog_api_key = None
|
||||||
self._qrz_callsign_data_cache = None
|
self._qrz_callsign_data_cache = None
|
||||||
self._lookup_lib_qrz = None
|
self._qrz_base_url = "https://xmldata.qrz.com/xml/current/"
|
||||||
self._qrz_available = None
|
# QRZ session keys expire after an hour; cache the login response for 55 minutes.
|
||||||
self._hamqth_available = None
|
self._qrz_session_cache = CachedSession("cache/qrz_session_cache",
|
||||||
|
expire_after=timedelta(minutes=55))
|
||||||
self._hamqth_callsign_data_cache = None
|
self._hamqth_callsign_data_cache = None
|
||||||
self._hamqth_base_url = "https://www.hamqth.com/xml.php"
|
self._hamqth_base_url = "https://www.hamqth.com/xml.php"
|
||||||
# HamQTH session keys expire after an hour. Rather than working out how much time has passed manually, we cheat
|
# HamQTH session keys expire after an hour. Rather than working out how much time has passed manually, we cheat
|
||||||
@@ -67,13 +101,8 @@ class LookupHelper:
|
|||||||
self._lookup_lib_basic = LookupLib(lookuptype="countryfile")
|
self._lookup_lib_basic = LookupLib(lookuptype="countryfile")
|
||||||
self._call_info_basic = Callinfo(self._lookup_lib_basic)
|
self._call_info_basic = Callinfo(self._lookup_lib_basic)
|
||||||
|
|
||||||
self._qrz_available = config["qrz-username"] != "" and config["qrz-password"] != ""
|
|
||||||
if self._qrz_available:
|
|
||||||
self._lookup_lib_qrz = LookupLib(lookuptype="qrz", username=config["qrz-username"],
|
|
||||||
pwd=config["qrz-password"])
|
|
||||||
self._qrz_callsign_data_cache = Cache('cache/qrz_callsign_lookup_cache')
|
self._qrz_callsign_data_cache = Cache('cache/qrz_callsign_lookup_cache')
|
||||||
|
|
||||||
self._hamqth_available = config["hamqth-username"] != "" and config["hamqth-password"] != ""
|
|
||||||
self._hamqth_callsign_data_cache = Cache('cache/hamqth_callsign_lookup_cache')
|
self._hamqth_callsign_data_cache = Cache('cache/hamqth_callsign_lookup_cache')
|
||||||
|
|
||||||
self._clublog_api_key = config["clublog-api-key"]
|
self._clublog_api_key = config["clublog-api-key"]
|
||||||
@@ -166,7 +195,7 @@ class LookupHelper:
|
|||||||
logging.error("Exception when downloading Clublog cty.xml", e)
|
logging.error("Exception when downloading Clublog cty.xml", e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def infer_country_from_callsign(self, call):
|
def infer_country_from_callsign(self, call, credentials=None):
|
||||||
"""Infer a country name from a callsign"""
|
"""Infer a country name from a callsign"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -176,12 +205,12 @@ class LookupHelper:
|
|||||||
country = None
|
country = None
|
||||||
# Couldn't get anything from basic call info database, try QRZ.com
|
# Couldn't get anything from basic call info database, try QRZ.com
|
||||||
if not country:
|
if not country:
|
||||||
qrz_data = self._get_qrz_data_for_callsign(call)
|
qrz_data = self._get_qrz_data_for_callsign(call, credentials)
|
||||||
if qrz_data and "country" in qrz_data:
|
if qrz_data and "country" in qrz_data:
|
||||||
country = qrz_data["country"]
|
country = qrz_data["country"]
|
||||||
# Couldn't get anything from QRZ.com database, try HamQTH
|
# Couldn't get anything from QRZ.com database, try HamQTH
|
||||||
if not country:
|
if not country:
|
||||||
hamqth_data = self._get_hamqth_data_for_callsign(call)
|
hamqth_data = self._get_hamqth_data_for_callsign(call, credentials)
|
||||||
if hamqth_data and "country" in hamqth_data:
|
if hamqth_data and "country" in hamqth_data:
|
||||||
country = hamqth_data["country"]
|
country = hamqth_data["country"]
|
||||||
# Couldn't get anything from HamQTH database, try Clublog data
|
# Couldn't get anything from HamQTH database, try Clublog data
|
||||||
@@ -200,7 +229,7 @@ class LookupHelper:
|
|||||||
country = dxcc_data["name"]
|
country = dxcc_data["name"]
|
||||||
return country
|
return country
|
||||||
|
|
||||||
def infer_dxcc_id_from_callsign(self, call):
|
def infer_dxcc_id_from_callsign(self, call, credentials=None):
|
||||||
"""Infer a DXCC ID from a callsign"""
|
"""Infer a DXCC ID from a callsign"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -210,12 +239,12 @@ class LookupHelper:
|
|||||||
dxcc = None
|
dxcc = None
|
||||||
# Couldn't get anything from basic call info database, try QRZ.com
|
# Couldn't get anything from basic call info database, try QRZ.com
|
||||||
if not dxcc:
|
if not dxcc:
|
||||||
qrz_data = self._get_qrz_data_for_callsign(call)
|
qrz_data = self._get_qrz_data_for_callsign(call, credentials)
|
||||||
if qrz_data and "adif" in qrz_data:
|
if qrz_data and "adif" in qrz_data:
|
||||||
dxcc = qrz_data["adif"]
|
dxcc = qrz_data["adif"]
|
||||||
# Couldn't get anything from QRZ.com database, try HamQTH
|
# Couldn't get anything from QRZ.com database, try HamQTH
|
||||||
if not dxcc:
|
if not dxcc:
|
||||||
hamqth_data = self._get_hamqth_data_for_callsign(call)
|
hamqth_data = self._get_hamqth_data_for_callsign(call, credentials)
|
||||||
if hamqth_data and "adif" in hamqth_data:
|
if hamqth_data and "adif" in hamqth_data:
|
||||||
dxcc = hamqth_data["adif"]
|
dxcc = hamqth_data["adif"]
|
||||||
# Couldn't get anything from HamQTH database, try Clublog data
|
# Couldn't get anything from HamQTH database, try Clublog data
|
||||||
@@ -234,7 +263,7 @@ class LookupHelper:
|
|||||||
dxcc = dxcc_data["entityCode"]
|
dxcc = dxcc_data["entityCode"]
|
||||||
return dxcc
|
return dxcc
|
||||||
|
|
||||||
def infer_continent_from_callsign(self, call):
|
def infer_continent_from_callsign(self, call, credentials=None):
|
||||||
"""Infer a continent shortcode from a callsign"""
|
"""Infer a continent shortcode from a callsign"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -244,7 +273,7 @@ class LookupHelper:
|
|||||||
continent = None
|
continent = None
|
||||||
# Couldn't get anything from basic call info database, try HamQTH
|
# Couldn't get anything from basic call info database, try HamQTH
|
||||||
if not continent:
|
if not continent:
|
||||||
hamqth_data = self._get_hamqth_data_for_callsign(call)
|
hamqth_data = self._get_hamqth_data_for_callsign(call, credentials)
|
||||||
if hamqth_data and "continent" in hamqth_data:
|
if hamqth_data and "continent" in hamqth_data:
|
||||||
continent = hamqth_data["continent"]
|
continent = hamqth_data["continent"]
|
||||||
# Couldn't get anything from HamQTH database, try Clublog data
|
# Couldn't get anything from HamQTH database, try Clublog data
|
||||||
@@ -264,7 +293,7 @@ class LookupHelper:
|
|||||||
continent = dxcc_data["continent"][0]
|
continent = dxcc_data["continent"][0]
|
||||||
return continent
|
return continent
|
||||||
|
|
||||||
def infer_cq_zone_from_callsign(self, call):
|
def infer_cq_zone_from_callsign(self, call, credentials=None):
|
||||||
"""Infer a CQ zone from a callsign"""
|
"""Infer a CQ zone from a callsign"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -274,12 +303,12 @@ class LookupHelper:
|
|||||||
cqz = None
|
cqz = None
|
||||||
# Couldn't get anything from basic call info database, try QRZ.com
|
# Couldn't get anything from basic call info database, try QRZ.com
|
||||||
if not cqz:
|
if not cqz:
|
||||||
qrz_data = self._get_qrz_data_for_callsign(call)
|
qrz_data = self._get_qrz_data_for_callsign(call, credentials)
|
||||||
if qrz_data and "cqz" in qrz_data:
|
if qrz_data and "cqz" in qrz_data:
|
||||||
cqz = qrz_data["cqz"]
|
cqz = qrz_data["cqz"]
|
||||||
# Couldn't get anything from QRZ.com database, try HamQTH
|
# Couldn't get anything from QRZ.com database, try HamQTH
|
||||||
if not cqz:
|
if not cqz:
|
||||||
hamqth_data = self._get_hamqth_data_for_callsign(call)
|
hamqth_data = self._get_hamqth_data_for_callsign(call, credentials)
|
||||||
if hamqth_data and "cq" in hamqth_data:
|
if hamqth_data and "cq" in hamqth_data:
|
||||||
cqz = hamqth_data["cq"]
|
cqz = hamqth_data["cq"]
|
||||||
# Couldn't get anything from HamQTH database, try Clublog data
|
# Couldn't get anything from HamQTH database, try Clublog data
|
||||||
@@ -299,7 +328,7 @@ class LookupHelper:
|
|||||||
cqz = dxcc_data["cq"][0]
|
cqz = dxcc_data["cq"][0]
|
||||||
return cqz
|
return cqz
|
||||||
|
|
||||||
def infer_itu_zone_from_callsign(self, call):
|
def infer_itu_zone_from_callsign(self, call, credentials=None):
|
||||||
"""Infer a ITU zone from a callsign"""
|
"""Infer a ITU zone from a callsign"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -309,12 +338,12 @@ class LookupHelper:
|
|||||||
ituz = None
|
ituz = None
|
||||||
# Couldn't get anything from basic call info database, try QRZ.com
|
# Couldn't get anything from basic call info database, try QRZ.com
|
||||||
if not ituz:
|
if not ituz:
|
||||||
qrz_data = self._get_qrz_data_for_callsign(call)
|
qrz_data = self._get_qrz_data_for_callsign(call, credentials)
|
||||||
if qrz_data and "ituz" in qrz_data:
|
if qrz_data and "ituz" in qrz_data:
|
||||||
ituz = qrz_data["ituz"]
|
ituz = qrz_data["ituz"]
|
||||||
# Couldn't get anything from QRZ.com database, try HamQTH
|
# Couldn't get anything from QRZ.com database, try HamQTH
|
||||||
if not ituz:
|
if not ituz:
|
||||||
hamqth_data = self._get_hamqth_data_for_callsign(call)
|
hamqth_data = self._get_hamqth_data_for_callsign(call, credentials)
|
||||||
if hamqth_data and "itu" in hamqth_data:
|
if hamqth_data and "itu" in hamqth_data:
|
||||||
ituz = hamqth_data["itu"]
|
ituz = hamqth_data["itu"]
|
||||||
# Couldn't get anything from HamQTH database, Clublog doesn't provide this, so try DXCC data
|
# Couldn't get anything from HamQTH database, Clublog doesn't provide this, so try DXCC data
|
||||||
@@ -330,31 +359,31 @@ class LookupHelper:
|
|||||||
|
|
||||||
return self._dxcc_data[dxcc]["flag"] if dxcc in self._dxcc_data else None
|
return self._dxcc_data[dxcc]["flag"] if dxcc in self._dxcc_data else None
|
||||||
|
|
||||||
def infer_name_from_callsign_online_lookup(self, call):
|
def infer_name_from_callsign_online_lookup(self, call, credentials=None):
|
||||||
"""Infer an operator name from a callsign (requires QRZ.com/HamQTH)"""
|
"""Infer an operator name from a callsign (requires QRZ.com/HamQTH)"""
|
||||||
|
|
||||||
data = self._get_qrz_data_for_callsign(call)
|
data = self._get_qrz_data_for_callsign(call, credentials)
|
||||||
if data and "fname" in data:
|
if data and "fname" in data:
|
||||||
name = data["fname"]
|
name = data["fname"]
|
||||||
if "name" in data:
|
if "name" in data:
|
||||||
name = name + " " + data["name"]
|
name = name + " " + data["name"]
|
||||||
return name
|
return name
|
||||||
data = self._get_hamqth_data_for_callsign(call)
|
data = self._get_hamqth_data_for_callsign(call, credentials)
|
||||||
if data and "nick" in data:
|
if data and "nick" in data:
|
||||||
return data["nick"]
|
return data["nick"]
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def infer_latlon_from_callsign_online_lookup(self, call):
|
def infer_latlon_from_callsign_online_lookup(self, call, credentials=None):
|
||||||
"""Infer a latitude and longitude from a callsign (requires QRZ.com/HamQTH)
|
"""Infer a latitude and longitude from a callsign (requires QRZ.com/HamQTH)
|
||||||
Coordinates that look default are rejected (apologies if your position really is 0,0, enjoy your voyage)"""
|
Coordinates that look default are rejected (apologies if your position really is 0,0, enjoy your voyage)"""
|
||||||
|
|
||||||
data = self._get_qrz_data_for_callsign(call)
|
data = self._get_qrz_data_for_callsign(call, credentials)
|
||||||
if data and "latitude" in data and "longitude" in data and (
|
if data and "latitude" in data and "longitude" in data and (
|
||||||
float(data["latitude"]) != 0 or float(data["longitude"]) != 0) and -89.9 < float(
|
float(data["latitude"]) != 0 or float(data["longitude"]) != 0) and -89.9 < float(
|
||||||
data["latitude"]) < 89.9:
|
data["latitude"]) < 89.9:
|
||||||
return [float(data["latitude"]), float(data["longitude"])]
|
return [float(data["latitude"]), float(data["longitude"])]
|
||||||
data = self._get_hamqth_data_for_callsign(call)
|
data = self._get_hamqth_data_for_callsign(call, credentials)
|
||||||
if data and "latitude" in data and "longitude" in data and (
|
if data and "latitude" in data and "longitude" in data and (
|
||||||
float(data["latitude"]) != 0 or float(data["longitude"]) != 0) and -89.9 < float(
|
float(data["latitude"]) != 0 or float(data["longitude"]) != 0) and -89.9 < float(
|
||||||
data["latitude"]) < 89.9:
|
data["latitude"]) < 89.9:
|
||||||
@@ -362,28 +391,28 @@ class LookupHelper:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def infer_grid_from_callsign_online_lookup(self, call):
|
def infer_grid_from_callsign_online_lookup(self, call, credentials=None):
|
||||||
"""Infer a grid locator from a callsign (requires QRZ.com/HamQTH).
|
"""Infer a grid locator from a callsign (requires QRZ.com/HamQTH).
|
||||||
Grids that look default are rejected (apologies if your grid really is AA00aa, enjoy your research)"""
|
Grids that look default are rejected (apologies if your grid really is AA00aa, enjoy your research)"""
|
||||||
|
|
||||||
data = self._get_qrz_data_for_callsign(call)
|
data = self._get_qrz_data_for_callsign(call, credentials)
|
||||||
if data and "locator" in data and data["locator"].upper() != "AA00" and data["locator"].upper() != "AA00AA" and \
|
if data and "locator" in data and data["locator"].upper() != "AA00" and data["locator"].upper() != "AA00AA" and \
|
||||||
data["locator"].upper() != "AA00AA00":
|
data["locator"].upper() != "AA00AA00":
|
||||||
return data["locator"]
|
return data["locator"]
|
||||||
data = self._get_hamqth_data_for_callsign(call)
|
data = self._get_hamqth_data_for_callsign(call, credentials)
|
||||||
if data and "grid" in data and data["grid"].upper() != "AA00" and data["grid"].upper() != "AA00AA" and data[
|
if data and "grid" in data and data["grid"].upper() != "AA00" and data["grid"].upper() != "AA00AA" and data[
|
||||||
"grid"].upper() != "AA00AA00":
|
"grid"].upper() != "AA00AA00":
|
||||||
return data["grid"]
|
return data["grid"]
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def infer_qth_from_callsign_online_lookup(self, call):
|
def infer_qth_from_callsign_online_lookup(self, call, credentials=None):
|
||||||
"""Infer a textual QTH from a callsign (requires QRZ.com/HamQTH)"""
|
"""Infer a textual QTH from a callsign (requires QRZ.com/HamQTH)"""
|
||||||
|
|
||||||
data = self._get_qrz_data_for_callsign(call)
|
data = self._get_qrz_data_for_callsign(call, credentials)
|
||||||
if data and "addr2" in data:
|
if data and "addr2" in data:
|
||||||
return data["addr2"]
|
return data["addr2"]
|
||||||
data = self._get_hamqth_data_for_callsign(call)
|
data = self._get_hamqth_data_for_callsign(call, credentials)
|
||||||
if data and "qth" in data:
|
if data and "qth" in data:
|
||||||
return data["qth"]
|
return data["qth"]
|
||||||
else:
|
else:
|
||||||
@@ -422,79 +451,116 @@ class LookupHelper:
|
|||||||
logging.debug("Invalid lat/lon received for DXCC")
|
logging.debug("Invalid lat/lon received for DXCC")
|
||||||
return grid
|
return grid
|
||||||
|
|
||||||
def _get_qrz_data_for_callsign(self, call):
|
def _get_qrz_data_for_callsign(self, call, credentials):
|
||||||
"""Utility method to get QRZ.com data from cache if possible, if not get it from the API and cache it"""
|
"""Utility method to get QRZ.com data from cache if possible, if not get it from the API and cache it.
|
||||||
|
Returns None immediately if no credentials are provided."""
|
||||||
|
|
||||||
# Fetch from cache if we can, otherwise fetch from the API and cache it
|
# Return from cache if available (a cached None means 'not found in QRZ')
|
||||||
if call in self._qrz_callsign_data_cache:
|
if call in self._qrz_callsign_data_cache:
|
||||||
return self._qrz_callsign_data_cache.get(call)
|
return self._qrz_callsign_data_cache.get(call)
|
||||||
elif self._qrz_available:
|
|
||||||
|
# Obtain session key from credentials
|
||||||
|
session_key = None
|
||||||
|
if credentials and credentials.qrz_session_key:
|
||||||
|
session_key = credentials.qrz_session_key
|
||||||
|
elif credentials and credentials.qrz_username and credentials.qrz_password:
|
||||||
try:
|
try:
|
||||||
data = self._lookup_lib_qrz.lookup_callsign(callsign=call)
|
login_response = self._qrz_session_cache.get(
|
||||||
self._qrz_callsign_data_cache.add(call, data, expire=604800) # 1 week in seconds
|
self._qrz_base_url + "?username=" + urllib.parse.quote_plus(credentials.qrz_username) +
|
||||||
return data
|
"&password=" + urllib.parse.quote_plus(credentials.qrz_password) + "&agent=spothole",
|
||||||
except (KeyError, ValueError):
|
headers=HTTP_HEADERS).content
|
||||||
# QRZ had no info for the call, but maybe it had prefixes or suffixes. Try again with the base call.
|
login_data = xmltodict.parse(login_response)
|
||||||
try:
|
session = login_data.get("QRZDatabase", {}).get("Session", {})
|
||||||
data = self._lookup_lib_qrz.lookup_callsign(callsign=callinfo.Callinfo.get_homecall(call))
|
if "Key" in session:
|
||||||
self._qrz_callsign_data_cache.add(call, data, expire=604800) # 1 week in seconds
|
session_key = session["Key"]
|
||||||
return data
|
else:
|
||||||
except (KeyError, ValueError):
|
logging.warning("QRZ.com login details incorrect, failed to look up with QRZ.")
|
||||||
# QRZ had no info for the call, that's OK. Cache a None so we don't try to look this up again
|
|
||||||
self._qrz_callsign_data_cache.add(call, None, expire=604800) # 1 week in seconds
|
|
||||||
return None
|
return None
|
||||||
except Exception:
|
except Exception:
|
||||||
# General exception like a timeout when communicating with QRZ. Return None this time, but don't cache
|
logging.error("Exception when getting QRZ.com session key")
|
||||||
# that, so we can try again next time.
|
|
||||||
logging.error("Exception when looking up QRZ data")
|
|
||||||
return None
|
return None
|
||||||
else:
|
|
||||||
|
if not session_key:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _get_hamqth_data_for_callsign(self, call):
|
# Try the call as given, then fall back to the base call (strips /P, /M etc.)
|
||||||
"""Utility method to get HamQTH data from cache if possible, if not get it from the API and cache it"""
|
calls_to_try = [call]
|
||||||
|
home_call = callinfo.Callinfo.get_homecall(call)
|
||||||
|
if home_call != call:
|
||||||
|
calls_to_try.append(home_call)
|
||||||
|
|
||||||
# Fetch from cache if we can, otherwise fetch from the API and cache it
|
for lookup_call in calls_to_try:
|
||||||
|
try:
|
||||||
|
lookup_response = requests.get(
|
||||||
|
self._qrz_base_url + "?s=" + session_key + "&callsign=" + urllib.parse.quote_plus(lookup_call),
|
||||||
|
headers=HTTP_HEADERS, timeout=10).content
|
||||||
|
raw = xmltodict.parse(lookup_response).get("QRZDatabase", {}).get("Callsign")
|
||||||
|
if raw:
|
||||||
|
data = _normalize_qrz_data(raw)
|
||||||
|
self._qrz_callsign_data_cache.add(call, data, expire=604800) # 1 week in seconds
|
||||||
|
return data
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
logging.error("Exception when looking up QRZ data")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Not found in QRZ; cache None so we don't keep retrying
|
||||||
|
self._qrz_callsign_data_cache.add(call, None, expire=604800) # 1 week in seconds
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_hamqth_data_for_callsign(self, call, credentials):
|
||||||
|
"""Utility method to get HamQTH data from cache if possible, if not get it from the API and cache it.
|
||||||
|
Returns None immediately if no credentials are provided."""
|
||||||
|
|
||||||
|
# Return from cache if available
|
||||||
if call in self._hamqth_callsign_data_cache:
|
if call in self._hamqth_callsign_data_cache:
|
||||||
return self._hamqth_callsign_data_cache.get(call)
|
return self._hamqth_callsign_data_cache.get(call)
|
||||||
elif self._hamqth_available:
|
|
||||||
|
# Obtain session ID from credentials
|
||||||
|
session_id = None
|
||||||
|
if credentials and credentials.hamqth_session_id:
|
||||||
|
session_id = credentials.hamqth_session_id
|
||||||
|
elif credentials and credentials.hamqth_username and credentials.hamqth_password:
|
||||||
try:
|
try:
|
||||||
# First we need to log in and get a session token.
|
|
||||||
session_data = self._hamqth_session_lookup_cache.get(
|
session_data = self._hamqth_session_lookup_cache.get(
|
||||||
self._hamqth_base_url + "?u=" + urllib.parse.quote_plus(config["hamqth-username"]) +
|
self._hamqth_base_url + "?u=" + urllib.parse.quote_plus(credentials.hamqth_username) +
|
||||||
"&p=" + urllib.parse.quote_plus(config["hamqth-password"]), headers=HTTP_HEADERS).content
|
"&p=" + urllib.parse.quote_plus(credentials.hamqth_password), headers=HTTP_HEADERS).content
|
||||||
dict_data = xmltodict.parse(session_data)
|
dict_data = xmltodict.parse(session_data)
|
||||||
if "session_id" in dict_data["HamQTH"]["session"]:
|
if "session_id" in dict_data["HamQTH"]["session"]:
|
||||||
session_id = dict_data["HamQTH"]["session"]["session_id"]
|
session_id = dict_data["HamQTH"]["session"]["session_id"]
|
||||||
|
|
||||||
# Now look up the actual data.
|
|
||||||
try:
|
|
||||||
lookup_data = SEMI_STATIC_URL_DATA_CACHE.get(
|
|
||||||
self._hamqth_base_url + "?id=" + session_id + "&callsign=" + urllib.parse.quote_plus(
|
|
||||||
call) + "&prg=" + HAMQTH_PRG, headers=HTTP_HEADERS).content
|
|
||||||
data = xmltodict.parse(lookup_data)["HamQTH"]["search"]
|
|
||||||
self._hamqth_callsign_data_cache.add(call, data, expire=604800) # 1 week in seconds
|
|
||||||
return data
|
|
||||||
except (KeyError, ValueError):
|
|
||||||
# HamQTH had no info for the call, but maybe it had prefixes or suffixes. Try again with the base call.
|
|
||||||
try:
|
|
||||||
lookup_data = SEMI_STATIC_URL_DATA_CACHE.get(
|
|
||||||
self._hamqth_base_url + "?id=" + session_id + "&callsign=" + urllib.parse.quote_plus(
|
|
||||||
callinfo.Callinfo.get_homecall(call)) + "&prg=" + HAMQTH_PRG,
|
|
||||||
headers=HTTP_HEADERS).content
|
|
||||||
data = xmltodict.parse(lookup_data)["HamQTH"]["search"]
|
|
||||||
self._hamqth_callsign_data_cache.add(call, data, expire=604800) # 1 week in seconds
|
|
||||||
return data
|
|
||||||
except (KeyError, ValueError):
|
|
||||||
# HamQTH had no info for the call, that's OK. Cache a None so we don't try to look this up again
|
|
||||||
self._hamqth_callsign_data_cache.add(call, None, expire=604800) # 1 week in seconds
|
|
||||||
return None
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logging.warning("HamQTH login details incorrect, failed to look up with HamQTH.")
|
logging.warning("HamQTH login details incorrect, failed to look up with HamQTH.")
|
||||||
except:
|
return None
|
||||||
|
except Exception:
|
||||||
|
logging.error("Exception when getting HamQTH session ID")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not session_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Try the call as given, then fall back to the base call (strips /P, /M etc.)
|
||||||
|
calls_to_try = [call]
|
||||||
|
home_call = callinfo.Callinfo.get_homecall(call)
|
||||||
|
if home_call != call:
|
||||||
|
calls_to_try.append(home_call)
|
||||||
|
|
||||||
|
for lookup_call in calls_to_try:
|
||||||
|
try:
|
||||||
|
lookup_data = SEMI_STATIC_URL_DATA_CACHE.get(
|
||||||
|
self._hamqth_base_url + "?id=" + session_id + "&callsign=" + urllib.parse.quote_plus(
|
||||||
|
lookup_call) + "&prg=" + HAMQTH_PRG, headers=HTTP_HEADERS).content
|
||||||
|
data = xmltodict.parse(lookup_data)["HamQTH"]["search"]
|
||||||
|
self._hamqth_callsign_data_cache.add(call, data, expire=604800) # 1 week in seconds
|
||||||
|
return data
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
logging.error("Exception when looking up HamQTH data")
|
logging.error("Exception when looking up HamQTH data")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Not found in HamQTH; cache None so we don't keep retrying
|
||||||
|
self._hamqth_callsign_data_cache.add(call, None, expire=604800) # 1 week in seconds
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _get_clublog_api_data_for_callsign(self, call):
|
def _get_clublog_api_data_for_callsign(self, call):
|
||||||
@@ -551,6 +617,7 @@ class LookupHelper:
|
|||||||
"""Shutdown method to close down any caches neatly."""
|
"""Shutdown method to close down any caches neatly."""
|
||||||
|
|
||||||
self._qrz_callsign_data_cache.close()
|
self._qrz_callsign_data_cache.close()
|
||||||
|
self._hamqth_callsign_data_cache.close()
|
||||||
self._clublog_callsign_data_cache.close()
|
self._clublog_callsign_data_cache.close()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class Alert:
|
|||||||
# The ID the source gave it, if any.
|
# The ID the source gave it, if any.
|
||||||
source_id: str = None
|
source_id: str = None
|
||||||
|
|
||||||
def infer_missing(self):
|
def infer_missing(self, credentials=None):
|
||||||
"""Infer missing parameters where possible"""
|
"""Infer missing parameters where possible"""
|
||||||
|
|
||||||
# If we somehow don't have a start time, set it to zero so it sorts off the bottom of any list but
|
# If we somehow don't have a start time, set it to zero so it sorts off the bottom of any list but
|
||||||
@@ -84,15 +84,15 @@ class Alert:
|
|||||||
# DX country, continent, zones etc. from callsign. CQ/ITU zone are better looked up with a location but we don't
|
# DX country, continent, zones etc. from callsign. CQ/ITU zone are better looked up with a location but we don't
|
||||||
# have a real location for alerts.
|
# have a real location for alerts.
|
||||||
if self.dx_calls and self.dx_calls[0] and not self.dx_country:
|
if self.dx_calls and self.dx_calls[0] and not self.dx_country:
|
||||||
self.dx_country = lookup_helper.infer_country_from_callsign(self.dx_calls[0])
|
self.dx_country = lookup_helper.infer_country_from_callsign(self.dx_calls[0], credentials)
|
||||||
if self.dx_calls and self.dx_calls[0] and not self.dx_continent:
|
if self.dx_calls and self.dx_calls[0] and not self.dx_continent:
|
||||||
self.dx_continent = lookup_helper.infer_continent_from_callsign(self.dx_calls[0])
|
self.dx_continent = lookup_helper.infer_continent_from_callsign(self.dx_calls[0], credentials)
|
||||||
if self.dx_calls and self.dx_calls[0] and not self.dx_cq_zone:
|
if self.dx_calls and self.dx_calls[0] and not self.dx_cq_zone:
|
||||||
self.dx_cq_zone = lookup_helper.infer_cq_zone_from_callsign(self.dx_calls[0])
|
self.dx_cq_zone = lookup_helper.infer_cq_zone_from_callsign(self.dx_calls[0], credentials)
|
||||||
if self.dx_calls and self.dx_calls[0] and not self.dx_itu_zone:
|
if self.dx_calls and self.dx_calls[0] and not self.dx_itu_zone:
|
||||||
self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_calls[0])
|
self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_calls[0], credentials)
|
||||||
if self.dx_calls and self.dx_calls[0] and not self.dx_dxcc_id:
|
if self.dx_calls and self.dx_calls[0] and not self.dx_dxcc_id:
|
||||||
self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_calls[0])
|
self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_calls[0], credentials)
|
||||||
if self.dx_dxcc_id and not self.dx_flag:
|
if self.dx_dxcc_id and not self.dx_flag:
|
||||||
self.dx_flag = lookup_helper.get_flag_for_dxcc(self.dx_dxcc_id)
|
self.dx_flag = lookup_helper.get_flag_for_dxcc(self.dx_dxcc_id)
|
||||||
|
|
||||||
@@ -108,21 +108,25 @@ class Alert:
|
|||||||
if self.sig_refs and len(self.sig_refs) > 0 and self.sig_refs[0] and not self.sig:
|
if self.sig_refs and len(self.sig_refs) > 0 and self.sig_refs[0] and not self.sig:
|
||||||
self.sig = self.sig_refs[0].sig
|
self.sig = self.sig_refs[0].sig
|
||||||
|
|
||||||
# DX operator details lookup, using QRZ.com. This should be the last resort compared to taking the data from
|
|
||||||
# the actual alertting service, e.g. we don't want to accidentally use a user's QRZ.com home lat/lon instead of
|
|
||||||
# the one from the park reference they're at.
|
|
||||||
if self.dx_calls and not self.dx_names:
|
|
||||||
self.dx_names = list(map(lambda c: lookup_helper.infer_name_from_callsign_online_lookup(c), self.dx_calls))
|
|
||||||
|
|
||||||
# Always create an ID based on a hash of every parameter *except* received_time. This is used as the index
|
# Always create an ID based on a hash of every parameter *except* received_time. This is used as the index
|
||||||
# to a map, which as a byproduct avoids us having multiple duplicate copies of the object that are identical
|
# to a map, which as a byproduct avoids us having multiple duplicate copies of the object that are identical
|
||||||
# apart from that they were retrieved from the API at different times. Note that the simple Python hash()
|
# apart from that they were retrieved from the API at different times. Note that the simple Python hash()
|
||||||
# function includes a seed randomly generated at runtime; this is therefore not consistent between runs. But we
|
# function includes a seed randomly generated at runtime; this is therefore not consistent between runs. But we
|
||||||
# use diskcache to store our data between runs, so we use SHA256 which does not include this random element.
|
# use diskcache to store our data between runs, so we use SHA256 which does not include this random element.
|
||||||
self_copy = copy.deepcopy(self)
|
# The ID is computed before the online lookups below so that it is stable regardless of whether credentials
|
||||||
self_copy.received_time = 0
|
# are provided, allowing the enriched API response to be matched to the stored alert by ID.
|
||||||
self_copy.received_time_iso = ""
|
if not self.id:
|
||||||
self.id = hashlib.sha256(str(self_copy).encode("utf-8")).hexdigest()
|
self_copy = copy.deepcopy(self)
|
||||||
|
self_copy.received_time = 0
|
||||||
|
self_copy.received_time_iso = ""
|
||||||
|
self.id = hashlib.sha256(str(self_copy).encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
# DX operator details lookup, using QRZ.com/HamQTH. This should be the last resort compared to taking the data
|
||||||
|
# from the actual alerting service, e.g. we don't want to accidentally use a user's QRZ.com home lat/lon
|
||||||
|
# instead of the one from the park reference they're at.
|
||||||
|
if self.dx_calls and not self.dx_names:
|
||||||
|
self.dx_names = list(
|
||||||
|
map(lambda c: lookup_helper.infer_name_from_callsign_online_lookup(c, credentials), self.dx_calls))
|
||||||
|
|
||||||
def to_json(self):
|
def to_json(self):
|
||||||
"""JSON serialise"""
|
"""JSON serialise"""
|
||||||
|
|||||||
27
data/lookup_credentials.py
Normal file
27
data/lookup_credentials.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LookupCredentials:
|
||||||
|
"""Per-request credentials for QRZ.com and HamQTH online callsign lookups."""
|
||||||
|
qrz_username: str = ""
|
||||||
|
qrz_password: str = ""
|
||||||
|
qrz_session_key: str = "" # alternative to username/password
|
||||||
|
hamqth_username: str = ""
|
||||||
|
hamqth_password: str = ""
|
||||||
|
hamqth_session_id: str = "" # alternative to username/password
|
||||||
|
|
||||||
|
|
||||||
|
def extract_credentials(query_params):
|
||||||
|
"""Build a LookupCredentials from HTTP query params; returns None if no usable credentials are present."""
|
||||||
|
creds = LookupCredentials(
|
||||||
|
qrz_username=query_params.get("qrz_username", ""),
|
||||||
|
qrz_password=query_params.get("qrz_password", ""),
|
||||||
|
qrz_session_key=query_params.get("qrz_session_key", ""),
|
||||||
|
hamqth_username=query_params.get("hamqth_username", ""),
|
||||||
|
hamqth_password=query_params.get("hamqth_password", ""),
|
||||||
|
hamqth_session_id=query_params.get("hamqth_session_id", ""),
|
||||||
|
)
|
||||||
|
has_qrz = creds.qrz_session_key or (creds.qrz_username and creds.qrz_password)
|
||||||
|
has_hamqth = creds.hamqth_session_id or (creds.hamqth_username and creds.hamqth_password)
|
||||||
|
return creds if (has_qrz or has_hamqth) else None
|
||||||
68
data/spot.py
68
data/spot.py
@@ -12,8 +12,9 @@ from pyhamtools.locator import locator_to_latlong, latlong_to_locator
|
|||||||
from core.config import MAX_SPOT_AGE
|
from core.config import MAX_SPOT_AGE
|
||||||
from core.constants import MODE_ALIASES
|
from core.constants import MODE_ALIASES
|
||||||
from core.geo_utils import lat_lon_to_cq_zone, lat_lon_to_itu_zone
|
from core.geo_utils import lat_lon_to_cq_zone, lat_lon_to_itu_zone
|
||||||
from core.lookup_helper import lookup_helper, infer_band_from_freq, infer_mode_from_comment, infer_mode_from_frequency, \
|
from core.lookup_helper import lookup_helper, infer_band_from_freq, infer_mode_from_comment, \
|
||||||
infer_mode_type_from_mode
|
infer_mode_from_frequency, infer_mode_type_from_mode
|
||||||
|
from data.lookup_credentials import LookupCredentials
|
||||||
from core.sig_utils import populate_sig_ref_info, ANY_SIG_REGEX, get_ref_regex_for_sig
|
from core.sig_utils import populate_sig_ref_info, ANY_SIG_REGEX, get_ref_regex_for_sig
|
||||||
from data.sig_ref import SIGRef
|
from data.sig_ref import SIGRef
|
||||||
|
|
||||||
@@ -131,7 +132,7 @@ class Spot:
|
|||||||
# The ID the source gave it, if any.
|
# The ID the source gave it, if any.
|
||||||
source_id: str = None
|
source_id: str = None
|
||||||
|
|
||||||
def infer_missing(self):
|
def infer_missing(self, credentials=None):
|
||||||
"""Infer missing parameters where possible"""
|
"""Infer missing parameters where possible"""
|
||||||
|
|
||||||
# If we somehow don't have a spot time, set it to zero so it sorts off the bottom of any list but
|
# If we somehow don't have a spot time, set it to zero so it sorts off the bottom of any list but
|
||||||
@@ -158,11 +159,11 @@ class Spot:
|
|||||||
|
|
||||||
# DX country, continent etc. from callsign
|
# DX country, continent etc. from callsign
|
||||||
if self.dx_call and not self.dx_country:
|
if self.dx_call and not self.dx_country:
|
||||||
self.dx_country = lookup_helper.infer_country_from_callsign(self.dx_call)
|
self.dx_country = lookup_helper.infer_country_from_callsign(self.dx_call, credentials)
|
||||||
if self.dx_call and not self.dx_continent:
|
if self.dx_call and not self.dx_continent:
|
||||||
self.dx_continent = lookup_helper.infer_continent_from_callsign(self.dx_call)
|
self.dx_continent = lookup_helper.infer_continent_from_callsign(self.dx_call, credentials)
|
||||||
if self.dx_call and not self.dx_dxcc_id:
|
if self.dx_call and not self.dx_dxcc_id:
|
||||||
self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_call)
|
self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_call, credentials)
|
||||||
if self.dx_dxcc_id and not self.dx_flag:
|
if self.dx_dxcc_id and not self.dx_flag:
|
||||||
self.dx_flag = lookup_helper.get_flag_for_dxcc(self.dx_dxcc_id)
|
self.dx_flag = lookup_helper.get_flag_for_dxcc(self.dx_dxcc_id)
|
||||||
|
|
||||||
@@ -192,11 +193,11 @@ class Spot:
|
|||||||
if self.de_call and any(char.isdigit() for char in self.de_call) and not (
|
if self.de_call and any(char.isdigit() for char in self.de_call) and not (
|
||||||
self.de_call.startswith("T2") and self.source == "APRS-IS"):
|
self.de_call.startswith("T2") and self.source == "APRS-IS"):
|
||||||
if not self.de_country:
|
if not self.de_country:
|
||||||
self.de_country = lookup_helper.infer_country_from_callsign(self.de_call)
|
self.de_country = lookup_helper.infer_country_from_callsign(self.de_call, credentials)
|
||||||
if not self.de_continent:
|
if not self.de_continent:
|
||||||
self.de_continent = lookup_helper.infer_continent_from_callsign(self.de_call)
|
self.de_continent = lookup_helper.infer_continent_from_callsign(self.de_call, credentials)
|
||||||
if not self.de_dxcc_id:
|
if not self.de_dxcc_id:
|
||||||
self.de_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.de_call)
|
self.de_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.de_call, credentials)
|
||||||
if self.de_dxcc_id and not self.de_flag:
|
if self.de_dxcc_id and not self.de_flag:
|
||||||
self.de_flag = lookup_helper.get_flag_for_dxcc(self.de_dxcc_id)
|
self.de_flag = lookup_helper.get_flag_for_dxcc(self.de_dxcc_id)
|
||||||
|
|
||||||
@@ -306,27 +307,40 @@ class Spot:
|
|||||||
if self.comment and not self.qrt:
|
if self.comment and not self.qrt:
|
||||||
self.qrt = "QRT" in self.comment.upper()
|
self.qrt = "QRT" in self.comment.upper()
|
||||||
|
|
||||||
# DX operator details lookup, using QRZ.com. This should be the last resort compared to taking the data from
|
# Always create an ID based on a hash of every parameter *except* received_time. This is used as the index
|
||||||
# the actual spotting service, e.g. we don't want to accidentally use a user's QRZ.com home lat/lon instead of
|
# to a map, which as a byproduct avoids us having multiple duplicate copies of the object that are identical
|
||||||
# the one from the park reference they're at.
|
# apart from that they were retrieved from the API at different times. Note that the simple Python hash()
|
||||||
|
# function includes a seed randomly generated at runtime; this is therefore not consistent between runs. But we
|
||||||
|
# use diskcache to store our data between runs, so we use SHA256 which does not include this random element.
|
||||||
|
# The ID is computed before the online lookups below so that it is stable regardless of whether credentials
|
||||||
|
# are provided, allowing the enriched API response to be matched to the stored spot by ID.
|
||||||
|
if not self.id:
|
||||||
|
self_copy = copy.deepcopy(self)
|
||||||
|
self_copy.received_time = 0
|
||||||
|
self_copy.received_time_iso = ""
|
||||||
|
self.id = hashlib.sha256(str(self_copy).encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
# DX operator details lookup, using QRZ.com/HamQTH. This should be the last resort compared to taking the data
|
||||||
|
# from the actual spotting service, e.g. we don't want to accidentally use a user's QRZ.com home lat/lon
|
||||||
|
# instead of the one from the park reference they're at.
|
||||||
if self.dx_call and not self.dx_name:
|
if self.dx_call and not self.dx_name:
|
||||||
self.dx_name = lookup_helper.infer_name_from_callsign_online_lookup(self.dx_call)
|
self.dx_name = lookup_helper.infer_name_from_callsign_online_lookup(self.dx_call, credentials)
|
||||||
if self.dx_call and not self.dx_latitude:
|
if self.dx_call and not self.dx_latitude:
|
||||||
latlon = lookup_helper.infer_latlon_from_callsign_online_lookup(self.dx_call)
|
latlon = lookup_helper.infer_latlon_from_callsign_online_lookup(self.dx_call, credentials)
|
||||||
if latlon:
|
if latlon:
|
||||||
self.dx_latitude = latlon[0]
|
self.dx_latitude = latlon[0]
|
||||||
self.dx_longitude = latlon[1]
|
self.dx_longitude = latlon[1]
|
||||||
self.dx_grid = lookup_helper.infer_grid_from_callsign_online_lookup(self.dx_call)
|
self.dx_grid = lookup_helper.infer_grid_from_callsign_online_lookup(self.dx_call, credentials)
|
||||||
self.dx_location_source = "HOME QTH"
|
self.dx_location_source = "HOME QTH"
|
||||||
|
|
||||||
# Determine a "QTH" string. If we have a SIG ref, pick the first one and turn it into a suitable stirng,
|
# Determine a "QTH" string. If we have a SIG ref, pick the first one and turn it into a suitable string,
|
||||||
# otherwise see what they have set on an online lookup service.
|
# otherwise see what they have set on an online lookup service.
|
||||||
if self.sig_refs and len(self.sig_refs) > 0:
|
if self.sig_refs and len(self.sig_refs) > 0:
|
||||||
self.dx_qth = self.sig_refs[0].id
|
self.dx_qth = self.sig_refs[0].id
|
||||||
if self.sig_refs[0].name:
|
if self.sig_refs[0].name:
|
||||||
self.dx_qth = self.dx_qth + " " + self.sig_refs[0].name
|
self.dx_qth = self.dx_qth + " " + self.sig_refs[0].name
|
||||||
else:
|
else:
|
||||||
self.dx_qth = lookup_helper.infer_qth_from_callsign_online_lookup(self.dx_call)
|
self.dx_qth = lookup_helper.infer_qth_from_callsign_online_lookup(self.dx_call, credentials)
|
||||||
|
|
||||||
# Last resort for getting a DX position, use the DXCC entity.
|
# Last resort for getting a DX position, use the DXCC entity.
|
||||||
if self.dx_call and not self.dx_latitude:
|
if self.dx_call and not self.dx_latitude:
|
||||||
@@ -352,12 +366,12 @@ class Spot:
|
|||||||
if self.dx_latitude:
|
if self.dx_latitude:
|
||||||
self.dx_cq_zone = lat_lon_to_cq_zone(self.dx_latitude, self.dx_longitude)
|
self.dx_cq_zone = lat_lon_to_cq_zone(self.dx_latitude, self.dx_longitude)
|
||||||
elif self.dx_call:
|
elif self.dx_call:
|
||||||
self.dx_cq_zone = lookup_helper.infer_cq_zone_from_callsign(self.dx_call)
|
self.dx_cq_zone = lookup_helper.infer_cq_zone_from_callsign(self.dx_call, credentials)
|
||||||
if not self.dx_itu_zone:
|
if not self.dx_itu_zone:
|
||||||
if self.dx_latitude:
|
if self.dx_latitude:
|
||||||
self.dx_itu_zone = lat_lon_to_itu_zone(self.dx_latitude, self.dx_longitude)
|
self.dx_itu_zone = lat_lon_to_itu_zone(self.dx_latitude, self.dx_longitude)
|
||||||
elif self.dx_call:
|
elif self.dx_call:
|
||||||
self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_call)
|
self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_call, credentials)
|
||||||
|
|
||||||
# DX Location is "good" if it is from a spot, or from QRZ if the callsign doesn't contain a slash, so the operator
|
# DX Location is "good" if it is from a spot, or from QRZ if the callsign doesn't contain a slash, so the operator
|
||||||
# is likely at home.
|
# is likely at home.
|
||||||
@@ -369,13 +383,13 @@ class Spot:
|
|||||||
# DE with no digits and APRS servers starting "T2" are not things we can look up location for
|
# DE with no digits and APRS servers starting "T2" are not things we can look up location for
|
||||||
if self.de_call and any(char.isdigit() for char in self.de_call) and not (
|
if self.de_call and any(char.isdigit() for char in self.de_call) and not (
|
||||||
self.de_call.startswith("T2") and self.source == "APRS-IS"):
|
self.de_call.startswith("T2") and self.source == "APRS-IS"):
|
||||||
# DE operator position lookup, using QRZ.com.
|
# DE operator position lookup, using QRZ.com/HamQTH.
|
||||||
if not self.de_latitude:
|
if not self.de_latitude:
|
||||||
latlon = lookup_helper.infer_latlon_from_callsign_online_lookup(self.de_call)
|
latlon = lookup_helper.infer_latlon_from_callsign_online_lookup(self.de_call, credentials)
|
||||||
if latlon:
|
if latlon:
|
||||||
self.de_latitude = latlon[0]
|
self.de_latitude = latlon[0]
|
||||||
self.de_longitude = latlon[1]
|
self.de_longitude = latlon[1]
|
||||||
self.de_grid = lookup_helper.infer_grid_from_callsign_online_lookup(self.de_call)
|
self.de_grid = lookup_helper.infer_grid_from_callsign_online_lookup(self.de_call, credentials)
|
||||||
|
|
||||||
# Last resort for getting a DE position, use the DXCC entity.
|
# Last resort for getting a DE position, use the DXCC entity.
|
||||||
if not self.de_latitude:
|
if not self.de_latitude:
|
||||||
@@ -385,16 +399,6 @@ class Spot:
|
|||||||
self.de_longitude = latlon[1]
|
self.de_longitude = latlon[1]
|
||||||
self.de_grid = lookup_helper.infer_grid_from_callsign_dxcc(self.de_call)
|
self.de_grid = lookup_helper.infer_grid_from_callsign_dxcc(self.de_call)
|
||||||
|
|
||||||
# Always create an ID based on a hash of every parameter *except* received_time. This is used as the index
|
|
||||||
# to a map, which as a byproduct avoids us having multiple duplicate copies of the object that are identical
|
|
||||||
# apart from that they were retrieved from the API at different times. Note that the simple Python hash()
|
|
||||||
# function includes a seed randomly generated at runtime; this is therefore not consistent between runs. But we
|
|
||||||
# use diskcache to store our data between runs, so we use SHA256 which does not include this random element.
|
|
||||||
self_copy = copy.deepcopy(self)
|
|
||||||
self_copy.received_time = 0
|
|
||||||
self_copy.received_time_iso = ""
|
|
||||||
self.id = hashlib.sha256(str(self_copy).encode("utf-8")).hexdigest()
|
|
||||||
|
|
||||||
def to_json(self):
|
def to_json(self):
|
||||||
"""JSON serialise"""
|
"""JSON serialise"""
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import copy
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -9,6 +10,8 @@ import tornado_eventsource.handler
|
|||||||
|
|
||||||
from core.prometheus_metrics_handler import api_requests_counter
|
from core.prometheus_metrics_handler import api_requests_counter
|
||||||
from core.utils import serialize_everything, empty_queue
|
from core.utils import serialize_everything, empty_queue
|
||||||
|
from data.lookup_credentials import extract_credentials
|
||||||
|
|
||||||
|
|
||||||
SSE_HANDLER_MAX_QUEUE_SIZE = 100
|
SSE_HANDLER_MAX_QUEUE_SIZE = 100
|
||||||
SSE_HANDLER_QUEUE_CHECK_INTERVAL = 5000
|
SSE_HANDLER_QUEUE_CHECK_INTERVAL = 5000
|
||||||
@@ -21,6 +24,15 @@ class APIAlertsHandler(tornado.web.RequestHandler):
|
|||||||
self._alerts = alerts
|
self._alerts = alerts
|
||||||
self._web_server_metrics = web_server_metrics
|
self._web_server_metrics = web_server_metrics
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _enrich(alerts, credentials):
|
||||||
|
enriched = []
|
||||||
|
for alert in alerts:
|
||||||
|
alert_copy = copy.deepcopy(alert)
|
||||||
|
alert_copy.infer_missing(credentials)
|
||||||
|
enriched.append(alert_copy)
|
||||||
|
return enriched
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
try:
|
try:
|
||||||
# Metrics
|
# Metrics
|
||||||
@@ -33,8 +45,11 @@ class APIAlertsHandler(tornado.web.RequestHandler):
|
|||||||
# reduce that to just the first entry, and convert bytes to string
|
# reduce that to just the first entry, and convert bytes to string
|
||||||
query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()}
|
query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()}
|
||||||
|
|
||||||
# Fetch all alerts matching the query
|
# Fetch all alerts matching the query, then optionally enrich with online data
|
||||||
|
credentials = extract_credentials(query_params)
|
||||||
data = get_alert_list_with_filters(self._alerts, query_params)
|
data = get_alert_list_with_filters(self._alerts, query_params)
|
||||||
|
if credentials:
|
||||||
|
data = self._enrich(data, credentials)
|
||||||
self.write(json.dumps(data, default=serialize_everything))
|
self.write(json.dumps(data, default=serialize_everything))
|
||||||
self.set_status(200)
|
self.set_status(200)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -73,6 +88,7 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
|
|||||||
# request.arguments contains lists for each param key because technically the client can supply multiple,
|
# request.arguments contains lists for each param key because technically the client can supply multiple,
|
||||||
# reduce that to just the first entry, and convert bytes to string
|
# reduce that to just the first entry, and convert bytes to string
|
||||||
self._query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()}
|
self._query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()}
|
||||||
|
self._credentials = extract_credentials(self._query_params)
|
||||||
|
|
||||||
# Create a alert queue and add it to the web server's list. The web server will fill this when alerts arrive
|
# Create a alert queue and add it to the web server's list. The web server will fill this when alerts arrive
|
||||||
self._alert_queue = Queue(maxsize=SSE_HANDLER_MAX_QUEUE_SIZE)
|
self._alert_queue = Queue(maxsize=SSE_HANDLER_MAX_QUEUE_SIZE)
|
||||||
@@ -110,6 +126,9 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
|
|||||||
alert = self._alert_queue.get()
|
alert = self._alert_queue.get()
|
||||||
# If the new alert matches our param filters, send it to the client. If not, ignore it.
|
# If the new alert matches our param filters, send it to the client. If not, ignore it.
|
||||||
if alert_allowed_by_query(alert, self._query_params):
|
if alert_allowed_by_query(alert, self._query_params):
|
||||||
|
if self._credentials:
|
||||||
|
alert = copy.deepcopy(alert)
|
||||||
|
alert.infer_missing(self._credentials)
|
||||||
self.write_message(msg=json.dumps(alert, default=serialize_everything))
|
self.write_message(msg=json.dumps(alert, default=serialize_everything))
|
||||||
|
|
||||||
if self._alert_queue not in self._sse_alert_queues:
|
if self._alert_queue not in self._sse_alert_queues:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from core.geo_utils import lat_lon_for_grid_sw_corner_plus_size, lat_lon_to_cq_z
|
|||||||
from core.prometheus_metrics_handler import api_requests_counter
|
from core.prometheus_metrics_handler import api_requests_counter
|
||||||
from core.sig_utils import get_ref_regex_for_sig, populate_sig_ref_info
|
from core.sig_utils import get_ref_regex_for_sig, populate_sig_ref_info
|
||||||
from core.utils import serialize_everything
|
from core.utils import serialize_everything
|
||||||
|
from data.lookup_credentials import extract_credentials
|
||||||
from data.sig_ref import SIGRef
|
from data.sig_ref import SIGRef
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
|
|
||||||
@@ -39,8 +40,9 @@ class APILookupCallHandler(tornado.web.RequestHandler):
|
|||||||
if re.match(r"^[A-Z0-9/\-]*$", call):
|
if re.match(r"^[A-Z0-9/\-]*$", call):
|
||||||
# Take the callsign, make a "fake spot" so we can run infer_missing() on it, then repack the
|
# Take the callsign, make a "fake spot" so we can run infer_missing() on it, then repack the
|
||||||
# resulting data in the correct way for the API response.
|
# resulting data in the correct way for the API response.
|
||||||
|
credentials = extract_credentials(query_params)
|
||||||
fake_spot = Spot(dx_call=call)
|
fake_spot = Spot(dx_call=call)
|
||||||
fake_spot.infer_missing()
|
fake_spot.infer_missing(credentials)
|
||||||
data = {
|
data = {
|
||||||
"call": call,
|
"call": call,
|
||||||
"name": fake_spot.dx_name,
|
"name": fake_spot.dx_name,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import copy
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@@ -9,6 +10,8 @@ import tornado_eventsource.handler
|
|||||||
|
|
||||||
from core.prometheus_metrics_handler import api_requests_counter
|
from core.prometheus_metrics_handler import api_requests_counter
|
||||||
from core.utils import serialize_everything, empty_queue
|
from core.utils import serialize_everything, empty_queue
|
||||||
|
from data.lookup_credentials import extract_credentials
|
||||||
|
|
||||||
|
|
||||||
SSE_HANDLER_MAX_QUEUE_SIZE = 1000
|
SSE_HANDLER_MAX_QUEUE_SIZE = 1000
|
||||||
SSE_HANDLER_QUEUE_CHECK_INTERVAL = 5000
|
SSE_HANDLER_QUEUE_CHECK_INTERVAL = 5000
|
||||||
@@ -21,6 +24,15 @@ class APISpotsHandler(tornado.web.RequestHandler):
|
|||||||
self._spots = spots
|
self._spots = spots
|
||||||
self._web_server_metrics = web_server_metrics
|
self._web_server_metrics = web_server_metrics
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _enrich(spots, credentials):
|
||||||
|
enriched = []
|
||||||
|
for spot in spots:
|
||||||
|
spot_copy = copy.deepcopy(spot)
|
||||||
|
spot_copy.infer_missing(credentials)
|
||||||
|
enriched.append(spot_copy)
|
||||||
|
return enriched
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
try:
|
try:
|
||||||
# Metrics
|
# Metrics
|
||||||
@@ -33,8 +45,11 @@ class APISpotsHandler(tornado.web.RequestHandler):
|
|||||||
# reduce that to just the first entry, and convert bytes to string
|
# reduce that to just the first entry, and convert bytes to string
|
||||||
query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()}
|
query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()}
|
||||||
|
|
||||||
# Fetch all spots matching the query
|
# Fetch all spots matching the query, then optionally enrich with online data
|
||||||
|
credentials = extract_credentials(query_params)
|
||||||
data = get_spot_list_with_filters(self._spots, query_params)
|
data = get_spot_list_with_filters(self._spots, query_params)
|
||||||
|
if credentials:
|
||||||
|
data = self._enrich(data, credentials)
|
||||||
self.write(json.dumps(data, default=serialize_everything))
|
self.write(json.dumps(data, default=serialize_everything))
|
||||||
self.set_status(200)
|
self.set_status(200)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -75,6 +90,7 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
|
|||||||
# request.arguments contains lists for each param key because technically the client can supply multiple,
|
# request.arguments contains lists for each param key because technically the client can supply multiple,
|
||||||
# reduce that to just the first entry, and convert bytes to string
|
# reduce that to just the first entry, and convert bytes to string
|
||||||
self._query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()}
|
self._query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()}
|
||||||
|
self._credentials = extract_credentials(self._query_params)
|
||||||
|
|
||||||
# Create a spot queue and add it to the web server's list. The web server will fill this when spots arrive
|
# Create a spot queue and add it to the web server's list. The web server will fill this when spots arrive
|
||||||
self._spot_queue = Queue(maxsize=SSE_HANDLER_MAX_QUEUE_SIZE)
|
self._spot_queue = Queue(maxsize=SSE_HANDLER_MAX_QUEUE_SIZE)
|
||||||
@@ -112,6 +128,9 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
|
|||||||
spot = self._spot_queue.get()
|
spot = self._spot_queue.get()
|
||||||
# If the new spot matches our param filters, send it to the client. If not, ignore it.
|
# If the new spot matches our param filters, send it to the client. If not, ignore it.
|
||||||
if spot_allowed_by_query(spot, self._query_params):
|
if spot_allowed_by_query(spot, self._query_params):
|
||||||
|
if self._credentials:
|
||||||
|
spot = copy.deepcopy(spot)
|
||||||
|
spot.infer_missing(self._credentials)
|
||||||
self.write_message(msg=json.dumps(spot, default=serialize_everything))
|
self.write_message(msg=json.dumps(spot, default=serialize_everything))
|
||||||
|
|
||||||
if self._spot_queue not in self._sse_spot_queues:
|
if self._spot_queue not in self._sse_spot_queues:
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
<p>This software is dedicated to the memory of Tom G1PJB, SK, a friend and colleague who sadly passed away around the time I started writing it in Autumn 2025. I was looking forward to showing it to you when it was done.</p>
|
<p>This software is dedicated to the memory of Tom G1PJB, SK, a friend and colleague who sadly passed away around the time I started writing it in Autumn 2025. I was looking forward to showing it to you when it was done.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=1777825937"></script>
|
<script src="/js/common.js?v=1778337803"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -69,8 +69,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=1777825937"></script>
|
<script src="/js/common.js?v=1778337803"></script>
|
||||||
<script src="/js/add-spot.js?v=1777825937"></script>
|
<script src="/js/add-spot.js?v=1778337803"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -56,8 +56,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=1777825937"></script>
|
<script src="/js/common.js?v=1778337803"></script>
|
||||||
<script src="/js/alerts.js?v=1777825937"></script>
|
<script src="/js/alerts.js?v=1778337803"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -62,9 +62,9 @@
|
|||||||
<script>
|
<script>
|
||||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common.js?v=1777825937"></script>
|
<script src="/js/common.js?v=1778337803"></script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=1777825937"></script>
|
<script src="/js/spotsbandsandmap.js?v=1778337803"></script>
|
||||||
<script src="/js/bands.js?v=1777825937"></script>
|
<script src="/js/bands.js?v=1778337803"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
<title>Spothole</title>
|
<title>Spothole</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/css/style.css?v=1777825937" type="text/css">
|
<link rel="stylesheet" href="/css/style.css?v=1778337803" type="text/css">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||||
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
||||||
<link href="/fa/css/fontawesome.min.css" rel="stylesheet" />
|
<link href="/fa/css/fontawesome.min.css" rel="stylesheet" />
|
||||||
@@ -46,9 +46,9 @@
|
|||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/tinycolor2@1.6.0/cjs/tinycolor.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/tinycolor2@1.6.0/cjs/tinycolor.min.js"></script>
|
||||||
|
|
||||||
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1777825937"></script>
|
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1778337803"></script>
|
||||||
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1777825937"></script>
|
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1778337803"></script>
|
||||||
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1777825937"></script>
|
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1778337803"></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -230,8 +230,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
|
||||||
<script src="/js/common.js?v=1777825937"></script>
|
<script src="/js/common.js?v=1778337803"></script>
|
||||||
<script src="/js/conditions.js?v=1777825937"></script>
|
<script src="/js/conditions.js?v=1778337803"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-conditions").addClass("active");
|
$("#nav-link-conditions").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -79,9 +79,9 @@
|
|||||||
<script>
|
<script>
|
||||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common.js?v=1777825936"></script>
|
<script src="/js/common.js?v=1778337802"></script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=1777825936"></script>
|
<script src="/js/spotsbandsandmap.js?v=1778337802"></script>
|
||||||
<script src="/js/map.js?v=1777825936"></script>
|
<script src="/js/map.js?v=1778337802"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -90,9 +90,9 @@
|
|||||||
<script>
|
<script>
|
||||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common.js?v=1777825936"></script>
|
<script src="/js/common.js?v=1778337802"></script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=1777825936"></script>
|
<script src="/js/spotsbandsandmap.js?v=1778337802"></script>
|
||||||
<script src="/js/spots.js?v=1777825936"></script>
|
<script src="/js/spots.js?v=1778337802"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -59,8 +59,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=1777825937"></script>
|
<script src="/js/common.js?v=1778337803"></script>
|
||||||
<script src="/js/status.js?v=1777825937"></script>
|
<script src="/js/status.js?v=1778337803"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav -->
|
$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav -->
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ info:
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### 1.3
|
||||||
|
|
||||||
|
* `/spots`, `/spots/stream`, `/alerts`, `/alerts/stream`, and `/lookup/call` now accept optional QRZ.com and HamQTH credentials as query parameters. When supplied, returned data is enriched with operator name, home location etc. from those services.
|
||||||
|
|
||||||
### 1.2
|
### 1.2
|
||||||
|
|
||||||
* Added `/dxstats` endpoint for inter-continent DX spot statistics.
|
* Added `/dxstats` endpoint for inter-continent DX spot statistics.
|
||||||
@@ -29,7 +33,7 @@ info:
|
|||||||
license:
|
license:
|
||||||
name: The Unlicense
|
name: The Unlicense
|
||||||
url: https://unlicense.org/#the-unlicense
|
url: https://unlicense.org/#the-unlicense
|
||||||
version: v1.2
|
version: v1.3
|
||||||
servers:
|
servers:
|
||||||
- url: https://spothole.app/api/v1
|
- url: https://spothole.app/api/v1
|
||||||
paths:
|
paths:
|
||||||
@@ -38,7 +42,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- Spots
|
- Spots
|
||||||
summary: Get spots
|
summary: Get spots
|
||||||
description: The main API call that retrieves spots from the system. Supply this with no query parameters to retrieve all spots known to the system. Supply query parameters to filter what is retrieved.
|
description: The main API call that retrieves spots from the system. Supply this with no query parameters to retrieve all spots known to the system. Supply query parameters to filter what is retrieved. If QRZ.com or HamQTH credentials are supplied, returned spots will be enriched with operator name, home location etc. from those services.
|
||||||
operationId: spots
|
operationId: spots
|
||||||
parameters:
|
parameters:
|
||||||
- name: limit
|
- name: limit
|
||||||
@@ -160,6 +164,12 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
|
- $ref: '#/components/parameters/QrzUsername'
|
||||||
|
- $ref: '#/components/parameters/QrzPassword'
|
||||||
|
- $ref: '#/components/parameters/QrzSessionKey'
|
||||||
|
- $ref: '#/components/parameters/HamqthUsername'
|
||||||
|
- $ref: '#/components/parameters/HamqthPassword'
|
||||||
|
- $ref: '#/components/parameters/HamqthSessionId'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Success
|
description: Success
|
||||||
@@ -175,7 +185,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- Spots
|
- Spots
|
||||||
summary: Get spot stream
|
summary: Get spot stream
|
||||||
description: Request a Server-Sent Event stream which will return individual spots immediately when they are added to the system. Only spots that match the provided filters will be returned.
|
description: Request a Server-Sent Event stream which will return individual spots immediately when they are added to the system. Only spots that match the provided filters will be returned. If QRZ.com or HamQTH credentials are supplied, streamed spots will be enriched with operator name, home location etc. from those services.
|
||||||
operationId: spots-stream
|
operationId: spots-stream
|
||||||
parameters:
|
parameters:
|
||||||
- name: source
|
- name: source
|
||||||
@@ -266,6 +276,12 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
|
- $ref: '#/components/parameters/QrzUsername'
|
||||||
|
- $ref: '#/components/parameters/QrzPassword'
|
||||||
|
- $ref: '#/components/parameters/QrzSessionKey'
|
||||||
|
- $ref: '#/components/parameters/HamqthUsername'
|
||||||
|
- $ref: '#/components/parameters/HamqthPassword'
|
||||||
|
- $ref: '#/components/parameters/HamqthSessionId'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Success
|
description: Success
|
||||||
@@ -280,7 +296,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- Alerts
|
- Alerts
|
||||||
summary: Get alerts
|
summary: Get alerts
|
||||||
description: Retrieves alerts (indications of upcoming activations) from the system. Supply this with no query parameters to retrieve all alerts known to the system. Supply query parameters to filter what is retrieved.
|
description: Retrieves alerts (indications of upcoming activations) from the system. Supply this with no query parameters to retrieve all alerts known to the system. Supply query parameters to filter what is retrieved. If QRZ.com or HamQTH credentials are supplied, returned alerts will be enriched with operator names from those services.
|
||||||
operationId: alerts
|
operationId: alerts
|
||||||
parameters:
|
parameters:
|
||||||
- name: limit
|
- name: limit
|
||||||
@@ -337,6 +353,12 @@ paths:
|
|||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
- $ref: '#/components/parameters/QrzUsername'
|
||||||
|
- $ref: '#/components/parameters/QrzPassword'
|
||||||
|
- $ref: '#/components/parameters/QrzSessionKey'
|
||||||
|
- $ref: '#/components/parameters/HamqthUsername'
|
||||||
|
- $ref: '#/components/parameters/HamqthPassword'
|
||||||
|
- $ref: '#/components/parameters/HamqthSessionId'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Success
|
description: Success
|
||||||
@@ -353,7 +375,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- Alerts
|
- Alerts
|
||||||
summary: Get alert stream
|
summary: Get alert stream
|
||||||
description: Request a Server-Sent Event stream which will return individual alerts immediately when they are added to the system. Only alerts that match the provided filters will be returned.
|
description: Request a Server-Sent Event stream which will return individual alerts immediately when they are added to the system. Only alerts that match the provided filters will be returned. If QRZ.com or HamQTH credentials are supplied, streamed alerts will be enriched with operator names from those services.
|
||||||
operationId: alerts-stream
|
operationId: alerts-stream
|
||||||
parameters:
|
parameters:
|
||||||
- name: max_duration
|
- name: max_duration
|
||||||
@@ -398,6 +420,12 @@ paths:
|
|||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
- $ref: '#/components/parameters/QrzUsername'
|
||||||
|
- $ref: '#/components/parameters/QrzPassword'
|
||||||
|
- $ref: '#/components/parameters/QrzSessionKey'
|
||||||
|
- $ref: '#/components/parameters/HamqthUsername'
|
||||||
|
- $ref: '#/components/parameters/HamqthPassword'
|
||||||
|
- $ref: '#/components/parameters/HamqthSessionId'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Success
|
description: Success
|
||||||
@@ -607,7 +635,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- Utilities
|
- Utilities
|
||||||
summary: Look up callsign details
|
summary: Look up callsign details
|
||||||
description: Perform a lookup of data about a certain callsign, using any of the lookup services available to the Spothole server.
|
description: Perform a lookup of data about a certain callsign, using any of the lookup services available to the Spothole server. If QRZ.com or HamQTH credentials are supplied, the response will be able to use these services to perform a lookup.
|
||||||
operationId: call
|
operationId: call
|
||||||
parameters:
|
parameters:
|
||||||
- name: call
|
- name: call
|
||||||
@@ -616,6 +644,12 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
example: M0TRT
|
example: M0TRT
|
||||||
|
- $ref: '#/components/parameters/QrzUsername'
|
||||||
|
- $ref: '#/components/parameters/QrzPassword'
|
||||||
|
- $ref: '#/components/parameters/QrzSessionKey'
|
||||||
|
- $ref: '#/components/parameters/HamqthUsername'
|
||||||
|
- $ref: '#/components/parameters/HamqthPassword'
|
||||||
|
- $ref: '#/components/parameters/HamqthSessionId'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Success
|
description: Success
|
||||||
@@ -838,6 +872,50 @@ paths:
|
|||||||
example: "Failed"
|
example: "Failed"
|
||||||
|
|
||||||
components:
|
components:
|
||||||
|
parameters:
|
||||||
|
QrzUsername:
|
||||||
|
name: qrz_username
|
||||||
|
in: query
|
||||||
|
description: "QRZ.com username for online callsign lookup, which will enrich the returned spots and alerts with extra data. Requires a QRZ.com XML Subscriber (paid) account. Supply together with `qrz_password`, or supply `qrz_session_key` instead."
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
QrzPassword:
|
||||||
|
name: qrz_password
|
||||||
|
in: query
|
||||||
|
description: "QRZ.com password. Supply together with `qrz_username`."
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
QrzSessionKey:
|
||||||
|
name: qrz_session_key
|
||||||
|
in: query
|
||||||
|
description: "A pre-obtained QRZ.com XML session key, as an alternative to supplying `qrz_username` and `qrz_password`. See https://www.qrz.com/docs/xml/current_spec.html for details on how to obtain one for the user."
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
HamqthUsername:
|
||||||
|
name: hamqth_username
|
||||||
|
in: query
|
||||||
|
description: "HamQTH username for online callsign lookup, which will enrich the returned spots and alerts with extra data. Supply together with `hamqth_password`, or supply `hamqth_session_id` instead."
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
HamqthPassword:
|
||||||
|
name: hamqth_password
|
||||||
|
in: query
|
||||||
|
description: "HamQTH password. Supply together with `hamqth_username`."
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
HamqthSessionId:
|
||||||
|
name: hamqth_session_id
|
||||||
|
in: query
|
||||||
|
description: "A pre-obtained HamQTH session ID, as an alternative to supplying `hamqth_username` and `hamqth_password`. See https://www.hamqth.com/developers.php for details on how to retrieve one for a user."
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
|
||||||
schemas:
|
schemas:
|
||||||
Source:
|
Source:
|
||||||
type: string
|
type: string
|
||||||
|
|||||||
Reference in New Issue
Block a user