mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-20 22:37:44 +00:00
Compare commits
9
Commits
38bc03a603
...
2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14e4886c1b | ||
|
|
f346444ec3 | ||
|
|
6f21ac83b7 | ||
|
|
a20c29cdd1 | ||
|
|
1fd6611ac2 | ||
|
|
a27a04b65d | ||
|
|
c5d6a65786 | ||
|
|
85f63a1258 | ||
|
|
7f1a5ae3be |
+2
-2
@@ -67,7 +67,7 @@ spot_providers:
|
|||||||
|
|
||||||
- class: "DXCluster"
|
- class: "DXCluster"
|
||||||
# Clusters have a "name" property in case you want to say which cluster node the data is from, or connect to
|
# Clusters have a "name" property in case you want to say which cluster node the data is from, or connect to
|
||||||
# several clusters and name them separately.
|
# several clusters and name them separately. The name will be used as the "source" field for the spots received.
|
||||||
name: "HRD Cluster"
|
name: "HRD Cluster"
|
||||||
enabled: true
|
enabled: true
|
||||||
host: "hrd.wa9pie.net"
|
host: "hrd.wa9pie.net"
|
||||||
@@ -88,7 +88,7 @@ spot_providers:
|
|||||||
enabled: false
|
enabled: false
|
||||||
host: "w3lpl.net"
|
host: "w3lpl.net"
|
||||||
port: 7373
|
port: 7373
|
||||||
login_prompt: "Please enter your call:"
|
login_prompt: "login:"
|
||||||
login_callsign: "N0CALL-99"
|
login_callsign: "N0CALL-99"
|
||||||
allow_rbn_spots: false
|
allow_rbn_spots: false
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from core.constants import CALL_ONLY_PATTERN
|
import re
|
||||||
|
|
||||||
from core.data_providers import DATA_PROVIDERS
|
from core.data_providers import DATA_PROVIDERS
|
||||||
from data.callsign import Callsign
|
from data.callsign import Callsign
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ def get_call_info(callsign, lookup_credentials):
|
|||||||
callsign_data = Callsign(call=callsign)
|
callsign_data = Callsign(call=callsign)
|
||||||
|
|
||||||
# First check our input looks like a real callsign
|
# First check our input looks like a real callsign
|
||||||
if callsign and CALL_ONLY_PATTERN.match(callsign):
|
if callsign and re.match(r"^[A-Za-z0-9/\-]*$", callsign):
|
||||||
# Sort callsign providers by priority order, so we query the highest priority (lowest numbers) first, and only
|
# Sort callsign providers by priority order, so we query the highest priority (lowest numbers) first, and only
|
||||||
# query other providers for data we are missing as we go along.
|
# query other providers for data we are missing as we go along.
|
||||||
for p in sorted(DATA_PROVIDERS.callsign_data_providers, key=lambda p2: p2.priority):
|
for p in sorted(DATA_PROVIDERS.callsign_data_providers, key=lambda p2: p2.priority):
|
||||||
|
|||||||
+7
-8
@@ -1,21 +1,14 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
from core.config import SERVER_OWNER_CALLSIGN
|
from core.config import SERVER_OWNER_CALLSIGN
|
||||||
from data.band import Band
|
from data.band import Band
|
||||||
from data.sig import SIG
|
from data.sig import SIG
|
||||||
|
|
||||||
# General software
|
# General software
|
||||||
SOFTWARE_VERSION = "2.0-pre"
|
SOFTWARE_VERSION = "2.0"
|
||||||
|
|
||||||
# HTTP headers used for spot providers that use HTTP
|
# HTTP headers used for spot providers that use HTTP
|
||||||
HTTP_HEADERS = {"User-Agent": f"Spothole v{SOFTWARE_VERSION} (operated by {SERVER_OWNER_CALLSIGN})"}
|
HTTP_HEADERS = {"User-Agent": f"Spothole v{SOFTWARE_VERSION} (operated by {SERVER_OWNER_CALLSIGN})"}
|
||||||
HAMQTH_PRG = f"Spothole v{SOFTWARE_VERSION} operated by {SERVER_OWNER_CALLSIGN}".replace(" ", "_")
|
HAMQTH_PRG = f"Spothole v{SOFTWARE_VERSION} operated by {SERVER_OWNER_CALLSIGN}".replace(" ", "_")
|
||||||
|
|
||||||
# Generally useful regexes
|
|
||||||
CALL_REGEX = r"[A-Za-z0-9/\-]+"
|
|
||||||
CALL_PATTERN = re.compile(CALL_REGEX)
|
|
||||||
CALL_ONLY_PATTERN = re.compile(rf"^{CALL_REGEX}$")
|
|
||||||
|
|
||||||
# Special Interest Groups
|
# Special Interest Groups
|
||||||
SIGS = [
|
SIGS = [
|
||||||
SIG(
|
SIG(
|
||||||
@@ -60,6 +53,12 @@ SIGS = [
|
|||||||
description="Islands on the Air",
|
description="Islands on the Air",
|
||||||
ref_regex=r"[A-Z]{2}\-\d{3}",
|
ref_regex=r"[A-Z]{2}\-\d{3}",
|
||||||
),
|
),
|
||||||
|
SIG(
|
||||||
|
name="GMA Island",
|
||||||
|
comment_names=[],
|
||||||
|
description="Global Mountain Activity - Islands",
|
||||||
|
ref_regex=r"(([A-Z]{2}\-\d{3})|([A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}))",
|
||||||
|
),
|
||||||
SIG(
|
SIG(
|
||||||
name="MOTA",
|
name="MOTA",
|
||||||
comment_names=["MOTA"],
|
comment_names=["MOTA"],
|
||||||
|
|||||||
@@ -73,6 +73,20 @@ def get_sig_ref_info(sig, ref_id):
|
|||||||
sig_ref.url = f"https://www.beachesontheair.com/beaches/{sig_ref.name.lower().replace(' ', '-')}"
|
sig_ref.url = f"https://www.beachesontheair.com/beaches/{sig_ref.name.lower().replace(' ', '-')}"
|
||||||
return sig_ref
|
return sig_ref
|
||||||
|
|
||||||
|
elif sig.upper() == "GMA Islands":
|
||||||
|
# GMA Islands is a bit of a mess of GMA and IOTA references. Try looking them both up and see what returns
|
||||||
|
# the best result.
|
||||||
|
iota_lookup = get_sig_ref_info("IOTA", ref_id)
|
||||||
|
gma_lookup = get_sig_ref_info("GMA", ref_id)
|
||||||
|
for key, value in iota_lookup.__dict__.items():
|
||||||
|
if value is not None and sig_ref.__dict__.get(key) is None:
|
||||||
|
sig_ref.__dict__[key] = value
|
||||||
|
for key, value in gma_lookup.__dict__.items():
|
||||||
|
if value is not None and sig_ref.__dict__.get(key) is None:
|
||||||
|
sig_ref.__dict__[key] = value
|
||||||
|
sig_ref.ref_type = "Island"
|
||||||
|
return sig_ref
|
||||||
|
|
||||||
### ACTUAL LOOKUP ###
|
### ACTUAL LOOKUP ###
|
||||||
#
|
#
|
||||||
# OK, this is something we have to look up. Now check to see if our data store contains reference data and use
|
# OK, this is something we have to look up. Now check to see if our data store contains reference data and use
|
||||||
|
|||||||
+3
-3
@@ -11,7 +11,7 @@ from pyhamtools.locator import latlong_to_locator, locator_to_latlong
|
|||||||
|
|
||||||
from core.call_lookup_helper import get_call_info
|
from core.call_lookup_helper import get_call_info
|
||||||
from core.config import MAX_SPOT_AGE
|
from core.config import MAX_SPOT_AGE
|
||||||
from core.constants import CALL_REGEX, MODE_ALIASES, PROPAGATION_MODES
|
from core.constants import MODE_ALIASES, PROPAGATION_MODES
|
||||||
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.sig_lookup_helper import populate_missing_sig_ref_info
|
from core.sig_lookup_helper import populate_missing_sig_ref_info
|
||||||
from core.sig_utils import (
|
from core.sig_utils import (
|
||||||
@@ -201,14 +201,14 @@ class Spot:
|
|||||||
# If we have a spotter of "RBNHOLE", we should have the actual spotter callsign in the comment, so extract it.
|
# If we have a spotter of "RBNHOLE", we should have the actual spotter callsign in the comment, so extract it.
|
||||||
# RBNHole posts come from a number of providers, so it's dealt with here in the generic spot handling code.
|
# RBNHole posts come from a number of providers, so it's dealt with here in the generic spot handling code.
|
||||||
if self.de_call == "RBNHOLE" and self.comment:
|
if self.de_call == "RBNHOLE" and self.comment:
|
||||||
rbnhole_call_match = re.search(rf"\Wat ({CALL_REGEX})\W", self.comment, re.IGNORECASE)
|
rbnhole_call_match = re.search(r"\Wat ([a-z0-9/]+)\W", self.comment, re.IGNORECASE)
|
||||||
if rbnhole_call_match:
|
if rbnhole_call_match:
|
||||||
self.de_call = rbnhole_call_match.group(1).upper()
|
self.de_call = rbnhole_call_match.group(1).upper()
|
||||||
|
|
||||||
# If we have a spotter of "SOTAMAT", we might have the actual spotter callsign in the comment, if so extract it.
|
# If we have a spotter of "SOTAMAT", we might have the actual spotter callsign in the comment, if so extract it.
|
||||||
# SOTAMAT can do POTA as well as SOTA, so it's dealt with here in the generic spot handling code.
|
# SOTAMAT can do POTA as well as SOTA, so it's dealt with here in the generic spot handling code.
|
||||||
if self.de_call == "SOTAMAT" and self.comment:
|
if self.de_call == "SOTAMAT" and self.comment:
|
||||||
sotamat_call_match = re.search(rf"\Wfrom ({CALL_REGEX})]", self.comment, re.IGNORECASE)
|
sotamat_call_match = re.search(r"\Wfrom ([a-z0-9/]+)]", self.comment, re.IGNORECASE)
|
||||||
if sotamat_call_match:
|
if sotamat_call_match:
|
||||||
self.de_call = sotamat_call_match.group(1).upper()
|
self.de_call = sotamat_call_match.group(1).upper()
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import pytz
|
|||||||
from rss_parser import Parser
|
from rss_parser import Parser
|
||||||
from rss_parser.models.rss import RSS
|
from rss_parser.models.rss import RSS
|
||||||
|
|
||||||
from core.constants import CALL_REGEX
|
|
||||||
from data.alert import Alert
|
from data.alert import Alert
|
||||||
from providers.alert.http_alert_provider import HTTPAlertProvider
|
from providers.alert.http_alert_provider import HTTPAlertProvider
|
||||||
|
|
||||||
@@ -16,7 +15,7 @@ class NG3K(HTTPAlertProvider):
|
|||||||
|
|
||||||
POLL_INTERVAL_SEC = 1800
|
POLL_INTERVAL_SEC = 1800
|
||||||
ALERTS_URL = "https://www.ng3k.com/adxo.xml"
|
ALERTS_URL = "https://www.ng3k.com/adxo.xml"
|
||||||
AS_CALL_PATTERN = re.compile(rf"as ({CALL_REGEX})", re.IGNORECASE)
|
AS_CALL_PATTERN = re.compile("as ([a-z0-9/]+)", re.IGNORECASE)
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__("NG3K", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_SEC)
|
super().__init__("NG3K", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_SEC)
|
||||||
|
|||||||
+54
-47
@@ -1,6 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
from xml.parsers.expat import ExpatError
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from rss_parser import Parser as RSSParser
|
from rss_parser import Parser as RSSParser
|
||||||
@@ -25,56 +26,62 @@ class WOTA(HTTPAlertProvider):
|
|||||||
|
|
||||||
def _http_response_to_alerts(self, http_response):
|
def _http_response_to_alerts(self, http_response):
|
||||||
new_alerts = []
|
new_alerts = []
|
||||||
rss = cast(RSS, RSSParser.parse(http_response.content.decode("utf-8-sig")))
|
|
||||||
# Iterate through source data
|
|
||||||
for source_alert in rss.channel.items:
|
|
||||||
# Reject GUID missing or zero
|
|
||||||
if (
|
|
||||||
not source_alert.guid
|
|
||||||
or not source_alert.guid.content
|
|
||||||
or source_alert.guid.content == "http://www.wota.org.uk/alerts/0"
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Pick apart the title
|
try:
|
||||||
dx_call = None
|
rss = cast(RSS, RSSParser.parse(http_response.content.decode("utf-8-sig")))
|
||||||
ref = None
|
# Iterate through source data
|
||||||
ref_name = None
|
for source_alert in rss.channel.items:
|
||||||
try:
|
# Reject GUID missing or zero
|
||||||
title_split = source_alert.title.split(" on ")
|
if (
|
||||||
dx_call = title_split[0]
|
not source_alert.guid
|
||||||
if len(title_split) > 1:
|
or not source_alert.guid.content
|
||||||
ref_split = title_split[1].split(" - ")
|
or source_alert.guid.content == "http://www.wota.org.uk/alerts/0"
|
||||||
ref = str(ref_split[0])
|
):
|
||||||
if len(ref_split) > 1:
|
continue
|
||||||
ref_name = str(ref_split[1])
|
|
||||||
except Exception:
|
|
||||||
logger.warning(f"Could not parse WOTA alert title: {source_alert.description}")
|
|
||||||
|
|
||||||
# Pick apart the description
|
# Pick apart the title
|
||||||
comment = None
|
dx_call = None
|
||||||
freqs_modes = None
|
ref = None
|
||||||
try:
|
ref_name = None
|
||||||
desc_split = source_alert.description.split(". ")
|
try:
|
||||||
freqs_modes = desc_split[0].replace("Frequencies/modes:", "").strip()
|
title_split = source_alert.title.split(" on ")
|
||||||
if len(desc_split) > 1:
|
dx_call = title_split[0]
|
||||||
comment = desc_split[1].strip()
|
if len(title_split) > 1:
|
||||||
except Exception:
|
ref_split = title_split[1].split(" - ")
|
||||||
logger.warning(f"Could not parse WOTA alert description: {source_alert.description}")
|
ref = str(ref_split[0])
|
||||||
|
if len(ref_split) > 1:
|
||||||
|
ref_name = str(ref_split[1])
|
||||||
|
except Exception:
|
||||||
|
logger.warning(f"Could not parse WOTA alert title: {source_alert.description}")
|
||||||
|
|
||||||
time = datetime.strptime(source_alert.pub_date.content, self.RSS_DATE_TIME_FORMAT).astimezone(pytz.UTC)
|
# Pick apart the description
|
||||||
|
comment = None
|
||||||
|
freqs_modes = None
|
||||||
|
try:
|
||||||
|
desc_split = source_alert.description.split(". ")
|
||||||
|
freqs_modes = desc_split[0].replace("Frequencies/modes:", "").strip()
|
||||||
|
if len(desc_split) > 1:
|
||||||
|
comment = desc_split[1].strip()
|
||||||
|
except Exception:
|
||||||
|
logger.warning(f"Could not parse WOTA alert description: {source_alert.description}")
|
||||||
|
|
||||||
# Convert to our alert format
|
time = datetime.strptime(source_alert.pub_date.content, self.RSS_DATE_TIME_FORMAT).astimezone(pytz.UTC)
|
||||||
alert = Alert(
|
|
||||||
source=self.name,
|
# Convert to our alert format
|
||||||
source_id=source_alert.guid.content,
|
alert = Alert(
|
||||||
dx_calls=[dx_call],
|
source=self.name,
|
||||||
freqs_modes=freqs_modes,
|
source_id=source_alert.guid.content,
|
||||||
comment=comment,
|
dx_calls=[dx_call],
|
||||||
sig_refs=[SIGRef(id=ref, sig="WOTA", name=ref_name)] if ref else [],
|
freqs_modes=freqs_modes,
|
||||||
start_time=time.timestamp(),
|
comment=comment,
|
||||||
)
|
sig_refs=[SIGRef(id=ref, sig="WOTA", name=ref_name)] if ref else [],
|
||||||
|
start_time=time.timestamp(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add to our list.
|
||||||
|
new_alerts.append(alert)
|
||||||
|
|
||||||
|
except ExpatError:
|
||||||
|
logger.warning("WOTA alert RSS feed was fetched but was invalid")
|
||||||
|
|
||||||
# Add to our list.
|
|
||||||
new_alerts.append(alert)
|
|
||||||
return new_alerts
|
return new_alerts
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ from providers.solarconditions.solar_conditions_provider import SolarConditionsP
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Each station gets polled roughly once every hour (3600 seconds). Note that to avoid a burst of requests to the server
|
# Each station gets polled roughly once every two hours (7200 seconds). Note that to avoid a burst of requests to the
|
||||||
# every hour, the requests for data from each station are spaced out throughout the hour, leading to one request being
|
# server every two hours, the requests for data from each station are spaced out throughout the period, leading to one
|
||||||
# sent every 1-2 minutes.
|
# request being sent every 3-4 minutes.
|
||||||
POLL_INTERVAL = 3600
|
POLL_INTERVAL = 7200
|
||||||
# To avoid looking up all stations in the GIRO system and working out which ones are providing live data, this has been
|
# To avoid looking up all stations in the GIRO system and working out which ones are providing live data, this has been
|
||||||
# manually determined and a CSV provided of all the stations that we can query for live data.
|
# manually determined and a CSV provided of all the stations that we can query for live data.
|
||||||
STATIONS_INDEX = "datafiles/didbase-stations.csv"
|
STATIONS_INDEX = "datafiles/didbase-stations.csv"
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import pytz
|
|||||||
import telnetlib3
|
import telnetlib3
|
||||||
|
|
||||||
from core.config import SERVER_OWNER_CALLSIGN
|
from core.config import SERVER_OWNER_CALLSIGN
|
||||||
from core.constants import CALL_REGEX
|
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from providers.spot.spot_provider import SpotProvider
|
from providers.spot.spot_provider import SpotProvider
|
||||||
|
|
||||||
@@ -20,11 +19,11 @@ class DXCluster(SpotProvider):
|
|||||||
See config-example.yml for examples."""
|
See config-example.yml for examples."""
|
||||||
|
|
||||||
_LINE_PATTERN_EXCLUDE_RBN = re.compile(
|
_LINE_PATTERN_EXCLUDE_RBN = re.compile(
|
||||||
rf"^DX de ({CALL_REGEX}):\s+([0-9.]+)\s+({CALL_REGEX})\s+(.*)\s+(\d{4}Z)",
|
r"^DX de ([a-z0-9/]+):\s+([0-9.]+)\s+([a-z0-9/]+)\s+(.*)\s+(\d{4}Z)",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
_LINE_PATTERN_ALLOW_RBN = re.compile(
|
_LINE_PATTERN_ALLOW_RBN = re.compile(
|
||||||
rf"^DX de ({CALL_REGEX})-?#?:\s+([0-9.]+)\s+({CALL_REGEX})\s+(.*)\s+(\d{4}Z)",
|
r"^DX de ([a-z0-9/]+)-?#?:\s+([0-9.]+)\s+([a-z0-9/]+)\s+(.*)\s+(\d{4}Z)",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,7 +50,8 @@ class DXCluster(SpotProvider):
|
|||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self._running = False
|
self._running = False
|
||||||
self._telnet.close()
|
if self._telnet:
|
||||||
|
self._telnet.close()
|
||||||
self._thread.join()
|
self._thread.join()
|
||||||
|
|
||||||
def _handle(self):
|
def _handle(self):
|
||||||
|
|||||||
@@ -117,6 +117,9 @@ class GMA(HTTPSpotProvider):
|
|||||||
case "IOTA Island":
|
case "IOTA Island":
|
||||||
spot.sig_refs[0].sig = "IOTA"
|
spot.sig_refs[0].sig = "IOTA"
|
||||||
spot.sig = "IOTA"
|
spot.sig = "IOTA"
|
||||||
|
case "GMA Island":
|
||||||
|
spot.sig_refs[0].sig = "GMA Island"
|
||||||
|
spot.sig = "GMA Island"
|
||||||
case "Lighthouse (ILLW)":
|
case "Lighthouse (ILLW)":
|
||||||
spot.sig_refs[0].sig = "ILLW"
|
spot.sig_refs[0].sig = "ILLW"
|
||||||
spot.sig = "ILLW"
|
spot.sig = "ILLW"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from datetime import datetime
|
|||||||
import pytz
|
import pytz
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from core.constants import HTTP_HEADERS, CALL_REGEX
|
from core.constants import HTTP_HEADERS
|
||||||
from data.sig_ref import SIGRef
|
from data.sig_ref import SIGRef
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||||
@@ -60,7 +60,7 @@ class ParksNPeaks(HTTPSpotProvider):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Extract a de_call if it's in the comment but not in the "actSpoter" field
|
# Extract a de_call if it's in the comment but not in the "actSpoter" field
|
||||||
m = re.search(rf"\(de ({CALL_REGEX})\)", spot.comment or "")
|
m = re.search(r"\(de ([A-Za-z0-9]*)\)", spot.comment or "")
|
||||||
if not spot.de_call and m:
|
if not spot.de_call and m:
|
||||||
spot.de_call = str(m.group(1))
|
spot.de_call = str(m.group(1))
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import pytz
|
|||||||
import telnetlib3
|
import telnetlib3
|
||||||
|
|
||||||
from core.config import SERVER_OWNER_CALLSIGN
|
from core.config import SERVER_OWNER_CALLSIGN
|
||||||
from core.constants import CALL_REGEX
|
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from providers.spot.spot_provider import SpotProvider
|
from providers.spot.spot_provider import SpotProvider
|
||||||
|
|
||||||
@@ -20,7 +19,7 @@ class RBN(SpotProvider):
|
|||||||
(port 7001) you need to instantiate two copies of this. The port is provided as an argument to the constructor."""
|
(port 7001) you need to instantiate two copies of this. The port is provided as an argument to the constructor."""
|
||||||
|
|
||||||
_LINE_PATTERN = re.compile(
|
_LINE_PATTERN = re.compile(
|
||||||
rf"^DX de ({CALL_REGEX})-.*:\s+([0-9.]+)\s+({CALL_REGEX})\s+(.*)\s+(\d{4}Z)",
|
r"^DX de ([a-z0-9/]+)-.*:\s+([0-9.]+)\s+([a-z0-9/]+)\s+(.*)\s+(\d{4}Z)",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,7 +39,8 @@ class RBN(SpotProvider):
|
|||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self._running = False
|
self._running = False
|
||||||
self._telnet.close()
|
if self._telnet:
|
||||||
|
self._telnet.close()
|
||||||
self._thread.join()
|
self._thread.join()
|
||||||
|
|
||||||
def _handle(self):
|
def _handle(self):
|
||||||
|
|||||||
+10
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@@ -7,6 +8,8 @@ from data.sig_ref import SIGRef
|
|||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Tiles(HTTPSpotProvider):
|
class Tiles(HTTPSpotProvider):
|
||||||
"""Spot provider for Tiles on the Air"""
|
"""Spot provider for Tiles on the Air"""
|
||||||
@@ -39,13 +42,19 @@ class Tiles(HTTPSpotProvider):
|
|||||||
# Iterate through source data
|
# Iterate through source data
|
||||||
for source_spot in http_response.json()["spots"]:
|
for source_spot in http_response.json()["spots"]:
|
||||||
# Convert to our spot format
|
# Convert to our spot format
|
||||||
|
freq = None
|
||||||
|
try:
|
||||||
|
freq = float(strip_extra_decimal_points(source_spot["frequency"])) * 1000000
|
||||||
|
except ValueError:
|
||||||
|
logger.debug(f"Malformed frequency received from Tiles API: {source_spot['frequency']}")
|
||||||
|
|
||||||
spot = Spot(
|
spot = Spot(
|
||||||
source=self.name,
|
source=self.name,
|
||||||
source_id=source_spot["id"],
|
source_id=source_spot["id"],
|
||||||
dx_call=source_spot["call_sign"].upper(),
|
dx_call=source_spot["call_sign"].upper(),
|
||||||
# No separate spotter callsign, assume all spots are self-spots
|
# No separate spotter callsign, assume all spots are self-spots
|
||||||
de_call=source_spot["call_sign"].upper(),
|
de_call=source_spot["call_sign"].upper(),
|
||||||
freq=float(strip_extra_decimal_points(source_spot["frequency"])) * 1000000,
|
freq=freq,
|
||||||
mode=source_spot["mode"].upper(),
|
mode=source_spot["mode"].upper(),
|
||||||
comment=source_spot["notes"],
|
comment=source_spot["notes"],
|
||||||
sig="Tiles",
|
sig="Tiles",
|
||||||
|
|||||||
+69
-63
@@ -2,6 +2,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
from xml.parsers.expat import ExpatError
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from rss_parser import Parser
|
from rss_parser import Parser
|
||||||
@@ -26,73 +27,78 @@ class WOTA(HTTPSpotProvider):
|
|||||||
|
|
||||||
def _http_response_to_spots(self, http_response):
|
def _http_response_to_spots(self, http_response):
|
||||||
new_spots = []
|
new_spots = []
|
||||||
rss = cast(RSS, Parser.parse(http_response.content.decode("utf-8-sig")))
|
try:
|
||||||
# Iterate through source data
|
rss = cast(RSS, Parser.parse(http_response.content.decode("utf-8-sig")))
|
||||||
for source_spot in rss.channel.items:
|
# Iterate through source data
|
||||||
try:
|
for source_spot in rss.channel.items:
|
||||||
# Reject GUID missing or zero
|
|
||||||
if (
|
|
||||||
not source_spot.guid
|
|
||||||
or not source_spot.guid.content
|
|
||||||
or source_spot.guid.content == "http://www.wota.org.uk/spots/0"
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Pick apart the title
|
|
||||||
dx_call = None
|
|
||||||
ref = None
|
|
||||||
ref_name = None
|
|
||||||
try:
|
try:
|
||||||
title_split = source_spot.title.split(" on ")
|
# Reject GUID missing or zero
|
||||||
dx_call = title_split[0]
|
if (
|
||||||
if len(title_split) > 1:
|
not source_spot.guid
|
||||||
ref_split = title_split[1].split(" - ")
|
or not source_spot.guid.content
|
||||||
ref = str(ref_split[0])
|
or source_spot.guid.content == "http://www.wota.org.uk/spots/0"
|
||||||
if len(ref_split) > 1:
|
):
|
||||||
ref_name = str(ref_split[1])
|
continue
|
||||||
|
|
||||||
|
# Pick apart the title
|
||||||
|
dx_call = None
|
||||||
|
ref = None
|
||||||
|
ref_name = None
|
||||||
|
try:
|
||||||
|
title_split = source_spot.title.split(" on ")
|
||||||
|
dx_call = title_split[0]
|
||||||
|
if len(title_split) > 1:
|
||||||
|
ref_split = title_split[1].split(" - ")
|
||||||
|
ref = str(ref_split[0])
|
||||||
|
if len(ref_split) > 1:
|
||||||
|
ref_name = str(ref_split[1])
|
||||||
|
except Exception:
|
||||||
|
logger.warning(f"Could not parse WOTA spot title: {source_spot.title}")
|
||||||
|
|
||||||
|
# Pick apart the description
|
||||||
|
freq_hz = None
|
||||||
|
mode = None
|
||||||
|
comment = None
|
||||||
|
spotter = None
|
||||||
|
try:
|
||||||
|
desc_split = source_spot.description.split(". ")
|
||||||
|
freq_mode = desc_split[0].replace("Frequencies/modes:", "").strip()
|
||||||
|
if freq_mode and freq_mode != "-":
|
||||||
|
freq_mode_split = re.split(r"[\-\s]+", freq_mode)
|
||||||
|
freq_hz = float(freq_mode_split[0].replace("'", ".")) * 1000000
|
||||||
|
if len(freq_mode_split) > 1:
|
||||||
|
mode = freq_mode_split[1].upper()
|
||||||
|
|
||||||
|
if len(desc_split) > 1:
|
||||||
|
comment = desc_split[1].strip()
|
||||||
|
if len(desc_split) > 2:
|
||||||
|
spotter = desc_split[2].replace("Spotted by ", "").replace(".", "").upper().strip()
|
||||||
|
except Exception:
|
||||||
|
logger.warning(f"Could not parse WOTA spot description: {source_spot.description}")
|
||||||
|
|
||||||
|
time = datetime.strptime(source_spot.pub_date.content, self.RSS_DATE_TIME_FORMAT).astimezone(pytz.UTC)
|
||||||
|
|
||||||
|
# Convert to our spot format
|
||||||
|
spot = Spot(
|
||||||
|
source=self.name,
|
||||||
|
source_id=source_spot.guid.content,
|
||||||
|
dx_call=dx_call,
|
||||||
|
de_call=spotter,
|
||||||
|
freq=freq_hz,
|
||||||
|
mode=mode,
|
||||||
|
comment=comment,
|
||||||
|
sig="WOTA",
|
||||||
|
sig_refs=[SIGRef(id=ref, sig="WOTA", name=ref_name)] if ref else [],
|
||||||
|
time=time.timestamp(),
|
||||||
|
)
|
||||||
|
|
||||||
|
new_spots.append(spot)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning(f"Could not parse WOTA spot title: {source_spot.title}")
|
logger.exception("Exception parsing WOTA spot")
|
||||||
|
|
||||||
# Pick apart the description
|
except ExpatError:
|
||||||
freq_hz = None
|
logger.warning("WOTA spot RSS feed was fetched but was invalid")
|
||||||
mode = None
|
|
||||||
comment = None
|
|
||||||
spotter = None
|
|
||||||
try:
|
|
||||||
desc_split = source_spot.description.split(". ")
|
|
||||||
freq_mode = desc_split[0].replace("Frequencies/modes:", "").strip()
|
|
||||||
if freq_mode and freq_mode != "-":
|
|
||||||
freq_mode_split = re.split(r"[\-\s]+", freq_mode)
|
|
||||||
freq_hz = float(freq_mode_split[0].replace("'", ".")) * 1000000
|
|
||||||
if len(freq_mode_split) > 1:
|
|
||||||
mode = freq_mode_split[1].upper()
|
|
||||||
|
|
||||||
if len(desc_split) > 1:
|
|
||||||
comment = desc_split[1].strip()
|
|
||||||
if len(desc_split) > 2:
|
|
||||||
spotter = desc_split[2].replace("Spotted by ", "").replace(".", "").upper().strip()
|
|
||||||
except Exception:
|
|
||||||
logger.warning(f"Could not parse WOTA spot description: {source_spot.description}")
|
|
||||||
|
|
||||||
time = datetime.strptime(source_spot.pub_date.content, self.RSS_DATE_TIME_FORMAT).astimezone(pytz.UTC)
|
|
||||||
|
|
||||||
# Convert to our spot format
|
|
||||||
spot = Spot(
|
|
||||||
source=self.name,
|
|
||||||
source_id=source_spot.guid.content,
|
|
||||||
dx_call=dx_call,
|
|
||||||
de_call=spotter,
|
|
||||||
freq=freq_hz,
|
|
||||||
mode=mode,
|
|
||||||
comment=comment,
|
|
||||||
sig="WOTA",
|
|
||||||
sig_refs=[SIGRef(id=ref, sig="WOTA", name=ref_name)] if ref else [],
|
|
||||||
time=time.timestamp(),
|
|
||||||
)
|
|
||||||
|
|
||||||
new_spots.append(spot)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Exception parsing WOTA spot")
|
|
||||||
return new_spots
|
return new_spots
|
||||||
|
|
||||||
def can_submit_spot(self, sig):
|
def can_submit_spot(self, sig):
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from tornado import httputil
|
|||||||
from tornado.web import Application
|
from tornado.web import Application
|
||||||
|
|
||||||
from core.config import ALLOW_SPOTTING, ALLOW_UPSTREAM_SPOTTING, RECAPTCHA_SECRET_KEY
|
from core.config import ALLOW_SPOTTING, ALLOW_UPSTREAM_SPOTTING, RECAPTCHA_SECRET_KEY
|
||||||
from core.constants import CALL_ONLY_PATTERN, UNKNOWN_BAND
|
from core.constants import UNKNOWN_BAND
|
||||||
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
|
from core.sig_utils import get_ref_regex_for_sig
|
||||||
from core.utils import infer_band_from_freq, safe_json_dumps
|
from core.utils import infer_band_from_freq, safe_json_dumps
|
||||||
@@ -121,13 +121,13 @@ class APISpotHandler(tornado.web.RequestHandler):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Reject invalid-looking callsigns
|
# Reject invalid-looking callsigns
|
||||||
if not CALL_ONLY_PATTERN.match(spot.dx_call):
|
if not re.match(r"^[A-Za-z0-9/\-]*$", spot.dx_call):
|
||||||
self.set_status(422)
|
self.set_status(422)
|
||||||
self.write(safe_json_dumps(f"Error - '{spot.dx_call}' does not look like a valid callsign."))
|
self.write(safe_json_dumps(f"Error - '{spot.dx_call}' does not look like a valid callsign."))
|
||||||
self.set_header("Cache-Control", "no-store")
|
self.set_header("Cache-Control", "no-store")
|
||||||
self.set_header("Content-Type", "application/json")
|
self.set_header("Content-Type", "application/json")
|
||||||
return
|
return
|
||||||
if not CALL_ONLY_PATTERN.match(spot.de_call):
|
if not re.match(r"^[A-Za-z0-9/\-]*$", spot.de_call):
|
||||||
self.set_status(422)
|
self.set_status(422)
|
||||||
self.write(safe_json_dumps(f"Error - '{spot.de_call}' does not look like a valid callsign."))
|
self.write(safe_json_dumps(f"Error - '{spot.de_call}' does not look like a valid callsign."))
|
||||||
self.set_header("Cache-Control", "no-store")
|
self.set_header("Cache-Control", "no-store")
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
|
|||||||
self._credentials = None
|
self._credentials = None
|
||||||
super().__init__(application, request, **kwargs)
|
super().__init__(application, request, **kwargs)
|
||||||
|
|
||||||
def initialize(self, _sse_alert_broadcaster, web_server_metrics):
|
def initialize(self, sse_alert_broadcaster, web_server_metrics):
|
||||||
self._sse_alert_broadcaster = _sse_alert_broadcaster
|
self._sse_alert_broadcaster = sse_alert_broadcaster
|
||||||
self._web_server_metrics = web_server_metrics
|
self._web_server_metrics = web_server_metrics
|
||||||
|
|
||||||
def custom_headers(self):
|
def custom_headers(self):
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from tornado import httputil
|
|||||||
from tornado.web import Application
|
from tornado.web import Application
|
||||||
|
|
||||||
from core.call_lookup_helper import get_call_info
|
from core.call_lookup_helper import get_call_info
|
||||||
from core.constants import CALL_ONLY_PATTERN, SIGS
|
from core.constants import SIGS
|
||||||
from core.geo_utils import (
|
from core.geo_utils import (
|
||||||
lat_lon_for_grid_sw_corner_plus_size,
|
lat_lon_for_grid_sw_corner_plus_size,
|
||||||
lat_lon_to_cq_zone,
|
lat_lon_to_cq_zone,
|
||||||
@@ -55,7 +55,7 @@ class APILookupCallHandler(tornado.web.RequestHandler):
|
|||||||
# The "call" query param must exist and look like a callsign
|
# The "call" query param must exist and look like a callsign
|
||||||
if "call" in query_params:
|
if "call" in query_params:
|
||||||
call = str(query_params.get("call")).upper()
|
call = str(query_params.get("call")).upper()
|
||||||
if CALL_ONLY_PATTERN.match(call):
|
if re.match(r"^[A-Z0-9/\-]*$", call):
|
||||||
credentials = extract_credentials(self.request.headers)
|
credentials = extract_credentials(self.request.headers)
|
||||||
callsign_data = get_call_info(call, credentials)
|
callsign_data = get_call_info(call, credentials)
|
||||||
self.write(safe_json_dumps(callsign_data))
|
self.write(safe_json_dumps(callsign_data))
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from tornado import httputil
|
|||||||
from tornado.web import Application
|
from tornado.web import Application
|
||||||
|
|
||||||
from core.config import ALLOW_SPOTTING
|
from core.config import ALLOW_SPOTTING
|
||||||
from core.constants import CALL_ONLY_PATTERN, UNKNOWN_BAND
|
from core.constants import UNKNOWN_BAND
|
||||||
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
|
from core.sig_utils import get_ref_regex_for_sig
|
||||||
from core.utils import infer_band_from_freq, safe_json_dumps
|
from core.utils import infer_band_from_freq, safe_json_dumps
|
||||||
@@ -83,13 +83,13 @@ class V1APISpotHandler(tornado.web.RequestHandler):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Reject invalid-looking callsigns
|
# Reject invalid-looking callsigns
|
||||||
if not CALL_ONLY_PATTERN.match(spot.dx_call):
|
if not re.match(r"^[A-Za-z0-9/\-]*$", spot.dx_call):
|
||||||
self.set_status(422)
|
self.set_status(422)
|
||||||
self.write(safe_json_dumps(f"Error - '{spot.dx_call}' does not look like a valid callsign."))
|
self.write(safe_json_dumps(f"Error - '{spot.dx_call}' does not look like a valid callsign."))
|
||||||
self.set_header("Cache-Control", "no-store")
|
self.set_header("Cache-Control", "no-store")
|
||||||
self.set_header("Content-Type", "application/json")
|
self.set_header("Content-Type", "application/json")
|
||||||
return
|
return
|
||||||
if not CALL_ONLY_PATTERN.match(spot.de_call):
|
if not re.match(r"^[A-Za-z0-9/\-]*$", spot.de_call):
|
||||||
self.set_status(422)
|
self.set_status(422)
|
||||||
self.write(safe_json_dumps(f"Error - '{spot.de_call}' does not look like a valid callsign."))
|
self.write(safe_json_dumps(f"Error - '{spot.de_call}' does not look like a valid callsign."))
|
||||||
self.set_header("Cache-Control", "no-store")
|
self.set_header("Cache-Control", "no-store")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import tornado
|
|||||||
from tornado.httpclient import AsyncHTTPClient
|
from tornado.httpclient import AsyncHTTPClient
|
||||||
from tornado.httputil import HTTPHeaders
|
from tornado.httputil import HTTPHeaders
|
||||||
|
|
||||||
|
|
||||||
_LEGACY_PARAM_TO_HEADER_MAP = {
|
_LEGACY_PARAM_TO_HEADER_MAP = {
|
||||||
"qrz_username": "X-QRZ-Username",
|
"qrz_username": "X-QRZ-Username",
|
||||||
"qrz_password": "X-QRZ-Password",
|
"qrz_password": "X-QRZ-Password",
|
||||||
@@ -11,7 +12,6 @@ _LEGACY_PARAM_TO_HEADER_MAP = {
|
|||||||
"hamqth_session_id": "X-HamQTH-Session-ID",
|
"hamqth_session_id": "X-HamQTH-Session-ID",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class V1RedirectHandler(tornado.web.RequestHandler):
|
class V1RedirectHandler(tornado.web.RequestHandler):
|
||||||
"""Transparently proxies requests from the old API to the new one,
|
"""Transparently proxies requests from the old API to the new one,
|
||||||
returning whatever the v2 endpoint returns, for endpoints with no breaking changes."""
|
returning whatever the v2 endpoint returns, for endpoints with no breaking changes."""
|
||||||
@@ -34,7 +34,7 @@ class V1RedirectHandler(tornado.web.RequestHandler):
|
|||||||
response = await client.fetch(
|
response = await client.fetch(
|
||||||
new_url,
|
new_url,
|
||||||
method=self.request.method,
|
method=self.request.method,
|
||||||
headers=headers,
|
headers=self.request.headers,
|
||||||
body=None if self.request.method == "GET" else (self.request.body or b""),
|
body=None if self.request.method == "GET" else (self.request.body or b""),
|
||||||
raise_error=False,
|
raise_error=False,
|
||||||
follow_redirects=False,
|
follow_redirects=False,
|
||||||
@@ -60,4 +60,4 @@ class V1RedirectHandler(tornado.web.RequestHandler):
|
|||||||
await self._proxy(path)
|
await self._proxy(path)
|
||||||
|
|
||||||
async def post(self, path):
|
async def post(self, path):
|
||||||
await self._proxy(path)
|
await self._proxy(path)
|
||||||
+1
-1
@@ -33,7 +33,7 @@ if __name__ == "__main__":
|
|||||||
root.setLevel(LOG_LEVEL)
|
root.setLevel(LOG_LEVEL)
|
||||||
handler = logging.StreamHandler(sys.stdout)
|
handler = logging.StreamHandler(sys.stdout)
|
||||||
handler.setLevel(LOG_LEVEL)
|
handler.setLevel(LOG_LEVEL)
|
||||||
formatter = logging.Formatter("%(levelname)s : %(message)s")
|
formatter = logging.Formatter("%(asctime)s | %(levelname)7s | %(message)s", "%Y-%m-%d %H:%M:%S")
|
||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
root.handlers.clear()
|
root.handlers.clear()
|
||||||
root.addHandler(handler)
|
root.addHandler(handler)
|
||||||
|
|||||||
@@ -527,7 +527,8 @@ components:
|
|||||||
in: query
|
in: query
|
||||||
description: >
|
description: >
|
||||||
Limit the spots to only ones from one or more sources. To select more than one source, supply a
|
Limit the spots to only ones from one or more sources. To select more than one source, supply a
|
||||||
comma-separated list.
|
comma-separated list. Note that this enumeration is not exhaustive; in some cases the server owner may have
|
||||||
|
named some sources like clusters differently, and provided multiple options.
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Source"
|
$ref: "#/components/schemas/Source"
|
||||||
SpotSig:
|
SpotSig:
|
||||||
|
|||||||
@@ -522,7 +522,6 @@ function loadMapURLParams() {
|
|||||||
function setUpMap() {
|
function setUpMap() {
|
||||||
// Create map
|
// Create map
|
||||||
map = L.map('map', {
|
map = L.map('map', {
|
||||||
zoomControl: false,
|
|
||||||
minZoom: 2,
|
minZoom: 2,
|
||||||
maxZoom: 12
|
maxZoom: 12
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -345,6 +345,7 @@ const SIG_ICONS = {
|
|||||||
"SOTA": "fa-mountain-sun",
|
"SOTA": "fa-mountain-sun",
|
||||||
"WWFF": "fa-seedling",
|
"WWFF": "fa-seedling",
|
||||||
"GMA": "fa-person-hiking",
|
"GMA": "fa-person-hiking",
|
||||||
|
"GMA Island": "fa-person-hiking",
|
||||||
"WWBOTA": "fa-radiation",
|
"WWBOTA": "fa-radiation",
|
||||||
"HEMA": "fa-mound",
|
"HEMA": "fa-mound",
|
||||||
"IOTA": "fa-book-atlas",
|
"IOTA": "fa-book-atlas",
|
||||||
@@ -372,6 +373,7 @@ const SIG_NAMES = {
|
|||||||
"SOTA": "Summits on the Air",
|
"SOTA": "Summits on the Air",
|
||||||
"WWFF": "Worldwide Flora & Fauna",
|
"WWFF": "Worldwide Flora & Fauna",
|
||||||
"GMA": "Global Mountain Activity",
|
"GMA": "Global Mountain Activity",
|
||||||
|
"GMA Island": "Global Mountain Activity - Islands",
|
||||||
"WWBOTA": "Bunkers on the Air",
|
"WWBOTA": "Bunkers on the Air",
|
||||||
"HEMA": "Humps Excluding Marilyns Award",
|
"HEMA": "Humps Excluding Marilyns Award",
|
||||||
"IOTA": "Islands on the Air",
|
"IOTA": "Islands on the Air",
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
<p>Between the various data sources, the following Special Interest Groups (SIGs) are supported: Parks on the Air
|
<p>Between the various data sources, the following Special Interest Groups (SIGs) are supported: Parks on the Air
|
||||||
(POTA), Summits on the Air (SOTA), Worldwide Flora & Fauna (WWFF), Global Mountain Activity (GMA), Worldwide
|
(POTA), Summits on the Air (SOTA), Worldwide Flora & Fauna (WWFF), Global Mountain Activity (GMA), Worldwide
|
||||||
Bunkers on the Air (WWBOTA), HuMPs Excluding Marilyns Award (HEMA), Islands on the Air (IOTA), Mills on the Air
|
Bunkers on the Air (WWBOTA), HuMPs Excluding Marilyns Award (HEMA), Islands on the Air (IOTA), Mills on the Air
|
||||||
(MOTA), the Amateur Radio Lighthouse Socirty (ARLHS), International Lighthouse Lightship Weekend (ILLW), Silos
|
(MOTA), the Amateur Radio Lighthouse Society (ARLHS), International Lighthouse Lightship Weekend (ILLW), Silos
|
||||||
on the Air (SIOTA), World Castles Award (WCA), New Zealand on the Air (ZLOTA), Keith Roget Memorial National
|
on the Air (SIOTA), World Castles Award (WCA), New Zealand on the Air (ZLOTA), Keith Roget Memorial National
|
||||||
Parks Award (KRMNPA), South Australia National Parks and Conservation Parks Award (SANPCPA), Wainwrights on the
|
Parks Award (KRMNPA), South Australia National Parks and Conservation Parks Award (SANPCPA), Wainwrights on the
|
||||||
Air (WOTA), Beaches on the Air (BOTA), Lagos y Lagunas On the Air (LLOTA), Towers on the Air (WWTOTA), Tiles on
|
Air (WOTA), Beaches on the Air (BOTA), Lagos y Lagunas On the Air (LLOTA), Towers on the Air (WWTOTA), Tiles on
|
||||||
@@ -171,10 +171,6 @@
|
|||||||
section, Privacy, for details of how these are handled. If you are looking at the map and see lots of spots in
|
section, Privacy, for details of how these are handled. If you are looking at the map and see lots of spots in
|
||||||
the geographic centre of countries, allowing these lookups will help. For QRZ.com a paid account is required to
|
the geographic centre of countries, allowing these lookups will help. For QRZ.com a paid account is required to
|
||||||
look up operator locations.</p>
|
look up operator locations.</p>
|
||||||
<p>IOTA in particular causes a mapping problem, firstly that there is no official set of geodata beyond the names of
|
|
||||||
islands. We also have no way of telling whether priority should be given to IOTA reference or e.g. QRZ lookup,
|
|
||||||
for example for a G callsign in EU-005 Great Britain the QRZ home address is probably more accurate, but in
|
|
||||||
EU-002 Aaland islands, it's more likely a DXpedition and the IOTA would be more accurate.</p>
|
|
||||||
<h2 id="privacy" class="mt-4">Privacy</h2>
|
<h2 id="privacy" class="mt-4">Privacy</h2>
|
||||||
<p>Spothole collects no data about you on a permanent basis. All spots and alerts are "timed out" and deleted from
|
<p>Spothole collects no data about you on a permanent basis. All spots and alerts are "timed out" and deleted from
|
||||||
the system after a set interval, which by default is one hour for spots and one week for alerts.</p>
|
the system after a set interval, which by default is one hour for spots and one week for alerts.</p>
|
||||||
@@ -182,11 +178,13 @@
|
|||||||
filters. They are also stored in your browser's local storage, so that your preferences are remembered between
|
filters. They are also stored in your browser's local storage, so that your preferences are remembered between
|
||||||
sessions.</p>
|
sessions.</p>
|
||||||
<p>The data you provide can optionally include your login credentials for QRZ.com and HamQTH. You can provide these
|
<p>The data you provide can optionally include your login credentials for QRZ.com and HamQTH. You can provide these
|
||||||
in the "Data" menu of most pages. If you do, Spothole will augment the data it produces with lookups from these
|
in the "Your Data" menu of most pages. If you do, Spothole will augment the data it produces with lookups from these
|
||||||
services, which can for example provide more accurate markers on the map tab, and operator names when you mouse
|
services, which can for example provide more accurate markers on the map tab, and operator names when you mouse
|
||||||
over a DX callsign. Spothole will still work fine if you don't provide these. The values you enter are sent to
|
over a DX callsign. Spothole will still work fine if you don't provide these. The values you enter are sent to
|
||||||
Spothole via HTTPS so are protected in transit, though of course you do have to trust Spothole with this
|
Spothole via HTTPS so are protected in transit, though of course you do have to trust Spothole with this
|
||||||
sensitive data in order to use this feature.</p>
|
sensitive data in order to use this feature.</p>
|
||||||
|
<p>Any data you send as part of a query, such as your QRZ or HamQTH credentials, is used only for the lifetime of that
|
||||||
|
query and is not saved anywhere apart from your own device.</p>
|
||||||
<p>Spothole uses no trackers, no ads, and no cookies.</p>
|
<p>Spothole uses no trackers, no ads, and no cookies.</p>
|
||||||
{% if len(web_ui_options["support_button_html"]) > 0 %}
|
{% if len(web_ui_options["support_button_html"]) > 0 %}
|
||||||
<p><strong>Caveat: </strong> The owner of this server has chosen to inject their own content into the "spots" page.
|
<p><strong>Caveat: </strong> The owner of this server has chosen to inject their own content into the "spots" page.
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/add-spot.js?v=1786980624"></script>
|
<script src="/static/js/add-spot.js?v=1787382663"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-add-spot").addClass("active");
|
$("#nav-link-add-spot").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/alerts.js?v=1786980625"></script>
|
<script src="/static/js/alerts.js?v=1787382663"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-alerts").addClass("active");
|
$("#nav-link-alerts").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -76,8 +76,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/spotsbandsandmap.js?v=1786980624"></script>
|
<script src="/static/js/spotsbandsandmap.js?v=1787382663"></script>
|
||||||
<script src="/static/js/bands.js?v=1786980624"></script>
|
<script src="/static/js/bands.js?v=1787382663"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-bands").addClass("active");
|
$("#nav-link-bands").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
{% extends "skeleton.html" %}
|
{% extends "skeleton.html" %}
|
||||||
{% block head_extra %}
|
{% block head_extra %}
|
||||||
<link rel="stylesheet" href="/static/css/style.css?v=1786980624" type="text/css">
|
<link rel="stylesheet" href="/static/css/style.css?v=1787382663" type="text/css">
|
||||||
<link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
|
<link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
|
||||||
<link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet">
|
<link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet">
|
||||||
<link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
|
<link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
|
||||||
@@ -15,10 +15,10 @@
|
|||||||
window.fetchEventSource = fetchEventSource;
|
window.fetchEventSource = fetchEventSource;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="/static/js/utils.js?v=1786980624"></script>
|
<script src="/static/js/utils.js?v=1787382663"></script>
|
||||||
<script src="/static/js/ui-ham.js?v=1786980624"></script>
|
<script src="/static/js/ui-ham.js?v=1787382663"></script>
|
||||||
<script src="/static/js/geo.js?v=1786980624"></script>
|
<script src="/static/js/geo.js?v=1787382663"></script>
|
||||||
<script src="/static/js/common.js?v=1786980624"></script>
|
<script src="/static/js/common.js?v=1787382663"></script>
|
||||||
{% end %}
|
{% end %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|||||||
@@ -284,7 +284,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script>
|
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script>
|
||||||
<script src="/static/js/conditions.js?v=1786980624"></script>
|
<script src="/static/js/conditions.js?v=1787382663"></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>
|
||||||
|
|||||||
+2
-2
@@ -109,8 +109,8 @@
|
|||||||
<script src="/static/vendor/js/leaflet-cqzones.js"></script>
|
<script src="/static/vendor/js/leaflet-cqzones.js"></script>
|
||||||
<script src="/static/vendor/js/leaflet-workedallbritainireland.js" type="module"></script>
|
<script src="/static/vendor/js/leaflet-workedallbritainireland.js" type="module"></script>
|
||||||
|
|
||||||
<script src="/static/js/spotsbandsandmap.js?v=1786980625"></script>
|
<script src="/static/js/spotsbandsandmap.js?v=1787382663"></script>
|
||||||
<script src="/static/js/map.js?v=1786980625"></script>
|
<script src="/static/js/map.js?v=1787382663"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-map").addClass("active");
|
$("#nav-link-map").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -113,8 +113,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/spotsbandsandmap.js?v=1786980624"></script>
|
<script src="/static/js/spotsbandsandmap.js?v=1787382663"></script>
|
||||||
<script src="/static/js/spots.js?v=1786980624"></script>
|
<script src="/static/js/spots.js?v=1787382663"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-spots").addClass("active");
|
$("#nav-link-spots").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/status.js?v=1786980624"></script>
|
<script src="/static/js/status.js?v=1787382663"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$("#nav-link-status").addClass("active");
|
$("#nav-link-status").addClass("active");
|
||||||
|
|||||||
Reference in New Issue
Block a user