Merge pull request #91 from craigerl/small_refactor

Small refactor
This commit is contained in:
Walter A. Boring IV 2022-11-23 13:33:23 -05:00 committed by GitHub
commit e66dc344b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 2090 additions and 874 deletions

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8, 3.9]
python-version: ["3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}

View File

@ -25,7 +25,7 @@ docs: build
cp Changelog docs/changelog.rst
tox -edocs
clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
clean: clean-build clean-pyc clean-test clean-dev ## remove all build, test, coverage and Python artifacts
clean-build: ## remove build artifacts
rm -fr build/
@ -46,6 +46,9 @@ clean-test: ## remove test and coverage artifacts
rm -fr htmlcov/
rm -fr .pytest_cache
clean-dev:
rm -rf $(VENVDIR)
test: dev ## Run all the tox tests
tox -p all
@ -73,5 +76,9 @@ docker-dev: test ## Make a development docker container tagged with hemna6969/a
docker build -t hemna6969/aprsd:master -f docker/Dockerfile-dev docker
update-requirements: dev ## Update the requirements.txt and dev-requirements.txt files
rm requirements.txt
rm dev-requirements.txt
touch requirements.txt
touch dev-requirements.txt
$(VENV)/pip-compile requirements.in
$(VENV)/pip-compile dev-requirements.in

View File

@ -68,9 +68,9 @@ def main():
# The commands themselves live in the cmds directory
from .cmds import ( # noqa
completion, dev, healthcheck, list_plugins, listen, send_message,
server,
server, webchat,
)
cli()
cli(auto_envvar_prefix="APRSD")
def signal_handler(sig, frame):

View File

@ -4,7 +4,8 @@ import typing as t
import click
from aprsd import config as aprsd_config
from aprsd import log
from aprsd.logging import log
from aprsd.utils import trace
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
@ -59,6 +60,8 @@ def process_standard_options(f: F) -> F:
ctx.obj["loglevel"],
ctx.obj["quiet"],
)
if ctx.obj["config"]["aprsd"].get("trace", False):
trace.setup_tracing(["method", "api"])
del kwargs["loglevel"]
del kwargs["config_file"]

View File

@ -6,8 +6,9 @@ import aprslib
from aprslib.exceptions import LoginError
from aprsd import config as aprsd_config
from aprsd import exception, trace
from aprsd import exception
from aprsd.clients import aprsis, kiss
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")
@ -51,7 +52,8 @@ class Client:
def reset(self):
"""Call this to force a rebuild/reconnect."""
del self._client
if self._client:
del self._client
@abc.abstractmethod
def setup_connection(self):
@ -129,6 +131,7 @@ class APRSISClient(Client):
backoff = backoff * 2
continue
LOG.debug(f"Logging in to APRS-IS with user '{user}'")
self._client = aprs_client
return aprs_client
@ -153,8 +156,8 @@ class KISSClient(Client):
# Ensure that the config vars are correctly set
if KISSClient.is_enabled(config):
config.check_option(
"kiss.callsign",
default_fail=aprsd_config.DEFAULT_CONFIG_DICT["kiss"]["callsign"],
"aprsd.callsign",
default_fail=aprsd_config.DEFAULT_CONFIG_DICT["aprsd"]["callsign"],
)
transport = KISSClient.transport(config)
if transport == TRANSPORT_SERIALKISS:
@ -192,8 +195,8 @@ class KISSClient(Client):
@trace.trace
def setup_connection(self):
ax25client = kiss.Aioax25Client(self.config)
return ax25client
client = kiss.KISS3Client(self.config)
return client
class ClientFactory:
@ -220,7 +223,7 @@ class ClientFactory:
elif KISSClient.is_enabled(self.config):
key = KISSClient.transport(self.config)
LOG.debug(f"GET client {key}")
LOG.debug(f"GET client '{key}'")
builder = self._builders.get(key)
if not builder:
raise ValueError(key)

View File

@ -1,5 +1,7 @@
import logging
import select
import socket
import threading
import aprslib
from aprslib import is_py3
@ -7,6 +9,7 @@ from aprslib.exceptions import (
ConnectionDrop, ConnectionError, GenericError, LoginError, ParseError,
UnknownFormat,
)
import wrapt
import aprsd
from aprsd import stats
@ -23,11 +26,30 @@ class Aprsdis(aprslib.IS):
# timeout in seconds
select_timeout = 1
lock = threading.Lock()
def stop(self):
self.thread_stop = True
LOG.info("Shutdown Aprsdis client.")
def is_socket_closed(self, sock: socket.socket) -> bool:
try:
# this will try to read bytes without blocking and also without removing them from buffer (peek only)
data = sock.recv(16, socket.MSG_DONTWAIT | socket.MSG_PEEK)
if len(data) == 0:
return True
except BlockingIOError:
return False # socket is open and reading from it would block
except ConnectionResetError:
return True # socket was closed for some other reason
except Exception:
self.logger.exception(
"unexpected exception when checking if a socket is closed",
)
return False
return False
@wrapt.synchronized(lock)
def send(self, msg):
"""Send an APRS Message object."""
line = str(msg)

View File

