1
0
mirror of https://github.com/craigerl/aprsd.git synced 2026-08-17 17:13:49 -04:00

Compare commits

..

18 Commits

Author SHA1 Message Date
hemna 7292744a78 Restore previous conversations in webchat
This patch saves the webchat conversations messages in the browser's
local storage.  When the user comes back to the page, the
conversations are restored.
2023-09-05 14:14:52 -04:00
hemna 619b1b708e Remove VIM from Dockerfile
If you need vim, you can just ssh into the container and apt-get
install vim.
2023-09-05 08:01:42 -04:00
hemna 008b2ab09e recreate client during reset()
This patch re-creates the client object during a client.reset() call.
2023-09-01 16:11:19 -04:00
hemna 4b56e99689 updated github workflows 2023-09-01 14:51:55 -04:00
hemna 10bf04929e Updated documentation build 2023-09-01 14:43:29 -04:00
hemna a9e8050ae6 Removed admin_web.py
This patch removes the old admin_web.py.   Use the aprsd.wsgi
for the admin interface.
2023-09-01 14:38:55 -04:00
hemna 82f77b7a6a Removed some RPC server log noise 2023-08-28 09:19:54 -04:00
hemna 570fdb98a7 Fixed admin page packet date
The date timestamp was always showing as 1970.  Had to adjust
the javascript conversion from epoch to Date object
2023-08-28 09:18:50 -04:00
hemna 9582812041 RPC Server logs the client IP on failed auth
this patch adds an error log for the client IP of who connects
to the rpc server without proper auth key.
2023-08-23 13:48:09 -04:00
hemna 859f904602 Start keepalive thread first
This patch changes the order of the threads starting.  The Keepalive
thread's job is to test the aprsis/kiss client to see if it's up and
running, and then issue a reset if it's down.   On SIGINT, the keepalive
might issue that reset in the middle of a shutdown, which might cause
things to hang when everything should be shutting down.  Making the
KeepaliveThread first, means it will be the first to be shut down as
well, preventing the next loop from resetting the client.
2023-08-23 13:45:46 -04:00
hemna 34311f0fbd fixed an issue in the mobile webchat
The global socket var wasn't defined globally in send-message-mobile.js
2023-08-23 13:15:50 -04:00
hemna 2416f0ea1a Added dupe checkig code to webchat mobile 2023-08-22 16:03:27 -04:00
hemna 377842c2ec click on the div after added. 2023-08-22 13:46:43 -04:00
hemna a8dd9ce012 Webchat suppress to display of dupe messages
This patch updates the web ui for webchat to suppress the displaying
of duplicate recieved messages.  Dupes can happen over the KISS
interface due to packets being encapsulated by nearby repeaters into 3rd
party packets.

When a dupe message is recieved, the dupe message is flashed 3 times.
2023-08-22 13:37:43 -04:00
hemna 1d6a667987 Convert webchat internet urls to local static urls 2023-08-22 12:51:50 -04:00
hemna 2e9a204c74 Make use of webchat gps config options
This patch makes use of the gps settings in the webchat section.
If the user sets the latitude and longitude in the config file, then
the gps beacon button will be enabled.  The gps button will still be
enabled if the http connection is over SSL.
2023-08-22 12:31:44 -04:00
hemna f922b3f97b Added new webchat config section
This patch adds a new webchat config section to specify:
web_ip (the ip address to listen on)
web_port
latitude (latitude to use for the GPS beacon button)
longitude (long to use for the GPS beacon button)
2023-08-22 12:01:34 -04:00
hemna 8dd3b05bb1 fixed webchat logging.logformat typeoh
This fixes a problem with webchat when specifying the logfile
in aprsd config
2023-08-15 21:49:43 -04:00
37 changed files with 1758 additions and 517 deletions
+1 -1
View File
@@ -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 }}
+1 -1
View File
@@ -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 }}
+1 -2
View File
@@ -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
-1
View File
@@ -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
+15
View File
@@ -1,9 +1,24 @@
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
-360
View File
@@ -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()
+6 -5
View File
@@ -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
@@ -67,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
@@ -86,7 +89,7 @@ class Client:
pass
class APRSISClient(Client):
class APRSISClient(Client, metaclass=trace.TraceWrapperMetaclass):
_client = None
@@ -135,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
@@ -172,7 +174,7 @@ class APRSISClient(Client):
return aprs_client
class KISSClient(Client):
class KISSClient(Client, metaclass=trace.TraceWrapperMetaclass):
_client = None
@@ -241,7 +243,6 @@ class KISSClient(Client):
else:
return packet
@trace.trace
def setup_connection(self):
self._client = kiss.KISS3Client()
return self._client
+3 -3
View File
@@ -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()
+20 -8
View File
@@ -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,
)
@@ -377,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),
@@ -427,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
@@ -450,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
@@ -468,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,
@@ -479,16 +494,13 @@ 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,
# This is broken for now after removing cryptography
# and pyopenssl
# ssl_context="adhoc",
host=CONF.admin.web_ip,
host=CONF.webchat.web_ip,
port=port,
allow_unsafe_werkzeug=True,
)
+30
View File
@@ -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,
}
+7 -10
View File
@@ -16,8 +16,13 @@ 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:
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}'",
@@ -52,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)
-1
View File
@@ -246,7 +246,6 @@ class APRSDStats:
},
"plugins": plugin_stats,
}
LOG.info("APRSD Stats: DONE")
return stats
def __str__(self):
+5 -5
View File
@@ -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';
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+9 -1
View File
@@ -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);
}
+208 -56
View File
@@ -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
+24 -8
View File
@@ -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
@@ -27,10 +35,18 @@
init_chat();
reset_Tabs();
if (location.protocol != 'https:') {
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>
@@ -69,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>
@@ -78,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;">
&nbsp;
</div>
</div>
+22 -6
View File
@@ -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>
+1 -1
View File
@@ -22,7 +22,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
+1 -1
View File
@@ -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
+8
View File
@@ -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
-----------------------------
-29
View File
@@ -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
View File
@@ -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
---------------