mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-20 14:27:42 +00:00
Start converting some enum-like strings to proper enums, plus global reformat
This commit is contained in:
@@ -276,7 +276,6 @@ DATA_MODES = [
|
||||
"MSK144",
|
||||
]
|
||||
ALL_MODES = CW_MODES + PHONE_MODES + DATA_MODES
|
||||
MODE_TYPES = ["CW", "PHONE", "DATA"]
|
||||
|
||||
# Mode aliases. Sometimes we get spots with a mode described in a different way that is effectively the same as a mode
|
||||
# we already know, or we want to normalise things for consistency. The lookup table for this is here. Incoming spots
|
||||
|
||||
@@ -11,6 +11,13 @@ class Continent(str, Enum):
|
||||
AN = "AN"
|
||||
|
||||
|
||||
class ModeType(str, Enum):
|
||||
PHONE = "PHONE"
|
||||
CW = "CW"
|
||||
DATA = "DATA"
|
||||
UNKNOWN = "UNKNOWN"
|
||||
|
||||
|
||||
class ModeSource(str, Enum):
|
||||
"""Where the mode data came from in a spot."""
|
||||
|
||||
|
||||
+4
-4
@@ -15,7 +15,7 @@ from core.constants import (
|
||||
UNKNOWN_BAND,
|
||||
)
|
||||
from core.data_store import DATA_STORE
|
||||
from core.enums import Continent
|
||||
from core.enums import Continent, ModeType
|
||||
from data.callsign import Callsign, LocationSourceForCallsign
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -44,11 +44,11 @@ def infer_mode_type_from_mode(mode):
|
||||
"""Infer a "mode family" from a mode."""
|
||||
|
||||
if mode.upper() in CW_MODES:
|
||||
return "CW"
|
||||
return ModeType.CW
|
||||
elif mode.upper() in PHONE_MODES:
|
||||
return "PHONE"
|
||||
return ModeType.PHONE
|
||||
elif mode.upper() in DATA_MODES:
|
||||
return "DATA"
|
||||
return ModeType.DATA
|
||||
else:
|
||||
if mode.upper() != "OTHER" and mode != "?":
|
||||
logger.warning(f"Found an unrecognised mode: {mode}. Developer should categorise this.")
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from core.enums import LocationSourceForCallsign
|
||||
from core.enums import Continent, LocationSourceForCallsign
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -30,14 +30,14 @@ class Callsign:
|
||||
# Country in which the callsign indicates they are operating
|
||||
country: str | None = None
|
||||
# Continent in which the callsign indicates they are operating
|
||||
continent: str | None = None
|
||||
continent: Continent | None = None
|
||||
# DXCC ID in which the callsign indicates they are operating
|
||||
dxcc_id: int | None = None
|
||||
# CQ zone in which the callsign indicates they are operating
|
||||
cq_zone: int | None = None
|
||||
# ITU zone in which the callsign indicates they are operating
|
||||
itu_zone: int | None = None
|
||||
# Location source. This can be "HOME QTH" or "DXCC" depending on which provider gave us a location
|
||||
# Location source
|
||||
location_source: LocationSourceForCallsign = LocationSourceForCallsign.NONE
|
||||
|
||||
def fully_populated(self):
|
||||
|
||||
+4
-4
@@ -12,7 +12,7 @@ from pyhamtools.locator import latlong_to_locator, locator_to_latlong
|
||||
from core.call_lookup_helper import get_call_info
|
||||
from core.config import MAX_SPOT_AGE
|
||||
from core.constants import MODE_ALIASES, PROPAGATION_MODES, SIGS
|
||||
from core.enums import Continent, LocationSourceForSpot, ModeSource
|
||||
from core.enums import Continent, LocationSourceForSpot, ModeSource, ModeType
|
||||
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_utils import (
|
||||
@@ -104,9 +104,9 @@ class Spot:
|
||||
|
||||
# Reported mode, such as SSB, PHONE, CW, FT8...
|
||||
mode: str | None = None
|
||||
# Inferred mode "family". One of "CW", "PHONE" or "DIGI".
|
||||
mode_type: str | None = None
|
||||
# Source of the mode information. "SPOT", "COMMENT", "BANDPLAN" or "NONE"
|
||||
# Inferred mode "family".
|
||||
mode_type: ModeType = ModeType.UNKNOWN
|
||||
# Source of the mode information.
|
||||
mode_source: ModeSource = ModeSource.NONE
|
||||
# Frequency, in Hz
|
||||
freq: float | None = None
|
||||
|
||||
+12
-11
@@ -23,15 +23,16 @@ You can replace `#main` with any other branch or tag reference, for example `#1.
|
||||
|
||||
Save the file. You will still need to create a copy of `config-example.yml` and name it `config.yml`, though with the
|
||||
Docker setup nothing has actually been downloaded yet, so you will have to copy the example from the repository some
|
||||
other way, e.g. [from the repo in a web browser](https://git.ianrenton.com/ian/spothole/src/branch/main/config-example.yml).
|
||||
other way,
|
||||
e.g. [from the repo in a web browser](https://git.ianrenton.com/ian/spothole/src/branch/main/config-example.yml).
|
||||
|
||||
With that in place, run `docker compose up` and you should be good to go. To detach, press `d` or run the command with
|
||||
the `-d` flag.
|
||||
|
||||
### nginx Reverse Proxy with Docker
|
||||
|
||||
In a containerised setup, it's typical to run an nginx reverse proxy in one container, alongside certbot for renewal
|
||||
of HTTPS certificates, and then applications like Spothole in a separate container. In this case, there are a couple of
|
||||
In a containerised setup, it's typical to run an nginx reverse proxy in one container, alongside certbot for renewal of
|
||||
HTTPS certificates, and then applications like Spothole in a separate container. In this case, there are a couple of
|
||||
variations of the docker compose file above, and the nginx reverse proxy configuration covered [here](./nginx.md), that
|
||||
you will want to make.
|
||||
|
||||
@@ -60,8 +61,8 @@ networks:
|
||||
```
|
||||
|
||||
In your nginx site configuration, you'll want to refer to the Spothole container directly, and drop the block that
|
||||
allows nginx to access static files directly, as these will be inaccessible in another container. So you may end up
|
||||
with something like:
|
||||
allows nginx to access static files directly, as these will be inaccessible in another container. So you may end up with
|
||||
something like:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
@@ -146,13 +147,13 @@ server {
|
||||
```
|
||||
|
||||
If desired, you could even change the port on which Spothole runs from 8080 to a plain 80, in which case your
|
||||
`proxy_pass` statements could drop the `:8080` suffix. Since Spothole is in a container, it can serve HTTP on port 80
|
||||
if desired, because it doesn't conflict with the host system.
|
||||
`proxy_pass` statements could drop the `:8080` suffix. Since Spothole is in a container, it can serve HTTP on port 80 if
|
||||
desired, because it doesn't conflict with the host system.
|
||||
|
||||
### Restoring the static files bypass
|
||||
|
||||
If you would still like to bypass Spothole's web server for the static files, and serve them with nginx, you can
|
||||
do. The easiest way is to run another nginx container to serve the files, so your Spothole `compose.yaml` becomes:
|
||||
If you would still like to bypass Spothole's web server for the static files, and serve them with nginx, you can do. The
|
||||
easiest way is to run another nginx container to serve the files, so your Spothole `compose.yaml` becomes:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
@@ -183,8 +184,8 @@ networks:
|
||||
external: true
|
||||
```
|
||||
|
||||
Then you can re-add the block that handles the `/static` path in your nginx reverse proxy config, but this time
|
||||
point it at the new container rather than at a filesystem path:
|
||||
Then you can re-add the block that handles the `/static` path in your nginx reverse proxy config, but this time point it
|
||||
at the new container rather than at a filesystem path:
|
||||
|
||||
```nginx
|
||||
# Load static assets from the spothole-static-nginx container
|
||||
|
||||
+2
-2
@@ -48,8 +48,8 @@ To navigate your way around the source code, this list may help.
|
||||
### Extending the server
|
||||
|
||||
Spothole is designed to be easily extensible. If you want to write your own spot provider, for example, simply add a
|
||||
module to the `providers.spot` package containing your class. (Currently, in order to be loaded correctly, the module (
|
||||
file) name should be the same as the class name, but lower case.)
|
||||
module to the `providers.spot` package containing your class. (Currently, in order to be loaded correctly, the module
|
||||
(file) name should be the same as the class name, but lower case.)
|
||||
|
||||
Your class should extend "SpotProvider"; if it operates by polling an HTTP Server on a timer, it can instead extend "
|
||||
HTTPSpotProvider" where some of the work is done for you.
|
||||
|
||||
+10
-15
@@ -2,8 +2,8 @@
|
||||
|
||||
Web servers generally serve their pages from port 80. However, it's best not to serve Spothole's web interface directly
|
||||
on port 80, as that requires root privileges on a Linux system. It also and prevents us using HTTPS to serve a secure
|
||||
site, since Spothole itself doesn't directly support acting as an HTTPS server. The normal solution to this is to use
|
||||
a "reverse proxy" setup, where a general web server handles HTTP and HTTP requests (to port 80 & 443 respectively), then
|
||||
site, since Spothole itself doesn't directly support acting as an HTTPS server. The normal solution to this is to use a
|
||||
"reverse proxy" setup, where a general web server handles HTTP and HTTP requests (to port 80 & 443 respectively), then
|
||||
passes on the request to the back-end application (in this case Spothole). nginx is a common choice for this general web
|
||||
server.
|
||||
|
||||
@@ -89,19 +89,14 @@ server {
|
||||
```
|
||||
|
||||
One further change you might want to make to the file above is the `add_header Access-Control-Allow-Origin` statements.
|
||||
These are what's used on
|
||||
my own Spothole server to make sure that other third-party web-based software can get the data from my instance, and
|
||||
applies to any endpoint underneath `/api`. If you want
|
||||
*your* Spothole instance to be set up the same way, so that others can write software in JavaScript that can access it,
|
||||
leave this intact. But if you want your Spothole instance to only be usable by scripts running on the web server you
|
||||
write,
|
||||
you can remove these lines. (Note that this doesn't stop other people writing *non-web-based* software that accesses
|
||||
your
|
||||
Spothole API—the enforcement of cross-origin headers only happens within the user's browser. If you need to lock
|
||||
your
|
||||
instance down so that no-one else can access it with *any* software, that's an aspect of nginx or firewall config that
|
||||
you will need
|
||||
to find help with elsewhere.)
|
||||
These are what's used on my own Spothole server to make sure that other third-party web-based software can get the data
|
||||
from my instance, and applies to any endpoint underneath `/api`. If you want *your* Spothole instance to be set up the
|
||||
same way, so that others can write software in JavaScript that can access it, leave this intact. But if you want your
|
||||
Spothole instance to only be usable by scripts running on the web server you write, you can remove these lines. (Note
|
||||
that this doesn't stop other people writing *non-web-based* software that accesses your Spothole API—the
|
||||
enforcement of cross-origin headers only happens within the user's browser. If you need to lock your instance down so
|
||||
that no-one else can access it with *any* software, that's an aspect of nginx or firewall config that you will need to
|
||||
find help with elsewhere.)
|
||||
|
||||
Now, make a symbolic link to enable the site:
|
||||
|
||||
|
||||
@@ -10,12 +10,10 @@ from requests_cache import CachedSession
|
||||
|
||||
from core.constants import HTTP_HEADERS
|
||||
from core.data_store import CACHE_DIR, DATA_STORE
|
||||
from core.enums import LocationSourceForCallsign, Continent
|
||||
from core.enums import Continent, LocationSourceForCallsign
|
||||
from core.url_data_cache import URLDataCache
|
||||
from data.callsign import Callsign
|
||||
from providers.callsigndata.api_query_callsign_data_provider import (
|
||||
APIQueryCallsignDataProvider,
|
||||
)
|
||||
from providers.callsigndata.api_query_callsign_data_provider import APIQueryCallsignDataProvider
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -20,14 +20,7 @@ class DTMBA(FileDownloadSIGRefDataProvider):
|
||||
split = row.split(";")
|
||||
ref_id = split[0]
|
||||
ref_name = split[1]
|
||||
new_data.append(
|
||||
SIGRef(
|
||||
sig=self.SIG,
|
||||
id=ref_id,
|
||||
name=ref_name,
|
||||
ref_type="Building"
|
||||
)
|
||||
)
|
||||
new_data.append(SIGRef(sig=self.SIG, id=ref_id, name=ref_name, ref_type="Building"))
|
||||
|
||||
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
||||
# the data in this case
|
||||
|
||||
@@ -76,7 +76,9 @@ class WOTA(HTTPSpotProvider):
|
||||
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)
|
||||
time = datetime.strptime(source_spot.pub_date.content, self.RSS_DATE_TIME_FORMAT).astimezone(
|
||||
pytz.UTC
|
||||
)
|
||||
|
||||
# Convert to our spot format
|
||||
spot = Spot(
|
||||
|
||||
@@ -8,8 +8,8 @@ from tornado import httputil
|
||||
from tornado.web import Application
|
||||
|
||||
from core.config import ALLOW_SPOTTING, MAX_SPOT_AGE
|
||||
from core.constants import ALL_MODES, BANDS, MODE_TYPES, PROPAGATION_MODES, SIGS
|
||||
from core.enums import Continent
|
||||
from core.constants import ALL_MODES, BANDS, PROPAGATION_MODES, SIGS
|
||||
from core.enums import Continent, ModeType
|
||||
from core.prometheus_metrics_handler import api_requests_counter
|
||||
from core.utils import safe_json_dumps
|
||||
|
||||
@@ -84,7 +84,7 @@ class APIOptionsHandler(tornado.web.RequestHandler):
|
||||
options = {
|
||||
"bands": BANDS,
|
||||
"modes": ALL_MODES,
|
||||
"mode_types": MODE_TYPES,
|
||||
"mode_types": [t.value for t in ModeType],
|
||||
"sigs": SIGS,
|
||||
"spot_providers": spot_providers,
|
||||
"spot_providers_enabled_by_default": spot_providers_enabled_by_default,
|
||||
|
||||
@@ -2,7 +2,6 @@ import tornado
|
||||
from tornado.httpclient import AsyncHTTPClient
|
||||
from tornado.httputil import HTTPHeaders
|
||||
|
||||
|
||||
_LEGACY_PARAM_TO_HEADER_MAP = {
|
||||
"qrz_username": "X-QRZ-Username",
|
||||
"qrz_password": "X-QRZ-Password",
|
||||
@@ -12,6 +11,7 @@ _LEGACY_PARAM_TO_HEADER_MAP = {
|
||||
"hamqth_session_id": "X-HamQTH-Session-ID",
|
||||
}
|
||||
|
||||
|
||||
class V1RedirectHandler(tornado.web.RequestHandler):
|
||||
"""Transparently proxies requests from the old API to the new one,
|
||||
returning whatever the v2 endpoint returns, for endpoints with no breaking changes."""
|
||||
@@ -60,4 +60,4 @@ class V1RedirectHandler(tornado.web.RequestHandler):
|
||||
await self._proxy(path)
|
||||
|
||||
async def post(self, path):
|
||||
await self._proxy(path)
|
||||
await self._proxy(path)
|
||||
|
||||
@@ -23,6 +23,7 @@ def _handle_legacy_params(handler):
|
||||
if value:
|
||||
handler.request.headers[header] = value
|
||||
|
||||
|
||||
class V1APISpotsHandler(APISpotsHandler):
|
||||
"""API request handler for /api/v1/spots (GET). Included in early Spothole v2 for backwards compatibility."""
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ info:
|
||||
* Added DTMBA, FEA, BIWOTA, COTA & PGA SIGs
|
||||
* Removed the distinction between LSB & USB (both will now show as SSB) and between the various digital voice modes, which will now show as DV.
|
||||
* Added `icon`, `region_flag` and `refs_globally_unique` to SIG information
|
||||
* Unknown mode types now return "UNKNOWN" not null
|
||||
|
||||
### 2.0
|
||||
|
||||
@@ -945,6 +946,7 @@ components:
|
||||
- CW
|
||||
- PHONE
|
||||
- DATA
|
||||
- UNKNOWN
|
||||
example: CW
|
||||
|
||||
ModeSource:
|
||||
|
||||
@@ -179,13 +179,15 @@
|
||||
filters. They are also stored in your browser's local storage, so that your preferences are remembered between
|
||||
sessions.</p>
|
||||
<p>The data you provide can optionally include your login credentials for QRZ.com and HamQTH. You can provide these
|
||||
in the "Your 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
|
||||
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
|
||||
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>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>
|
||||
{% 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.
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
{% block content %}
|
||||
|
||||
<div id="add-spot-intro-box" class="permanently-dismissible-box mt-3">
|
||||
<div class="alert alert-primary alert-dismissible fade show" role="alert"> <!-- TODO Remove when feature available -->
|
||||
<div class="alert alert-primary alert-dismissible fade show" role="alert">
|
||||
<!-- TODO Remove when feature available -->
|
||||
<i class="fa-solid fa-circle-info"></i> <strong>Adding spots to Spothole</strong><br/>This page is implemented
|
||||
as a proof of concept for adding spots to the Spothole system. Currently, spots added in this way are only
|
||||
visible within Spothole and are not sent "upstream" to DX clusters or xOTA spotting sites. The functionality
|
||||
@@ -76,7 +77,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/add-spot.js?v=1788331562"></script>
|
||||
<script src="/static/js/add-spot.js?v=1788334633"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-add-spot").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/alerts.js?v=1788331562"></script>
|
||||
<script src="/static/js/alerts.js?v=1788334633"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-alerts").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -76,8 +76,8 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1788331562"></script>
|
||||
<script src="/static/js/bands.js?v=1788331562"></script>
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1788334633"></script>
|
||||
<script src="/static/js/bands.js?v=1788334633"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-bands").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
+8
-7
@@ -1,6 +1,6 @@
|
||||
{% extends "skeleton.html" %}
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=1788331562" type="text/css">
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=1788334633" type="text/css">
|
||||
<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/solid-6.7.2.min.css" rel="stylesheet">
|
||||
@@ -11,14 +11,15 @@
|
||||
<script src="/static/vendor/js/tinycolor2-1.6.0.min.js"></script>
|
||||
|
||||
<script type="module">
|
||||
import { fetchEventSource } from '/static/vendor/js/fetch-event-source-2.0.1/index.js';
|
||||
window.fetchEventSource = fetchEventSource;
|
||||
import {fetchEventSource} from '/static/vendor/js/fetch-event-source-2.0.1/index.js';
|
||||
|
||||
window.fetchEventSource = fetchEventSource;
|
||||
</script>
|
||||
|
||||
<script src="/static/js/utils.js?v=1788331562"></script>
|
||||
<script src="/static/js/ui-ham.js?v=1788331562"></script>
|
||||
<script src="/static/js/geo.js?v=1788331562"></script>
|
||||
<script src="/static/js/common.js?v=1788331562"></script>
|
||||
<script src="/static/js/utils.js?v=1788334633"></script>
|
||||
<script src="/static/js/ui-ham.js?v=1788334633"></script>
|
||||
<script src="/static/js/geo.js?v=1788334633"></script>
|
||||
<script src="/static/js/common.js?v=1788334633"></script>
|
||||
{% end %}
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
|
||||
@@ -284,7 +284,7 @@
|
||||
</div>
|
||||
|
||||
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script>
|
||||
<script src="/static/js/conditions.js?v=1788331562"></script>
|
||||
<script src="/static/js/conditions.js?v=1788334633"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-conditions").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
+2
-2
@@ -113,8 +113,8 @@
|
||||
const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}";
|
||||
</script>
|
||||
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1788331562"></script>
|
||||
<script src="/static/js/map.js?v=1788331562"></script>
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1788334633"></script>
|
||||
<script src="/static/js/map.js?v=1788334633"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-map").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -113,8 +113,8 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1788331562"></script>
|
||||
<script src="/static/js/spots.js?v=1788331562"></script>
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1788334633"></script>
|
||||
<script src="/static/js/spots.js?v=1788334633"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-spots").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/status.js?v=1788331562"></script>
|
||||
<script src="/static/js/status.js?v=1788334633"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$("#nav-link-status").addClass("active");
|
||||
|
||||
Reference in New Issue
Block a user