@ -1,20 +1,19 @@
import asyncio
import logging
from aioax25 import interface
from aioax25 import kiss as kiss
from aioax25.aprs import APRSInterface
import aprslib
from ax253 import Frame
import kiss
from aprsd import messaging
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")
class Aioax25Client:
class KISS3Client:
def __init__(self, config):
self.config = config
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
self.loop = asyncio.get_event_loop()
self.setup()
def setup(self):
@ -24,71 +23,89 @@ class Aioax25Client:
False,
):
LOG.debug(
"Setting up Serial KISS connection to {}".format(
"KISS({}) Serial connection to {}".format(
kiss.__version__,
self.config["kiss"]["serial"]["device"],
),
)
self.kissdev = kiss.SerialKISSDevice(
device=self.config["kiss"]["serial"]["device"],
baudrate=self.config["kiss"]["serial"].get("baudrate", 9600),
loop=self.loop,
self.kiss = kiss.SerialKISS(
port=self.config["kiss"]["serial"]["device"],
speed=self.config["kiss"]["serial"].get("baudrate", 9600),
strip_df_start=True,
)
elif "tcp" in self.config["kiss"] and self.config["kiss"]["tcp"].get(
"enabled",
False,
):
LOG.debug(
"Setting up KISSTCP Connection to {}:{}".format(
"KISS({}) TCP Connection to {}:{}".format(
kiss.__version__,
self.config["kiss"]["tcp"]["host"],
self.config["kiss"]["tcp"]["port"],
),
)
self.kissdev = kiss.TCPKISSDevice(
self.config["kiss"]["tcp"]["host"],
self.config["kiss"]["tcp"]["port"],
loop=self.loop,
log=LOG,
self.kiss = kiss.TCPKISS(
host=self.config["kiss"]["tcp"]["host"],
port=int(self.config["kiss"]["tcp"]["port"]),
strip_df_start=True,
)
self.kissdev.open()
self.kissport0 = self.kissdev[0]
LOG.debug("Creating AX25Interface")
self.ax25int = interface.AX25Interface(kissport=self.kissport0, loop=self.loop)
LOG.debug("Creating APRSInterface")
self.aprsint = APRSInterface(
ax25int=self.ax25int,
mycall=self.config["kiss"]["callsign"],
log=LOG,
)
LOG.debug("Starting KISS interface connection")
self.kiss.start()
@trace.trace
def stop(self):
LOG.debug(self.kissdev)
self.kissdev._close()
self.loop.stop()
try:
self.kiss.stop()
self.kiss.loop.call_soon_threadsafe(
self.kiss.protocol.transport.close,
)
except Exception as ex:
LOG.exception(ex)
def set_filter(self, filter):
# This does nothing right now.
pass
def consumer(self, callback, blocking=True, immortal=False, raw=False):
callsign = self.config["kiss"]["callsign"]
call = callsign.split("-")
if len(call) > 1:
callsign = call[0]
ssid = int(call[1])
else:
ssid = 0
self.aprsint.bind(callback=callback, callsign=callsign, ssid=ssid, regex=False)
self.loop.run_forever()
def parse_frame(self, frame_bytes):
frame = Frame.from_bytes(frame_bytes)
# Now parse it with aprslib
packet = aprslib.parse(str(frame))
kwargs = {
"frame": str(frame),
"packet": packet,
}
self._parse_callback(**kwargs)
def consumer(self, callback, blocking=False, immortal=False, raw=False):
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")
def send(self, msg):
"""Send an APRS Message object."""
payload = f"{msg._filter_for_send()}"
self.aprsint.send_message(
addressee=msg.tocall,
message=payload,
# payload = (':%-9s:%s' % (
# msg.tocall,
# payload
# )).encode('US-ASCII'),
# payload = str(msg).encode('US-ASCII')
if isinstance(msg, messaging.AckMessage):
msg_payload = f"ack{msg.id}"
else:
msg_payload = f"{msg.message}{{{str(msg.id)}"
payload = (
":{:<9}:{}".format(
msg.tocall,
msg_payload,
)
).encode("US-ASCII")
LOG.debug(f"Send '{payload}' TO KISS")
frame = Frame.ui(
destination=msg.tocall,
source=msg.fromcall,
path=["WIDE1-1", "WIDE2-1"],
oneshot=True,
info=payload,
)
self.kiss.write(frame)

View File

@ -8,8 +8,9 @@ import logging
import click
# local imports here
from aprsd import cli_helper, client, messaging, packets, plugin, stats, trace
from aprsd import cli_helper, client, messaging, packets, plugin, stats, utils
from aprsd.aprsd import cli
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")
@ -69,6 +70,14 @@ def test_plugin(
"""Test an individual APRSD plugin given a python path."""
config = ctx.obj["config"]
flat_config = utils.flatten_dict(config)
LOG.info("Using CONFIG values:")
for x in flat_config:
if "password" in x or "aprsd.web.users.admin" in x:
LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX")
else:
LOG.info(f"{x} = {flat_config[x]}")
if not aprs_login:
if not config.exists("aprs.login"):
click.echo("Must set --aprs_login or APRS_LOGIN")

View File

@ -14,10 +14,9 @@ from rich.console import Console
# local imports here
import aprsd
from aprsd import (
cli_helper, client, messaging, packets, stats, threads, trace, utils,
)
from aprsd import cli_helper, client, messaging, packets, stats, threads, utils
from aprsd.aprsd import cli
from aprsd.utils import trace
# setup the global logger
@ -140,19 +139,24 @@ def listen(
# Creates the client object
LOG.info("Creating client connection")
client.factory.create().client
aprs_client = client.factory.create().client
aprs_client = client.factory.create()
console.log(aprs_client)
LOG.debug(f"Filter by '{filter}'")
aprs_client.set_filter(filter)
aprs_client.client.set_filter(filter)
packets.PacketList(config=config)
keepalive = threads.KeepAliveThread(config=config)
keepalive.start()
while True:
try:
# This will register a packet consumer with aprslib
# When new packets come in the consumer will process
# the packet
with console.status("Listening for packets"):
aprs_client.consumer(rx_packet, raw=False)
# with console.status("Listening for packets"):
aprs_client.client.consumer(rx_packet, raw=False)
except aprslib.exceptions.ConnectionDrop:
LOG.error("Connection dropped, reconnecting")
time.sleep(5)

View File

@ -7,10 +7,11 @@ import click
import aprsd
from aprsd import (
cli_helper, client, flask, messaging, packets, plugin, stats, threads,
trace, utils,
utils,
)
from aprsd import aprsd as aprsd_main
from aprsd.aprsd import cli
from aprsd.threads import rx
LOG = logging.getLogger("APRSD")
@ -58,8 +59,6 @@ def server(ctx, flush):
else:
LOG.info(f"{x} = {flat_config[x]}")
if config["aprsd"].get("trace", False):
trace.setup_tracing(["method", "api"])
stats.APRSDStats(config)
# Initialize the client factory and create
@ -97,7 +96,7 @@ def server(ctx, flush):
plugin_manager = plugin.PluginManager(config)
plugin_manager.setup_plugins()
rx_thread = threads.APRSDRXThread(
rx_thread = rx.APRSDPluginRXThread(
msg_queues=threads.msg_queues,
config=config,
)

617
aprsd/cmds/webchat.py Normal file
View File

@ -0,0 +1,617 @@
import datetime
import json
import logging
from logging.handlers import RotatingFileHandler
import queue
import signal
import sys
import threading
import time
import aprslib
import click
import flask
from flask import request
from flask.logging import default_handler
import flask_classful
from flask_httpauth import HTTPBasicAuth
from flask_socketio import Namespace, SocketIO
from werkzeug.security import check_password_hash, generate_password_hash
import wrapt
import aprsd
from aprsd import cli_helper, client
from aprsd import config as aprsd_config
from aprsd import messaging, packets, stats, threads, utils
from aprsd.aprsd import cli
from aprsd.logging import rich as aprsd_logging
from aprsd.threads import aprsd as aprsd_thread
from aprsd.threads import rx
from aprsd.utils import objectstore, trace
LOG = logging.getLogger("APRSD")
auth = HTTPBasicAuth()
users = None
rx_msg_queue = queue.Queue(maxsize=20)
tx_msg_queue = queue.Queue(maxsize=20)
control_queue = queue.Queue(maxsize=20)
msg_queues = {
"rx": rx_msg_queue,
"control": control_queue,
"tx": tx_msg_queue,
}
def signal_handler(sig, frame):
click.echo("signal_handler: called")
LOG.info(
f"Ctrl+C, Sending all threads({len(threads.APRSDThreadList())}) exit! "
f"Can take up to 10 seconds {datetime.datetime.now()}",
)
threads.APRSDThreadList().stop_all()
if "subprocess" not in str(frame):
time.sleep(1.5)
# messaging.MsgTrack().save()
# packets.WatchList().save()
# packets.SeenList().save()
LOG.info(stats.APRSDStats())
LOG.info("Telling flask to bail.")
signal.signal(signal.SIGTERM, sys.exit(0))
sys.exit(0)
class SentMessages(objectstore.ObjectStoreMixin):
_instance = None
lock = threading.Lock()
data = {}
def __new__(cls, *args, **kwargs):
"""This magic turns this into a singleton."""
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
@wrapt.synchronized(lock)
def add(self, msg):
self.data[msg.id] = self.create(msg.id)
self.data[msg.id]["from"] = msg.fromcall
self.data[msg.id]["to"] = msg.tocall
self.data[msg.id]["message"] = msg.message.rstrip("\n")
self.data[msg.id]["raw"] = str(msg).rstrip("\n")
def create(self, id):
return {
"id": id,
"ts": time.time(),
"ack": False,
"from": None,
"to": None,
"raw": None,
"message": None,
"status": None,
"last_update": None,
"reply": None,
}
@wrapt.synchronized(lock)
def __len__(self):
return len(self.data.keys())
@wrapt.synchronized(lock)
def get(self, id):
if id in self.data:
return self.data[id]
@wrapt.synchronized(lock)
def get_all(self):
return self.data
@wrapt.synchronized(lock)
def set_status(self, id, status):
if id in self.data:
self.data[id]["last_update"] = str(datetime.datetime.now())
self.data[id]["status"] = status
@wrapt.synchronized(lock)
def ack(self, id):
"""The message got an ack!"""
if id in self.data:
self.data[id]["last_update"] = str(datetime.datetime.now())
self.data[id]["ack"] = True
@wrapt.synchronized(lock)
def reply(self, id, packet):
"""We got a packet back from the sent message."""
if id in self.data:
self.data[id]["reply"] = packet
# 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 WebChatRXThread(rx.APRSDRXThread):
"""Class that connects to aprsis/kiss and waits for messages."""
def connected(self, connected=True):
self.connected = connected
def stop(self):
self.thread_stop = True
client.factory.create().client.stop()
def loop(self):
# setup the consumer of messages and block until a messages
msg = None
try:
msg = self.msg_queues["tx"].get_nowait()
except queue.Empty:
pass
try:
if msg:
LOG.debug("GOT msg from TX queue!!")
msg.send()
except (
aprslib.exceptions.ConnectionDrop,
aprslib.exceptions.ConnectionError,
):
LOG.error("Connection dropped, reconnecting")
# Put it back on the queue to send.
self.msg_queues["tx"].put(msg)
# Force the deletion of the client object connected to aprs
# This will cause a reconnect, next time client.get_client()
# is called
self._client.reset()
time.sleep(2)
try:
# When new packets come in the consumer will process
# the packet
# This call blocks until thread stop() is called.
self._client.client.consumer(
self.process_packet, raw=False, blocking=False,
)
except (
aprslib.exceptions.ConnectionDrop,
aprslib.exceptions.ConnectionError,
):
LOG.error("Connection dropped, reconnecting")
time.sleep(5)
# Force the deletion of the client object connected to aprs
# This will cause a reconnect, next time client.get_client()
# is called
self._client.reset()
return True
return True
def process_packet(self, *args, **kwargs):
# packet = self._client.decode_packet(*args, **kwargs)
if "packet" in kwargs:
packet = kwargs["packet"]
else:
packet = self._client.decode_packet(*args, **kwargs)
LOG.debug(f"GOT Packet {packet}")
self.msg_queues["rx"].put(packet)
class WebChatTXThread(aprsd_thread.APRSDThread):
"""Class that """
def __init__(self, msg_queues, config, socketio):
super().__init__("_TXThread_")
self.msg_queues = msg_queues
self.config = config
self.socketio = socketio
self.connected = False
def loop(self):
try:
msg = self.msg_queues["control"].get_nowait()
self.connected = msg["connected"]
except queue.Empty:
pass
try:
packet = self.msg_queues["rx"].get_nowait()
if packet:
# we got a packet and we need to send it to the
# web socket
self.process_packet(packet)
except queue.Empty:
pass
except Exception as ex:
LOG.exception(ex)
time.sleep(1)
return True
def process_ack_packet(self, packet):
ack_num = packet.get("msgNo")
LOG.info(f"We got ack for our sent message {ack_num}")
messaging.log_packet(packet)
SentMessages().ack(int(ack_num))
self.socketio.emit(
"ack", SentMessages().get(int(ack_num)),
namespace="/sendmsg",
)
stats.APRSDStats().ack_rx_inc()
self.got_ack = True
def process_packet(self, packet):
LOG.info(f"process PACKET {packet}")
tocall = packet.get("addresse", None)
fromcall = packet["from"]
msg = packet.get("message_text", None)
msg_id = packet.get("msgNo", "0")
msg_response = packet.get("response", None)
if tocall == self.config["aprsd"]["callsign"] and msg_response == "ack":
self.process_ack_packet(packet)
elif tocall == self.config["aprsd"]["callsign"]:
messaging.log_message(
"Received Message",
packet["raw"],
msg,
fromcall=fromcall,
msg_num=msg_id,
)
# let any threads do their thing, then ack
# send an ack last
ack = messaging.AckMessage(
self.config["aprsd"]["callsign"],
fromcall,
msg_id=msg_id,
)
ack.send()
packets.PacketList().add(packet)
stats.APRSDStats().msgs_rx_inc()
message = packet.get("message_text", None)
msg = {
"id": 0,
"ts": time.time(),
"ack": False,
"from": fromcall,
"to": packet["to"],
"raw": packet["raw"],
"message": message,
"status": None,
"last_update": None,
"reply": None,
}
self.socketio.emit(
"new", msg,
namespace="/sendmsg",
)
class WebChatFlask(flask_classful.FlaskView):
config = None
def set_config(self, config):
global users
self.config = config
self.users = {}
for user in self.config["aprsd"]["web"]["users"]:
self.users[user] = generate_password_hash(
self.config["aprsd"]["web"]["users"][user],
)
users = self.users
@auth.login_required
def index(self):
stats = self._stats()
if self.config["aprs"].get("enabled", True):
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.is_enabled(self.config):
transport = client.KISSClient.transport(self.config)
if transport == client.TRANSPORT_TCPKISS:
aprs_connection = (
"TCPKISS://{}:{}".format(
self.config["kiss"]["tcp"]["host"],
self.config["kiss"]["tcp"]["port"],
)
)
elif transport == client.TRANSPORT_SERIALKISS:
aprs_connection = (
"SerialKISS://{}@{} baud".format(
self.config["kiss"]["serial"]["device"],
self.config["kiss"]["serial"]["baudrate"],
)
)
stats["transport"] = transport
stats["aprs_connection"] = aprs_connection
LOG.debug(f"initial stats = {stats}")
return flask.render_template(
"index.html",
initial_stats=stats,
aprs_connection=aprs_connection,
callsign=self.config["aprsd"]["callsign"],
version=aprsd.__version__,
)
@auth.login_required
def send_message_status(self):
LOG.debug(request)
msgs = SentMessages()
info = msgs.get_all()
return json.dumps(info)
def _stats(self):
stats_obj = stats.APRSDStats()
now = datetime.datetime.now()
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"]
# del stats_dict["email"]
# del stats_dict["plugins"]
# del stats_dict["messages"]
result = {
"time": now.strftime(time_format),
"stats": stats_dict,
}
return result
def stats(self):
return json.dumps(self._stats())
class SendMessageNamespace(Namespace):
"""Class to handle the socketio interactions."""
_config = None
got_ack = False
reply_sent = False
msg = None
request = None
def __init__(self, namespace=None, config=None, msg_queues=None):
self._config = config
self._msg_queues = msg_queues
super().__init__(namespace)
def on_connect(self):
global socketio
LOG.debug("Web socket connected")
socketio.emit(
"connected", {"data": "/sendmsg Connected"},
namespace="/sendmsg",
)
msg = {"connected": True}
self._msg_queues["control"].put(msg)
def on_disconnect(self):
LOG.debug("WS Disconnected")
msg = {"connected": False}
self._msg_queues["control"].put(msg)
def on_send(self, data):
global socketio
LOG.debug(f"WS: on_send {data}")
self.request = data
data["from"] = self._config["aprs"]["login"]
msg = messaging.TextMessage(
data["from"],
data["to"],
data["message"],
)
self.msg = msg
msgs = SentMessages()
msgs.add(msg)
msgs.set_status(msg.id, "Sending")
obj = msgs.get(self.msg.id)
socketio.emit(
"sent", obj,
namespace="/sendmsg",
)
msg.send()
# self._msg_queues["tx"].put(msg)
def handle_message(self, data):
LOG.debug(f"WS Data {data}")
def handle_json(self, data):
LOG.debug(f"WS json {data}")
def setup_logging(config, flask_app, loglevel, quiet):
flask_log = logging.getLogger("werkzeug")
flask_app.logger.removeHandler(default_handler)
flask_log.removeHandler(default_handler)
log_level = aprsd_config.LOG_LEVELS[loglevel]
flask_log.setLevel(log_level)
date_format = config["aprsd"].get(
"dateformat",
aprsd_config.DEFAULT_DATE_FORMAT,
)
if not config["aprsd"]["web"].get("logging_enabled", False):
# disable web logging
flask_log.disabled = True
flask_app.logger.disabled = True
# return
if config["aprsd"].get("rich_logging", False) and not quiet:
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 = config["aprsd"].get("logfile", None)
if log_file:
log_format = config["aprsd"].get(
"logformat",
aprsd_config.DEFAULT_LOG_FORMAT,
)
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)
@trace.trace
def init_flask(config, loglevel, quiet):
global socketio
flask_app = flask.Flask(
"aprsd",
static_url_path="/static",
static_folder="web/chat/static",
template_folder="web/chat/templates",
)
setup_logging(config, flask_app, loglevel, quiet)
server = WebChatFlask()
server.set_config(config)
flask_app.route("/", methods=["GET"])(server.index)
flask_app.route("/stats", methods=["GET"])(server.stats)
# flask_app.route("/send-message", methods=["GET"])(server.send_message)
flask_app.route("/send-message-status", methods=["GET"])(server.send_message_status)
socketio = SocketIO(
flask_app, logger=False, engineio_logger=False,
async_mode="threading",
)
# async_mode="gevent",
# async_mode="eventlet",
# import eventlet
# eventlet.monkey_patch()
socketio.on_namespace(
SendMessageNamespace(
"/sendmsg", config=config,
msg_queues=msg_queues,
),
)
return socketio, flask_app
# main() ###
@cli.command()
@cli_helper.add_options(cli_helper.common_options)
@click.option(
"-f",
"--flush",
"flush",
is_flag=True,
show_default=True,
default=False,
help="Flush out all old aged messages on disk.",
)
@click.option(
"-p",
"--port",
"port",
show_default=True,
default=80,
help="Port to listen to web requests",
)
@click.pass_context
@cli_helper.process_standard_options
def webchat(ctx, flush, port):
"""Web based HAM Radio chat program!"""
ctx.obj["config_file"]
loglevel = ctx.obj["loglevel"]
quiet = ctx.obj["quiet"]
config = ctx.obj["config"]
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
if not quiet:
click.echo("Load config")
level, msg = utils._check_version()
if level:
LOG.warning(msg)
else:
LOG.info(msg)
LOG.info(f"APRSD Started version: {aprsd.__version__}")
flat_config = utils.flatten_dict(config)
LOG.info("Using CONFIG values:")
for x in flat_config:
if "password" in x or "aprsd.web.users.admin" in x:
LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX")
else:
LOG.info(f"{x} = {flat_config[x]}")
stats.APRSDStats(config)
# Initialize the client factory and create
# The correct client object ready for use
client.ClientFactory.setup(config)
# Make sure we have 1 client transport enabled
if not client.factory.is_client_enabled():
LOG.error("No Clients are enabled in config.")
sys.exit(-1)
if not client.factory.is_client_configured():
LOG.error("APRS client is not properly configured in config file.")
sys.exit(-1)
packets.PacketList(config=config)
messaging.MsgTrack(config=config)
packets.WatchList(config=config)
packets.SeenList(config=config)
(socketio, app) = init_flask(config, loglevel, quiet)
rx_thread = WebChatRXThread(
msg_queues=msg_queues,
config=config,
)
LOG.info("Start RX Thread")
rx_thread.start()
tx_thread = WebChatTXThread(
msg_queues=msg_queues,
config=config,
socketio=socketio,
)
LOG.info("Start TX Thread")
tx_thread.start()
keepalive = threads.KeepAliveThread(config=config)
LOG.info("Start KeepAliveThread")
keepalive.start()
LOG.info("Start socketio.run()")
socketio.run(
app,
host=config["aprsd"]["web"]["host"],
port=port,
)
LOG.info("WebChat exiting!!!! Bye.")

View File

@ -57,13 +57,13 @@ DEFAULT_CONFIG_DICT = {
"ham": {"callsign": "NOCALL"},
"aprs": {
"enabled": True,
# Only used as the login for aprsis.
"login": "CALLSIGN",
"password": "00000",
"host": "rotate.aprs2.net",
"port": 14580,
},
"kiss": {
"callsign": "NOCALL",
"tcp": {
"enabled": False,
"host": "direwolf.ip.address",
@ -76,11 +76,14 @@ DEFAULT_CONFIG_DICT = {
},
},
"aprsd": {
# Callsign to use for all packets to/from aprsd instance
# regardless of the client (aprsis vs kiss)
"callsign": "NOCALL",
"logfile": "/tmp/aprsd.log",
"logformat": DEFAULT_LOG_FORMAT,
"dateformat": DEFAULT_DATE_FORMAT,
"save_location": DEFAULT_CONFIG_DIR,
"rich_logging": False,
"rich_logging": True,
"trace": False,
"enabled_plugins": CORE_MESSAGE_PLUGINS,
"units": "imperial",
@ -177,16 +180,35 @@ class Config(collections.UserDict):
if not self.exists(path):
if type(path) is list:
path = ".".join(path)
raise exception.MissingConfigOption(path)
raise exception.MissingConfigOptionException(path)
val = self.get(path)
if val == default_fail:
# We have to fail and bail if the user hasn't edited
# this config option.
raise exception.ConfigOptionBogusDefaultException(path, default_fail)
raise exception.ConfigOptionBogusDefaultException(
path, default_fail,
)
def add_config_comments(raw_yaml):
end_idx = utils.end_substr(raw_yaml, "ham:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # Callsign that owns this instance of APRSD.",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "aprsd:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # Callsign to use for all APRSD Packets as the to/from."
"\n # regardless of client type (aprsis vs tcpkiss vs serial)",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "aprs:")
if end_idx != -1:
# lets insert a comment
@ -326,6 +348,11 @@ def parse_config(config_file):
config,
["aprsd"],
)
check_option(
config,
"aprsd.callsign",
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["callsign"],
)
# Ensure they change the admin password
if config.get("aprsd.web.enabled") is True:

View File

@ -18,9 +18,10 @@ from werkzeug.security import check_password_hash, generate_password_hash
import aprsd
from aprsd import client
from aprsd import config as aprsd_config
from aprsd import log, messaging, packets, plugin, stats, threads, utils
from aprsd import messaging, packets, plugin, stats, threads, utils
from aprsd.clients import aprsis
from aprsd.logging import logging as aprsd_logging
from aprsd.logging import log
from aprsd.logging import rich as aprsd_logging
LOG = logging.getLogger("APRSD")
@ -600,8 +601,8 @@ def init_flask(config, loglevel, quiet):
flask_app = flask.Flask(
"aprsd",
static_url_path="/static",
static_folder="web/static",
template_folder="web/templates",
static_folder="web/admin/static",
template_folder="web/admin/templates",
)
setup_logging(config, flask_app, loglevel, quiet)
server = APRSDFlask()

View File

@ -5,7 +5,7 @@ import queue
import sys
from aprsd import config as aprsd_config
from aprsd.logging import logging as aprsd_logging
from aprsd.logging import rich as aprsd_logging
LOG = logging.getLogger("APRSD")

View File

@ -6,7 +6,8 @@ import re
import threading
import time
from aprsd import client, objectstore, packets, stats, threads
from aprsd import client, packets, stats, threads
from aprsd.utils import objectstore
LOG = logging.getLogger("APRSD")
@ -238,7 +239,10 @@ class RawMessage(Message):
last_send_age = last_send_time = None
def __init__(self, message, allow_delay=True):
super().__init__(fromcall=None, tocall=None, msg_id=None, allow_delay=allow_delay)
super().__init__(
fromcall=None, tocall=None, msg_id=None,
allow_delay=allow_delay,
)
self._raw_message = message
def dict(self):
@ -282,12 +286,8 @@ class TextMessage(Message):
last_send_time = last_send_age = None
def __init__(
self,
fromcall,
tocall,
message,
msg_id=None,
allow_delay=True,
self, fromcall, tocall, message,
msg_id=None, allow_delay=True,
):
super().__init__(
fromcall=fromcall, tocall=tocall,

View File

@ -3,7 +3,10 @@ import logging
import threading
import time
from aprsd import objectstore, utils
import wrapt
from aprsd import utils
from aprsd.utils import objectstore
LOG = logging.getLogger("APRSD")
@ -17,6 +20,7 @@ class PacketList:
"""Class to track all of the packets rx'd and tx'd by aprsd."""
_instance = None
lock = threading.Lock()
config = None
packet_list = {}
@ -28,7 +32,6 @@ class PacketList:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.packet_list = utils.RingBuffer(1000)
cls._instance.lock = threading.Lock()
cls._instance.config = kwargs["config"]
return cls._instance
@ -36,50 +39,51 @@ class PacketList:
if config:
self.config = config
@wrapt.synchronized(lock)
def __iter__(self):
with self.lock:
return iter(self.packet_list)
return iter(self.packet_list)
@wrapt.synchronized(lock)
def add(self, packet):
with self.lock:
packet["ts"] = time.time()
if (
"fromcall" in packet
and packet["fromcall"] == self.config["aprs"]["login"]
):
self.total_tx += 1
else:
self.total_recv += 1
self.packet_list.append(packet)
SeenList().update_seen(packet)
packet["ts"] = time.time()
if (
"fromcall" in packet
and packet["fromcall"] == self.config["aprs"]["login"]
):
self.total_tx += 1
else:
self.total_recv += 1
self.packet_list.append(packet)
SeenList().update_seen(packet)
@wrapt.synchronized(lock)
def get(self):
with self.lock:
return self.packet_list.get()
return self.packet_list.get()
@wrapt.synchronized(lock)
def total_received(self):
with self.lock:
return self.total_recv
return self.total_recv
@wrapt.synchronized(lock)
def total_sent(self):
with self.lock:
return self.total_tx
return self.total_tx
class WatchList(objectstore.ObjectStoreMixin):
"""Global watch list and info for callsigns."""
_instance = None
lock = threading.Lock()
data = {}
config = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.lock = threading.Lock()
cls._instance.config = kwargs["config"]
if "config" in kwargs:
cls._instance.config = kwargs["config"]
cls._instance._init_store()
cls._instance.data = {}
cls._instance._init_store()
return cls._instance
def __init__(self, config=None):
@ -110,12 +114,12 @@ class WatchList(objectstore.ObjectStoreMixin):
def callsign_in_watchlist(self, callsign):
return callsign in self.data
@wrapt.synchronized(lock)
def update_seen(self, packet):
with self.lock:
callsign = packet["from"]
if self.callsign_in_watchlist(callsign):
self.data[callsign]["last"] = datetime.datetime.now()
self.data[callsign]["packets"].append(packet)
callsign = packet["from"]
if self.callsign_in_watchlist(callsign):
self.data[callsign]["last"] = datetime.datetime.now()
self.data[callsign]["packets"].append(packet)
def last_seen(self, callsign):
if self.callsign_in_watchlist(callsign):
@ -158,18 +162,20 @@ class SeenList(objectstore.ObjectStoreMixin):
"""Global callsign seen list."""
_instance = None
lock = threading.Lock()
data = {}
config = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.lock = threading.Lock()
cls._instance.config = kwargs["config"]
if "config" in kwargs:
cls._instance.config = kwargs["config"]
cls._instance._init_store()
cls._instance.data = {}
cls._instance._init_store()
return cls._instance
@wrapt.synchronized(lock)
def update_seen(self, packet):
callsign = None
if "fromcall" in packet:

View File

@ -492,4 +492,5 @@ class PluginManager:
self._pluggy_pm.register(obj)
def get_plugins(self):
return self._pluggy_pm.get_plugins()
if self._pluggy_pm:
return self._pluggy_pm.get_plugins()

View File

@ -11,7 +11,8 @@ import time
import imapclient
from validate_email import validate_email
from aprsd import messaging, plugin, stats, threads, trace
from aprsd import messaging, plugin, stats, threads
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")

View File

@ -2,7 +2,8 @@ import logging
import shutil
import subprocess
from aprsd import plugin, trace
from aprsd import plugin
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")

View File

@ -2,7 +2,8 @@ import logging
import re
import time
from aprsd import plugin, plugin_utils, trace
from aprsd import plugin, plugin_utils
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")

View File

@ -1,6 +1,7 @@
import logging
from aprsd import messaging, packets, plugin, trace
from aprsd import messaging, packets, plugin
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")

View File

@ -1,7 +1,8 @@
import logging
import time
from aprsd import plugin, trace
from aprsd import plugin
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")

View File

@ -2,7 +2,8 @@ import datetime
import logging
import re
from aprsd import messaging, plugin, trace
from aprsd import messaging, plugin
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")

View File

@ -2,10 +2,10 @@ import logging
import re
import time
from opencage.geocoder import OpenCageGeocode
import pytz
from aprsd import fuzzyclock, plugin, plugin_utils, trace
from aprsd import plugin, plugin_utils
from aprsd.utils import fuzzy, trace
LOG = logging.getLogger("APRSD")
@ -32,7 +32,7 @@ class TimePlugin(plugin.APRSDRegexCommandPluginBase):
local_short_str = local_t.strftime("%H:%M %Z")
local_hour = local_t.strftime("%H")
local_min = local_t.strftime("%M")
cur_time = fuzzyclock.fuzzy(int(local_hour), int(local_min), 1)
cur_time = fuzzy(int(local_hour), int(local_min), 1)
reply = "{} ({})".format(
cur_time,
@ -49,68 +49,6 @@ class TimePlugin(plugin.APRSDRegexCommandPluginBase):
return self.build_date_str(localzone)
class TimeOpenCageDataPlugin(TimePlugin, plugin.APRSFIKEYMixin):
"""geocage based timezone fetching."""
command_regex = "^[tT]"
command_name = "time"
short_description = "Current time of GPS beacon timezone. Uses OpenCage"
def setup(self):
self.ensure_aprs_fi_key()
@trace.trace
def process(self, packet):
fromcall = packet.get("from")
message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
api_key = self.config["services"]["aprs.fi"]["apiKey"]
# optional second argument is a callsign to search
a = re.search(r"^.*\s+(.*)", message)
if a is not None:
searchcall = a.group(1)
searchcall = searchcall.upper()
else:
# if no second argument, search for calling station
searchcall = fromcall
try:
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 location"
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
if not len(aprs_data["entries"]):
LOG.error("Didn't get any entries from aprs.fi")
return "Failed to fetch aprs.fi location"
lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"]
try:
self.config.exists("opencagedata.apiKey")
except Exception as ex:
LOG.error(f"Failed to find config opencage:apiKey {ex}")
return "No opencage apiKey found"
try:
opencage_key = self.config["opencagedata"]["apiKey"]
geocoder = OpenCageGeocode(opencage_key)
results = geocoder.reverse_geocode(lat, lon)
except Exception as ex:
LOG.error(f"Couldn't fetch opencagedata api '{ex}'")
# Default to UTC instead
localzone = pytz.timezone("UTC")
else:
tzone = results[0]["annotations"]["timezone"]["name"]
localzone = pytz.timezone(tzone)
return self.build_date_str(localzone)
class TimeOWMPlugin(TimePlugin, plugin.APRSFIKEYMixin):
"""OpenWeatherMap based timezone fetching."""

View File

@ -1,7 +1,8 @@
import logging
import aprsd
from aprsd import plugin, stats, trace
from aprsd import plugin, stats
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")

View File

@ -4,7 +4,8 @@ import re
import requests
from aprsd import plugin, plugin_utils, trace
from aprsd import plugin, plugin_utils
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")

View File

@ -2,6 +2,8 @@ import datetime
import logging
import threading
import wrapt
import aprsd
from aprsd import packets, plugin, utils
@ -12,7 +14,7 @@ LOG = logging.getLogger("APRSD")
class APRSDStats:
_instance = None
lock = None
lock = threading.Lock()
config = None
start_time = None
@ -39,7 +41,6 @@ class APRSDStats:
if cls._instance is None:
cls._instance = super().__new__(cls)
# any initializetion here
cls._instance.lock = threading.Lock()
cls._instance.start_time = datetime.datetime.now()
cls._instance._aprsis_keepalive = datetime.datetime.now()
return cls._instance
@ -48,128 +49,129 @@ class APRSDStats:
if config:
self.config = config
@wrapt.synchronized(lock)
@property
def uptime(self):
with self.lock:
return datetime.datetime.now() - self.start_time
return datetime.datetime.now() - self.start_time
@wrapt.synchronized(lock)
@property
def memory(self):
with self.lock:
return self._mem_current
return self._mem_current
@wrapt.synchronized(lock)
def set_memory(self, memory):
with self.lock:
self._mem_current = memory
self._mem_current = memory
@wrapt.synchronized(lock)
@property
def memory_peak(self):
with self.lock:
return self._mem_peak
return self._mem_peak
@wrapt.synchronized(lock)
def set_memory_peak(self, memory):
with self.lock:
self._mem_peak = memory
self._mem_peak = memory
@wrapt.synchronized(lock)
@property
def aprsis_server(self):
with self.lock:
return self._aprsis_server
return self._aprsis_server
@wrapt.synchronized(lock)
def set_aprsis_server(self, server):
with self.lock:
self._aprsis_server = server
self._aprsis_server = server
@wrapt.synchronized(lock)
@property
def aprsis_keepalive(self):
with self.lock:
return self._aprsis_keepalive
return self._aprsis_keepalive
@wrapt.synchronized(lock)
def set_aprsis_keepalive(self):
with self.lock:
self._aprsis_keepalive = datetime.datetime.now()
self._aprsis_keepalive = datetime.datetime.now()
@wrapt.synchronized(lock)
@property
def msgs_tx(self):
with self.lock:
return self._msgs_tx
return self._msgs_tx
@wrapt.synchronized(lock)
def msgs_tx_inc(self):
with self.lock:
self._msgs_tx += 1
self._msgs_tx += 1
@wrapt.synchronized(lock)
@property
def msgs_rx(self):
with self.lock:
return self._msgs_rx
return self._msgs_rx
@wrapt.synchronized(lock)
def msgs_rx_inc(self):
with self.lock:
self._msgs_rx += 1
self._msgs_rx += 1
@wrapt.synchronized(lock)
@property
def msgs_mice_rx(self):
with self.lock:
return self._msgs_mice_rx
return self._msgs_mice_rx
@wrapt.synchronized(lock)
def msgs_mice_inc(self):
with self.lock:
self._msgs_mice_rx += 1
self._msgs_mice_rx += 1
@wrapt.synchronized(lock)
@property
def ack_tx(self):
with self.lock:
return self._ack_tx
return self._ack_tx
@wrapt.synchronized(lock)
def ack_tx_inc(self):
with self.lock:
self._ack_tx += 1
self._ack_tx += 1
@wrapt.synchronized(lock)
@property
def ack_rx(self):
with self.lock:
return self._ack_rx
return self._ack_rx
@wrapt.synchronized(lock)
def ack_rx_inc(self):
with self.lock:
self._ack_rx += 1
self._ack_rx += 1
@wrapt.synchronized(lock)
@property
def msgs_tracked(self):
with self.lock:
return self._msgs_tracked
return self._msgs_tracked
@wrapt.synchronized(lock)
def msgs_tracked_inc(self):
with self.lock:
self._msgs_tracked += 1
self._msgs_tracked += 1
@wrapt.synchronized(lock)
@property
def email_tx(self):
with self.lock:
return self._email_tx
return self._email_tx
@wrapt.synchronized(lock)
def email_tx_inc(self):
with self.lock:
self._email_tx += 1
self._email_tx += 1
@wrapt.synchronized(lock)
@property
def email_rx(self):
with self.lock:
return self._email_rx
return self._email_rx
@wrapt.synchronized(lock)
def email_rx_inc(self):
with self.lock:
self._email_rx += 1
self._email_rx += 1
@wrapt.synchronized(lock)
@property
def email_thread_time(self):
with self.lock:
return self._email_thread_last_time
return self._email_thread_last_time
@wrapt.synchronized(lock)
def email_thread_update(self):
with self.lock:
self._email_thread_last_time = datetime.datetime.now()
self._email_thread_last_time = datetime.datetime.now()
@wrapt.synchronized(lock)
def stats(self):
now = datetime.datetime.now()
if self._email_thread_last_time:
@ -185,20 +187,20 @@ class APRSDStats:
pm = plugin.PluginManager()
plugins = pm.get_plugins()
plugin_stats = {}
if plugins:
def full_name_with_qualname(obj):
return "{}.{}".format(
obj.__class__.__module__,
obj.__class__.__qualname__,
)
def full_name_with_qualname(obj):
return "{}.{}".format(
obj.__class__.__module__,
obj.__class__.__qualname__,
)
for p in plugins:
plugin_stats[full_name_with_qualname(p)] = {
"enabled": p.enabled,
"rx": p.rx_count,
"tx": p.tx_count,
"version": p.version,
}
for p in plugins:
plugin_stats[full_name_with_qualname(p)] = {
"enabled": p.enabled,
"rx": p.rx_count,
"tx": p.tx_count,
"version": p.version,
}
wl = packets.WatchList()
sl = packets.SeenList()
@ -207,30 +209,30 @@ class APRSDStats:
"aprsd": {
"version": aprsd.__version__,
"uptime": utils.strfdelta(self.uptime),
"memory_current": self.memory,
"memory_current": int(self.memory),
"memory_current_str": utils.human_size(self.memory),
"memory_peak": self.memory_peak,
"memory_peak": int(self.memory_peak),
"memory_peak_str": utils.human_size(self.memory_peak),
"watch_list": wl.get_all(),
"seen_list": sl.get_all(),
},
"aprs-is": {
"server": self.aprsis_server,
"server": str(self.aprsis_server),
"callsign": self.config["aprs"]["login"],
"last_update": last_aprsis_keepalive,
},
"messages": {
"tracked": self.msgs_tracked,
"sent": self.msgs_tx,
"recieved": self.msgs_rx,
"ack_sent": self.ack_tx,
"ack_recieved": self.ack_rx,
"mic-e recieved": self.msgs_mice_rx,
"tracked": int(self.msgs_tracked),
"sent": int(self.msgs_tx),
"recieved": int(self.msgs_rx),
"ack_sent": int(self.ack_tx),
"ack_recieved": int(self.ack_rx),
"mic-e recieved": int(self.msgs_mice_rx),
},
"email": {
"enabled": self.config["aprsd"]["email"]["enabled"],
"sent": self._email_tx,
"recieved": self._email_rx,
"sent": int(self._email_tx),
"recieved": int(self._email_rx),
"thread_last_update": last_update,
},
"plugins": plugin_stats,

13
aprsd/threads/__init__.py Normal file
View File

@ -0,0 +1,13 @@
import queue
# Make these available to anyone importing
# aprsd.threads
from .aprsd import APRSDThread, APRSDThreadList # noqa: F401
from .keep_alive import KeepAliveThread # noqa: F401
from .rx import APRSDRXThread # noqa: F401
rx_msg_queue = queue.Queue(maxsize=20)
msg_queues = {
"rx": rx_msg_queue,
}

94
aprsd/threads/aprsd.py Normal file
View File

@ -0,0 +1,94 @@
import abc
import logging
from queue import Queue
import threading
import wrapt
LOG = logging.getLogger("APRSD")
class APRSDThreadList:
"""Singleton class that keeps track of application wide threads."""
_instance = None
threads_list = []
lock = threading.Lock()
global_queue = Queue()
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls.threads_list = []
return cls._instance
@wrapt.synchronized(lock)
def add(self, thread_obj):
thread_obj.set_global_queue(self.global_queue)
self.threads_list.append(thread_obj)
@wrapt.synchronized(lock)
def remove(self, thread_obj):
self.threads_list.remove(thread_obj)
@wrapt.synchronized(lock)
def stop_all(self):
self.global_queue.put_nowait({"quit": True})
"""Iterate over all threads and call stop on them."""
for th in self.threads_list:
LOG.info(f"Stopping Thread {th.name}")
th.stop()
@wrapt.synchronized(lock)
def __len__(self):
return len(self.threads_list)
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
global_queue = None
def __init__(self, name):
super().__init__(name=name)
self.thread_stop = False
APRSDThreadList().add(self)
def set_global_queue(self, global_queue):
self.global_queue = global_queue
def _should_quit(self):
""" see if we have a quit message from the global queue."""
if self.thread_stop:
return True
if self.global_queue.empty():
return False
msg = self.global_queue.get(timeout=1)
if not msg:
return False
if "quit" in msg and msg["quit"] is True:
# put the message back on the queue for others
self.global_queue.put_nowait(msg)
self.thread_stop = True
return True
def stop(self):
self.thread_stop = True
@abc.abstractmethod
def loop(self):
pass
def _cleanup(self):
"""Add code to subclass to do any cleanup"""
def run(self):
LOG.debug("Starting")
while not self._should_quit():
can_loop = self.loop()
if not can_loop:
self.stop()
self._cleanup()
APRSDThreadList().remove(self)
LOG.debug("Exiting")

View File

@ -0,0 +1,88 @@
import datetime
import logging
import time
import tracemalloc
from aprsd import client, messaging, packets, stats, utils
from aprsd.threads import APRSDThread, APRSDThreadList
LOG = logging.getLogger("APRSD")
class KeepAliveThread(APRSDThread):
cntr = 0
checker_time = datetime.datetime.now()
def __init__(self, config):
tracemalloc.start()
super().__init__("KeepAlive")
self.config = config
max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0}
self.max_delta = datetime.timedelta(**max_timeout)
def loop(self):
if self.cntr % 60 == 0:
tracker = messaging.MsgTrack()
stats_obj = stats.APRSDStats()
pl = packets.PacketList()
thread_list = APRSDThreadList()
now = datetime.datetime.now()
last_email = stats_obj.email_thread_time
if last_email:
email_thread_time = utils.strfdelta(now - last_email)
else:
email_thread_time = "N/A"
last_msg_time = utils.strfdelta(now - stats_obj.aprsis_keepalive)
current, peak = tracemalloc.get_traced_memory()
stats_obj.set_memory(current)
stats_obj.set_memory_peak(peak)
try:
login = self.config["aprsd"]["callsign"]
except KeyError:
login = self.config["ham"]["callsign"]
keepalive = (
"{} - Uptime {} RX:{} TX:{} Tracker:{} Msgs TX:{} RX:{} "
"Last:{} Email: {} - RAM Current:{} Peak:{} Threads:{}"
).format(
login,
utils.strfdelta(stats_obj.uptime),
pl.total_recv,
pl.total_tx,
len(tracker),
stats_obj.msgs_tx,
stats_obj.msgs_rx,
last_msg_time,
email_thread_time,
utils.human_size(current),
utils.human_size(peak),
len(thread_list),
)
LOG.info(keepalive)
# See if we should reset the aprs-is client
# Due to losing a keepalive from them
delta_dict = utils.parse_delta_str(last_msg_time)
delta = datetime.timedelta(**delta_dict)
if delta > self.max_delta:
# We haven't gotten a keepalive from aprs-is in a while
# reset the connection.a
if not client.KISSClient.is_enabled(self.config):
LOG.warning(f"Resetting connection to APRS-IS {delta}")
client.factory.create().reset()
# Check version every hour
delta = now - self.checker_time
if delta > datetime.timedelta(hours=1):
self.checker_time = now
level, msg = utils._check_version()
if level:
LOG.warning(msg)
self.cntr += 1
time.sleep(1)
return True

View File

@ -1,162 +1,15 @@
import abc
import datetime
import logging
import queue
import threading
import time
import tracemalloc
import aprslib
from aprsd import client, messaging, packets, plugin, stats, utils
from aprsd import client, messaging, packets, plugin, stats
from aprsd.threads import APRSDThread
LOG = logging.getLogger("APRSD")
RX_THREAD = "RX"
EMAIL_THREAD = "Email"
rx_msg_queue = queue.Queue(maxsize=20)
msg_queues = {
"rx": rx_msg_queue,
}
class APRSDThreadList:
"""Singleton class that keeps track of application wide threads."""
_instance = None
threads_list = []
lock = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls.lock = threading.Lock()
cls.threads_list = []
return cls._instance
def add(self, thread_obj):
with self.lock:
self.threads_list.append(thread_obj)
def remove(self, thread_obj):
with self.lock:
self.threads_list.remove(thread_obj)
def stop_all(self):
"""Iterate over all threads and call stop on them."""
with self.lock:
for th in self.threads_list:
LOG.debug(f"Stopping Thread {th.name}")
th.stop()
def __len__(self):
with self.lock:
return len(self.threads_list)
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
def __init__(self, name):
super().__init__(name=name)
self.thread_stop = False
APRSDThreadList().add(self)
def stop(self):
self.thread_stop = True
@abc.abstractmethod
def loop(self):
pass
def run(self):
LOG.debug("Starting")
while not self.thread_stop:
can_loop = self.loop()
if not can_loop:
self.stop()
APRSDThreadList().remove(self)
LOG.debug("Exiting")
class KeepAliveThread(APRSDThread):
cntr = 0
checker_time = datetime.datetime.now()
def __init__(self, config):
tracemalloc.start()
super().__init__("KeepAlive")
self.config = config
max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0}
self.max_delta = datetime.timedelta(**max_timeout)
def loop(self):
if self.cntr % 60 == 0:
tracker = messaging.MsgTrack()
stats_obj = stats.APRSDStats()
pl = packets.PacketList()
thread_list = APRSDThreadList()
now = datetime.datetime.now()
last_email = stats_obj.email_thread_time
if last_email:
email_thread_time = utils.strfdelta(now - last_email)
else:
email_thread_time = "N/A"
last_msg_time = utils.strfdelta(now - stats_obj.aprsis_keepalive)
current, peak = tracemalloc.get_traced_memory()
stats_obj.set_memory(current)
stats_obj.set_memory_peak(peak)
try:
login = self.config["aprs"]["login"]
except KeyError:
login = self.config["ham"]["callsign"]
keepalive = (
"{} - Uptime {} RX:{} TX:{} Tracker:{} Msgs TX:{} RX:{} "
"Last:{} Email: {} - RAM Current:{} Peak:{} Threads:{}"
).format(
login,
utils.strfdelta(stats_obj.uptime),
pl.total_recv,
pl.total_tx,
len(tracker),
stats_obj.msgs_tx,
stats_obj.msgs_rx,
last_msg_time,
email_thread_time,
utils.human_size(current),
utils.human_size(peak),
len(thread_list),
)
LOG.info(keepalive)
# See if we should reset the aprs-is client
# Due to losing a keepalive from them
delta_dict = utils.parse_delta_str(last_msg_time)
delta = datetime.timedelta(**delta_dict)
if delta > self.max_delta:
# We haven't gotten a keepalive from aprs-is in a while
# reset the connection.a
if not client.KISSClient.is_enabled(self.config):
LOG.warning("Resetting connection to APRS-IS.")
client.factory.create().reset()
# Check version every hour
delta = now - self.checker_time
if delta > datetime.timedelta(hours=1):
self.checker_time = now
level, msg = utils._check_version()
if level:
LOG.warning(msg)
self.cntr += 1
time.sleep(1)
return True
class APRSDRXThread(APRSDThread):
def __init__(self, msg_queues, config):
@ -186,7 +39,10 @@ class APRSDRXThread(APRSDThread):
self.process_packet, raw=False, blocking=False,
)
except aprslib.exceptions.ConnectionDrop:
except (
aprslib.exceptions.ConnectionDrop,
aprslib.exceptions.ConnectionError,
):
LOG.error("Connection dropped, reconnecting")
time.sleep(5)
# Force the deletion of the client object connected to aprs
@ -196,6 +52,12 @@ class APRSDRXThread(APRSDThread):
# Continue to loop
return True
@abc.abstractmethod
def process_packet(self, *args, **kwargs):
pass
class APRSDPluginRXThread(APRSDRXThread):
def process_packet(self, *args, **kwargs):
packet = self._client.decode_packet(*args, **kwargs)
thread = APRSDProcessPacketThread(packet=packet, config=self.config)
@ -239,7 +101,7 @@ class APRSDProcessPacketThread(APRSDThread):
# We don't put ack packets destined for us through the
# plugins.
if tocall == self.config["aprs"]["login"] and msg_response == "ack":
if tocall == self.config["aprsd"]["callsign"] and msg_response == "ack":
self.process_ack_packet(packet)
else:
# It's not an ACK for us, so lets run it through
@ -253,12 +115,12 @@ class APRSDProcessPacketThread(APRSDThread):
)
# Only ack messages that were sent directly to us
if tocall == self.config["aprs"]["login"]:
if tocall == self.config["aprsd"]["callsign"]:
stats.APRSDStats().msgs_rx_inc()
# let any threads do their thing, then ack
# send an ack last
ack = messaging.AckMessage(
self.config["aprs"]["login"],
self.config["aprsd"]["callsign"],
fromcall,
msg_id=msg_id,
)
@ -280,7 +142,7 @@ class APRSDProcessPacketThread(APRSDThread):
subreply.send()
else:
msg = messaging.TextMessage(
self.config["aprs"]["login"],
self.config["aprsd"]["callsign"],
fromcall,
subreply,
)
@ -300,7 +162,7 @@ class APRSDProcessPacketThread(APRSDThread):
LOG.debug(f"Sending '{reply}'")
msg = messaging.TextMessage(
self.config["aprs"]["login"],
self.config["aprsd"]["callsign"],
fromcall,
reply,
)
@ -308,10 +170,10 @@ class APRSDProcessPacketThread(APRSDThread):
# If the message was for us and we didn't have a
# response, then we send a usage statement.
if tocall == self.config["aprs"]["login"] and not replied:
if tocall == self.config["aprsd"]["callsign"] and not replied:
LOG.warning("Sending help!")
msg = messaging.TextMessage(
self.config["aprs"]["login"],
self.config["aprsd"]["callsign"],
fromcall,
"Unknown command! Send 'help' message for help",
)
@ -320,10 +182,10 @@ class APRSDProcessPacketThread(APRSDThread):
LOG.error("Plugin failed!!!")
LOG.exception(ex)
# Do we need to send a reply?
if tocall == self.config["aprs"]["login"]:
if tocall == self.config["aprsd"]["callsign"]:
reply = "A Plugin failed! try again?"
msg = messaging.TextMessage(
self.config["aprs"]["login"],
self.config["aprsd"]["callsign"],
fromcall,
reply,
)

View File

@ -2,25 +2,17 @@
import collections
import errno
import functools
import os
import re
import threading
import update_checker
import aprsd
def synchronized(wrapped):
lock = threading.Lock()
@functools.wraps(wrapped)
def _wrap(*args, **kwargs):
with lock:
return wrapped(*args, **kwargs)
return _wrap
from .fuzzyclock import fuzzy # noqa: F401
# Make these available by anyone importing
# aprsd.utils
from .ring_buffer import RingBuffer # noqa: F401
def env(*vars, **kwargs):
@ -129,42 +121,3 @@ def parse_delta_str(s):
else:
m = re.match(r"(?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d[\.\d+]*)", s)
return {key: float(val) for key, val in m.groupdict().items()}
class RingBuffer:
"""class that implements a not-yet-full buffer"""
def __init__(self, size_max):
self.max = size_max
self.data = []
class __Full:
"""class that implements a full buffer"""
def append(self, x):
"""Append an element overwriting the oldest one."""
self.data[self.cur] = x
self.cur = (self.cur + 1) % self.max
def get(self):
"""return list of elements in correct order"""
return self.data[self.cur :] + self.data[: self.cur]
def __len__(self):
return len(self.data)
def append(self, x):
"""append an element at the end of the buffer"""
self.data.append(x)
if len(self.data) == self.max:
self.cur = 0
# Permanently change self's class from non-full to full
self.__class__ = self.__Full
def get(self):
"""Return a list of elements from the oldest to the newest."""
return self.data
def __len__(self):
return len(self.data)

View File

@ -0,0 +1,37 @@
class RingBuffer:
"""class that implements a not-yet-full buffer"""
def __init__(self, size_max):
self.max = size_max
self.data = []
class __Full:
"""class that implements a full buffer"""
def append(self, x):
"""Append an element overwriting the oldest one."""
self.data[self.cur] = x
self.cur = (self.cur + 1) % self.max
def get(self):
"""return list of elements in correct order"""
return self.data[self.cur :] + self.data[: self.cur]
def __len__(self):
return len(self.data)
def append(self, x):
"""append an element at the end of the buffer"""
self.data.append(x)
if len(self.data) == self.max:
self.cur = 0
# Permanently change self's class from non-full to full
self.__class__ = self.__Full
def get(self):
"""Return a list of elements from the oldest to the newest."""
return self.data
def __len__(self):
return len(self.data)

0
aprsd/web/__init__.py Normal file
View File

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,94 @@
body {
background: #eeeeee;
margin: 2em;
text-align: center;
font-family: system-ui, sans-serif;
}
footer {
padding: 2em;
text-align: center;
height: 10vh;
}
.ui.segment {
background: #eeeeee;
}
ul.list {
list-style-type: disc;
}
ul.list li {
list-style-position: outside;
}
#left {
margin-right: 2px;
height: 300px;
}
#right {
height: 300px;
}
#center {
height: 300px;
}
#title {
font-size: 4em;
}
#version{
font-size: .5em;
}
#uptime, #aprsis {
font-size: 1em;
}
#callsign {
font-size: 1.4em;
color: #00F;
padding-top: 8px;
margin:10px;
}
#title_rx {
background-color: darkseagreen;
text-align: left;
}
#title_tx {
background-color: lightcoral;
text-align: left;
}
.aprsd_1 {
background-image: url(/static/images/aprs-symbols-16-0.png);
background-repeat: no-repeat;
background-position: -160px -48px;
width: 16px;
height: 16px;
}
#msgsTabsDiv .ui.tab {
margin:0px;
padding:0px;
display: block;
}
#msgsTabsDiv .header, .tiny.text, .content, .break,
.thumbs.down.outline.icon,
.phone.volume.icon
{
display: inline-block;
float: left;
position: relative;
}
#msgsTabsDiv .tiny.text {
width:100px;
}
#msgsTabsDiv .tiny.header {
width:100px;
text-align: left;
}
#msgsTabsDiv .break {
margin: 2px;
text-align: left;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,41 @@
* {box-sizing: border-box}
/* Style the tab */
.tab {
border: 1px solid #ccc;
background-color: #f1f1f1;
height: 450px;
}
/* Style the buttons inside the tab */
.tab div {
display: block;
background-color: inherit;
color: black;
padding: 10px;
width: 100%;
border: none;
outline: none;
text-align: left;
cursor: pointer;
transition: 0.3s;
font-size: 17px;
}
/* Change background color of buttons on hover */
.tab div:hover {
background-color: #ddd;
}
/* Create an active/current "tab button" class */
.tab div.active {
background-color: #ccc;
}
/* Style the tab content */
.tabcontent {
border: 1px solid #ccc;
height: 450px;
overflow-y: scroll;
background-color: white;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,44 @@
function aprs_img(item, x_offset, y_offset) {
var x = x_offset * -16;
if (y_offset > 5) {
y_offset = 5;
}
var y = y_offset * -16;
var loc = x + 'px '+ y + 'px'
item.css('background-position', loc);
}
function show_aprs_icon(item, symbol) {
var offset = ord(symbol) - 33;
var col = Math.floor(offset / 16);
var row = offset % 16;
//console.log("'" + symbol+"' off: "+offset+" row: "+ row + " col: " + col)
aprs_img(item, row, col);
}
function ord(str){return str.charCodeAt(0);}
function update_stats( data ) {
$("#version").text( data["stats"]["aprsd"]["version"] );
$("#aprs_connection").html( data["aprs_connection"] );
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] );
short_time = data["time"].split(/\s(.+)/)[1];
}
function start_update() {
(function statsworker() {
$.ajax({
url: "/stats",
type: 'GET',
dataType: 'json',
success: function(data) {
update_stats(data);
},
complete: function() {
setTimeout(statsworker, 10000);
}
});
})();
}

