mirror of
https://github.com/craigerl/aprsd.git
synced 2026-08-18 17:44:02 -04:00
Compare commits
34 Commits
v3.1.0
...
webchat-saved
| Author | SHA1 | Date | |
|---|---|---|---|
| 7292744a78 | |||
| 619b1b708e | |||
| 008b2ab09e | |||
| 4b56e99689 | |||
| 10bf04929e | |||
| a9e8050ae6 | |||
| 82f77b7a6a | |||
| 570fdb98a7 | |||
| 9582812041 | |||
| 859f904602 | |||
| 34311f0fbd | |||
| 2416f0ea1a | |||
| 377842c2ec | |||
| a8dd9ce012 | |||
| 1d6a667987 | |||
| 2e9a204c74 | |||
| f922b3f97b | |||
| 8dd3b05bb1 | |||
| e06305fceb | |||
| 33c7871dbe | |||
| b2f95b0f4e | |||
| ae9e4d31ad | |||
| 65a5a90458 | |||
| 182887c20a | |||
| f228144f4b | |||
| db9e1d23d1 | |||
| 986df391b2 | |||
| 3994235380 | |||
| 9ebf2f9a30 | |||
| 011cfc55e1 | |||
| e0c3c5cbbf | |||
| 26f354b3a9 | |||
| 922a6dbb35 | |||
| d03c4fc096 |
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
python-version: ["3.9", "3.10", "3.11"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10"]
|
||||
python-version: ["3.9", "3.10", "3.11"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
|
||||
@@ -6,8 +6,7 @@ on:
|
||||
aprsd_version:
|
||||
required: true
|
||||
options:
|
||||
- 2.5.9
|
||||
- 2.6.0
|
||||
- 3.0.0
|
||||
logLevel:
|
||||
description: 'Log level'
|
||||
required: true
|
||||
|
||||
@@ -5,7 +5,6 @@ repos:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- id: detect-private-key
|
||||
- id: check-merge-conflict
|
||||
- id: check-case-conflict
|
||||
|
||||
@@ -1,9 +1,50 @@
|
||||
CHANGES
|
||||
=======
|
||||
|
||||
* Removed admin\_web.py
|
||||
* Removed some RPC server log noise
|
||||
* Fixed admin page packet date
|
||||
* RPC Server logs the client IP on failed auth
|
||||
* Start keepalive thread first
|
||||
* fixed an issue in the mobile webchat
|
||||
* Added dupe checkig code to webchat mobile
|
||||
* click on the div after added
|
||||
* Webchat suppress to display of dupe messages
|
||||
* Convert webchat internet urls to local static urls
|
||||
* Make use of webchat gps config options
|
||||
* Added new webchat config section
|
||||
* fixed webchat logging.logformat typeoh
|
||||
|
||||
v3.1.3
|
||||
------
|
||||
|
||||
* prep for 3.1.3
|
||||
* Forcefully allow development webchat flask
|
||||
|
||||
v3.1.2
|
||||
------
|
||||
|
||||
* Updated Changelog for 3.1.2
|
||||
* Added support for ThirdParty packet types
|
||||
* Disable the Send GPS Beacon button
|
||||
* Removed adhoc ssl support in webchat
|
||||
|
||||
v3.1.1
|
||||
------
|
||||
|
||||
* Updated Changelog for v3.1.1
|
||||
* Fixed pep8 failures
|
||||
* re-enable USWeatherPlugin to use mapClick
|
||||
* Fix sending packets over KISS interface
|
||||
* Use config web\_ip for running admin ui from module
|
||||
* remove loop log
|
||||
* Max out the client reconnect backoff to 5
|
||||
* Update the Dockerfile
|
||||
|
||||
v3.1.0
|
||||
------
|
||||
|
||||
* Changelog updates for v3.1.0
|
||||
* Use CONF.admin.web\_port for single launch web admin
|
||||
* Fixed sio namespace registration
|
||||
* Update Dockerfile-dev to include uwsgi
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import time
|
||||
|
||||
import flask
|
||||
from flask.logging import default_handler
|
||||
import flask_classful
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from flask_socketio import Namespace, SocketIO
|
||||
from oslo_config import cfg
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
import aprsd
|
||||
from aprsd import cli_helper, client, conf, packets, plugin, threads
|
||||
from aprsd.log import rich as aprsd_logging
|
||||
from aprsd.rpc import client as aprsd_rpc_client
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
auth = HTTPBasicAuth()
|
||||
users = None
|
||||
app = None
|
||||
|
||||
|
||||
# HTTPBasicAuth doesn't work on a class method.
|
||||
# This has to be out here. Rely on the APRSDFlask
|
||||
# class to initialize the users from the config
|
||||
@auth.verify_password
|
||||
def verify_password(username, password):
|
||||
global users
|
||||
|
||||
if username in users and check_password_hash(users.get(username), password):
|
||||
return username
|
||||
|
||||
|
||||
class APRSDFlask(flask_classful.FlaskView):
|
||||
|
||||
def set_config(self):
|
||||
global users
|
||||
self.users = {}
|
||||
user = CONF.admin.user
|
||||
self.users[user] = generate_password_hash(CONF.admin.password)
|
||||
users = self.users
|
||||
|
||||
@auth.login_required
|
||||
def index(self):
|
||||
stats = self._stats()
|
||||
print(stats)
|
||||
LOG.debug(
|
||||
"watch list? {}".format(
|
||||
CONF.watch_list.callsigns,
|
||||
),
|
||||
)
|
||||
wl = aprsd_rpc_client.RPCClient().get_watch_list()
|
||||
if wl and wl.is_enabled():
|
||||
watch_count = len(wl)
|
||||
watch_age = wl.max_delta()
|
||||
else:
|
||||
watch_count = 0
|
||||
watch_age = 0
|
||||
|
||||
sl = aprsd_rpc_client.RPCClient().get_seen_list()
|
||||
if sl:
|
||||
seen_count = len(sl)
|
||||
else:
|
||||
seen_count = 0
|
||||
|
||||
pm = plugin.PluginManager()
|
||||
plugins = pm.get_plugins()
|
||||
plugin_count = len(plugins)
|
||||
|
||||
if CONF.aprs_network.enabled:
|
||||
transport = "aprs-is"
|
||||
aprs_connection = (
|
||||
"APRS-IS Server: <a href='http://status.aprs2.net' >"
|
||||
"{}</a>".format(stats["stats"]["aprs-is"]["server"])
|
||||
)
|
||||
else:
|
||||
# We might be connected to a KISS socket?
|
||||
if client.KISSClient.kiss_enabled():
|
||||
transport = client.KISSClient.transport()
|
||||
if transport == client.TRANSPORT_TCPKISS:
|
||||
aprs_connection = (
|
||||
"TCPKISS://{}:{}".format(
|
||||
CONF.kiss_tcp.host,
|
||||
CONF.kiss_tcp.port,
|
||||
)
|
||||
)
|
||||
elif transport == client.TRANSPORT_SERIALKISS:
|
||||
aprs_connection = (
|
||||
"SerialKISS://{}@{} baud".format(
|
||||
CONF.kiss_serial.device,
|
||||
CONF.kiss_serial.baudrate,
|
||||
)
|
||||
)
|
||||
|
||||
stats["transport"] = transport
|
||||
stats["aprs_connection"] = aprs_connection
|
||||
entries = conf.conf_to_dict()
|
||||
|
||||
return flask.render_template(
|
||||
"index.html",
|
||||
initial_stats=stats,
|
||||
aprs_connection=aprs_connection,
|
||||
callsign=CONF.callsign,
|
||||
version=aprsd.__version__,
|
||||
config_json=json.dumps(
|
||||
entries, indent=4,
|
||||
sort_keys=True, default=str,
|
||||
),
|
||||
watch_count=watch_count,
|
||||
watch_age=watch_age,
|
||||
seen_count=seen_count,
|
||||
plugin_count=plugin_count,
|
||||
)
|
||||
|
||||
@auth.login_required
|
||||
def messages(self):
|
||||
track = packets.PacketTrack()
|
||||
msgs = []
|
||||
for id in track:
|
||||
LOG.info(track[id].dict())
|
||||
msgs.append(track[id].dict())
|
||||
|
||||
return flask.render_template("messages.html", messages=json.dumps(msgs))
|
||||
|
||||
@auth.login_required
|
||||
def packets(self):
|
||||
packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
|
||||
if packet_list:
|
||||
packets = packet_list.get()
|
||||
tmp_list = []
|
||||
for pkt in packets:
|
||||
tmp_list.append(pkt.json)
|
||||
|
||||
return json.dumps(tmp_list)
|
||||
else:
|
||||
return json.dumps([])
|
||||
|
||||
@auth.login_required
|
||||
def plugins(self):
|
||||
pm = plugin.PluginManager()
|
||||
pm.reload_plugins()
|
||||
|
||||
return "reloaded"
|
||||
|
||||
@auth.login_required
|
||||
def save(self):
|
||||
"""Save the existing queue to disk."""
|
||||
track = packets.PacketTrack()
|
||||
track.save()
|
||||
return json.dumps({"messages": "saved"})
|
||||
|
||||
def _stats(self):
|
||||
track = aprsd_rpc_client.RPCClient().get_packet_track()
|
||||
now = datetime.datetime.now()
|
||||
|
||||
time_format = "%m-%d-%Y %H:%M:%S"
|
||||
|
||||
stats_dict = aprsd_rpc_client.RPCClient().get_stats_dict()
|
||||
if not stats_dict:
|
||||
stats_dict = {
|
||||
"aprsd": {},
|
||||
"aprs-is": {"server": ""},
|
||||
"messages": {
|
||||
"sent": 0,
|
||||
"received": 0,
|
||||
},
|
||||
"email": {
|
||||
"sent": 0,
|
||||
"received": 0,
|
||||
},
|
||||
"seen_list": {
|
||||
"sent": 0,
|
||||
"received": 0,
|
||||
},
|
||||
}
|
||||
|
||||
# Convert the watch_list entries to age
|
||||
wl = aprsd_rpc_client.RPCClient().get_watch_list()
|
||||
new_list = {}
|
||||
if wl:
|
||||
for call in wl.get_all():
|
||||
# call_date = datetime.datetime.strptime(
|
||||
# str(wl.last_seen(call)),
|
||||
# "%Y-%m-%d %H:%M:%S.%f",
|
||||
# )
|
||||
|
||||
# We have to convert the RingBuffer to a real list
|
||||
# so that json.dumps works.
|
||||
# pkts = []
|
||||
# for pkt in wl.get(call)["packets"].get():
|
||||
# pkts.append(pkt)
|
||||
|
||||
new_list[call] = {
|
||||
"last": wl.age(call),
|
||||
# "packets": pkts
|
||||
}
|
||||
|
||||
stats_dict["aprsd"]["watch_list"] = new_list
|
||||
packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
|
||||
rx = tx = 0
|
||||
if packet_list:
|
||||
rx = packet_list.total_rx()
|
||||
tx = packet_list.total_tx()
|
||||
stats_dict["packets"] = {
|
||||
"sent": tx,
|
||||
"received": rx,
|
||||
}
|
||||
if track:
|
||||
size_tracker = len(track)
|
||||
else:
|
||||
size_tracker = 0
|
||||
|
||||
result = {
|
||||
"time": now.strftime(time_format),
|
||||
"size_tracker": size_tracker,
|
||||
"stats": stats_dict,
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def stats(self):
|
||||
return json.dumps(self._stats())
|
||||
|
||||
|
||||
class LogUpdateThread(threads.APRSDThread):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("LogUpdate")
|
||||
|
||||
def loop(self):
|
||||
global socketio
|
||||
|
||||
if socketio:
|
||||
log_entries = aprsd_rpc_client.RPCClient().get_log_entries()
|
||||
|
||||
if log_entries:
|
||||
for entry in log_entries:
|
||||
socketio.emit(
|
||||
"log_entry", entry,
|
||||
namespace="/logs",
|
||||
)
|
||||
|
||||
time.sleep(5)
|
||||
return True
|
||||
|
||||
|
||||
class LoggingNamespace(Namespace):
|
||||
log_thread = None
|
||||
|
||||
def on_connect(self):
|
||||
global socketio
|
||||
socketio.emit(
|
||||
"connected", {"data": "/logs Connected"},
|
||||
namespace="/logs",
|
||||
)
|
||||
self.log_thread = LogUpdateThread()
|
||||
self.log_thread.start()
|
||||
|
||||
def on_disconnect(self):
|
||||
LOG.debug("LOG Disconnected")
|
||||
if self.log_thread:
|
||||
self.log_thread.stop()
|
||||
|
||||
|
||||
def setup_logging(flask_app, loglevel, quiet):
|
||||
flask_log = logging.getLogger("werkzeug")
|
||||
flask_app.logger.removeHandler(default_handler)
|
||||
flask_log.removeHandler(default_handler)
|
||||
|
||||
log_level = conf.log.LOG_LEVELS[loglevel]
|
||||
flask_log.setLevel(log_level)
|
||||
date_format = CONF.logging.date_format
|
||||
flask_log.disabled = True
|
||||
flask_app.logger.disabled = True
|
||||
|
||||
if CONF.logging.rich_logging:
|
||||
log_format = "%(message)s"
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
rh = aprsd_logging.APRSDRichHandler(
|
||||
show_thread=True, thread_width=15,
|
||||
rich_tracebacks=True, omit_repeated_times=False,
|
||||
)
|
||||
rh.setFormatter(log_formatter)
|
||||
flask_log.addHandler(rh)
|
||||
|
||||
log_file = CONF.logging.logfile
|
||||
|
||||
if log_file:
|
||||
log_format = CONF.logging.logformat
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
fh = RotatingFileHandler(
|
||||
log_file, maxBytes=(10248576 * 5),
|
||||
backupCount=4,
|
||||
)
|
||||
fh.setFormatter(log_formatter)
|
||||
flask_log.addHandler(fh)
|
||||
|
||||
|
||||
def init_flask(loglevel, quiet):
|
||||
global socketio
|
||||
|
||||
flask_app = flask.Flask(
|
||||
"aprsd",
|
||||
static_url_path="/static",
|
||||
static_folder="web/admin/static",
|
||||
template_folder="web/admin/templates",
|
||||
)
|
||||
setup_logging(flask_app, loglevel, quiet)
|
||||
server = APRSDFlask()
|
||||
server.set_config()
|
||||
flask_app.route("/", methods=["GET"])(server.index)
|
||||
flask_app.route("/stats", methods=["GET"])(server.stats)
|
||||
flask_app.route("/messages", methods=["GET"])(server.messages)
|
||||
flask_app.route("/packets", methods=["GET"])(server.packets)
|
||||
flask_app.route("/save", methods=["GET"])(server.save)
|
||||
flask_app.route("/plugins", methods=["GET"])(server.plugins)
|
||||
|
||||
socketio = SocketIO(
|
||||
flask_app, logger=False, engineio_logger=False,
|
||||
# async_mode="threading",
|
||||
)
|
||||
# import eventlet
|
||||
# eventlet.monkey_patch()
|
||||
gunicorn_logger = logging.getLogger("gunicorn.error")
|
||||
flask_app.logger.handlers = gunicorn_logger.handlers
|
||||
flask_app.logger.setLevel(gunicorn_logger.level)
|
||||
|
||||
socketio.on_namespace(LoggingNamespace("/logs"))
|
||||
return socketio, flask_app
|
||||
|
||||
|
||||
def create_app(config_file=None, log_level=None):
|
||||
global socketio
|
||||
global app
|
||||
|
||||
default_config_file = cli_helper.DEFAULT_CONFIG_FILE
|
||||
if not config_file:
|
||||
config_file = default_config_file
|
||||
|
||||
CONF(
|
||||
[], project="aprsd", version=aprsd.__version__,
|
||||
default_config_files=[config_file],
|
||||
)
|
||||
|
||||
if not log_level:
|
||||
log_level = CONF.logging.log_level
|
||||
|
||||
socketio, app = init_flask(log_level, False)
|
||||
setup_logging(app, log_level, False)
|
||||
return app
|
||||
|
||||
|
||||
if __name__ == "aprsd.flask":
|
||||
sio, app = create_app()
|
||||
+18
-7
@@ -24,7 +24,7 @@ TRANSPORT_SERIALKISS = "serialkiss"
|
||||
factory = None
|
||||
|
||||
|
||||
class Client:
|
||||
class Client(metaclass=trace.TraceWrapperMetaclass):
|
||||
"""Singleton client class that constructs the aprslib connection."""
|
||||
|
||||
_instance = None
|
||||
@@ -49,8 +49,10 @@ class Client:
|
||||
@property
|
||||
def client(self):
|
||||
if not self._client:
|
||||
LOG.info("Creating APRS client")
|
||||
self._client = self.setup_connection()
|
||||
if self.filter:
|
||||
LOG.info("Creating APRS client filter")
|
||||
self._client.set_filter(self.filter)
|
||||
return self._client
|
||||
|
||||
@@ -65,6 +67,9 @@ class Client:
|
||||
else:
|
||||
LOG.warning("Client not initialized, nothing to reset.")
|
||||
|
||||
# Recreate the client
|
||||
LOG.info(f"Creating new client {self.client}")
|
||||
|
||||
@abc.abstractmethod
|
||||
def setup_connection(self):
|
||||
pass
|
||||
@@ -84,7 +89,7 @@ class Client:
|
||||
pass
|
||||
|
||||
|
||||
class APRSISClient(Client):
|
||||
class APRSISClient(Client, metaclass=trace.TraceWrapperMetaclass):
|
||||
|
||||
_client = None
|
||||
|
||||
@@ -133,7 +138,6 @@ class APRSISClient(Client):
|
||||
"""APRS lib already decodes this."""
|
||||
return core.Packet.factory(args[0])
|
||||
|
||||
@trace.trace
|
||||
def setup_connection(self):
|
||||
user = CONF.aprs_network.login
|
||||
password = CONF.aprs_network.password
|
||||
@@ -159,14 +163,18 @@ class APRSISClient(Client):
|
||||
LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
|
||||
connected = False
|
||||
time.sleep(backoff)
|
||||
backoff = backoff * 2
|
||||
# Don't allow the backoff to go to inifinity.
|
||||
if backoff > 5:
|
||||
backoff = 5
|
||||
else:
|
||||
backoff += 1
|
||||
continue
|
||||
LOG.debug(f"Logging in to APRS-IS with user '{user}'")
|
||||
self._client = aprs_client
|
||||
return aprs_client
|
||||
|
||||
|
||||
class KISSClient(Client):
|
||||
class KISSClient(Client, metaclass=trace.TraceWrapperMetaclass):
|
||||
|
||||
_client = None
|
||||
|
||||
@@ -229,9 +237,12 @@ class KISSClient(Client):
|
||||
# LOG.debug(f"Decoding {msg}")
|
||||
|
||||
raw = aprslib.parse(str(frame))
|
||||
return core.Packet.factory(raw)
|
||||
packet = core.Packet.factory(raw)
|
||||
if isinstance(packet, core.ThirdParty):
|
||||
return packet.subpacket
|
||||
else:
|
||||
return packet
|
||||
|
||||
@trace.trace
|
||||
def setup_connection(self):
|
||||
self._client = kiss.KISS3Client()
|
||||
return self._client
|
||||
|
||||
+9
-15
@@ -81,27 +81,18 @@ class KISS3Client:
|
||||
LOG.debug("Start blocking KISS consumer")
|
||||
self._parse_callback = callback
|
||||
self.kiss.read(callback=self.parse_frame, min_frames=None)
|
||||
LOG.debug("END blocking KISS consumer")
|
||||
LOG.debug(f"END blocking KISS consumer {self.kiss}")
|
||||
|
||||
def send(self, packet):
|
||||
"""Send an APRS Message object."""
|
||||
|
||||
# payload = (':%-9s:%s' % (
|
||||
# msg.tocall,
|
||||
# payload
|
||||
# )).encode('US-ASCII'),
|
||||
# payload = str(msg).encode('US-ASCII')
|
||||
payload = None
|
||||
path = ["WIDE1-1", "WIDE2-1"]
|
||||
if isinstance(packet, core.AckPacket):
|
||||
msg_payload = f"ack{packet.msgNo}"
|
||||
elif isinstance(packet, core.Packet):
|
||||
payload = packet.raw.encode("US-ASCII")
|
||||
path = ["WIDE2-1"]
|
||||
if isinstance(packet, core.Packet):
|
||||
packet.prepare()
|
||||
payload = packet.payload.encode("US-ASCII")
|
||||
else:
|
||||
msg_payload = f"{packet.raw}{{{str(packet.msgNo)}"
|
||||
|
||||
if not payload:
|
||||
payload = (
|
||||
":{:<9}:{}".format(
|
||||
packet.to_call,
|
||||
@@ -109,9 +100,12 @@ class KISS3Client:
|
||||
)
|
||||
).encode("US-ASCII")
|
||||
|
||||
LOG.debug(f"Send '{payload}' TO KISS")
|
||||
LOG.debug(
|
||||
f"KISS Send '{payload}' TO '{packet.to_call}' From "
|
||||
f"'{packet.from_call}' with PATH '{path}'",
|
||||
)
|
||||
frame = Frame.ui(
|
||||
destination=packet.to_call,
|
||||
destination="APZ100",
|
||||
source=packet.from_call,
|
||||
path=path,
|
||||
info=payload,
|
||||
|
||||
@@ -94,6 +94,9 @@ def server(ctx, flush):
|
||||
packets.WatchList().load()
|
||||
packets.SeenList().load()
|
||||
|
||||
keepalive = threads.KeepAliveThread()
|
||||
keepalive.start()
|
||||
|
||||
rx_thread = rx.APRSDPluginRXThread(
|
||||
packet_queue=threads.packet_queue,
|
||||
)
|
||||
@@ -105,9 +108,6 @@ def server(ctx, flush):
|
||||
|
||||
packets.PacketTrack().restart()
|
||||
|
||||
keepalive = threads.KeepAliveThread()
|
||||
keepalive.start()
|
||||
|
||||
if CONF.rpc_settings.enabled:
|
||||
rpc = rpc_server.APRSDRPCThread()
|
||||
rpc.start()
|
||||
|
||||
+30
-11
@@ -163,7 +163,7 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
|
||||
|
||||
message = packet.get("message_text", None)
|
||||
msg = {
|
||||
"id": 0,
|
||||
"id": packet.msgNo,
|
||||
"ts": packet.get("timestamp", time.time()),
|
||||
"ack": False,
|
||||
"from": fromcall,
|
||||
@@ -239,6 +239,13 @@ def index():
|
||||
stats["transport"] = transport
|
||||
stats["aprs_connection"] = aprs_connection
|
||||
LOG.debug(f"initial stats = {stats}")
|
||||
latitude = CONF.webchat.latitude
|
||||
if latitude:
|
||||
latitude = float(CONF.webchat.latitude)
|
||||
|
||||
longitude = CONF.webchat.longitude
|
||||
if longitude:
|
||||
longitude = float(longitude)
|
||||
|
||||
return flask.render_template(
|
||||
html_template,
|
||||
@@ -246,6 +253,8 @@ def index():
|
||||
aprs_connection=aprs_connection,
|
||||
callsign=CONF.callsign,
|
||||
version=aprsd.__version__,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
)
|
||||
|
||||
|
||||
@@ -265,8 +274,12 @@ def _stats():
|
||||
time_format = "%m-%d-%Y %H:%M:%S"
|
||||
stats_dict = stats_obj.stats()
|
||||
# Webchat doesnt need these
|
||||
del stats_dict["aprsd"]["watch_list"]
|
||||
del stats_dict["aprsd"]["seen_list"]
|
||||
if "watch_list" in stats_dict["aprsd"]:
|
||||
del stats_dict["aprsd"]["watch_list"]
|
||||
if "seen_list" in stats_dict["aprsd"]:
|
||||
del stats_dict["aprsd"]["seen_list"]
|
||||
if "threads" in stats_dict["aprsd"]:
|
||||
del stats_dict["aprsd"]["threads"]
|
||||
# del stats_dict["email"]
|
||||
# del stats_dict["plugins"]
|
||||
# del stats_dict["messages"]
|
||||
@@ -373,7 +386,7 @@ def setup_logging(flask_app, loglevel, quiet):
|
||||
log_file = CONF.logging.logfile
|
||||
|
||||
if log_file:
|
||||
log_format = CONF.loging.logformat
|
||||
log_format = CONF.logging.logformat
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
fh = RotatingFileHandler(
|
||||
log_file, maxBytes=(10248576 * 5),
|
||||
@@ -423,8 +436,8 @@ def init_flask(loglevel, quiet):
|
||||
"--port",
|
||||
"port",
|
||||
show_default=True,
|
||||
default=80,
|
||||
help="Port to listen to web requests",
|
||||
default=None,
|
||||
help="Port to listen to web requests. This overrides the config.webchat.web_port setting.",
|
||||
)
|
||||
@click.pass_context
|
||||
@cli_helper.process_standard_options
|
||||
@@ -446,6 +459,8 @@ def webchat(ctx, flush, port):
|
||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||
user = CONF.admin.user
|
||||
users[user] = generate_password_hash(CONF.admin.password)
|
||||
if not port:
|
||||
port = CONF.webchat.web_port
|
||||
|
||||
# Initialize the client factory and create
|
||||
# The correct client object ready for use
|
||||
@@ -464,6 +479,10 @@ def webchat(ctx, flush, port):
|
||||
packets.WatchList()
|
||||
packets.SeenList()
|
||||
|
||||
keepalive = threads.KeepAliveThread()
|
||||
LOG.info("Start KeepAliveThread")
|
||||
keepalive.start()
|
||||
|
||||
socketio = init_flask(loglevel, quiet)
|
||||
rx_thread = rx.APRSDPluginRXThread(
|
||||
packet_queue=threads.packet_queue,
|
||||
@@ -475,15 +494,15 @@ def webchat(ctx, flush, port):
|
||||
)
|
||||
process_thread.start()
|
||||
|
||||
keepalive = threads.KeepAliveThread()
|
||||
LOG.info("Start KeepAliveThread")
|
||||
keepalive.start()
|
||||
LOG.info("Start socketio.run()")
|
||||
socketio.run(
|
||||
flask_app,
|
||||
ssl_context="adhoc",
|
||||
host=CONF.admin.web_ip,
|
||||
# This is broken for now after removing cryptography
|
||||
# and pyopenssl
|
||||
# ssl_context="adhoc",
|
||||
host=CONF.webchat.web_ip,
|
||||
port=port,
|
||||
allow_unsafe_werkzeug=True,
|
||||
)
|
||||
|
||||
LOG.info("WebChat exiting!!!! Bye.")
|
||||
|
||||
@@ -19,6 +19,10 @@ rpc_group = cfg.OptGroup(
|
||||
name="rpc_settings",
|
||||
title="RPC Settings for admin <--> web",
|
||||
)
|
||||
webchat_group = cfg.OptGroup(
|
||||
name="webchat",
|
||||
title="Settings specific to the webchat command",
|
||||
)
|
||||
|
||||
|
||||
aprsd_opts = [
|
||||
@@ -163,6 +167,29 @@ enabled_plugins_opts = [
|
||||
),
|
||||
]
|
||||
|
||||
webchat_opts = [
|
||||
cfg.IPOpt(
|
||||
"web_ip",
|
||||
default="0.0.0.0",
|
||||
help="The ip address to listen on",
|
||||
),
|
||||
cfg.PortOpt(
|
||||
"web_port",
|
||||
default=8001,
|
||||
help="The port to listen on",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"latitude",
|
||||
default=None,
|
||||
help="Latitude for the GPS Beacon button. If not set, the button will not be enabled.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"longitude",
|
||||
default=None,
|
||||
help="Longitude for the GPS Beacon button. If not set, the button will not be enabled.",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def register_opts(config):
|
||||
config.register_opts(aprsd_opts)
|
||||
@@ -173,6 +200,8 @@ def register_opts(config):
|
||||
config.register_opts(watch_list_opts, group=watch_list_group)
|
||||
config.register_group(rpc_group)
|
||||
config.register_opts(rpc_opts, group=rpc_group)
|
||||
config.register_group(webchat_group)
|
||||
config.register_opts(webchat_opts, group=webchat_group)
|
||||
|
||||
|
||||
def list_opts():
|
||||
@@ -181,4 +210,5 @@ def list_opts():
|
||||
admin_group.name: admin_opts,
|
||||
watch_list_group.name: watch_list_opts,
|
||||
rpc_group.name: rpc_opts,
|
||||
webchat_group.name: webchat_opts,
|
||||
}
|
||||
|
||||
+95
-39
@@ -25,6 +25,7 @@ PACKET_TYPE_OBJECT = "object"
|
||||
PACKET_TYPE_UNKNOWN = "unknown"
|
||||
PACKET_TYPE_STATUS = "status"
|
||||
PACKET_TYPE_BEACON = "beacon"
|
||||
PACKET_TYPE_THIRDPARTY = "thirdparty"
|
||||
PACKET_TYPE_UNCOMPRESSED = "uncompressed"
|
||||
|
||||
|
||||
@@ -57,6 +58,8 @@ class Packet(metaclass=abc.ABCMeta):
|
||||
# or holds the raw string from input packet
|
||||
raw: str = None
|
||||
raw_dict: dict = field(repr=False, default_factory=lambda: {})
|
||||
# Built by calling prepare(). raw needs this built first.
|
||||
payload: str = None
|
||||
|
||||
# Fields related to sending packets out
|
||||
send_count: int = field(repr=False, default=0)
|
||||
@@ -92,14 +95,28 @@ class Packet(metaclass=abc.ABCMeta):
|
||||
def prepare(self):
|
||||
"""Do stuff here that is needed prior to sending over the air."""
|
||||
# now build the raw message for sending
|
||||
self._build_payload()
|
||||
self._build_raw()
|
||||
|
||||
def _build_payload(self):
|
||||
"""The payload is the non headers portion of the packet."""
|
||||
msg = self._filter_for_send().rstrip("\n")
|
||||
self.payload = (
|
||||
f":{self.to_call.ljust(9)}"
|
||||
f":{msg}"
|
||||
)
|
||||
|
||||
def _build_raw(self):
|
||||
"""Build the self.raw string which is what is sent over the air."""
|
||||
self.raw = self._filter_for_send().rstrip("\n")
|
||||
"""Build the self.raw which is what is sent over the air."""
|
||||
self.raw = "{}>APZ100:{}".format(
|
||||
self.from_call,
|
||||
self.payload,
|
||||
)
|
||||
LOG.debug(f"_build_raw: payload '{self.payload}' raw '{self.raw}'")
|
||||
|
||||
@staticmethod
|
||||
def factory(raw_packet):
|
||||
"""Factory method to create a packet from a raw packet string."""
|
||||
raw = raw_packet
|
||||
raw["raw_dict"] = raw.copy()
|
||||
translate_fields = {
|
||||
@@ -118,6 +135,19 @@ class Packet(metaclass=abc.ABCMeta):
|
||||
packet_type = get_packet_type(raw)
|
||||
raw["packet_type"] = packet_type
|
||||
class_name = TYPE_LOOKUP[packet_type]
|
||||
if packet_type == PACKET_TYPE_THIRDPARTY:
|
||||
# We have an encapsulated packet!
|
||||
# So we need to decode it and return the inner packet
|
||||
# as the packet we are going to process.
|
||||
# This is a recursive call to the factory
|
||||
subpacket_raw = raw["subpacket"]
|
||||
subpacket = Packet.factory(subpacket_raw)
|
||||
del raw["subpacket"]
|
||||
# raw["subpacket"] = subpacket
|
||||
packet = dacite.from_dict(data_class=class_name, data=raw)
|
||||
packet.subpacket = subpacket
|
||||
return packet
|
||||
|
||||
if packet_type == PACKET_TYPE_UNKNOWN:
|
||||
# Try and figure it out here
|
||||
if "latitude" in raw:
|
||||
@@ -233,7 +263,7 @@ class PathPacket(Packet):
|
||||
path: List[str] = field(default_factory=list)
|
||||
via: str = None
|
||||
|
||||
def _build_raw(self):
|
||||
def _build_payload(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -245,13 +275,8 @@ class AckPacket(PathPacket):
|
||||
if self.response:
|
||||
LOG.warning("Response set!")
|
||||
|
||||
def _build_raw(self):
|
||||
"""Build the self.raw which is what is sent over the air."""
|
||||
self.raw = "{}>APZ100::{}:ack{}".format(
|
||||
self.from_call,
|
||||
self.to_call.ljust(9),
|
||||
self.msgNo,
|
||||
)
|
||||
def _build_payload(self):
|
||||
self.payload = f":{self.to_call.ljust(9)}:ack{self.msgNo}"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -262,13 +287,8 @@ class RejectPacket(PathPacket):
|
||||
if self.response:
|
||||
LOG.warning("Response set!")
|
||||
|
||||
def _build_raw(self):
|
||||
"""Build the self.raw which is what is sent over the air."""
|
||||
self.raw = "{}>APZ100::{} :rej{}".format(
|
||||
self.from_call,
|
||||
self.to_call.ljust(9),
|
||||
self.msgNo,
|
||||
)
|
||||
def _build_payload(self):
|
||||
self.payload = f":{self.to_call.ljust(9)} :rej{self.msgNo}"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -285,10 +305,8 @@ class MessagePacket(PathPacket):
|
||||
# We all miss George Carlin
|
||||
return re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message)
|
||||
|
||||
def _build_raw(self):
|
||||
"""Build the self.raw which is what is sent over the air."""
|
||||
self.raw = "{}>APZ100::{}:{}{{{}".format(
|
||||
self.from_call,
|
||||
def _build_payload(self):
|
||||
self.payload = ":{}:{}{{{}".format(
|
||||
self.to_call.ljust(9),
|
||||
self._filter_for_send().rstrip("\n"),
|
||||
str(self.msgNo),
|
||||
@@ -301,7 +319,7 @@ class StatusPacket(PathPacket):
|
||||
messagecapable: bool = False
|
||||
comment: str = None
|
||||
|
||||
def _build_raw(self):
|
||||
def _build_payload(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -400,16 +418,24 @@ class GPSPacket(PathPacket):
|
||||
time_zulu = result_utc_datetime.strftime("%d%H%M")
|
||||
return time_zulu
|
||||
|
||||
def _build_raw(self):
|
||||
def _build_payload(self):
|
||||
"""The payload is the non headers portion of the packet."""
|
||||
time_zulu = self._build_time_zulu()
|
||||
lat = self.latitude
|
||||
long = self.longitude
|
||||
self.payload = (
|
||||
f"@{time_zulu}z{lat}{self.symbol_table}"
|
||||
f"{long}{self.symbol}"
|
||||
)
|
||||
|
||||
if self.comment:
|
||||
self.payload = f"{self.payload}{self.comment}"
|
||||
|
||||
def _build_raw(self):
|
||||
self.raw = (
|
||||
f"{self.from_call}>{self.to_call},WIDE2-1:"
|
||||
f"@{time_zulu}z{self.latitude}{self.symbol_table}"
|
||||
f"{self.longitude}{self.symbol}"
|
||||
f"{self.payload}"
|
||||
)
|
||||
if self.comment:
|
||||
self.raw = f"{self.raw}{self.comment}"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -422,7 +448,7 @@ class MicEPacket(GPSPacket):
|
||||
# 0 to 360
|
||||
course: int = 0
|
||||
|
||||
def _build_raw(self):
|
||||
def _build_payload(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -436,6 +462,19 @@ class ObjectPacket(GPSPacket):
|
||||
# 0 to 360
|
||||
course: int = 0
|
||||
|
||||
def _build_payload(self):
|
||||
time_zulu = self._build_time_zulu()
|
||||
lat = self.convert_latitude(self.latitude)
|
||||
long = self.convert_longitude(self.longitude)
|
||||
|
||||
self.payload = (
|
||||
f"*{time_zulu}z{lat}{self.symbol_table}"
|
||||
f"{long}{self.symbol}"
|
||||
)
|
||||
|
||||
if self.comment:
|
||||
self.payload = f"{self.payload}{self.comment}"
|
||||
|
||||
def _build_raw(self):
|
||||
"""
|
||||
REPEAT builds packets like
|
||||
@@ -446,17 +485,11 @@ class ObjectPacket(GPSPacket):
|
||||
callsign is the station callsign for the object
|
||||
The frequency, uplink_tone, offset is part of the comment
|
||||
"""
|
||||
time_zulu = self._build_time_zulu()
|
||||
lat = self.convert_latitude(self.latitude)
|
||||
long = self.convert_longitude(self.longitude)
|
||||
|
||||
self.raw = (
|
||||
f"{self.from_call}>APZ100:;{self.to_call:9s}"
|
||||
f"*{time_zulu}z{lat}{self.symbol_table}"
|
||||
f"{long}{self.symbol}"
|
||||
f"{self.payload}"
|
||||
)
|
||||
if self.comment:
|
||||
self.raw = f"{self.raw}{self.comment}"
|
||||
|
||||
|
||||
@dataclass()
|
||||
@@ -474,7 +507,7 @@ class WeatherPacket(GPSPacket):
|
||||
pressure: float = 0.00
|
||||
comment: str = None
|
||||
|
||||
def _build_raw(self):
|
||||
def _build_payload(self):
|
||||
"""Build an uncompressed weather packet
|
||||
|
||||
Format =
|
||||
@@ -502,7 +535,6 @@ class WeatherPacket(GPSPacket):
|
||||
time_zulu = self._build_time_zulu()
|
||||
|
||||
contents = [
|
||||
f"{self.from_call}>{self.to_call},WIDE1-1,WIDE2-1:",
|
||||
f"@{time_zulu}z{self.latitude}{self.symbol_table}",
|
||||
f"{self.longitude}{self.symbol}",
|
||||
f"{self.wind_direction:03d}",
|
||||
@@ -523,11 +555,32 @@ class WeatherPacket(GPSPacket):
|
||||
# Barometric pressure (in tenths of millibars/tenths of hPascal)
|
||||
f"b{self.pressure:05.0f}",
|
||||
]
|
||||
|
||||
if self.comment:
|
||||
contents.append(self.comment)
|
||||
self.payload = "".join(contents)
|
||||
|
||||
self.raw = "".join(contents)
|
||||
def _build_raw(self):
|
||||
|
||||
self.raw = (
|
||||
f"{self.from_call}>{self.to_call},WIDE1-1,WIDE2-1:"
|
||||
f"{self.payload}"
|
||||
)
|
||||
|
||||
|
||||
class ThirdParty(Packet):
|
||||
# Holds the encapsulated packet
|
||||
subpacket: Packet = None
|
||||
|
||||
def __repr__(self):
|
||||
"""Build the repr version of the packet."""
|
||||
repr_str = (
|
||||
f"{self.__class__.__name__}:"
|
||||
f" From: {self.from_call} "
|
||||
f" To: {self.to_call} "
|
||||
f" Subpacket: {repr(self.subpacket)}"
|
||||
)
|
||||
|
||||
return repr_str
|
||||
|
||||
|
||||
TYPE_LOOKUP = {
|
||||
@@ -540,6 +593,7 @@ TYPE_LOOKUP = {
|
||||
PACKET_TYPE_STATUS: StatusPacket,
|
||||
PACKET_TYPE_BEACON: GPSPacket,
|
||||
PACKET_TYPE_UNKNOWN: Packet,
|
||||
PACKET_TYPE_THIRDPARTY: ThirdParty,
|
||||
}
|
||||
|
||||
|
||||
@@ -566,6 +620,8 @@ def get_packet_type(packet: dict):
|
||||
elif pkt_format == PACKET_TYPE_UNCOMPRESSED:
|
||||
if packet.get("symbol", None) == "_":
|
||||
packet_type = PACKET_TYPE_WX
|
||||
elif pkt_format == PACKET_TYPE_THIRDPARTY:
|
||||
packet_type = PACKET_TYPE_THIRDPARTY
|
||||
return packet_type
|
||||
|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@ def get_weather_gov_for_gps(lat, lon):
|
||||
)
|
||||
try:
|
||||
url2 = (
|
||||
# "https://forecast.weather.gov/MapClick.php?lat=%s"
|
||||
# "&lon=%s&FcstType=json" % (lat, lon)
|
||||
f"https://api.weather.gov/points/{lat},{lon}"
|
||||
"https://forecast.weather.gov/MapClick.php?lat=%s"
|
||||
"&lon=%s&FcstType=json" % (lat, lon)
|
||||
# f"https://api.weather.gov/points/{lat},{lon}"
|
||||
)
|
||||
LOG.debug(f"Fetching weather '{url2}'")
|
||||
response = requests.get(url2, headers=headers)
|
||||
|
||||
@@ -37,11 +37,18 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
|
||||
def process(self, packet):
|
||||
LOG.info("Weather Plugin")
|
||||
fromcall = packet.from_call
|
||||
message = packet.get("message_text", None)
|
||||
# message = packet.get("message_text", None)
|
||||
# ack = packet.get("msgNo", "0")
|
||||
a = re.search(r"^.*\s+(.*)", message)
|
||||
if a is not None:
|
||||
searchcall = a.group(1)
|
||||
searchcall = searchcall.upper()
|
||||
else:
|
||||
searchcall = fromcall
|
||||
api_key = CONF.aprs_fi.apiKey
|
||||
try:
|
||||
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
|
||||
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to fetch aprs.fi data {ex}")
|
||||
return "Failed to fetch aprs.fi location"
|
||||
|
||||
+11
-11
@@ -16,9 +16,17 @@ LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
def magic_word_authenticator(sock):
|
||||
client_ip = sock.getpeername()[0]
|
||||
magic = sock.recv(len(CONF.rpc_settings.magic_word)).decode()
|
||||
if magic != CONF.rpc_settings.magic_word:
|
||||
raise AuthenticationError(f"wrong magic word {magic}")
|
||||
LOG.error(
|
||||
f"wrong magic word passed from {client_ip} "
|
||||
"'{magic}' != '{CONF.rpc_settings.magic_word}'",
|
||||
)
|
||||
raise AuthenticationError(
|
||||
f"wrong magic word passed in '{magic}'"
|
||||
f" != '{CONF.rpc_settings.magic_word}'",
|
||||
)
|
||||
return sock, None
|
||||
|
||||
|
||||
@@ -49,51 +57,43 @@ class APRSDService(rpyc.Service):
|
||||
def on_connect(self, conn):
|
||||
# code that runs when a connection is created
|
||||
# (to init the service, if needed)
|
||||
LOG.info("Connected")
|
||||
LOG.info("RPC Client Connected")
|
||||
self._conn = conn
|
||||
|
||||
def on_disconnect(self, conn):
|
||||
# code that runs after the connection has already closed
|
||||
# (to finalize the service, if needed)
|
||||
LOG.info("Disconnected")
|
||||
LOG.info("RPC Client Disconnected")
|
||||
self._conn = None
|
||||
|
||||
@rpyc.exposed
|
||||
def get_stats(self):
|
||||
LOG.info("get_stats")
|
||||
stat = stats.APRSDStats()
|
||||
stats_dict = stat.stats()
|
||||
return_str = json.dumps(stats_dict, indent=4, sort_keys=True, default=str)
|
||||
LOG.info(f"get_stats: return size {len(return_str)}")
|
||||
return return_str
|
||||
|
||||
@rpyc.exposed
|
||||
def get_stats_obj(self):
|
||||
LOG.info("get_stats_obj")
|
||||
return stats.APRSDStats()
|
||||
|
||||
@rpyc.exposed
|
||||
def get_packet_list(self):
|
||||
LOG.info("get_packet_list")
|
||||
return packets.PacketList()
|
||||
|
||||
@rpyc.exposed
|
||||
def get_packet_track(self):
|
||||
LOG.info("get_packet_track")
|
||||
return packets.PacketTrack()
|
||||
|
||||
@rpyc.exposed
|
||||
def get_watch_list(self):
|
||||
LOG.info("get_watch_list")
|
||||
return packets.WatchList()
|
||||
|
||||
@rpyc.exposed
|
||||
def get_seen_list(self):
|
||||
LOG.info("get_seen_list")
|
||||
return packets.SeenList()
|
||||
|
||||
@rpyc.exposed
|
||||
def get_log_entries(self):
|
||||
LOG.info("get_log_entries")
|
||||
entries = log_monitor.LogEntries().get_all_and_purge()
|
||||
return json.dumps(entries, default=str)
|
||||
|
||||
@@ -246,8 +246,6 @@ class APRSDStats:
|
||||
},
|
||||
"plugins": plugin_stats,
|
||||
}
|
||||
LOG.debug(f"STATS = {stats}")
|
||||
LOG.info("APRSD Stats: DONE")
|
||||
return stats
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -111,14 +111,14 @@ function update_packets( data ) {
|
||||
pkt = JSON.parse(val);
|
||||
|
||||
update_watchlist_from_packet(pkt['from_call'], pkt);
|
||||
if ( packet_list.hasOwnProperty(pkt.timestamp) == false ) {
|
||||
if ( packet_list.hasOwnProperty(pkt['timestamp']) == false ) {
|
||||
// Store the packet
|
||||
packet_list[pkt.timestamp] = pkt;
|
||||
packet_list[pkt['timestamp']] = pkt;
|
||||
//ts_str = val["timestamp"].toString();
|
||||
//ts = ts_str.split(".")[0]*1000;
|
||||
ts = pkt.timestamp
|
||||
var d = new Date(ts).toLocaleDateString("en-US");
|
||||
var t = new Date(ts).toLocaleTimeString("en-US");
|
||||
ts = pkt['timestamp'] * 1000;
|
||||
var d = new Date(ts).toLocaleDateString();
|
||||
var t = new Date(ts).toLocaleTimeString();
|
||||
var from_call = pkt.from_call;
|
||||
if (from_call == our_callsign) {
|
||||
title_id = 'title_tx';
|
||||
|
||||
+1311
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,9 +1,17 @@
|
||||
|
||||
function init_gps() {
|
||||
console.log("init_gps Called.")
|
||||
console.log("latitude: "+latitude)
|
||||
console.log("longitude: "+longitude)
|
||||
$("#send_beacon").click(function() {
|
||||
console.log("Send a beacon!")
|
||||
getLocation();
|
||||
if (!isNaN(latitude) && !isNaN(longitude)) {
|
||||
// webchat admin has hard coded lat/long in the config file
|
||||
showPosition({'coords': {'latitude': latitude, 'longitude': longitude}})
|
||||
} else {
|
||||
// Try to get the current location from the browser
|
||||
getLocation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
var cleared = false;
|
||||
var callsign_list = {};
|
||||
var message_list = {};
|
||||
var from_msg_list = {};
|
||||
const socket = io("/sendmsg");
|
||||
|
||||
function size_dict(d){c=0; for (i in d) ++c; return c}
|
||||
|
||||
function init_chat() {
|
||||
const socket = io("/sendmsg");
|
||||
socket.on('connect', function () {
|
||||
console.log("Connected to socketio");
|
||||
});
|
||||
@@ -43,7 +44,6 @@ function init_chat() {
|
||||
}
|
||||
socket.emit("send", msg);
|
||||
$('#message').val('');
|
||||
$('#to_call').val('');
|
||||
});
|
||||
|
||||
init_gps();
|
||||
@@ -118,8 +118,9 @@ function append_message_html(callsign, msg_html, new_callsign) {
|
||||
$(divname).animate({scrollTop: $(divname)[0].scrollHeight}, "slow");
|
||||
}
|
||||
|
||||
function create_message_html(time, from, to, message, ack) {
|
||||
msg_html = '<div class="item">';
|
||||
function create_message_html(time, from, to, message, ack, msg) {
|
||||
div_id = from + "_" + msg.id;
|
||||
msg_html = '<div class="item" id="'+div_id+'">';
|
||||
msg_html += '<div class="tiny text">'+time+'</div>';
|
||||
msg_html += '<div class="middle aligned content">';
|
||||
msg_html += '<div class="tiny red header">'+from+'</div>';
|
||||
@@ -136,6 +137,13 @@ function create_message_html(time, from, to, message, ack) {
|
||||
return msg_html
|
||||
}
|
||||
|
||||
function flash_message(msg) {
|
||||
// Callback function to bring a hidden box back
|
||||
id = msg.from + "_" + msg.id;
|
||||
var msgid = $('#'+id);
|
||||
msgid.effect("pulsate", { times:3 }, 2000);
|
||||
}
|
||||
|
||||
function sent_msg(msg) {
|
||||
var msgsdiv = $("#sendMsgsDiv");
|
||||
|
||||
@@ -147,12 +155,27 @@ function sent_msg(msg) {
|
||||
var d = new Date(ts).toLocaleDateString("en-US")
|
||||
var t = new Date(ts).toLocaleTimeString("en-US")
|
||||
|
||||
msg_html = create_message_html(t, msg['from'], msg['to'], msg['message'], ack_id);
|
||||
msg_html = create_message_html(t, msg['from'], msg['to'], msg['message'], ack_id, msg);
|
||||
append_message(msg['to'], msg, msg_html);
|
||||
}
|
||||
|
||||
function from_msg(msg) {
|
||||
var msgsdiv = $("#sendMsgsDiv");
|
||||
console.log(msg);
|
||||
if (!from_msg_list.hasOwnProperty(msg.from)) {
|
||||
from_msg_list[msg.from] = new Array();
|
||||
}
|
||||
|
||||
if (msg.id in from_msg_list[msg.from]) {
|
||||
// We already have this message
|
||||
console.log("We already have this message " + msg);
|
||||
// Do some flashy thing?
|
||||
flash_message(msg);
|
||||
return false
|
||||
} else {
|
||||
console.log("Adding message " + msg.id + " to " + msg.from);
|
||||
from_msg_list[msg.from][msg.id] = msg
|
||||
}
|
||||
|
||||
// We have an existing entry
|
||||
ts_str = msg["ts"].toString();
|
||||
@@ -164,7 +187,7 @@ function from_msg(msg) {
|
||||
var t = new Date(ts).toLocaleTimeString("en-US")
|
||||
|
||||
from = msg['from']
|
||||
msg_html = create_message_html(t, from, false, msg['message'], false);
|
||||
msg_html = create_message_html(t, from, false, msg['message'], false, msg);
|
||||
append_message(from, msg, msg_html);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
var cleared = false;
|
||||
var callsign_list = {};
|
||||
var message_list = {};
|
||||
var from_msg_list = {};
|
||||
const socket = io("/sendmsg");
|
||||
|
||||
MSG_TYPE_TX = "tx";
|
||||
MSG_TYPE_RX = "rx";
|
||||
MSG_TYPE_ACK = "ack";
|
||||
|
||||
function size_dict(d){c=0; for (i in d) ++c; return c}
|
||||
|
||||
function init_chat() {
|
||||
@@ -15,24 +20,28 @@ function init_chat() {
|
||||
});
|
||||
|
||||
socket.on("sent", function(msg) {
|
||||
if (cleared == false) {
|
||||
if (cleared === false) {
|
||||
console.log("CLEARING #msgsTabsDiv");
|
||||
var msgsdiv = $("#msgsTabsDiv");
|
||||
msgsdiv.html('')
|
||||
cleared = true
|
||||
msgsdiv.html('');
|
||||
cleared = true;
|
||||
}
|
||||
msg["type"] = MSG_TYPE_TX;
|
||||
sent_msg(msg);
|
||||
});
|
||||
|
||||
socket.on("ack", function(msg) {
|
||||
update_msg(msg);
|
||||
msg["type"] = MSG_TYPE_ACK;
|
||||
ack_msg(msg);
|
||||
});
|
||||
|
||||
socket.on("new", function(msg) {
|
||||
if (cleared == false) {
|
||||
if (cleared === false) {
|
||||
var msgsdiv = $("#msgsTabsDiv");
|
||||
msgsdiv.html('')
|
||||
cleared = true
|
||||
cleared = true;
|
||||
}
|
||||
msg["type"] = MSG_TYPE_RX;
|
||||
from_msg(msg);
|
||||
});
|
||||
|
||||
@@ -46,32 +55,124 @@ function init_chat() {
|
||||
});
|
||||
|
||||
init_gps();
|
||||
// Try and load any existing chat threads from last time
|
||||
init_messages();
|
||||
}
|
||||
|
||||
function add_callsign(callsign) {
|
||||
/* Ensure a callsign exists in the left hand nav */
|
||||
function message_ts_id(msg) {
|
||||
//Create a 'id' from the message timestamp
|
||||
ts_str = msg["ts"].toString();
|
||||
ts = ts_str.split(".")[0]*1000;
|
||||
id = ts_str.split('.')[0];
|
||||
return {'timestamp': ts, 'id': id};
|
||||
}
|
||||
|
||||
if (callsign in callsign_list) {
|
||||
return false
|
||||
}
|
||||
function time_ack_from_msg(msg) {
|
||||
// Return the time and ack_id from a message
|
||||
ts_id = message_ts_id(msg);
|
||||
ts = ts_id['timestamp'];
|
||||
id = ts_id['id'];
|
||||
ack_id = "ack_" + id
|
||||
|
||||
var d = new Date(ts).toLocaleDateString("en-US")
|
||||
var t = new Date(ts).toLocaleTimeString("en-US")
|
||||
return {'time': t, 'date': d, 'ack_id': ack_id};
|
||||
}
|
||||
|
||||
function save_data() {
|
||||
// Save the relevant data to local storage
|
||||
localStorage.setItem('callsign_list', JSON.stringify(callsign_list));
|
||||
localStorage.setItem('message_list', JSON.stringify(message_list));
|
||||
}
|
||||
|
||||
function init_messages() {
|
||||
// This tries to load any previous conversations from local storage
|
||||
callsign_list = JSON.parse(localStorage.getItem('callsign_list'));
|
||||
message_list = JSON.parse(localStorage.getItem('message_list'));
|
||||
console.log("init_messages");
|
||||
if (callsign_list == null) {
|
||||
callsign_list = {};
|
||||
}
|
||||
if (message_list == null) {
|
||||
message_list = {};
|
||||
}
|
||||
console.log(callsign_list);
|
||||
console.log(message_list);
|
||||
|
||||
// Now loop through each callsign and add the tabs
|
||||
first_callsign = null;
|
||||
for (callsign in callsign_list) {
|
||||
console.log("Adding callsign " + callsign);
|
||||
if (first_callsign === null) {
|
||||
first_callsign = callsign;
|
||||
console.log("first_callsign " + first_callsign)
|
||||
}
|
||||
create_callsign_tab(callsign);
|
||||
}
|
||||
// and then populate the messages in order
|
||||
for (callsign in message_list) {
|
||||
new_callsign = true;
|
||||
cleared = true;
|
||||
for (id in message_list[callsign]) {
|
||||
msg = message_list[callsign][id];
|
||||
info = time_ack_from_msg(msg);
|
||||
t = info['time'];
|
||||
ack_id = false;
|
||||
acked = false;
|
||||
if (msg['type'] == MSG_TYPE_TX) {
|
||||
ack_id = info['ack_id'];
|
||||
acked = msg['ack'];
|
||||
}
|
||||
msg_html = create_message_html(t, msg['from'], msg['to'], msg['message'], ack_id, msg, acked);
|
||||
append_message_html(callsign, msg_html, new_callsign);
|
||||
new_callsign = false;
|
||||
}
|
||||
}
|
||||
|
||||
//Click on the very first tab
|
||||
if (first_callsign !== null) {
|
||||
click_div = '#'+tab_string(first_callsign);
|
||||
var click_timer = setTimeout(function() {
|
||||
console.log("Click on first tab " + click_div);
|
||||
$(click_div).click();
|
||||
clearTimeout(click_timer);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
function create_callsign_tab(callsign) {
|
||||
//Create the html for the callsign tab and insert it into the DOM
|
||||
console.log("create_callsign_tab " + callsign)
|
||||
var callsignTabs = $("#callsignTabs");
|
||||
tab_name = tab_string(callsign);
|
||||
tab_content = tab_content_name(callsign);
|
||||
divname = content_divname(callsign);
|
||||
|
||||
item_html = '<div class="tablinks" id="'+tab_name+'" onclick="openCallsign(event, \''+callsign+'\');">'+callsign+'</div>';
|
||||
console.log(item_html);
|
||||
callsignTabs.append(item_html);
|
||||
}
|
||||
|
||||
function add_callsign(callsign) {
|
||||
/* Ensure a callsign exists in the left hand nav */
|
||||
if (callsign in callsign_list) {
|
||||
return false
|
||||
}
|
||||
create_callsign_tab(callsign);
|
||||
callsign_list[callsign] = true;
|
||||
return true
|
||||
}
|
||||
|
||||
function append_message(callsign, msg, msg_html) {
|
||||
console.log("append_message " + callsign + " " + msg + " " + msg_html);
|
||||
new_callsign = false
|
||||
if (!message_list.hasOwnProperty(callsign)) {
|
||||
message_list[callsign] = new Array();
|
||||
//message_list[callsign] = new Array();
|
||||
message_list[callsign] = {};
|
||||
}
|
||||
message_list[callsign].push(msg);
|
||||
ts_id = message_ts_id(msg);
|
||||
id = ts_id['id']
|
||||
message_list[callsign][id] = msg;
|
||||
|
||||
// Find the right div to place the html
|
||||
new_callsign = add_callsign(callsign);
|
||||
@@ -97,27 +198,43 @@ function content_divname(callsign) {
|
||||
|
||||
function append_message_html(callsign, msg_html, new_callsign) {
|
||||
var msgsTabs = $('#msgsTabsDiv');
|
||||
console.log("append_message_html " + callsign + " " + msg_html + " " + new_callsign);
|
||||
divname_str = tab_content_name(callsign);
|
||||
divname = content_divname(callsign);
|
||||
if (new_callsign) {
|
||||
// we have to add a new DIV
|
||||
console.log("new callsign, create msg_div_html ");
|
||||
msg_div_html = '<div class="tabcontent" id="'+divname_str+'" style="height:450px;">'+msg_html+'</div>';
|
||||
console.log(msg_div_html);
|
||||
msgsTabs.append(msg_div_html);
|
||||
} else {
|
||||
var msgDiv = $(divname);
|
||||
console.log("Appending ("+ msg_html + ") to " + divname);
|
||||
console.log(msgDiv);
|
||||
msgDiv.append(msg_html);
|
||||
console.log(msgDiv);
|
||||
}
|
||||
console.log("divname " + divname);
|
||||
console.log($(divname).length);
|
||||
|
||||
$(divname).animate({scrollTop: $(divname)[0].scrollHeight}, "slow");
|
||||
if ($(divname).length > 0) {
|
||||
$(divname).animate({scrollTop: $(divname)[0].scrollHeight}, "slow");
|
||||
}
|
||||
$(divname).trigger('click');
|
||||
}
|
||||
|
||||
function create_message_html(time, from, to, message, ack) {
|
||||
msg_html = '<div class="item">';
|
||||
function create_message_html(time, from, to, message, ack_id, msg, acked=false) {
|
||||
div_id = from + "_" + msg.id;
|
||||
msg_html = '<div class="item" id="'+div_id+'">';
|
||||
msg_html += '<div class="tiny text">'+time+'</div>';
|
||||
msg_html += '<div class="middle aligned content">';
|
||||
msg_html += '<div class="tiny red header">'+from+'</div>';
|
||||
if (ack) {
|
||||
msg_html += '<i class="thumbs down outline icon" id="' + ack_id + '" data-content="Waiting for ACK"></i>';
|
||||
if (ack_id) {
|
||||
if (acked) {
|
||||
msg_html += '<div class="middle aligned content"><i class="thumbs up outline icon" id="' + ack_id + '" data-content="Message ACKed"></i></div>';
|
||||
} else {
|
||||
msg_html += '<div class="middle aligned content"><i class="thumbs down outline icon" id="' + ack_id + '" data-content="Waiting for ACK"></i></div>';
|
||||
}
|
||||
} else {
|
||||
msg_html += '<i class="phone volume icon" data-content="Recieved Message"></i>';
|
||||
}
|
||||
@@ -129,60 +246,90 @@ function create_message_html(time, from, to, message, ack) {
|
||||
return msg_html
|
||||
}
|
||||
|
||||
function flash_message(msg) {
|
||||
// Callback function to bring a hidden box back
|
||||
id = msg.from + "_" + msg.id;
|
||||
var msgid = $('#'+id);
|
||||
msgid.effect("pulsate", { times:3 }, 2000);
|
||||
}
|
||||
|
||||
|
||||
function sent_msg(msg) {
|
||||
var msgsdiv = $("#sendMsgsDiv");
|
||||
info = time_ack_from_msg(msg);
|
||||
t = info['time'];
|
||||
ack_id = info['ack_id'];
|
||||
|
||||
ts_str = msg["ts"].toString();
|
||||
ts = ts_str.split(".")[0]*1000;
|
||||
id = ts_str.split('.')[0]
|
||||
ack_id = "ack_" + id
|
||||
|
||||
var d = new Date(ts).toLocaleDateString("en-US")
|
||||
var t = new Date(ts).toLocaleTimeString("en-US")
|
||||
|
||||
msg_html = create_message_html(t, msg['from'], msg['to'], msg['message'], ack_id);
|
||||
msg_html = create_message_html(t, msg['from'], msg['to'], msg['message'], ack_id, msg, false);
|
||||
append_message(msg['to'], msg, msg_html);
|
||||
save_data();
|
||||
}
|
||||
|
||||
function from_msg(msg) {
|
||||
var msgsdiv = $("#sendMsgsDiv");
|
||||
console.log(msg);
|
||||
if (!from_msg_list.hasOwnProperty(msg.from)) {
|
||||
from_msg_list[msg.from] = new Array();
|
||||
}
|
||||
|
||||
// We have an existing entry
|
||||
ts_str = msg["ts"].toString();
|
||||
ts = ts_str.split(".")[0]*1000;
|
||||
id = ts_str.split('.')[0]
|
||||
ack_id = "ack_" + id
|
||||
if (msg.id in from_msg_list[msg.from]) {
|
||||
// We already have this message
|
||||
console.log("We already have this message " + msg);
|
||||
// Do some flashy thing?
|
||||
flash_message(msg);
|
||||
return false
|
||||
} else {
|
||||
console.log("Adding message " + msg.id + " to " + msg.from);
|
||||
from_msg_list[msg.from][msg.id] = msg
|
||||
}
|
||||
|
||||
var d = new Date(ts).toLocaleDateString("en-US")
|
||||
var t = new Date(ts).toLocaleTimeString("en-US")
|
||||
info = time_ack_from_msg(msg);
|
||||
t = info['time'];
|
||||
ack_id = info['ack_id'];
|
||||
|
||||
from = msg['from']
|
||||
msg_html = create_message_html(t, from, false, msg['message'], false);
|
||||
msg_html = create_message_html(t, from, false, msg['message'], false, msg, false);
|
||||
append_message(from, msg, msg_html);
|
||||
save_data();
|
||||
}
|
||||
|
||||
function update_msg(msg) {
|
||||
var msgsdiv = $("#sendMsgsDiv");
|
||||
// We have an existing entry
|
||||
ts_str = msg["ts"].toString();
|
||||
id = ts_str.split('.')[0]
|
||||
pretty_id = "pretty_" + id
|
||||
loader_id = "loader_" + id
|
||||
ack_id = "ack_" + id
|
||||
span_id = "span_" + id
|
||||
function ack_msg(msg) {
|
||||
// Acknowledge a message
|
||||
console.log("ack_msg ");
|
||||
console.log(msg);
|
||||
console.log(message_list);
|
||||
|
||||
// We have an existing entry
|
||||
ts_id = message_ts_id(msg);
|
||||
console.log(ts_id)
|
||||
id = ts_id['id'];
|
||||
//Mark the message as acked
|
||||
callsign = msg['to'];
|
||||
// Ensure the message_list has this callsign
|
||||
if (!message_list.hasOwnProperty(callsign)) {
|
||||
console.log("No message_list for " + callsign);
|
||||
return false
|
||||
}
|
||||
// Ensure the message_list has this id
|
||||
if (!message_list[callsign].hasOwnProperty(id)) {
|
||||
console.log("No message_list for " + callsign + " " + id);
|
||||
return false
|
||||
}
|
||||
console.log("Marking message as acked " + callsign + " " + id)
|
||||
if (message_list[callsign][id]['ack'] == true) {
|
||||
console.log("Message already acked");
|
||||
return false;
|
||||
}
|
||||
message_list[callsign][id]['ack'] = true;
|
||||
ack_id = "ack_" + id
|
||||
|
||||
if (msg['ack'] == true) {
|
||||
var ack_div = $('#' + ack_id);
|
||||
//ack_div.removeClass('thumbs up outline icon');
|
||||
ack_div.removeClass('thumbs down outline icon');
|
||||
ack_div.addClass('thumbs up outline icon');
|
||||
}
|
||||
|
||||
if (msg['ack'] == true) {
|
||||
var loader_div = $('#' + loader_id);
|
||||
var ack_div = $('#' + ack_id);
|
||||
loader_div.removeClass('ui active inline loader');
|
||||
loader_div.addClass('ui disabled loader');
|
||||
ack_div.removeClass('thumbs up outline icon');
|
||||
ack_div.addClass('thumbs up outline icon');
|
||||
}
|
||||
|
||||
$('.ui.accordion').accordion('refresh');
|
||||
$('.ui.accordion').accordion('refresh');
|
||||
save_data();
|
||||
}
|
||||
|
||||
function callsign_select(callsign) {
|
||||
@@ -197,16 +344,21 @@ function reset_Tabs() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function openCallsign(evt, callsign) {
|
||||
// This is called when a callsign tab is clicked
|
||||
var i, tabcontent, tablinks;
|
||||
|
||||
tab_content = tab_content_name(callsign);
|
||||
|
||||
tabcontent = document.getElementsByClassName("tabcontent");
|
||||
console.log(tabcontent);
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
tablinks = document.getElementsByClassName("tablinks");
|
||||
console.log(tablinks);
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,13 +1,19 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
|
||||
<script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script>
|
||||
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> -->
|
||||
<script src="/static/js/upstream/jquery.min.js"></script>
|
||||
<!-- <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css"> -->
|
||||
<link rel="stylesheet" href="/static/css/upstream/jquery-ui.css">
|
||||
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script> -->
|
||||
<script src="/static/js/upstream/jquery-ui.min.js"></script>
|
||||
<!-- <script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script> -->
|
||||
<script src="/static/js/upstream/socket.io.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.js"></script>
|
||||
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.css"> -->
|
||||
<link rel="stylesheet" href="/static/css/upstream/semantic.min.css">
|
||||
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.js"></script> -->
|
||||
<script src="/static/js/upstream/semantic.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/static/css/index.css">
|
||||
<link rel="stylesheet" href="/static/css/tabs.css">
|
||||
@@ -17,6 +23,8 @@
|
||||
|
||||
<script type="text/javascript">
|
||||
var initial_stats = {{ initial_stats|tojson|safe }};
|
||||
var latitude = parseFloat('{{ latitude|safe }}');
|
||||
var longitude = parseFloat('{{ longitude|safe }}');
|
||||
|
||||
var memory_chart = null
|
||||
var message_chart = null
|
||||
@@ -26,6 +34,19 @@
|
||||
start_update();
|
||||
init_chat();
|
||||
reset_Tabs();
|
||||
|
||||
console.log("latitude", latitude);
|
||||
console.log("longitude", longitude);
|
||||
|
||||
if (isNaN(latitude) || isNaN(longitude) && location.protocol != 'https:') {
|
||||
// Have to disable the beacon button.
|
||||
$('#send_beacon').prop('disabled', true);
|
||||
}
|
||||
|
||||
$("#wipe_local").click(function() {
|
||||
console.log('Wipe local storage');
|
||||
localStorage.clear();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
@@ -64,6 +85,7 @@
|
||||
</div>
|
||||
<input type="submit" name="submit" class="ui button" id="send_msg" value="Send" />
|
||||
<button type="button" class="ui button" id="send_beacon" value="Send GPS Beacon">Send GPS Beacon</button>
|
||||
<button type="button" class="ui button" id="wipe_local" value="wipe local storage">Wipe LocalStorage</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,7 +95,6 @@
|
||||
<div class="tab" id="callsignTabs"></div>
|
||||
</div>
|
||||
<div class="ten wide column ui raised segment" id="msgsTabsDiv" style="height:450px;padding:0px;">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
|
||||
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
|
||||
<script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script>
|
||||
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> -->
|
||||
<script src="/static/js/upstream/jquery.min.js"></script>
|
||||
<!-- <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css"> -->
|
||||
<link rel="stylesheet" href="/static/css/upstream/jquery-ui.css">
|
||||
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script> -->
|
||||
<script src="/static/js/upstream/jquery-ui.min.js"></script>
|
||||
<!-- <script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script> -->
|
||||
<script src="/static/js/upstream/socket.io.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.js"></script>
|
||||
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.css"> -->
|
||||
<link rel="stylesheet" href="/static/css/upstream/semantic.min.css">
|
||||
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.js"></script> -->
|
||||
<script src="/static/js/upstream/semantic.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/static/css/index.css">
|
||||
<script src="/static/js/main.js"></script>
|
||||
@@ -16,6 +22,8 @@
|
||||
|
||||
<script type="text/javascript">
|
||||
var initial_stats = {{ initial_stats|tojson|safe }};
|
||||
var latitude = parseFloat('{{ latitude|safe }}');
|
||||
var longitude = parseFloat('{{ longitude|safe }}');
|
||||
|
||||
var memory_chart = null
|
||||
var message_chart = null
|
||||
@@ -24,6 +32,14 @@
|
||||
console.log(initial_stats);
|
||||
start_update();
|
||||
init_chat();
|
||||
|
||||
console.log("latitude", latitude);
|
||||
console.log("longitude", longitude);
|
||||
|
||||
if (isNaN(latitude) || isNaN(longitude) && location.protocol != 'https:') {
|
||||
// Have to disable the beacon button.
|
||||
$('#send_beacon').prop('disabled', true);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
+6
-1
@@ -330,7 +330,12 @@ if __name__ == "__main__":
|
||||
setup_logging(app, log_level)
|
||||
sio.register_namespace(LoggingNamespace("/logs"))
|
||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||
app.run(threaded=True, debug=True, port=CONF.admin.web_port)
|
||||
app.run(
|
||||
threaded=True,
|
||||
debug=False,
|
||||
port=CONF.admin.web_port,
|
||||
host=CONF.admin.web_ip,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "uwsgi_file_aprsd_wsgi":
|
||||
|
||||
+27
-27
@@ -4,7 +4,7 @@
|
||||
#
|
||||
# pip-compile --annotation-style=line dev-requirements.in
|
||||
#
|
||||
add-trailing-comma==3.0.0 # via gray
|
||||
add-trailing-comma==3.0.1 # via gray
|
||||
alabaster==0.7.13 # via sphinx
|
||||
attrs==23.1.0 # via jsonschema, referencing
|
||||
autoflake==1.5.3 # via gray
|
||||
@@ -13,75 +13,75 @@ black==23.7.0 # via gray
|
||||
build==0.10.0 # via pip-tools
|
||||
cachetools==5.3.1 # via tox
|
||||
certifi==2023.7.22 # via requests
|
||||
cfgv==3.3.1 # via pre-commit
|
||||
chardet==5.1.0 # via tox
|
||||
cfgv==3.4.0 # via pre-commit
|
||||
chardet==5.2.0 # via tox
|
||||
charset-normalizer==3.2.0 # via requests
|
||||
click==8.1.6 # via black, pip-tools
|
||||
colorama==0.4.6 # via tox
|
||||
commonmark==0.9.1 # via rich
|
||||
configargparse==1.7 # via gray
|
||||
coverage[toml]==7.2.7 # via pytest-cov
|
||||
coverage[toml]==7.3.0 # via pytest-cov
|
||||
distlib==0.3.7 # via virtualenv
|
||||
docutils==0.20.1 # via sphinx
|
||||
exceptiongroup==1.1.2 # via pytest
|
||||
exceptiongroup==1.1.3 # via pytest
|
||||
filelock==3.12.2 # via tox, virtualenv
|
||||
fixit==0.1.4 # via gray
|
||||
flake8==6.0.0 # via -r dev-requirements.in, fixit, pep8-naming
|
||||
flake8==6.1.0 # via -r dev-requirements.in, fixit, pep8-naming
|
||||
gray==0.13.0 # via -r dev-requirements.in
|
||||
identify==2.5.26 # via pre-commit
|
||||
idna==3.4 # via requests
|
||||
imagesize==1.4.1 # via sphinx
|
||||
importlib-resources==6.0.0 # via fixit
|
||||
importlib-resources==6.0.1 # via fixit
|
||||
iniconfig==2.0.0 # via pytest
|
||||
isort==5.12.0 # via -r dev-requirements.in, gray
|
||||
jinja2==3.1.2 # via sphinx
|
||||
jsonschema==4.18.4 # via fixit
|
||||
jsonschema==4.19.0 # via fixit
|
||||
jsonschema-specifications==2023.7.1 # via jsonschema
|
||||
libcst==1.0.1 # via fixit
|
||||
markupsafe==2.1.3 # via jinja2
|
||||
mccabe==0.7.0 # via flake8
|
||||
mypy==1.4.1 # via -r dev-requirements.in
|
||||
mypy==1.5.0 # via -r dev-requirements.in
|
||||
mypy-extensions==1.0.0 # via black, mypy, typing-inspect
|
||||
nodeenv==1.8.0 # via pre-commit
|
||||
packaging==23.1 # via black, build, pyproject-api, pytest, sphinx, tox
|
||||
pathspec==0.11.1 # via black
|
||||
pathspec==0.11.2 # via black
|
||||
pep8-naming==0.13.3 # via -r dev-requirements.in
|
||||
pip-tools==7.1.0 # via -r dev-requirements.in
|
||||
platformdirs==3.9.1 # via black, tox, virtualenv
|
||||
pip-tools==7.3.0 # via -r dev-requirements.in
|
||||
platformdirs==3.10.0 # via black, tox, virtualenv
|
||||
pluggy==1.2.0 # via pytest, tox
|
||||
pre-commit==3.3.3 # via -r dev-requirements.in
|
||||
pycodestyle==2.10.0 # via flake8
|
||||
pyflakes==3.0.1 # via autoflake, flake8
|
||||
pygments==2.15.1 # via rich, sphinx
|
||||
pycodestyle==2.11.0 # via flake8
|
||||
pyflakes==3.1.0 # via autoflake, flake8
|
||||
pygments==2.16.1 # via rich, sphinx
|
||||
pyproject-api==1.5.3 # via tox
|
||||
pyproject-hooks==1.0.0 # via build
|
||||
pytest==7.4.0 # via -r dev-requirements.in, pytest-cov
|
||||
pytest-cov==4.1.0 # via -r dev-requirements.in
|
||||
pyupgrade==3.9.0 # via gray
|
||||
pyupgrade==3.10.1 # via gray
|
||||
pyyaml==6.0.1 # via fixit, libcst, pre-commit
|
||||
referencing==0.30.0 # via jsonschema, jsonschema-specifications
|
||||
referencing==0.30.2 # via jsonschema, jsonschema-specifications
|
||||
requests==2.31.0 # via sphinx
|
||||
rich==12.6.0 # via gray
|
||||
rpds-py==0.9.2 # via jsonschema, referencing
|
||||
snowballstemmer==2.2.0 # via sphinx
|
||||
sphinx==7.0.1 # via -r dev-requirements.in
|
||||
sphinxcontrib-applehelp==1.0.4 # via sphinx
|
||||
sphinxcontrib-devhelp==1.0.2 # via sphinx
|
||||
sphinxcontrib-htmlhelp==2.0.1 # via sphinx
|
||||
sphinx==7.1.2 # via -r dev-requirements.in, sphinxcontrib-applehelp, sphinxcontrib-devhelp, sphinxcontrib-htmlhelp, sphinxcontrib-qthelp, sphinxcontrib-serializinghtml
|
||||
sphinxcontrib-applehelp==1.0.7 # via sphinx
|
||||
sphinxcontrib-devhelp==1.0.5 # via sphinx
|
||||
sphinxcontrib-htmlhelp==2.0.4 # via sphinx
|
||||
sphinxcontrib-jsmath==1.0.1 # via sphinx
|
||||
sphinxcontrib-qthelp==1.0.3 # via sphinx
|
||||
sphinxcontrib-serializinghtml==1.1.5 # via sphinx
|
||||
tokenize-rt==5.1.0 # via add-trailing-comma, pyupgrade
|
||||
sphinxcontrib-qthelp==1.0.6 # via sphinx
|
||||
sphinxcontrib-serializinghtml==1.1.8 # via sphinx
|
||||
tokenize-rt==5.2.0 # via add-trailing-comma, pyupgrade
|
||||
toml==0.10.2 # via autoflake
|
||||
tomli==2.0.1 # via black, build, coverage, mypy, pip-tools, pyproject-api, pyproject-hooks, pytest, tox
|
||||
tox==4.6.4 # via -r dev-requirements.in
|
||||
tox==4.8.0 # via -r dev-requirements.in
|
||||
typing-extensions==4.7.1 # via libcst, mypy, typing-inspect
|
||||
typing-inspect==0.9.0 # via libcst
|
||||
unify==0.5 # via gray
|
||||
untokenize==0.1.1 # via unify
|
||||
urllib3==2.0.4 # via requests
|
||||
virtualenv==20.24.1 # via pre-commit, tox
|
||||
wheel==0.41.0 # via pip-tools
|
||||
virtualenv==20.24.3 # via pre-commit, tox
|
||||
wheel==0.41.1 # via pip-tools
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# pip
|
||||
|
||||
+44
-45
@@ -1,62 +1,61 @@
|
||||
#FROM python:3-bullseye as aprsd
|
||||
FROM ubuntu:22.04 as aprsd
|
||||
FROM python:3.11-slim as build
|
||||
|
||||
# Dockerfile for building a container during aprsd development.
|
||||
|
||||
ARG UID
|
||||
ARG GID
|
||||
ARG TZ
|
||||
ARG VERSION=3.0.3
|
||||
ARG BUILDX_QEMU_ENV
|
||||
ENV APRS_USER=aprs
|
||||
ENV HOME=/home/aprs
|
||||
ARG VERSION=3.1.0
|
||||
ENV TZ=${TZ:-US/Eastern}
|
||||
ENV UID=${UID:-1000}
|
||||
ENV GID=${GID:-1000}
|
||||
ENV LC_ALL=C.UTF-8
|
||||
ENV LANG=C.UTF-8
|
||||
ENV APRSD_PIP_VERSION=${VERSION}
|
||||
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt update
|
||||
RUN apt install -y git build-essential
|
||||
RUN apt install -y libffi-dev python3-dev libssl-dev libxml2-dev libxslt-dev
|
||||
RUN apt install -y python3 python3-pip python3-dev python3-lxml python3-setuptools-rust
|
||||
RUN apt install -y libffi-dev cargo pkg-config
|
||||
|
||||
RUN pip3 install -U pip
|
||||
RUN pip3 install -U setuptools_rust
|
||||
ENV PIP_DEFAULT_TIMEOUT=100 \
|
||||
# Allow statements and log messages to immediately appear
|
||||
PYTHONUNBUFFERED=1 \
|
||||
# disable a pip version check to reduce run-time & log-spam
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||
# cache is useless in docker image, so disable to reduce image size
|
||||
PIP_NO_CACHE_DIR=1
|
||||
|
||||
|
||||
RUN addgroup --gid $GID $APRS_USER
|
||||
RUN useradd -m -u $UID -g $APRS_USER $APRS_USER
|
||||
RUN set -ex \
|
||||
# Create a non-root user
|
||||
&& addgroup --system --gid 1001 appgroup \
|
||||
&& useradd --uid 1001 --gid 1001 -s /usr/bin/bash -m -d /app appuser \
|
||||
# Upgrade the package index and install security upgrades
|
||||
&& apt-get update \
|
||||
&& apt-get upgrade -y \
|
||||
&& apt-get install -y git build-essential curl libffi-dev \
|
||||
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \
|
||||
# Install dependencies
|
||||
# Clean up
|
||||
&& apt-get autoremove -y \
|
||||
&& apt-get clean -y
|
||||
|
||||
# Handle an extremely specific issue when building the cryptography package for
|
||||
# 32-bit architectures within QEMU running on a 64-bit host (issue #30).
|
||||
RUN if [ "${BUILDX_QEMU_ENV}" = "true" -a "$(getconf LONG_BIT)" = "32" ]; then \
|
||||
pip3 install -U cryptography==3.3.2; \
|
||||
else \
|
||||
pip3 install cryptography ;\
|
||||
fi
|
||||
|
||||
# Ensure /config is there with a default config file
|
||||
USER root
|
||||
# Install aprsd
|
||||
### Final stage
|
||||
FROM build as final
|
||||
WORKDIR /app
|
||||
|
||||
RUN pip3 install aprsd==$APRSD_PIP_VERSION
|
||||
RUN mkdir -p /config
|
||||
RUN pip install gevent uwsgi
|
||||
RUN which aprsd
|
||||
RUN mkdir /config
|
||||
RUN chown -R appuser:appgroup /app
|
||||
RUN chown -R appuser:appgroup /config
|
||||
USER appuser
|
||||
RUN which aprsd
|
||||
RUN aprsd sample-config > /config/aprsd.conf
|
||||
RUN chown -R $APRS_USER:$APRS_USER /config
|
||||
RUN chown -R $APRS_USER:$APRS_USER $HOME
|
||||
|
||||
# override this to run another configuration
|
||||
ENV CONF default
|
||||
VOLUME ["/config", "/plugins"]
|
||||
ADD bin/run.sh /app
|
||||
ADD bin/listen.sh /app
|
||||
ADD bin/admin.sh /app
|
||||
|
||||
USER $APRS_USER
|
||||
ADD bin/run.sh /usr/local/bin
|
||||
ADD bin/listen.sh /usr/local/bin
|
||||
ENTRYPOINT ["/usr/local/bin/run.sh"]
|
||||
# For the web admin interface
|
||||
EXPOSE 8001
|
||||
|
||||
ENTRYPOINT ["/app/run.sh"]
|
||||
VOLUME ["/config"]
|
||||
|
||||
# Set the user to run the application
|
||||
USER appuser
|
||||
|
||||
HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \
|
||||
CMD aprsd healthcheck --config /config/aprsd.conf
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN set -ex \
|
||||
# Upgrade the package index and install security upgrades
|
||||
&& apt-get update \
|
||||
&& apt-get upgrade -y \
|
||||
&& apt-get install -y git build-essential curl vim libffi-dev \
|
||||
&& apt-get install -y git build-essential curl libffi-dev \
|
||||
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \
|
||||
# Install dependencies
|
||||
# Clean up
|
||||
|
||||
@@ -20,6 +20,14 @@ aprsd.cmds.dev module
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
aprsd.cmds.fetch\_stats module
|
||||
------------------------------
|
||||
|
||||
.. automodule:: aprsd.cmds.fetch_stats
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
aprsd.cmds.healthcheck module
|
||||
-----------------------------
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
aprsd.logging package
|
||||
=====================
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
aprsd.logging.log module
|
||||
------------------------
|
||||
|
||||
.. automodule:: aprsd.logging.log
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
aprsd.logging.rich module
|
||||
-------------------------
|
||||
|
||||
.. automodule:: aprsd.logging.rich
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: aprsd.logging
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
+12
-12
@@ -10,7 +10,7 @@ Subpackages
|
||||
aprsd.clients
|
||||
aprsd.cmds
|
||||
aprsd.conf
|
||||
aprsd.logging
|
||||
aprsd.log
|
||||
aprsd.packets
|
||||
aprsd.plugins
|
||||
aprsd.rpc
|
||||
@@ -21,14 +21,6 @@ Subpackages
|
||||
Submodules
|
||||
----------
|
||||
|
||||
aprsd.aprsd module
|
||||
------------------
|
||||
|
||||
.. automodule:: aprsd.aprsd
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
aprsd.cli\_helper module
|
||||
------------------------
|
||||
|
||||
@@ -53,10 +45,10 @@ aprsd.exception module
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
aprsd.flask module
|
||||
------------------
|
||||
aprsd.main module
|
||||
-----------------
|
||||
|
||||
.. automodule:: aprsd.flask
|
||||
.. automodule:: aprsd.main
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
@@ -93,6 +85,14 @@ aprsd.stats module
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
aprsd.wsgi module
|
||||
-----------------
|
||||
|
||||
.. automodule:: aprsd.wsgi
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
|
||||
@@ -29,14 +29,12 @@ kiss3
|
||||
attrs
|
||||
# for mobile checking
|
||||
user-agents
|
||||
pyopenssl
|
||||
dataclasses
|
||||
dacite2
|
||||
oslo.config
|
||||
rpyc
|
||||
# Pin this here so it doesn't require a compile on
|
||||
# raspi
|
||||
cryptography
|
||||
shellingham
|
||||
geopy
|
||||
rush
|
||||
|
||||
+7
-16
@@ -4,39 +4,33 @@
|
||||
#
|
||||
# pip-compile --annotation-style=line requirements.in
|
||||
#
|
||||
anyio==3.7.1 # via httpcore
|
||||
aprslib==0.7.2 # via -r requirements.in
|
||||
attrs==23.1.0 # via -r requirements.in, ax253, kiss3, rush
|
||||
ax253==0.1.5.post1 # via kiss3
|
||||
beautifulsoup4==4.12.2 # via -r requirements.in
|
||||
bidict==0.22.1 # via python-socketio
|
||||
bitarray==2.8.0 # via ax253, kiss3
|
||||
bitarray==2.8.1 # via ax253, kiss3
|
||||
blinker==1.6.2 # via flask
|
||||
certifi==2023.7.22 # via httpcore, requests
|
||||
cffi==1.15.1 # via cryptography
|
||||
certifi==2023.7.22 # via requests
|
||||
charset-normalizer==3.2.0 # via requests
|
||||
click==8.1.6 # via -r requirements.in, click-completion, click-params, flask
|
||||
click-completion==0.5.2 # via -r requirements.in
|
||||
click-params==0.4.1 # via -r requirements.in
|
||||
commonmark==0.9.1 # via rich
|
||||
cryptography==41.0.2 # via -r requirements.in, pyopenssl
|
||||
dacite2==2.0.0 # via -r requirements.in
|
||||
dataclasses==0.6 # via -r requirements.in
|
||||
debtcollector==2.5.0 # via oslo-config
|
||||
decorator==5.1.1 # via validators
|
||||
dnspython==2.4.0 # via eventlet
|
||||
dnspython==2.4.2 # via eventlet
|
||||
eventlet==0.33.3 # via -r requirements.in
|
||||
exceptiongroup==1.1.2 # via anyio
|
||||
flask==2.3.2 # via -r requirements.in, flask-httpauth, flask-socketio
|
||||
flask-httpauth==4.8.0 # via -r requirements.in
|
||||
flask-socketio==5.3.4 # via -r requirements.in
|
||||
flask-socketio==5.3.5 # via -r requirements.in
|
||||
geographiclib==2.0 # via geopy
|
||||
geopy==2.3.0 # via -r requirements.in
|
||||
gevent==23.7.0 # via -r requirements.in
|
||||
greenlet==2.0.2 # via eventlet, gevent
|
||||
h11==0.14.0 # via httpcore
|
||||
httpcore==0.17.3 # via dnspython
|
||||
idna==3.4 # via anyio, requests
|
||||
idna==3.4 # via requests
|
||||
imapclient==2.3.1 # via -r requirements.in
|
||||
importlib-metadata==6.8.0 # via ax253, kiss3
|
||||
itsdangerous==2.1.2 # via flask
|
||||
@@ -49,9 +43,7 @@ oslo-i18n==6.0.0 # via oslo-config
|
||||
pbr==5.11.1 # via -r requirements.in, oslo-i18n, stevedore
|
||||
pluggy==1.2.0 # via -r requirements.in
|
||||
plumbum==1.8.2 # via rpyc
|
||||
pycparser==2.21 # via cffi
|
||||
pygments==2.15.1 # via rich
|
||||
pyopenssl==23.2.0 # via -r requirements.in
|
||||
pygments==2.16.1 # via rich
|
||||
pyserial==3.5 # via pyserial-asyncio
|
||||
pyserial-asyncio==0.6 # via kiss3
|
||||
python-engineio==4.5.1 # via python-socketio
|
||||
@@ -65,7 +57,6 @@ rpyc==5.3.1 # via -r requirements.in
|
||||
rush==2021.4.0 # via -r requirements.in
|
||||
shellingham==1.5.0.post1 # via -r requirements.in, click-completion
|
||||
six==1.16.0 # via -r requirements.in, click-completion, eventlet, imapclient
|
||||
sniffio==1.3.0 # via anyio, dnspython, httpcore
|
||||
soupsieve==2.4.1 # via beautifulsoup4
|
||||
stevedore==5.1.0 # via oslo-config
|
||||
tabulate==0.9.0 # via -r requirements.in
|
||||
@@ -75,7 +66,7 @@ update-checker==0.18.0 # via -r requirements.in
|
||||
urllib3==2.0.4 # via requests
|
||||
user-agents==2.2.0 # via -r requirements.in
|
||||
validators==0.20.0 # via click-params
|
||||
werkzeug==2.3.6 # via -r requirements.in, flask
|
||||
werkzeug==2.3.7 # via -r requirements.in, flask
|
||||
wrapt==1.15.0 # via -r requirements.in, debtcollector
|
||||
zipp==3.16.2 # via importlib-metadata
|
||||
zope-event==5.0 # via gevent
|
||||
|
||||
Reference in New Issue
Block a user