View File

@ -0,0 +1,215 @@
var cleared = false;
var callsign_list = {};
var message_list = {};
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");
});
socket.on('connected', function(msg) {
console.log("Connected!");
console.log(msg);
});
socket.on("sent", function(msg) {
if (cleared == false) {
var msgsdiv = $("#msgsTabsDiv");
msgsdiv.html('')
cleared = true
}
sent_msg(msg);
});
socket.on("ack", function(msg) {
update_msg(msg);
});
socket.on("new", function(msg) {
if (cleared == false) {
var msgsdiv = $("#msgsTabsDiv");
msgsdiv.html('')
cleared = true
}
from_msg(msg);
});
$("#sendform").submit(function(event) {
event.preventDefault();
msg = {'to': $('#to_call').val(),
'message': $('#message').val(),
}
socket.emit("send", msg);
$('#message').val('');
});
}
function add_callsign(callsign) {
/* Ensure a callsign exists in the left hand nav */
if (callsign in callsign_list) {
return false
}
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>';
callsignTabs.append(item_html);
callsign_list[callsign] = true;
return true
}
function append_message(callsign, msg, msg_html) {
new_callsign = false
if (!message_list.hasOwnProperty(callsign)) {
message_list[callsign] = new Array();
}
message_list[callsign].push(msg);
// Find the right div to place the html
new_callsign = add_callsign(callsign);
append_message_html(callsign, msg_html, new_callsign);
if (new_callsign) {
//click on the new tab
click_div = '#'+tab_string(callsign);
$(click_div).click();
}
}
function tab_string(callsign) {
return "msgs"+callsign;
}
function tab_content_name(callsign) {
return tab_string(callsign)+"Content";
}
function content_divname(callsign) {
return "#"+tab_content_name(callsign);
}
function append_message_html(callsign, msg_html, new_callsign) {
var msgsTabs = $('#msgsTabsDiv');
divname_str = tab_content_name(callsign);
divname = content_divname(callsign);
if (new_callsign) {
// we have to add a new DIV
msg_div_html = '<div class="tabcontent" id="'+divname_str+'" style="height:450px;">'+msg_html+'</div>';
msgsTabs.append(msg_div_html);
} else {
var msgDiv = $(divname);
msgDiv.append(msg_html);
}
$(divname).animate({scrollTop: $(divname)[0].scrollHeight}, "slow");
}
function create_message_html(time, from, to, message, ack) {
msg_html = '<div class="item">';
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>';
} else {
msg_html += '<i class="phone volume icon" data-content="Recieved Message"></i>';
}
msg_html += '<div class="middle aligned content">>&nbsp;&nbsp;&nbsp;</div>';
msg_html += '</div>';
msg_html += '<div class="middle aligned content">'+message+'</div>';
msg_html += '</div><br>';
return msg_html
}
function sent_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
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);
append_message(msg['to'], msg, msg_html);
}
function from_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
// 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
var d = new Date(ts).toLocaleDateString("en-US")
var t = new Date(ts).toLocaleTimeString("en-US")
from = msg['from']
msg_html = create_message_html(t, from, false, msg['message'], false);
append_message(from, msg, msg_html);
}
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
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');
}
function callsign_select(callsign) {
var tocall = $("#to_call");
tocall.val(callsign);
}
function reset_Tabs() {
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
}
function openCallsign(evt, callsign) {
var i, tabcontent, tablinks;
tab_content = tab_content_name(callsign);
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(tab_content).style.display = "block";
evt.target.className += " active";
callsign_select(callsign);
}

View File

@ -0,0 +1,28 @@
function openTab(evt, tabName) {
// Declare all variables
var i, tabcontent, tablinks;
if (typeof tabName == 'undefined') {
return
}
// Get all elements with class="tabcontent" and hide them
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
// Get all elements with class="tablinks" and remove the class "active"
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
// Show the current tab, and add an "active" class to the button that opened the tab
document.getElementById(tabName).style.display = "block";
if (typeof evt.currentTarget == 'undefined') {
return
} else {
evt.currentTarget.className += " active";
}
}

View File

@ -0,0 +1,57 @@
/* Root element */
.json-document {
padding: 1em 2em;
}
/* Syntax highlighting for JSON objects */
ul.json-dict, ol.json-array {
list-style-type: none;
margin: 0 0 0 1px;
border-left: 1px dotted #ccc;
padding-left: 2em;
}
.json-string {
color: #0B7500;
}
.json-literal {
color: #1A01CC;
font-weight: bold;
}
/* Toggle button */
a.json-toggle {
position: relative;
color: inherit;
text-decoration: none;
}
a.json-toggle:focus {
outline: none;
}
a.json-toggle:before {
font-size: 1.1em;
color: #c0c0c0;
content: "\25BC"; /* down arrow */
position: absolute;
display: inline-block;
width: 1em;
text-align: center;
line-height: 1em;
left: -1.2em;
}
a.json-toggle:hover:before {
color: #aaa;
}
a.json-toggle.collapsed:before {
/* Use rotated down arrow, prevents right arrow appearing smaller than down arrow in some browsers */
transform: rotate(-90deg);
}
/* Collapsable placeholder links */
a.json-placeholder {
color: #aaa;
padding: 0 1em;
text-decoration: none;
}
a.json-placeholder:hover {
text-decoration: underline;
}

View File

@ -0,0 +1,158 @@
/**
* jQuery json-viewer
* @author: Alexandre Bodelot <alexandre.bodelot@gmail.com>
* @link: https://github.com/abodelot/jquery.json-viewer
*/
(function($) {
/**
* Check if arg is either an array with at least 1 element, or a dict with at least 1 key
* @return boolean
*/
function isCollapsable(arg) {
return arg instanceof Object && Object.keys(arg).length > 0;
}
/**
* Check if a string represents a valid url
* @return boolean
*/
function isUrl(string) {
var urlRegexp = /^(https?:\/\/|ftps?:\/\/)?([a-z0-9%-]+\.){1,}([a-z0-9-]+)?(:(\d{1,5}))?(\/([a-z0-9\-._~:/?#[\]@!$&'()*+,;=%]+)?)?$/i;
return urlRegexp.test(string);
}
/**
* Transform a json object into html representation
* @return string
*/
function json2html(json, options) {
var html = '';
if (typeof json === 'string') {
// Escape tags and quotes
json = json
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/'/g, '&apos;')
.replace(/"/g, '&quot;');
if (options.withLinks && isUrl(json)) {
html += '<a href="' + json + '" class="json-string" target="_blank">' + json + '</a>';
} else {
// Escape double quotes in the rendered non-URL string.
json = json.replace(/&quot;/g, '\\&quot;');
html += '<span class="json-string">"' + json + '"</span>';
}
} else if (typeof json === 'number') {
html += '<span class="json-literal">' + json + '</span>';
} else if (typeof json === 'boolean') {
html += '<span class="json-literal">' + json + '</span>';
} else if (json === null) {
html += '<span class="json-literal">null</span>';
} else if (json instanceof Array) {
if (json.length > 0) {
html += '[<ol class="json-array">';
for (var i = 0; i < json.length; ++i) {
html += '<li>';
// Add toggle button if item is collapsable
if (isCollapsable(json[i])) {
html += '<a href class="json-toggle"></a>';
}
html += json2html(json[i], options);
// Add comma if item is not last
if (i < json.length - 1) {
html += ',';
}
html += '</li>';
}
html += '</ol>]';
} else {
html += '[]';
}
} else if (typeof json === 'object') {
var keyCount = Object.keys(json).length;
if (keyCount > 0) {
html += '{<ul class="json-dict">';
for (var key in json) {
if (Object.prototype.hasOwnProperty.call(json, key)) {
html += '<li>';
var keyRepr = options.withQuotes ?
'<span class="json-string">"' + key + '"</span>' : key;
// Add toggle button if item is collapsable
if (isCollapsable(json[key])) {
html += '<a href class="json-toggle">' + keyRepr + '</a>';
} else {
html += keyRepr;
}
html += ': ' + json2html(json[key], options);
// Add comma if item is not last
if (--keyCount > 0) {
html += ',';
}
html += '</li>';
}
}
html += '</ul>}';
} else {
html += '{}';
}
}
return html;
}
/**
* jQuery plugin method
* @param json: a javascript object
* @param options: an optional options hash
*/
$.fn.jsonViewer = function(json, options) {
// Merge user options with default options
options = Object.assign({}, {
collapsed: false,
rootCollapsable: true,
withQuotes: false,
withLinks: true
}, options);
// jQuery chaining
return this.each(function() {
// Transform to HTML
var html = json2html(json, options);
if (options.rootCollapsable && isCollapsable(json)) {
html = '<a href class="json-toggle"></a>' + html;
}
// Insert HTML in target DOM element
$(this).html(html);
$(this).addClass('json-document');
// Bind click on toggle buttons
$(this).off('click');
$(this).on('click', 'a.json-toggle', function() {
var target = $(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array');
target.toggle();
if (target.is(':visible')) {
target.siblings('.json-placeholder').remove();
} else {
var count = target.children('li').length;
var placeholder = count + (count > 1 ? ' items' : ' item');
target.after('<a href class="json-placeholder">' + placeholder + '</a>');
}
return false;
});
// Simulate click on toggle button when placeholder is clicked
$(this).on('click', 'a.json-placeholder', function() {
$(this).siblings('a.json-toggle').click();
return false;
});
if (options.collapsed == true) {
// Trigger click to collapse all nodes
$(this).find('a.json-toggle').click();
}
});
};
})(jQuery);

View File

@ -0,0 +1,86 @@
<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>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
<script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.js"></script>
<link rel="stylesheet" href="/static/css/index.css">
<link rel="stylesheet" href="/static/css/tabs.css">
<script src="/static/js/main.js"></script>
<script src="/static/js/send-message.js"></script>
<script type="text/javascript">
var initial_stats = {{ initial_stats|tojson|safe }};
var memory_chart = null
var message_chart = null
$(document).ready(function() {
console.log(initial_stats);
start_update();
init_chat();
reset_Tabs();
});
</script>
</head>
<body>
<div class='ui text container'>
<h1 class='ui dividing header'>APRSD WebChat {{ version }}</h1>
</div>
<div class='ui grid text container'>
<div class='left floated ten wide column'>
<span style='color: green'>{{ callsign }}</span>
connected to
<span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
</div>
<div class='right floated four wide column'>
<span id='uptime'>NONE</span>
</div>
</div>
<div class="ui container">
<h3 class="ui dividing header">Send Message</h3>
<div id="sendMsgDiv" class="ui mini text">
<form id="sendform" name="sendmsg" action="">
<div class="ui corner labeled input">
<label for="to_call" class="ui label">Callsign</label>
<input type="text" name="to_call" id="to_call" placeholder="To Callsign" size="11" maxlength="9">
<div class="ui corner label">
<i class="asterisk icon"></i>
</div>
</div>
<div class="ui labeled input">
<label for="message" class="ui label">Message</label>
<input type="text" name="message" id="message" size="40" maxlength="40">
</div>
<input type="submit" name="submit" class="ui button" id="send_msg" value="Send" />
</form>
</div>
</div>
<div class="ui grid">
<div class="three wide column">
<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>
</div>
<div class="ui text container" style="padding-top: 40px">
<a href="https://badge.fury.io/py/aprsd"><img src="https://badge.fury.io/py/aprsd.svg" alt="PyPI version" height="18"></a>
<a href="https://github.com/craigerl/aprsd"><img src="https://img.shields.io/badge/Made%20with-Python-1f425f.svg" height="18"></a>
</div>
</body>
</html>

View File

@ -10,3 +10,5 @@ pip-tools
pytest
pytest-cov
gray
pip==22.0.4
pip-tools==5.4.0

View File

@ -1,257 +1,99 @@
#
# This file is autogenerated by pip-compile with python 3.8
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile dev-requirements.in
#
add-trailing-comma==2.1.0
# via gray
alabaster==0.7.12
# via sphinx
appdirs==1.4.4
# via black
attrs==21.2.0
# via
# jsonschema
# pytest
autoflake==1.4
# via gray
babel==2.9.1
# via sphinx
backports-entry-points-selectable==1.1.0
# via virtualenv
black==21.7b0
# via gray
bleach==4.1.0
# via readme-renderer
build==0.9.0
# via pip-tools
certifi==2021.5.30
# via requests
cfgv==3.3.1
# via pre-commit
charset-normalizer==2.0.4
# via requests
click==8.0.1
# via
# black
# pip-tools
colorama==0.4.4
# via twine
colorlog==6.4.1
# via prettylog
configargparse==1.5.2
# via gray
coverage==5.5
# via pytest-cov
distlib==0.3.2
# via virtualenv
docutils==0.17.1
# via
# readme-renderer
# sphinx
fast-json==0.3.2
# via prettylog
filelock==3.0.12
# via
# tox
# virtualenv
fixit==0.1.4
# via gray
flake8==3.9.2
# via
# -r dev-requirements.in
# fixit
# flake8-polyfill
# pep8-naming
flake8-polyfill==1.0.2
# via pep8-naming
gray==0.10.1
# via -r dev-requirements.in
identify==2.2.13
# via pre-commit
idna==3.2
# via requests
imagesize==1.2.0
# via sphinx
importlib-metadata==4.7.1
# via
# keyring
# twine
importlib-resources==5.2.2
# via fixit
iniconfig==1.1.1
# via pytest
isort==5.9.3
# via
# -r dev-requirements.in
# gray
jinja2==3.0.1
# via sphinx
jsonschema==3.2.0
# via fixit
keyring==23.1.0
# via twine
libcst==0.3.20
# via fixit
markupsafe==2.0.1
# via jinja2
mccabe==0.6.1
# via flake8
mypy==0.910
# via -r dev-requirements.in
mypy-extensions==0.4.3
# via
# black
# mypy
# typing-inspect
nodeenv==1.6.0
# via pre-commit
packaging==21.0
# via
# bleach
# build
# pytest
# sphinx
# tox
pathspec==0.9.0
# via black
pep517==0.11.0
# via build
pep8-naming==0.12.1
# via -r dev-requirements.in
pip-tools==6.9.0
# via -r dev-requirements.in
pkginfo==1.7.1
# via twine
platformdirs==2.2.0
# via virtualenv
pluggy==1.0.0
# via
# pytest
# tox
pre-commit==2.14.0
# via -r dev-requirements.in
prettylog==0.3.0
# via gray
py==1.10.0
# via
# pytest
# tox
pycodestyle==2.7.0
# via flake8
pyflakes==2.3.1
# via
# autoflake
# flake8
pygments==2.10.0
# via
# readme-renderer
# sphinx
pyparsing==2.4.7
# via packaging
pyrsistent==0.18.0
# via jsonschema
pytest==6.2.5
# via
# -r dev-requirements.in
# pytest-cov
pytest-cov==2.12.1
# via -r dev-requirements.in
pytz==2021.1
# via babel
pyupgrade==2.24.0
# via gray
pyyaml==5.4.1
# via
# fixit
# libcst
# pre-commit
readme-renderer==29.0
# via twine
regex==2021.8.27
# via black
requests==2.26.0
# via
# requests-toolbelt
# sphinx
# twine
requests-toolbelt==0.9.1
# via twine
rfc3986==1.5.0
# via twine
six==1.16.0
# via
# bleach
# jsonschema
# readme-renderer
# tox
# virtualenv
snowballstemmer==2.1.0
# via sphinx
sphinx==4.1.2
# via -r dev-requirements.in
sphinxcontrib-applehelp==1.0.2
# via sphinx
sphinxcontrib-devhelp==1.0.2
# via sphinx
sphinxcontrib-htmlhelp==2.0.0
# 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==4.1.0
# via
# add-trailing-comma
# pyupgrade
toml==0.10.2
# via
# mypy
# pre-commit
# pytest
# pytest-cov
# tox
tomli==1.2.1
# via
# black
# build
# pep517
tox==3.24.3
# via -r dev-requirements.in
tqdm==4.62.2
# via twine
twine==3.4.2
# via -r dev-requirements.in
typing-extensions==3.10.0.0
# via
# libcst
# mypy
# typing-inspect
typing-inspect==0.7.1
# via libcst
ujson==4.1.0
# via fast-json
unify==0.5
# via gray
untokenize==0.1.1
# via unify
urllib3==1.26.6
# via requests
virtualenv==20.7.2
# via
# pre-commit
# tox
webencodings==0.5.1
# via bleach
wheel==0.37.0
# via pip-tools
zipp==3.5.0
# via
# importlib-metadata
# importlib-resources
add-trailing-comma==2.3.0 # via gray
alabaster==0.7.12 # via sphinx
attrs==22.1.0 # via jsonschema, pytest
autoflake==1.7.7 # via gray
babel==2.11.0 # via sphinx
black==22.10.0 # via gray
bleach==5.0.1 # via readme-renderer
certifi==2022.9.24 # via requests
cfgv==3.3.1 # via pre-commit
charset-normalizer==2.1.1 # via requests
click==8.1.3 # via black, pip-tools
colorlog==6.7.0 # via prettylog
commonmark==0.9.1 # via rich
configargparse==1.5.3 # via gray
coverage[toml]==6.5.0 # via pytest-cov
distlib==0.3.6 # via virtualenv
docutils==0.19 # via readme-renderer, sphinx
exceptiongroup==1.0.4 # via pytest
fast-json==0.3.2 # via prettylog
filelock==3.8.0 # via tox, virtualenv
fixit==0.1.4 # via gray
flake8==5.0.4 # via -r dev-requirements.in, fixit, pep8-naming
gray==0.12.0 # via -r dev-requirements.in
identify==2.5.9 # via pre-commit
idna==3.4 # via requests
imagesize==1.4.1 # via sphinx
importlib-metadata==5.0.0 # via keyring, sphinx, twine
importlib-resources==5.10.0 # via fixit, jsonschema
iniconfig==1.1.1 # via pytest
isort==5.10.1 # via -r dev-requirements.in, gray
jaraco.classes==3.2.3 # via keyring
jinja2==3.1.2 # via sphinx
jsonschema==4.17.1 # via fixit
keyring==23.11.0 # via twine
libcst==0.4.9 # via fixit
markupsafe==2.1.1 # via jinja2
mccabe==0.7.0 # via flake8
more-itertools==9.0.0 # via jaraco.classes
mypy-extensions==0.4.3 # via black, mypy, typing-inspect
mypy==0.991 # via -r dev-requirements.in
nodeenv==1.7.0 # via pre-commit
packaging==21.3 # via pytest, sphinx, tox
pathspec==0.10.2 # via black
pep8-naming==0.13.2 # via -r dev-requirements.in
pip-tools==5.4.0 # via -r dev-requirements.in
pkginfo==1.8.3 # via twine
pkgutil-resolve-name==1.3.10 # via jsonschema
platformdirs==2.5.4 # via black, virtualenv
pluggy==1.0.0 # via pytest, tox
pre-commit==2.20.0 # via -r dev-requirements.in
prettylog==0.3.0 # via gray
py==1.11.0 # via tox
pycodestyle==2.9.1 # via flake8
pyflakes==2.5.0 # via autoflake, flake8
pygments==2.13.0 # via readme-renderer, rich, sphinx
pyparsing==3.0.9 # via packaging
pyrsistent==0.19.2 # via jsonschema
pytest-cov==4.0.0 # via -r dev-requirements.in
pytest==7.2.0 # via -r dev-requirements.in, pytest-cov
pytz==2022.6 # via babel
pyupgrade==3.2.2 # via gray
pyyaml==6.0 # via fixit, libcst, pre-commit
readme-renderer==37.3 # via twine
requests-toolbelt==0.10.1 # via twine
requests==2.28.1 # via requests-toolbelt, sphinx, twine
rfc3986==2.0.0 # via twine
rich==12.6.0 # via twine
six==1.16.0 # via bleach, pip-tools, tox
snowballstemmer==2.2.0 # via sphinx
sphinx==5.3.0 # via -r dev-requirements.in
sphinxcontrib-applehelp==1.0.2 # via sphinx
sphinxcontrib-devhelp==1.0.2 # via sphinx
sphinxcontrib-htmlhelp==2.0.0 # 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.0.0 # via add-trailing-comma, pyupgrade
toml==0.10.2 # via pre-commit
tomli==2.0.1 # via autoflake, black, coverage, mypy, pytest, tox
tox==3.27.1 # via -r dev-requirements.in
twine==4.0.1 # via -r dev-requirements.in
typing-extensions==4.4.0 # via black, libcst, mypy, rich, typing-inspect
typing-inspect==0.8.0 # via libcst
ujson==5.5.0 # via fast-json
unify==0.5 # via gray
untokenize==0.1.1 # via unify
urllib3==1.26.12 # via requests, twine
virtualenv==20.16.7 # via pre-commit, tox
webencodings==0.5.1 # via bleach
zipp==3.10.0 # via importlib-metadata, importlib-resources
# The following packages are considered to be unsafe in a requirements file:
# pip

View File

@ -1,20 +1,19 @@
aioax25>=0.0.10
aprslib>=0.7.0
click
click-completion
flask
flask==2.1.2
werkzeug==2.1.2
flask-classful
flask-httpauth
imapclient
opencage
pluggy
pbr
pyyaml
# Allowing a newer version can lead to a conflict with
# requests.
py3-validate-email
pytz
requests
pytz
six
thesmuggler
update_checker
@ -24,3 +23,7 @@ tabulate
rich
# For the list-plugins pypi.org search scraping
beautifulsoup4
wrapt
# kiss3 uses attrs
kiss3
attrs==22.1.0

View File

@ -1,130 +1,55 @@
#
# This file is autogenerated by pip-compile with python 3.8
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile requirements.in
#
aioax25==0.0.10
# via -r requirements.in
aprslib==0.7.0
# via -r requirements.in
backoff==1.11.1
# via opencage
beautifulsoup4==4.10.0
# via -r requirements.in
bidict==0.21.2
# via python-socketio
certifi==2021.5.30
# via requests
cffi==1.14.6
# via cryptography
charset-normalizer==2.0.4
# via requests
click==8.0.1
# via
# -r requirements.in
# click-completion
# flask
click-completion==0.5.2
# via -r requirements.in
colorama==0.4.4
# via rich
commonmark==0.9.1
# via rich
contexter==0.1.4
# via signalslot
cryptography==3.4.7
# via pyopenssl
dnspython==2.1.0
# via
# eventlet
# py3-validate-email
eventlet==0.33.1
# via -r requirements.in
filelock==3.0.12
# via py3-validate-email
flask==2.0.1
# via
# -r requirements.in
# flask-classful
# flask-httpauth
# flask-socketio
flask-classful==0.14.2
# via -r requirements.in
flask-httpauth==4.4.0
# via -r requirements.in
flask-socketio==5.1.1
# via -r requirements.in
greenlet==1.1.1
# via eventlet
idna==3.2
# via
# py3-validate-email
# requests
imapclient==2.2.0
# via -r requirements.in
itsdangerous==2.0.1
# via flask
jinja2==3.0.1
# via
# click-completion
# flask
markupsafe==2.0.1
# via jinja2
opencage==2.0.0
# via -r requirements.in
pbr==5.6.0
# via -r requirements.in
pluggy==1.0.0
# via -r requirements.in
py3-validate-email==1.0.1
# via -r requirements.in
pycparser==2.20
# via cffi
pygments==2.10.0
# via rich
pyopenssl==20.0.1
# via opencage
pyserial==3.5
# via aioax25
python-engineio==4.2.1
# via python-socketio
python-socketio==5.4.0
# via flask-socketio
pytz==2021.1
# via -r requirements.in
pyyaml==5.4.1
# via -r requirements.in
requests==2.26.0
# via
# -r requirements.in
# opencage
# update-checker
rich==10.15.2
# via -r requirements.in
shellingham==1.4.0
# via click-completion
signalslot==0.1.2
# via aioax25
six==1.16.0
# via
# -r requirements.in
# click-completion
# eventlet
# imapclient
# pyopenssl
# signalslot
soupsieve==2.3.1
# via beautifulsoup4
tabulate==0.8.9
# via -r requirements.in
thesmuggler==1.0.1
# via -r requirements.in
update-checker==0.18.0
# via -r requirements.in
urllib3==1.26.6
# via requests
weakrefmethod==1.0.3
# via signalslot
werkzeug==2.0.0
# via flask
aprslib==0.7.2 # via -r requirements.in
attrs==22.1.0 # via -r requirements.in, ax253, kiss3
ax253==0.1.5.post1 # via kiss3
beautifulsoup4==4.11.1 # via -r requirements.in
bidict==0.22.0 # via python-socketio
bitarray==2.6.0 # via ax253, kiss3
certifi==2022.9.24 # via requests
charset-normalizer==2.1.1 # via requests
click-completion==0.5.2 # via -r requirements.in
click==8.1.3 # via -r requirements.in, click-completion, flask
commonmark==0.9.1 # via rich
dnspython==2.2.1 # via eventlet, py3-validate-email
eventlet==0.33.2 # via -r requirements.in
filelock==3.8.0 # via py3-validate-email
flask-classful==0.14.2 # via -r requirements.in
flask-httpauth==4.7.0 # via -r requirements.in
flask-socketio==5.3.2 # via -r requirements.in
flask==2.1.2 # via -r requirements.in, flask-classful, flask-httpauth, flask-socketio
greenlet==2.0.1 # via eventlet
idna==3.4 # via py3-validate-email, requests
imapclient==2.3.1 # via -r requirements.in
importlib-metadata==5.0.0 # via ax253, flask, kiss3
itsdangerous==2.1.2 # via flask
jinja2==3.1.2 # via click-completion, flask
kiss3==8.0.0 # via -r requirements.in
markupsafe==2.1.1 # via jinja2
pbr==5.11.0 # via -r requirements.in
pluggy==1.0.0 # via -r requirements.in
py3-validate-email==1.0.5.post1 # via -r requirements.in
pygments==2.13.0 # via rich
pyserial-asyncio==0.6 # via kiss3
pyserial==3.5 # via pyserial-asyncio
python-engineio==4.3.4 # via python-socketio
python-socketio==5.7.2 # via flask-socketio
pytz==2022.6 # via -r requirements.in
pyyaml==6.0 # via -r requirements.in
requests==2.28.1 # via -r requirements.in, update-checker
rich==12.6.0 # via -r requirements.in
shellingham==1.5.0 # via click-completion
six==1.16.0 # via -r requirements.in, click-completion, eventlet, imapclient
soupsieve==2.3.2.post1 # via beautifulsoup4
tabulate==0.9.0 # via -r requirements.in
thesmuggler==1.0.1 # via -r requirements.in
typing-extensions==4.4.0 # via rich
update_checker==0.18.0 # via -r requirements.in
urllib3==1.26.12 # via requests
werkzeug==2.1.2 # via -r requirements.in, flask
wrapt==1.14.1 # via -r requirements.in
zipp==3.10.0 # via importlib-metadata

View File

@ -15,7 +15,10 @@ F = t.TypeVar("F", bound=t.Callable[..., t.Any])
class TestDevTestPluginCommand(unittest.TestCase):
def _build_config(self, login=None, password=None):
config = {"aprs": {}}
config = {
"aprs": {},
"aprsd": {"trace": False},
}
if login:
config["aprs"]["login"] = login
@ -25,7 +28,7 @@ class TestDevTestPluginCommand(unittest.TestCase):
return aprsd_config.Config(config)
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.log.setup_logging")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_login(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no login and config."""
@ -43,7 +46,7 @@ class TestDevTestPluginCommand(unittest.TestCase):
assert "Must set --aprs_login or APRS_LOGIN" in result.output
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.log.setup_logging")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_plugin_arg(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no login and config."""

View File

@ -15,7 +15,10 @@ F = t.TypeVar("F", bound=t.Callable[..., t.Any])
class TestSendMessageCommand(unittest.TestCase):
def _build_config(self, login=None, password=None):
config = {"aprs": {}}
config = {
"aprs": {},
"aprsd": {"trace": False},
}
if login:
config["aprs"]["login"] = login
@ -25,7 +28,7 @@ class TestSendMessageCommand(unittest.TestCase):
return aprsd_config.Config(config)
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.log.setup_logging")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_login(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no login and config."""
@ -43,7 +46,7 @@ class TestSendMessageCommand(unittest.TestCase):
assert "Must set --aprs_login or APRS_LOGIN" in result.output
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.log.setup_logging")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_password(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no password and config."""
@ -58,7 +61,7 @@ class TestSendMessageCommand(unittest.TestCase):
assert "Must set --aprs-password or APRS_PASSWORD" in result.output
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.log.setup_logging")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_tocallsign(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no tocallsign."""
@ -76,7 +79,7 @@ class TestSendMessageCommand(unittest.TestCase):
assert "Error: Missing argument 'TOCALLSIGN'" in result.output
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.log.setup_logging")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_command(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no command."""

View File

@ -2,8 +2,8 @@ from unittest import mock
import pytz
from aprsd.fuzzyclock import fuzzy
from aprsd.plugins import time as time_plugin
from aprsd.utils import fuzzy
from .. import fake, test_plugin

View File

@ -3,6 +3,9 @@ minversion = 2.9.0
skipdist = True
skip_missing_interpreters = true
envlist = pre-commit,pep8,py{36,37,38,39}
#requires = tox-pipenv
# pip==22.0.4
# pip-tools==5.4.0
# Activate isolated build environment. tox will use a virtual environment
# to build a source distribution from the source tree. For build tools and