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

Compare commits

...

64 Commits

Author SHA1 Message Date
hemna 95fecd2394 Ensure plugins are last to be loaded.
This patch initializes all of the MsgTrack, WatchList and SeenList
prior to the plugins loading.  Some plugins may kick off messages
being sent immediately.  So everything has to be ready to go
prior to the plugins being loaded.
2021-10-25 11:22:46 -04:00
hemna c8c23e6185 Fixed email connecting to smtp server
Fixed an issue with not passing config in the smtp_connect
2021-10-25 11:12:29 -04:00
hemna a3a3a5aa23 Updated Changelog for 2.4.0 release 2021-10-22 16:24:26 -04:00
hemna e009791b75 Converted MsgTrack to ObjectStoreMixin 2021-10-22 16:07:20 -04:00
hemna b0d25a76f7 Fixed unit tests
Had to initialize the watchlist and seenlist with a config dict.
2021-10-21 09:20:24 -04:00
hemna 89701c8a70 Make sure SeenList update has a from in packet
This makes sure that the packet being processed by the seenlist
has a from address.
2021-10-21 08:40:40 -04:00
hemna 66c5d85b89 Ensure PacketList is initialized 2021-10-20 15:48:35 -04:00
hemna 8ee8b149f1 Added SIGTERM to signal_handler 2021-10-20 15:37:54 -04:00
hemna 0d51634ec2 Enable configuring where to save the objectstore data
This patch adds a new config entry aprsd.save_location
which is the directory used to store and load the objectstore
data.
2021-10-20 14:39:12 -04:00
hemna 135e21cd8d PEP8 cleanup 2021-10-20 14:10:54 -04:00
hemna 4233827dea Added objectstore Mixin
This patch adds the new objectstore Mixin class that enables
classes that store their date in self.data as a serializeable dict,
to be able to be stored to disk at shutdown and loaded at startup.

The SeenList and WatchList are now saved/loaded to/from disk.
2021-10-20 14:07:22 -04:00
hemna 9b2212245f Added -num option to aprsd-dev test-plugin
This allows the user to specify how many times in a loop
to call the plugin.  The Default is 1.
2021-10-20 11:46:55 -04:00
hemna 9150f3b6ff Only call stop_threads if it exists 2021-10-10 14:50:04 -04:00
hemna 278bb6e882 Added new SeenList
This patch adds the seen list feature.  It tracks the callsign of every
packet that aprsd sees.
2021-10-09 14:29:25 -04:00
hemna 004795dbf1 Added plugin version to stats reporting
This patch adds version to the plugin stats collected.
2021-10-09 09:31:51 -04:00
hemna 3b7924b13d Added new HelpPlugin
This patch adds the always enabled HelpPlugin.  This plugin
now will respond to the 'help' or 'h' commands that will
automatically build a help string based on the number of
enabled plugins.  It will also respond to
help <plugin> with the plugin specific help
2021-10-08 12:01:04 -04:00
hemna 2bf85db21b Updated aprsd-dev to use config for logfile format
This patch updates the aprsd-dev command's log file format
to use what's defined as the default and/or use the config file
setting like aprsd server does.
2021-10-08 08:47:24 -04:00
hemna 14f77876f9 Merge pull request #70 from craigerl/utils_refactor
Refactoring/Cleanup
2021-10-08 08:41:14 -04:00
hemna db9cbf51df Updated build.sh
This patch forces the rebuild of the docker buildx build container.
Also makes the tag, version available from cmdln
2021-10-07 10:56:09 -04:00
hemna 5b17228811 removed usage of config.check_config_option
check_config_option has been superceeded by the config UserDict
object's ability to see if a config option exists.
2021-10-07 10:11:48 -04:00
hemna 725bb2fe35 Fixed send-message after config/client rework
This patch fixes the send-message from the command line
ability after the complete rework of the client classes.
2021-10-07 10:05:19 -04:00
hemna f8d87d05bb Fixed issue with flask config
Flask was trying to serialize the UserDict object.  Use the
data (dict) inside of it instead.
2021-10-06 15:17:09 -04:00
hemna 30671cbdbc Added some server startup info logs
This patch adds some general info logs around starting the
client connection as well as loading the plugins.
2021-10-06 12:55:17 -04:00
hemna fdc8c0cd66 Increase email delay to +10
This patch updates the increasing of the email check delay to += 10
seconds instead of +1.
2021-10-06 12:12:49 -04:00
hemna c097c31258 Updated dev to use plugin manager
Also ensure that main creates the client prior to starting the
plugins.
2021-10-06 12:09:52 -04:00
hemna e3c5c7b408 Fixed notify plugins
The notify base filter() was missing the @hookimpl
2021-10-06 12:08:29 -04:00
hemna 491644ece6 Added new Config object.
The config object now has builtin dot notation getter with default

config.get("some.path.here", default="Not found")
2021-10-04 15:37:14 -04:00
hemna a6ed7b894b Fixed email plugin's use of globals
The email plugin was still using globals for tracking
the check_email_delay as well as the config.  This
patch creates a new singleton thread safe mechanism for
check_email_delay with the EmailInfo class.
2021-10-04 11:36:13 -04:00
hemna 270be947b5 Refactored client classes
This patch completely refactors and simplifies how the clients
are created and used.  There is no need now to have a separate
KISSRXThread.  Since all the custom work for the KISS client is
encapsulated in the kiss client itself, the same RX thread and
callback mechanism works for both the APRSIS client and KISS Client
objects.  There is also no need to determine which transport
(aprsis vs kiss) is being used at runtime by any of the messages
objects.  The same API works for both APRSIS and KISS Client objects
2021-09-17 09:32:30 -04:00
hemna 23e3876e7b Refactor utils usage
This patch separates out the config from the utils.py
utils.py has grown into a catchall for everything and this
patch is the start of that cleanup.
2021-09-16 17:08:30 -04:00
hemna 65ea33290a 2.3.1 Changelog 2021-09-13 13:30:06 -04:00
hemna 560e152742 Fixed issue of aprs-is missing keepalive
Started noticing that aprs-is keepalive messages just stop
getting sent.  This causes aprsd to basically disconnect from
the APRS network.  Added a check into the KeepAlive thread to
restart the aprs-is connecter if the last time we got a keepalive
from apris is > 5 minutes.
2021-09-13 13:22:06 -04:00
hemna 69b215d4d8 Fixed packet processing issue with aprsd send-message
This patch adds the missing PacketList initialization
for the send-message command
2021-09-10 15:39:07 -04:00
hemna 4164e89016 Prep 2.3.0 2021-09-08 14:54:59 -04:00
hemna 1b9a9935fc Enable plugins to return message object
This patch enables the ability for plugins to return:
* string
* list of strings
* message object
* list of strings and message ojects

Each string will be encapsulated in a message object prior being sent.
each message object will be sent directly.
Each list will be iterated over and processed according to the above 2
options.
2021-09-08 14:45:15 -04:00
hemna 3faf41b203 Added enabled flag for every plugin object
This allows the admin interface to see which plugins are registered and
enabled.  Enabled is a flag that is set in the setup() method of the
plugin.  This gives the plugin developer a chance to disable the plugin
if something isn't right at setup time.   This allows aprsd to ignore
plugins that are registered but not emabled.
2021-09-08 14:25:12 -04:00
hemna 7e6dffb34b Ensure plugin threads are valid
This patch makes sure that the plugin threads returned from
create_threads is somewhat valid
2021-09-08 13:44:20 -04:00
hemna 605911cb84 Updated Dockerfile to use v2.3.0
Upcoming release of aprsd is v2.3.0
2021-09-07 13:56:06 -04:00
hemna 9eff99dde7 Removed fixed size on logging queue
If the logging queue gets full, due to a maxsize being set,
then any further logs will result on lots of errors being dumped
to stderr as the queue is full.
2021-09-07 13:43:48 -04:00
hemna d6b3df93f1 Added Logfile tab in Admin ui
This patch adds a live view of the aprsd logfile in
the admin ui.  This uses a new Log QueueHandler and the
threads.logging_queue to push log entries into a queue.
The flask websockets server will push those log entries up
to a connected client browser.
2021-09-07 13:13:36 -04:00
hemna 4f088e0a4a Updated Makefile clean target
This expands the clean target to clean up more stuff.
2021-09-05 19:47:26 -04:00
hemna d643ca3892 Added self creating Makefile help target
This updates the Makefile to include a default target of help
and it automatically generates the help based on the targets
and their descriptions.
2021-09-03 16:49:16 -04:00
hemna dfaf3aa3d1 Update dev.py
This patch ensures a valid packet is passed into the plugin prior to
testing.
2021-09-03 16:48:00 -04:00
hemna 62ce84b315 Allow passing in aprsis_client
When the admin user users the web ui to send a message
a new client instance is created with login credentials for
that particular message.  This patch ensures that send_direct
uses that client.
2021-09-02 11:17:15 -04:00
hemna 8ada789d4d Fixed a problem with the AVWX plugin not working
the regex for the plugin was not matching correctly
2021-09-02 11:06:25 -04:00
hemna 558710d348 Remove some noisy trace in email plugin
This removes the trace decorators from the email login
functions.  They have been stable for a while now.
2021-09-02 10:03:43 -04:00
hemna 1ea6c05dec Fixed issue at startup with notify plugin
Ensure that the aprsis client is configured prior to starting
any plugins.
2021-09-02 09:54:13 -04:00
hemna 0f6df5fc05 Fixed email validation
This patch adjusts the py3-email-validation usage.  Since we
upgraded to 1.0.2, the signature has changed.  This patch adjusts
the signature usage so it works again.
2021-09-02 09:43:33 -04:00
hemna 1635feb820 Removed values from forms 2021-09-02 09:12:44 -04:00
hemna c58031d772 Added send-message to the main admin UI 2021-09-02 08:56:25 -04:00
hemna 266ae7f217 Merge pull request #59 from craigerl/web-send-msg
Send Message via admin Web interface
2021-09-01 17:44:50 -04:00
hemna c537b54df6 Updated requirements 2021-09-01 17:38:59 -04:00
hemna 84ce60bc50 Cleaned up some pep8 failures 2021-09-01 17:11:35 -04:00
hemna c941379a5c Upgraded the send-message POC to use websockets
This patch updates the send message Admi page to use
websockets.  It makes updates to the messages list instant.
2021-09-01 17:10:59 -04:00
hemna 23cbf32814 New Admin ui send message page working. 2021-09-01 17:10:13 -04:00
hemna 6d3258e833 Send Message via admin Web interface
This patch adds the ability to send a message from the
admin interface's send-message.html page.
2021-09-01 17:06:56 -04:00
hemna d243e577f0 Merge pull request #50 from craigerl/tcpkiss
Added the ability to use direwolf KISS socket
2021-09-01 16:58:57 -04:00
hemna ca438c9c60 Updated Admin UI to show KISS connections
This updates the top area of the Admin UI to reflect the
connection type (aprs-is vs kiss).
2021-09-01 16:39:50 -04:00
hemna f4dee4b202 Got TX/RX working with aioax25+direwolf over TCP
This patch gets APRSD fully working with the TCPKISS socket
to direwolf.
2021-09-01 14:48:22 -04:00
hemna 54c9a6b55a Rebased from master 2021-08-30 13:34:25 -04:00
hemna b53e2ba7fe Added the ability to use direwolf KISS socket
This patch adds APRS KISS connectivity.  I have tested this with
a running Direwolf install via either a serial KISS connection or
the optional new TCPKISS connection, both to Direwolf.

This adds the new required aioax25 python library for the underlying
KISS and AX25 support.

NOTE: For the TCPKISS connection, this patch requires a pull request
patch the aioax25 library to include a TCP Based KISS TNC client to
enable the TCPKISS client  So you will need to pull down this PR
https://github.com/sjlongland/aioax25/pull/7

To enable this,
  Edit your aprsd.yml file and enable one of the 2 KISS connections.
  Only one is supported at a time.

  kiss:
     serial:
         enabled: True
         device: /dev/ttyS1
         baudrate: 9600

  or

  kiss:
      tcp:
          enabled: True
          host: "ip address/hostname of direwolf"
          port: "direwolf configured kiss port"

This patch alters the Message object classes to be able to
send messages out via the aprslib socket connection to the APRS-IS
network on the internet, or via the direwolf KISS TCP socket,
depending on the origination of the initial message coming in.

If an APRS message comes in via APRS-IS, then replies will go out
APRS-IS.  IF an APRS message comes in via direwolf, then replies
will go out via direwolf KISS TCP socket.   Both can work at the same
time.

TODO:  I need some real APRS message packets to verify that
the new thread is processing packets correctly through the plugins
and able to send the resulting messages back out to direwolf.

Have a hard coded callsign for now in the kissclient consumer call,
just so I can see messages coming in from direwolf.  I dont' have an
APRS capable radio at the moment to send messages directly to direwolf.
Might need to write a simple python socket server to send fake APRS
messages to aprsd kiss, just for finishing up development.
2021-08-30 13:28:39 -04:00
hemna a7d79a6e1b Update Dockerfile to use 2.2.1 2021-08-26 10:50:34 -04:00
hemna 44c4dd69c6 Update Changelog for 2.2.1 2021-08-25 08:28:21 -04:00
hemna ec92b07e31 Silence some log noise
Removed an email thread log at the start of the loop.
Also bumped the Keepalivethread time to 60 seconds
2021-08-25 08:25:36 -04:00
43 changed files with 4967 additions and 1025 deletions
+75
View File
@@ -1,9 +1,84 @@
CHANGES
=======
v2.4.0
------
* Converted MsgTrack to ObjectStoreMixin
* Fixed unit tests
* Make sure SeenList update has a from in packet
* Ensure PacketList is initialized
* Added SIGTERM to signal\_handler
* Enable configuring where to save the objectstore data
* PEP8 cleanup
* Added objectstore Mixin
* Added -num option to aprsd-dev test-plugin
* Only call stop\_threads if it exists
* Added new SeenList
* Added plugin version to stats reporting
* Added new HelpPlugin
* Updated aprsd-dev to use config for logfile format
* Updated build.sh
* removed usage of config.check\_config\_option
* Fixed send-message after config/client rework
* Fixed issue with flask config
* Added some server startup info logs
* Increase email delay to +10
* Updated dev to use plugin manager
* Fixed notify plugins
* Added new Config object
* Fixed email plugin's use of globals
* Refactored client classes
* Refactor utils usage
* 2.3.1 Changelog
v2.3.1
------
* Fixed issue of aprs-is missing keepalive
* Fixed packet processing issue with aprsd send-message
v2.3.0
------
* Prep 2.3.0
* Enable plugins to return message object
* Added enabled flag for every plugin object
* Ensure plugin threads are valid
* Updated Dockerfile to use v2.3.0
* Removed fixed size on logging queue
* Added Logfile tab in Admin ui
* Updated Makefile clean target
* Added self creating Makefile help target
* Update dev.py
* Allow passing in aprsis\_client
* Fixed a problem with the AVWX plugin not working
* Remove some noisy trace in email plugin
* Fixed issue at startup with notify plugin
* Fixed email validation
* Removed values from forms
* Added send-message to the main admin UI
* Updated requirements
* Cleaned up some pep8 failures
* Upgraded the send-message POC to use websockets
* New Admin ui send message page working
* Send Message via admin Web interface
* Updated Admin UI to show KISS connections
* Got TX/RX working with aioax25+direwolf over TCP
* Rebased from master
* Added the ability to use direwolf KISS socket
* Update Dockerfile to use 2.2.1
v2.2.1
------
* Update Changelog for 2.2.1
* Silence some log noise
v2.2.0
------
* Updated Changelog for v2.2.0
* Updated overview image
* Removed Black code style reference
* Removed TXThread
+43 -28
View File
@@ -1,5 +1,7 @@
REQUIREMENTS_TXT ?= requirements.txt dev-requirements.txt
.DEFAULT_GOAL := help
.PHONY: dev docs server test
include Makefile.venv
Makefile.venv:
curl \
@@ -7,52 +9,65 @@ Makefile.venv:
-L "https://github.com/sio/Makefile.venv/raw/v2020.08.14/Makefile.venv"
echo "5afbcf51a82f629cd65ff23185acde90ebe4dec889ef80bbdc12562fbd0b2611 *Makefile.fetched" \
| sha256sum --check - \
&& mv Makefile.fetched Makefile.venv
&& mv Makefile.fetched Makefile.venv
all: pip dev
help: # Help for the Makefile
@egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
.PHONY: dev
dev: venv
$(VENV)/pre-commit install
dev: venv ## Create the virtualenv with all the requirements installed
.PHONY: docs
docs: build
cp README.rst docs/readme.rst
cp Changelog docs/changelog.rst
tox -edocs
.PHONY: server
server: venv
$(VENV)/aprsd server --loglevel DEBUG
clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
clean: clean-venv
rm -rf dist/*
clean-build: ## remove build artifacts
rm -fr build/
rm -fr dist/
rm -fr .eggs/
find . -name '*.egg-info' -exec rm -fr {} +
find . -name '*.egg' -exec rm -f {} +
.PHONY: test
test: dev
clean-pyc: ## remove Python file artifacts
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rm -fr {} +
clean-test: ## remove test and coverage artifacts
rm -fr .tox/
rm -f .coverage
rm -fr htmlcov/
rm -fr .pytest_cache
test: dev ## Run all the tox tests
tox -p all
build: test
build: test ## Make the build artifact prior to doing an upload
$(VENV)/python3 setup.py sdist bdist_wheel
$(VENV)/twine check dist/*
upload: build
upload: build ## Upload a new version of the plugin
$(VENV)/twine upload dist/*
docker: test
docker build -t hemna6969/aprsd:latest -f docker/Dockerfile docker
docker-dev: test
docker build -t hemna6969/aprsd:master -f docker/Dockerfile-dev docker
update-requirements: dev
$(VENV)/pip-compile requirements.in
$(VENV)/pip-compile dev-requirements.in
check: dev # Code format check with isort and black
check: dev ## Code format check with tox and pep8
tox -efmt-check
tox -epep8
fix: dev # fixes code formatting with isort and black
fix: dev ## fixes code formatting with gray
tox -efmt
server: venv ## Create the virtual environment and run aprsd server --loglevel DEBUG
$(VENV)/aprsd server --loglevel DEBUG
docker: test ## Make a docker container tagged with hemna6969/aprsd:latest
docker build -t hemna6969/aprsd:latest -f docker/Dockerfile docker
docker-dev: test ## Make a development docker container tagged with hemna6969/aprsd:master
docker build -t hemna6969/aprsd:master -f docker/Dockerfile-dev docker
update-requirements: dev ## Update the requirements.txt and dev-requirements.txt files
$(VENV)/pip-compile requirements.in
$(VENV)/pip-compile dev-requirements.in
+133 -189
View File
@@ -1,26 +1,30 @@
import abc
import logging
import select
import time
import aprslib
from aprslib import is_py3
from aprslib.exceptions import (
ConnectionDrop, ConnectionError, GenericError, LoginError, ParseError,
UnknownFormat,
)
from aprslib.exceptions import LoginError
import aprsd
from aprsd import stats
from aprsd import trace
from aprsd.clients import aprsis, kiss
LOG = logging.getLogger("APRSD")
TRANSPORT_APRSIS = "aprsis"
TRANSPORT_TCPKISS = "tcpkiss"
TRANSPORT_SERIALKISS = "serialkiss"
# Main must create this from the ClientFactory
# object such that it's populated with the
# Correct config
factory = None
class Client:
"""Singleton client class that constructs the aprslib connection."""
_instance = None
aprs_client = None
_client = None
config = None
connected = False
@@ -40,14 +44,49 @@ class Client:
@property
def client(self):
if not self.aprs_client:
self.aprs_client = self.setup_connection()
return self.aprs_client
if not self._client:
self._client = self.setup_connection()
return self._client
def reset(self):
"""Call this to force a rebuild/reconnect."""
del self.aprs_client
del self._client
@abc.abstractmethod
def setup_connection(self):
pass
@staticmethod
@abc.abstractmethod
def is_enabled(config):
pass
@staticmethod
@abc.abstractmethod
def transport(config):
pass
@abc.abstractmethod
def decode_packet(self, *args, **kwargs):
pass
class APRSISClient(Client):
@staticmethod
def is_enabled(config):
# Defaults to True if the enabled flag is non existent
return config["aprs"].get("enabled", True)
@staticmethod
def transport(config):
return TRANSPORT_APRSIS
def decode_packet(self, *args, **kwargs):
"""APRS lib already decodes this."""
return args[0]
@trace.trace
def setup_connection(self):
user = self.config["aprs"]["login"]
password = self.config["aprs"]["password"]
@@ -55,10 +94,11 @@ class Client:
port = self.config["aprs"].get("port", 14580)
connected = False
backoff = 1
aprs_client = None
while not connected:
try:
LOG.info("Creating aprslib client")
aprs_client = Aprsdis(user, passwd=password, host=host, port=port)
aprs_client = aprsis.Aprsdis(user, passwd=password, host=host, port=port)
# Force the logging to be the same
aprs_client.logger = LOG
aprs_client.connect()
@@ -77,188 +117,92 @@ class Client:
return aprs_client
class Aprsdis(aprslib.IS):
"""Extend the aprslib class so we can exit properly."""
class KISSClient(Client):
# flag to tell us to stop
thread_stop = False
@staticmethod
def is_enabled(config):
"""Return if tcp or serial KISS is enabled."""
if "kiss" not in config:
return False
# timeout in seconds
select_timeout = 1
if config.get("kiss.serial.enabled", default=False):
return True
def stop(self):
self.thread_stop = True
LOG.info("Shutdown Aprsdis client.")
if config.get("kiss.tcp.enabled", default=False):
return True
def _socket_readlines(self, blocking=False):
"""
Generator for complete lines, received from the server
"""
try:
self.sock.setblocking(0)
except OSError as e:
self.logger.error(f"socket error when setblocking(0): {str(e)}")
raise aprslib.ConnectionDrop("connection dropped")
@staticmethod
def transport(config):
if config.get("kiss.serial.enabled", default=False):
return TRANSPORT_SERIALKISS
while not self.thread_stop:
short_buf = b""
newline = b"\r\n"
if config.get("kiss.tcp.enabled", default=False):
return TRANSPORT_TCPKISS
# set a select timeout, so we get a chance to exit
# when user hits CTRL-C
readable, writable, exceptional = select.select(
[self.sock],
[],
[],
self.select_timeout,
)
if not readable:
continue
def decode_packet(self, *args, **kwargs):
"""We get a frame, which has to be decoded."""
frame = kwargs["frame"]
LOG.debug(f"Got an APRS Frame '{frame}'")
# try and nuke the * from the fromcall sign.
frame.header._source._ch = False
payload = str(frame.payload.decode())
msg = f"{str(frame.header)}:{payload}"
# msg = frame.tnc2
LOG.debug(f"Decoding {msg}")
try:
short_buf = self.sock.recv(4096)
packet = aprslib.parse(msg)
return packet
# sock.recv returns empty if the connection drops
if not short_buf:
self.logger.error("socket.recv(): returned empty")
raise aprslib.ConnectionDrop("connection dropped")
except OSError as e:
# self.logger.error("socket error on recv(): %s" % str(e))
if "Resource temporarily unavailable" in str(e):
if not blocking:
if len(self.buf) == 0:
break
self.buf += short_buf
while newline in self.buf:
line, self.buf = self.buf.split(newline, 1)
yield line
def _send_login(self):
"""
Sends login string to server
"""
login_str = "user {0} pass {1} vers github.com/craigerl/aprsd {3}{2}\r\n"
login_str = login_str.format(
self.callsign,
self.passwd,
(" filter " + self.filter) if self.filter != "" else "",
aprsd.__version__,
)
self.logger.info("Sending login information")
try:
self._sendall(login_str)
self.sock.settimeout(5)
test = self.sock.recv(len(login_str) + 100)
if is_py3:
test = test.decode("latin-1")
test = test.rstrip()
self.logger.debug("Server: %s", test)
a, b, callsign, status, e = test.split(" ", 4)
s = e.split(",")
if len(s):
server_string = s[0].replace("server ", "")
else:
server_string = e.replace("server ", "")
self.logger.info(f"Connected to {server_string}")
self.server_string = server_string
stats.APRSDStats().set_aprsis_server(server_string)
if callsign == "":
raise LoginError("Server responded with empty callsign???")
if callsign != self.callsign:
raise LoginError(f"Server: {test}")
if status != "verified," and self.passwd != "-1":
raise LoginError("Password is incorrect")
if self.passwd == "-1":
self.logger.info("Login successful (receive only)")
else:
self.logger.info("Login successful")
except LoginError as e:
self.logger.error(str(e))
self.close()
raise
except Exception as e:
self.close()
self.logger.error(f"Failed to login '{e}'")
raise LoginError("Failed to login")
def consumer(self, callback, blocking=True, immortal=False, raw=False):
"""
When a position sentence is received, it will be passed to the callback function
blocking: if true (default), runs forever, otherwise will return after one sentence
You can still exit the loop, by raising StopIteration in the callback function
immortal: When true, consumer will try to reconnect and stop propagation of Parse exceptions
if false (default), consumer will return
raw: when true, raw packet is passed to callback, otherwise the result from aprs.parse()
"""
if not self._connected:
raise ConnectionError("not connected to a server")
line = b""
while True:
try:
for line in self._socket_readlines(blocking):
if line[0:1] != b"#":
if raw:
callback(line)
else:
callback(self._parse(line))
else:
self.logger.debug("Server: %s", line.decode("utf8"))
stats.APRSDStats().set_aprsis_keepalive()
except ParseError as exp:
self.logger.log(
11,
"%s\n Packet: %s",
exp,
exp.packet,
)
except UnknownFormat as exp:
self.logger.log(
9,
"%s\n Packet: %s",
exp,
exp.packet,
)
except LoginError as exp:
self.logger.error("%s: %s", exp.__class__.__name__, exp)
except (KeyboardInterrupt, SystemExit):
raise
except (ConnectionDrop, ConnectionError):
self.close()
if not immortal:
raise
else:
self.connect(blocking=blocking)
continue
except GenericError:
pass
except StopIteration:
break
except Exception:
self.logger.error("APRS Packet: %s", line)
raise
if not blocking:
break
@trace.trace
def setup_connection(self):
ax25client = kiss.Aioax25Client(self.config)
return ax25client
def get_client():
cl = Client()
return cl.client
class ClientFactory:
_instance = None
def __new__(cls, *args, **kwargs):
"""This magic turns this into a singleton."""
if cls._instance is None:
cls._instance = super().__new__(cls)
# Put any initialization here.
return cls._instance
def __init__(self, config):
self.config = config
self._builders = {}
def register(self, key, builder):
self._builders[key] = builder
def create(self, key=None):
if not key:
if APRSISClient.is_enabled(self.config):
key = TRANSPORT_APRSIS
elif KISSClient.is_enabled(self.config):
key = KISSClient.transport(self.config)
LOG.debug(f"GET client {key}")
builder = self._builders.get(key)
if not builder:
raise ValueError(key)
return builder(self.config)
def is_client_enabled(self):
"""Make sure at least one client is enabled."""
enabled = False
for key in self._builders.keys():
enabled |= self._builders[key].is_enabled(self.config)
return enabled
@staticmethod
def setup(config):
"""Create and register all possible client objects."""
global factory
factory = ClientFactory(config)
factory.register(TRANSPORT_APRSIS, APRSISClient)
factory.register(TRANSPORT_TCPKISS, KISSClient)
factory.register(TRANSPORT_SERIALKISS, KISSClient)
+209
View File
@@ -0,0 +1,209 @@
import logging
import select
import aprslib
from aprslib import is_py3
from aprslib.exceptions import (
ConnectionDrop, ConnectionError, GenericError, LoginError, ParseError,
UnknownFormat,
)
import aprsd
from aprsd import stats
LOG = logging.getLogger("APRSD")
class Aprsdis(aprslib.IS):
"""Extend the aprslib class so we can exit properly."""
# flag to tell us to stop
thread_stop = False
# timeout in seconds
select_timeout = 1
def stop(self):
self.thread_stop = True
LOG.info("Shutdown Aprsdis client.")
def send(self, msg):
"""Send an APRS Message object."""
line = str(msg)
self.sendall(line)
def _socket_readlines(self, blocking=False):
"""
Generator for complete lines, received from the server
"""
try:
self.sock.setblocking(0)
except OSError as e:
self.logger.error(f"socket error when setblocking(0): {str(e)}")
raise aprslib.ConnectionDrop("connection dropped")
while not self.thread_stop:
short_buf = b""
newline = b"\r\n"
# set a select timeout, so we get a chance to exit
# when user hits CTRL-C
readable, writable, exceptional = select.select(
[self.sock],
[],
[],
self.select_timeout,
)
if not readable:
if not blocking:
break
else:
continue
try:
short_buf = self.sock.recv(4096)
# sock.recv returns empty if the connection drops
if not short_buf:
if not blocking:
# We could just not be blocking, so empty is expected
continue
else:
self.logger.error("socket.recv(): returned empty")
raise aprslib.ConnectionDrop("connection dropped")
except OSError as e:
# self.logger.error("socket error on recv(): %s" % str(e))
if "Resource temporarily unavailable" in str(e):
if not blocking:
if len(self.buf) == 0:
break
self.buf += short_buf
while newline in self.buf:
line, self.buf = self.buf.split(newline, 1)
yield line
def _send_login(self):
"""
Sends login string to server
"""
login_str = "user {0} pass {1} vers github.com/craigerl/aprsd {3}{2}\r\n"
login_str = login_str.format(
self.callsign,
self.passwd,
(" filter " + self.filter) if self.filter != "" else "",
aprsd.__version__,
)
self.logger.info("Sending login information")
try:
self._sendall(login_str)
self.sock.settimeout(5)
test = self.sock.recv(len(login_str) + 100)
if is_py3:
test = test.decode("latin-1")
test = test.rstrip()
self.logger.debug("Server: %s", test)
a, b, callsign, status, e = test.split(" ", 4)
s = e.split(",")
if len(s):
server_string = s[0].replace("server ", "")
else:
server_string = e.replace("server ", "")
self.logger.info(f"Connected to {server_string}")
self.server_string = server_string
stats.APRSDStats().set_aprsis_server(server_string)
if callsign == "":
raise LoginError("Server responded with empty callsign???")
if callsign != self.callsign:
raise LoginError(f"Server: {test}")
if status != "verified," and self.passwd != "-1":
raise LoginError("Password is incorrect")
if self.passwd == "-1":
self.logger.info("Login successful (receive only)")
else:
self.logger.info("Login successful")
except LoginError as e:
self.logger.error(str(e))
self.close()
raise
except Exception as e:
self.close()
self.logger.error(f"Failed to login '{e}'")
raise LoginError("Failed to login")
def consumer(self, callback, blocking=True, immortal=False, raw=False):
"""
When a position sentence is received, it will be passed to the callback function
blocking: if true (default), runs forever, otherwise will return after one sentence
You can still exit the loop, by raising StopIteration in the callback function
immortal: When true, consumer will try to reconnect and stop propagation of Parse exceptions
if false (default), consumer will return
raw: when true, raw packet is passed to callback, otherwise the result from aprs.parse()
"""
if not self._connected:
raise ConnectionError("not connected to a server")
line = b""
while True and not self.thread_stop:
try:
for line in self._socket_readlines(blocking):
if line[0:1] != b"#":
if raw:
callback(line)
else:
callback(self._parse(line))
else:
self.logger.debug("Server: %s", line.decode("utf8"))
stats.APRSDStats().set_aprsis_keepalive()
except ParseError as exp:
self.logger.log(
11,
"%s\n Packet: %s",
exp,
exp.packet,
)
except UnknownFormat as exp:
self.logger.log(
9,
"%s\n Packet: %s",
exp,
exp.packet,
)
except LoginError as exp:
self.logger.error("%s: %s", exp.__class__.__name__, exp)
except (KeyboardInterrupt, SystemExit):
raise
except (ConnectionDrop, ConnectionError):
self.close()
if not immortal:
raise
else:
self.connect(blocking=blocking)
continue
except GenericError:
pass
except StopIteration:
break
except Exception:
self.logger.error("APRS Packet: %s", line)
raise
if not blocking:
break
+94
View File
@@ -0,0 +1,94 @@
import asyncio
import logging
from aioax25 import interface
from aioax25 import kiss as kiss
from aioax25.aprs import APRSInterface
LOG = logging.getLogger("APRSD")
class Aioax25Client:
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):
# we can be TCP kiss or Serial kiss
if "serial" in self.config["kiss"] and self.config["kiss"]["serial"].get(
"enabled",
False,
):
LOG.debug(
"Setting up Serial KISS connection to {}".format(
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,
)
elif "tcp" in self.config["kiss"] and self.config["kiss"]["tcp"].get(
"enabled",
False,
):
LOG.debug(
"Setting up KISSTCP Connection to {}:{}".format(
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.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,
)
def stop(self):
LOG.debug(self.kissdev)
self.kissdev._close()
self.loop.stop()
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 send(self, msg):
"""Send an APRS Message object."""
payload = f"{msg._filter_for_send()}"
self.aprsint.send_message(
addressee=msg.tocall,
message=payload,
path=["WIDE1-1", "WIDE2-1"],
oneshot=True,
)
+391
View File
@@ -0,0 +1,391 @@
import collections
import logging
import os
from pathlib import Path
import sys
import click
import yaml
from aprsd import utils
home = str(Path.home())
DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/"
DEFAULT_SAVE_FILE = f"{home}/.config/aprsd/aprsd.p"
DEFAULT_CONFIG_FILE = f"{home}/.config/aprsd/aprsd.yml"
LOG_LEVELS = {
"CRITICAL": logging.CRITICAL,
"ERROR": logging.ERROR,
"WARNING": logging.WARNING,
"INFO": logging.INFO,
"DEBUG": logging.DEBUG,
}
DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
DEFAULT_LOG_FORMAT = (
"[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s]"
" %(message)s - [%(pathname)s:%(lineno)d]"
)
QUEUE_DATE_FORMAT = "[%m/%d/%Y] [%I:%M:%S %p]"
QUEUE_LOG_FORMAT = (
"%(asctime)s [%(threadName)-20.20s] [%(levelname)-5.5s]"
" %(message)s - [%(pathname)s:%(lineno)d]"
)
CORE_MESSAGE_PLUGINS = [
"aprsd.plugins.email.EmailPlugin",
"aprsd.plugins.fortune.FortunePlugin",
"aprsd.plugins.location.LocationPlugin",
"aprsd.plugins.ping.PingPlugin",
"aprsd.plugins.query.QueryPlugin",
"aprsd.plugins.stock.StockPlugin",
"aprsd.plugins.time.TimePlugin",
"aprsd.plugins.weather.USWeatherPlugin",
"aprsd.plugins.version.VersionPlugin",
]
CORE_NOTIFY_PLUGINS = [
"aprsd.plugins.notify.NotifySeenPlugin",
]
# an example of what should be in the ~/.aprsd/config.yml
DEFAULT_CONFIG_DICT = {
"ham": {"callsign": "NOCALL"},
"aprs": {
"enabled": True,
"login": "CALLSIGN",
"password": "00000",
"host": "rotate.aprs2.net",
"port": 14580,
},
"kiss": {
"tcp": {
"enabled": False,
"host": "direwolf.ip.address",
"port": "8001",
},
"serial": {
"enabled": False,
"device": "/dev/ttyS0",
"baudrate": 9600,
},
},
"aprsd": {
"logfile": "/tmp/aprsd.log",
"logformat": DEFAULT_LOG_FORMAT,
"dateformat": DEFAULT_DATE_FORMAT,
"save_location": DEFAULT_CONFIG_DIR,
"trace": False,
"enabled_plugins": CORE_MESSAGE_PLUGINS,
"units": "imperial",
"watch_list": {
"enabled": False,
# Who gets the alert?
"alert_callsign": "NOCALL",
# 43200 is 12 hours
"alert_time_seconds": 43200,
# How many packets to save in a ring Buffer
# for a particular callsign
"packet_keep_count": 10,
"callsigns": [],
"enabled_plugins": CORE_NOTIFY_PLUGINS,
},
"web": {
"enabled": True,
"logging_enabled": True,
"host": "0.0.0.0",
"port": 8001,
"users": {
"admin": "password-here",
},
},
"email": {
"enabled": True,
"shortcuts": {
"aa": "5551239999@vtext.com",
"cl": "craiglamparter@somedomain.org",
"wb": "555309@vtext.com",
},
"smtp": {
"login": "SMTP_USERNAME",
"password": "SMTP_PASSWORD",
"host": "smtp.gmail.com",
"port": 465,
"use_ssl": False,
"debug": False,
},
"imap": {
"login": "IMAP_USERNAME",
"password": "IMAP_PASSWORD",
"host": "imap.gmail.com",
"port": 993,
"use_ssl": True,
"debug": False,
},
},
},
"services": {
"aprs.fi": {"apiKey": "APIKEYVALUE"},
"openweathermap": {"apiKey": "APIKEYVALUE"},
"opencagedata": {"apiKey": "APIKEYVALUE"},
"avwx": {"base_url": "http://host:port", "apiKey": "APIKEYVALUE"},
},
}
class Config(collections.UserDict):
def _get(self, d, keys, default=None):
"""
Example:
d = {'meta': {'status': 'OK', 'status_code': 200}}
_get(d, ['meta', 'status_code']) # => 200
_get(d, ['garbage', 'status_code']) # => None
_get(d, ['meta', 'garbage'], default='-') # => '-'
"""
if type(keys) is str and "." in keys:
keys = keys.split(".")
assert type(keys) is list
if d is None:
return default
if not keys:
return d
if type(d) is str:
return default
return self._get(d.get(keys[0]), keys[1:], default)
def get(self, path, default=None):
return self._get(self.data, path, default=default)
def exists(self, path):
"""See if a conf value exists."""
test = "-3.14TEST41.3-"
return self.get(path, default=test) != test
def check_option(self, path, default_fail=None):
"""Make sure the config option doesn't have default value."""
if not self.exists(path):
raise Exception(
"Option '{}' was not in config file".format(
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(
"Config file needs to be changed from provided"
" defaults for '{}'".format(
path,
),
)
def add_config_comments(raw_yaml):
end_idx = utils.end_substr(raw_yaml, "aprs:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # Set enabled to False if there is no internet connectivity."
"\n # This is useful for a direwolf KISS aprs connection only. "
"\n"
"\n # Get the passcode for your callsign here: "
"\n # https://apps.magicbug.co.uk/passcode",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "aprs.fi:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # Get the apiKey from your aprs.fi account here: "
"\n # http://aprs.fi/account",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "opencagedata:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # (Optional for TimeOpenCageDataPlugin) "
"\n # Get the apiKey from your opencagedata account here: "
"\n # https://opencagedata.com/dashboard#api-keys",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "openweathermap:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # (Optional for OWMWeatherPlugin) "
"\n # Get the apiKey from your "
"\n # openweathermap account here: "
"\n # https://home.openweathermap.org/api_keys",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "avwx:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # (Optional for AVWXWeatherPlugin) "
"\n # Use hosted avwx-api here: https://avwx.rest "
"\n # or deploy your own from here: "
"\n # https://github.com/avwx-rest/avwx-api",
end_idx,
)
return raw_yaml
def dump_default_cfg():
return add_config_comments(
yaml.dump(
DEFAULT_CONFIG_DICT,
indent=4,
),
)
def create_default_config():
"""Create a default config file."""
# make sure the directory location exists
config_file_expanded = os.path.expanduser(DEFAULT_CONFIG_FILE)
config_dir = os.path.dirname(config_file_expanded)
if not os.path.exists(config_dir):
click.echo(f"Config dir '{config_dir}' doesn't exist, creating.")
utils.mkdir_p(config_dir)
with open(config_file_expanded, "w+") as cf:
cf.write(dump_default_cfg())
def get_config(config_file):
"""This tries to read the yaml config from <config_file>."""
config_file_expanded = os.path.expanduser(config_file)
if os.path.exists(config_file_expanded):
with open(config_file_expanded) as stream:
config = yaml.load(stream, Loader=yaml.FullLoader)
return Config(config)
else:
if config_file == DEFAULT_CONFIG_FILE:
click.echo(
f"{config_file_expanded} is missing, creating config file",
)
create_default_config()
msg = (
"Default config file created at {}. Please edit with your "
"settings.".format(config_file)
)
click.echo(msg)
else:
# The user provided a config file path different from the
# Default, so we won't try and create it, just bitch and bail.
msg = f"Custom config file '{config_file}' is missing."
click.echo(msg)
sys.exit(-1)
# This method tries to parse the config yaml file
# and consume the settings.
# If the required params don't exist,
# it will look in the environment
def parse_config(config_file):
config = get_config(config_file)
def fail(msg):
click.echo(msg)
sys.exit(-1)
def check_option(config, path, default_fail=None):
try:
config.check_option(path, default_fail=default_fail)
except Exception as ex:
fail(repr(ex))
else:
return config
# special check here to make sure user has edited the config file
# and changed the ham callsign
check_option(
config,
"ham.callsign",
default_fail=DEFAULT_CONFIG_DICT["ham"]["callsign"],
)
check_option(
config,
["services", "aprs.fi", "apiKey"],
default_fail=DEFAULT_CONFIG_DICT["services"]["aprs.fi"]["apiKey"],
)
check_option(
config,
"aprs.login",
default_fail=DEFAULT_CONFIG_DICT["aprs"]["login"],
)
check_option(
config,
["aprs", "password"],
default_fail=DEFAULT_CONFIG_DICT["aprs"]["password"],
)
# Ensure they change the admin password
if config.get("aprsd.web.enabled") is True:
check_option(
config,
["aprsd", "web", "users", "admin"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["web"]["users"]["admin"],
)
if config.get("aprsd.watch_list.enabled") is True:
check_option(
config,
["aprsd", "watch_list", "alert_callsign"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["watch_list"]["alert_callsign"],
)
if config.get("aprsd.email.enabled") is True:
# Check IMAP server settings
check_option(config, ["aprsd", "email", "imap", "host"])
check_option(config, ["aprsd", "email", "imap", "port"])
check_option(
config,
["aprsd", "email", "imap", "login"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["login"],
)
check_option(
config,
["aprsd", "email", "imap", "password"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["password"],
)
# Check SMTP server settings
check_option(config, ["aprsd", "email", "smtp", "host"])
check_option(config, ["aprsd", "email", "smtp", "port"])
check_option(
config,
["aprsd", "email", "smtp", "login"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["login"],
)
check_option(
config,
["aprsd", "email", "smtp", "password"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["password"],
)
return config
+65 -16
View File
@@ -14,7 +14,9 @@ import click_completion
# local imports here
import aprsd
from aprsd import client, plugin, utils
from aprsd import client
from aprsd import config as aprsd_config
from aprsd import plugin
# setup the global logger
@@ -119,14 +121,23 @@ def install(append, case_insensitive, shell, path):
# to disable logging to stdout, but still log to file
# use the --quiet option on the cmdln
def setup_logging(config, loglevel, quiet):
log_level = LOG_LEVELS[loglevel]
log_level = aprsd_config.LOG_LEVELS[loglevel]
LOG.setLevel(log_level)
log_format = "[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s]" " %(message)s"
date_format = "%m/%d/%Y %I:%M:%S %p"
log_format = config["aprsd"].get(
"logformat",
aprsd_config.DEFAULT_LOG_FORMAT,
)
date_format = config["aprsd"].get(
"dateformat",
aprsd_config.DEFAULT_DATE_FORMAT,
)
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
log_file = config["aprs"].get("logfile", None)
log_file = config["aprsd"].get("logfile", None)
if log_file:
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
fh = RotatingFileHandler(
log_file, maxBytes=(10248576 * 5),
backupCount=4,
)
else:
fh = NullHandler()
@@ -156,7 +167,7 @@ def setup_logging(config, loglevel, quiet):
"--config",
"config_file",
show_default=True,
default=utils.DEFAULT_CONFIG_FILE,
default=aprsd_config.DEFAULT_CONFIG_FILE,
help="The aprsd config file to use for options.",
)
@click.option(
@@ -167,35 +178,73 @@ def setup_logging(config, loglevel, quiet):
default="aprsd.plugins.wx.WxPlugin",
help="The plugin to run",
)
@click.option(
"-a",
"--all",
"load_all",
show_default=True,
is_flag=True,
default=False,
help="Load all the plugins in config?",
)
@click.option(
"-n",
"--num",
"number",
show_default=True,
default=1,
help="Number of times to call the plugin",
)
@click.argument("fromcall")
@click.argument("message", nargs=-1, required=True)
def test_plugin(
loglevel,
config_file,
plugin_path,
load_all,
number,
fromcall,
message,
):
"""APRSD Plugin test app."""
config = utils.parse_config(config_file)
config = aprsd_config.parse_config(config_file)
setup_logging(config, loglevel, False)
LOG.info(f"Test APRSD PLugin version: {aprsd.__version__}")
LOG.info(f"Test APRSD Plgin version: {aprsd.__version__}")
if type(message) is tuple:
message = " ".join(message)
LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'")
client.Client(config)
pm = plugin.PluginManager(config)
if load_all:
pm.setup_plugins()
else:
pm._init()
obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config)
# Register the plugin they wanted tested.
LOG.info(
"Testing plugin {} Version {}".format(
obj.__class__, obj.version,
),
)
pm._pluggy_pm.register(obj)
login = config["aprs"]["login"]
packet = {"from": fromcall, "message_text": message, "msgNo": 1}
packet = {
"from": fromcall, "addresse": login,
"message_text": message,
"format": "message",
"msgNo": 1,
}
LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'")
reply = obj.filter(packet)
# Plugin might have threads, so lets stop them so we can exit.
obj.stop_threads()
LOG.info(f"Result = '{reply}'")
for x in range(number):
reply = pm.run(packet)
# Plugin might have threads, so lets stop them so we can exit.
# obj.stop_threads()
LOG.info(f"Result{x} = '{reply}'")
pm.stop()
if __name__ == "__main__":
+425 -9
View File
@@ -4,14 +4,23 @@ import logging
from logging import NullHandler
from logging.handlers import RotatingFileHandler
import sys
import threading
import time
import aprslib
from aprslib.exceptions import LoginError
import flask
from flask import request
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 aprsd
from aprsd import messaging, packets, plugin, stats, utils
from aprsd import client
from aprsd import config as aprsd_config
from aprsd import messaging, packets, plugin, stats, threads, utils
from aprsd.clients import aprsis
LOG = logging.getLogger("APRSD")
@@ -20,6 +29,72 @@ auth = HTTPBasicAuth()
users = None
class SentMessages:
_instance = None
lock = None
msgs = {}
def __new__(cls, *args, **kwargs):
"""This magic turns this into a singleton."""
if cls._instance is None:
cls._instance = super().__new__(cls)
# Put any initialization here.
cls.lock = threading.Lock()
return cls._instance
def add(self, msg):
with self.lock:
self.msgs[msg.id] = self._create(msg.id)
self.msgs[msg.id]["from"] = msg.fromcall
self.msgs[msg.id]["to"] = msg.tocall
self.msgs[msg.id]["message"] = msg.message.rstrip("\n")
self.msgs[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,
}
def __len__(self):
with self.lock:
return len(self.msgs.keys())
def get(self, id):
with self.lock:
if id in self.msgs:
return self.msgs[id]
def get_all(self):
with self.lock:
return self.msgs
def set_status(self, id, status):
with self.lock:
self.msgs[id]["last_update"] = str(datetime.datetime.now())
self.msgs[id]["status"] = status
def ack(self, id):
"""The message got an ack!"""
with self.lock:
self.msgs[id]["last_update"] = str(datetime.datetime.now())
self.msgs[id]["ack"] = True
def reply(self, id, packet):
"""We got a packet back from the sent message."""
with self.lock:
self.msgs[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
@@ -31,6 +106,172 @@ def verify_password(username, password):
return username
class SendMessageThread(threads.APRSDThread):
"""Thread for sending a message from web."""
aprsis_client = None
request = None
got_ack = False
got_reply = False
def __init__(self, config, info, msg, namespace):
self.config = config
self.request = info
self.msg = msg
self.namespace = namespace
self.start_time = datetime.datetime.now()
msg = "({} -> {}) : {}".format(
info["from"],
info["to"],
info["message"],
)
super().__init__(f"WEB_SEND_MSG-{msg}")
def setup_connection(self):
user = self.request["from"]
password = self.request["password"]
host = self.config["aprs"].get("host", "rotate.aprs.net")
port = self.config["aprs"].get("port", 14580)
connected = False
backoff = 1
while not connected:
try:
LOG.info("Creating aprslib client")
aprs_client = aprsis.Aprsdis(
user,
passwd=password,
host=host,
port=port,
)
# Force the logging to be the same
aprs_client.logger = LOG
aprs_client.connect()
connected = True
backoff = 1
except LoginError as e:
LOG.error(f"Failed to login to APRS-IS Server '{e}'")
connected = False
raise e
except Exception as e:
LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
time.sleep(backoff)
backoff = backoff * 2
continue
LOG.debug(f"Logging in to APRS-IS with user '{user}'")
return aprs_client
def run(self):
LOG.debug("Starting")
from_call = self.request["from"]
to_call = self.request["to"]
message = self.request["message"]
LOG.info(
"From: '{}' To: '{}' Send '{}'".format(
from_call,
to_call,
message,
),
)
try:
self.aprs_client = self.setup_connection()
except LoginError as e:
f"Failed to setup Connection {e}"
self.msg.send_direct(aprsis_client=self.aprs_client)
SentMessages().set_status(self.msg.id, "Sent")
while not self.thread_stop:
can_loop = self.loop()
if not can_loop:
self.stop()
threads.APRSDThreadList().remove(self)
LOG.debug("Exiting")
def rx_packet(self, packet):
global socketio
# LOG.debug("Got packet back {}".format(packet))
resp = packet.get("response", None)
if resp == "ack":
ack_num = packet.get("msgNo")
LOG.info(f"We got ack for our sent message {ack_num}")
messaging.log_packet(packet)
SentMessages().ack(self.msg.id)
socketio.emit(
"ack", SentMessages().get(self.msg.id),
namespace="/sendmsg",
)
stats.APRSDStats().ack_rx_inc()
self.got_ack = True
if self.request["wait_reply"] == "0" or self.got_reply:
# We aren't waiting for a reply, so we can bail
self.stop()
self.thread_stop = self.aprs_client.thread_stop = True
else:
packets.PacketList().add(packet)
stats.APRSDStats().msgs_rx_inc()
message = packet.get("message_text", None)
fromcall = packet["from"]
msg_number = packet.get("msgNo", "0")
messaging.log_message(
"Received Message",
packet["raw"],
message,
fromcall=fromcall,
ack=msg_number,
)
SentMessages().reply(self.msg.id, packet)
SentMessages().set_status(self.msg.id, "Got Reply")
socketio.emit(
"reply", SentMessages().get(self.msg.id),
namespace="/sendmsg",
)
# Send the ack back?
ack = messaging.AckMessage(
self.request["from"],
fromcall,
msg_id=msg_number,
)
ack.send_direct()
SentMessages().set_status(self.msg.id, "Ack Sent")
# Now we can exit, since we are done.
self.got_reply = True
if self.got_ack:
self.stop()
self.thread_stop = self.aprs_client.thread_stop = True
def loop(self):
# we have a general time limit expecting results of
# around 120 seconds before we exit
now = datetime.datetime.now()
start_delta = str(now - self.start_time)
delta = utils.parse_delta_str(start_delta)
d = datetime.timedelta(**delta)
max_timeout = {"hours": 0.0, "minutes": 1, "seconds": 0}
max_delta = datetime.timedelta(**max_timeout)
if d > max_delta:
LOG.error("XXXXXX Haven't completed everything in 60 seconds. BAIL!")
return False
if self.got_ack and self.got_reply:
LOG.warning("We got everything already. BAIL")
return False
try:
# This will register a packet consumer with aprslib
# When new packets come in the consumer will process
# the packet
self.aprs_client.consumer(self.rx_packet, raw=False, blocking=False)
except aprslib.exceptions.ConnectionDrop:
LOG.error("Connection dropped.")
return False
return True
class APRSDFlask(flask_classful.FlaskView):
config = None
@@ -55,24 +296,57 @@ class APRSDFlask(flask_classful.FlaskView):
)
wl = packets.WatchList()
if wl.is_enabled():
watch_count = len(wl.callsigns)
watch_count = len(wl)
watch_age = wl.max_delta()
else:
watch_count = 0
watch_age = 0
sl = packets.SeenList()
seen_count = len(sl)
pm = plugin.PluginManager()
plugins = pm.get_plugins()
plugin_count = len(plugins)
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.kiss_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
return flask.render_template(
"index.html",
initial_stats=stats,
aprs_connection=aprs_connection,
callsign=self.config["aprs"]["login"],
version=aprsd.__version__,
config_json=json.dumps(self.config),
config_json=json.dumps(self.config.data),
watch_count=watch_count,
watch_age=watch_age,
seen_count=seen_count,
plugin_count=plugin_count,
)
@@ -86,6 +360,23 @@ class APRSDFlask(flask_classful.FlaskView):
return flask.render_template("messages.html", messages=json.dumps(msgs))
@auth.login_required
def send_message_status(self):
LOG.debug(request)
msgs = SentMessages()
info = msgs.get_all()
return json.dumps(info)
@auth.login_required
def send_message(self):
LOG.debug(request)
if request.method == "GET":
return flask.render_template(
"send-message.html",
callsign=self.config["aprs"]["login"],
version=aprsd.__version__,
)
@auth.login_required
def packets(self):
packet_list = packets.PacketList().get()
@@ -117,14 +408,14 @@ class APRSDFlask(flask_classful.FlaskView):
# Convert the watch_list entries to age
wl = packets.WatchList()
new_list = {}
for call in wl.callsigns:
for call in wl.get_all():
# call_date = datetime.datetime.strptime(
# str(wl.last_seen(call)),
# "%Y-%m-%d %H:%M:%S.%f",
# )
new_list[call] = {
"last": wl.age(call),
"packets": wl.callsigns[call]["packets"].get(),
"packets": wl.get(call)["packets"].get(),
}
stats_dict["aprsd"]["watch_list"] = new_list
@@ -148,6 +439,117 @@ class APRSDFlask(flask_classful.FlaskView):
return json.dumps(self._stats())
class SendMessageNamespace(Namespace):
_config = None
got_ack = False
reply_sent = False
msg = None
request = None
def __init__(self, namespace=None, config=None):
self._config = config
super().__init__(namespace)
def on_connect(self):
global socketio
LOG.debug("Web socket connected")
socketio.emit(
"connected", {"data": "/sendmsg Connected"},
namespace="/sendmsg",
)
def on_disconnect(self):
LOG.debug("WS Disconnected")
def on_send(self, data):
global socketio
LOG.debug(f"WS: on_send {data}")
self.request = data
msg = messaging.TextMessage(
data["from"], data["to"],
data["message"],
)
self.msg = msg
msgs = SentMessages()
msgs.add(msg)
msgs.set_status(msg.id, "Sending")
socketio.emit(
"sent", SentMessages().get(self.msg.id),
namespace="/sendmsg",
)
socketio.start_background_task(self._start, self._config, data, msg, self)
LOG.warning("WS: on_send: exit")
def _start(self, config, data, msg, namespace):
msg_thread = SendMessageThread(self._config, data, msg, self)
msg_thread.start()
def handle_message(self, data):
LOG.debug(f"WS Data {data}")
def handle_json(self, data):
LOG.debug(f"WS json {data}")
class LogMonitorThread(threads.APRSDThread):
def __init__(self):
super().__init__("LogMonitorThread")
def loop(self):
global socketio
try:
record = threads.logging_queue.get(block=True, timeout=5)
json_record = self.json_record(record)
socketio.emit(
"log_entry", json_record,
namespace="/logs",
)
except Exception:
# Just ignore thi
pass
return True
def json_record(self, record):
entry = {}
entry["filename"] = record.filename
entry["funcName"] = record.funcName
entry["levelname"] = record.levelname
entry["lineno"] = record.lineno
entry["module"] = record.module
entry["name"] = record.name
entry["pathname"] = record.pathname
entry["process"] = record.process
entry["processName"] = record.processName
if hasattr(record, "stack_info"):
entry["stack_info"] = record.stack_info
else:
entry["stack_info"] = None
entry["thread"] = record.thread
entry["threadName"] = record.threadName
entry["message"] = record.getMessage()
return entry
class LoggingNamespace(Namespace):
def on_connect(self):
global socketio
LOG.debug("Web socket connected")
socketio.emit(
"connected", {"data": "/logs Connected"},
namespace="/logs",
)
self.log_thread = LogMonitorThread()
self.log_thread.start()
def on_disconnect(self):
LOG.debug("WS Disconnected")
self.log_thread.stop()
def setup_logging(config, flask_app, loglevel, quiet):
flask_log = logging.getLogger("werkzeug")
@@ -157,10 +559,10 @@ def setup_logging(config, flask_app, loglevel, quiet):
flask_app.logger.disabled = True
return
log_level = utils.LOG_LEVELS[loglevel]
log_level = aprsd_config.LOG_LEVELS[loglevel]
LOG.setLevel(log_level)
log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT)
date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT)
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
log_file = config["aprsd"].get("logfile", None)
if log_file:
@@ -182,6 +584,8 @@ def setup_logging(config, flask_app, loglevel, quiet):
def init_flask(config, loglevel, quiet):
global socketio
flask_app = flask.Flask(
"aprsd",
static_url_path="/static",
@@ -195,6 +599,18 @@ def init_flask(config, loglevel, quiet):
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("/send-message", methods=["GET"])(server.send_message)
flask_app.route("/send-message-status", methods=["GET"])(server.send_message_status)
flask_app.route("/save", methods=["GET"])(server.save)
flask_app.route("/plugins", methods=["GET"])(server.plugins)
return flask_app
socketio = SocketIO(
flask_app, logger=False, engineio_logger=False,
async_mode="threading",
)
# import eventlet
# eventlet.monkey_patch()
socketio.on_namespace(SendMessageNamespace("/sendmsg", config=config))
socketio.on_namespace(LoggingNamespace("/logs"))
return socketio, flask_app
+3 -3
View File
@@ -19,7 +19,7 @@ import requests
# local imports here
import aprsd
from aprsd import utils
from aprsd import config as aprsd_config
# setup the global logger
@@ -172,7 +172,7 @@ def parse_delta_str(s):
"--config",
"config_file",
show_default=True,
default=utils.DEFAULT_CONFIG_FILE,
default=aprsd_config.DEFAULT_CONFIG_FILE,
help="The aprsd config file to use for options.",
)
@click.option(
@@ -191,7 +191,7 @@ def parse_delta_str(s):
def check(loglevel, config_file, health_url, timeout):
"""APRSD Plugin test app."""
config = utils.parse_config(config_file)
config = aprsd_config.parse_config(config_file)
setup_logging(config, loglevel, False)
LOG.debug(f"APRSD HealthCheck version: {aprsd.__version__}")
+8 -6
View File
@@ -36,7 +36,9 @@ import click_completion
# local imports here
import aprsd
from aprsd import client, messaging, stats, threads, trace, utils
from aprsd import client
from aprsd import config as aprsd_config
from aprsd import messaging, stats, threads, trace, utils
# setup the global logger
@@ -169,10 +171,10 @@ def signal_handler(sig, frame):
# to disable logging to stdout, but still log to file
# use the --quiet option on the cmdln
def setup_logging(config, loglevel, quiet):
log_level = utils.LOG_LEVELS[loglevel]
log_level = aprsd_config.LOG_LEVELS[loglevel]
LOG.setLevel(log_level)
log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT)
date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT)
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
log_file = config["aprsd"].get("logfile", None)
if log_file:
@@ -218,7 +220,7 @@ def setup_logging(config, loglevel, quiet):
"--config",
"config_file",
show_default=True,
default=utils.DEFAULT_CONFIG_FILE,
default=aprsd_config.DEFAULT_CONFIG_FILE,
help="The aprsd config file to use for options.",
)
@click.option(
@@ -258,7 +260,7 @@ def listen(
"""Send a message to a callsign via APRS_IS."""
global got_ack, got_response
config = utils.parse_config(config_file)
config = aprsd_config.parse_config(config_file)
if not aprs_login:
click.echo("Must set --aprs_login or APRS_LOGIN")
return
+58 -57
View File
@@ -37,8 +37,10 @@ import click_completion
# local imports here
import aprsd
from aprsd import (
client, flask, messaging, packets, plugin, stats, threads, trace, utils,
flask, messaging, packets, plugin, stats, threads, trace, utils,
)
from aprsd import client
from aprsd import config as aprsd_config
# setup the global logger
@@ -47,22 +49,8 @@ LOG = logging.getLogger("APRSD")
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
flask_enabled = False
# server_event = threading.Event()
# localization, please edit:
# HOST = "noam.aprs2.net" # north america tier2 servers round robin
# USER = "KM6XXX-9" # callsign of this aprs client with SSID
# PASS = "99999" # google how to generate this
# BASECALLSIGN = "KM6XXX" # callsign of radio in the field to send email
# shortcuts = {
# "aa" : "5551239999@vtext.com",
# "cl" : "craiglamparter@somedomain.org",
# "wb" : "5553909472@vtext.com"
# }
def custom_startswith(string, incomplete):
"""A custom completion match that supports case insensitive matching."""
@@ -158,8 +146,9 @@ def signal_handler(sig, frame):
),
)
time.sleep(1.5)
tracker = messaging.MsgTrack()
tracker.save()
messaging.MsgTrack().save()
packets.WatchList().save()
packets.SeenList().save()
LOG.info(stats.APRSDStats())
# signal.signal(signal.SIGTERM, sys.exit(0))
# sys.exit(0)
@@ -171,10 +160,10 @@ def signal_handler(sig, frame):
# to disable logging to stdout, but still log to file
# use the --quiet option on the cmdln
def setup_logging(config, loglevel, quiet):
log_level = utils.LOG_LEVELS[loglevel]
log_level = aprsd_config.LOG_LEVELS[loglevel]
LOG.setLevel(log_level)
log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT)
date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT)
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
log_file = config["aprsd"].get("logfile", None)
if log_file:
@@ -186,14 +175,21 @@ def setup_logging(config, loglevel, quiet):
LOG.addHandler(fh)
imap_logger = None
if config["aprsd"]["email"].get("enabled", False) and config["aprsd"]["email"][
"imap"
].get("debug", False):
if config.get("aprsd.email.enabled", default=False) and config.get("aprsd.email.imap.debug", default=False):
imap_logger = logging.getLogger("imapclient.imaplib")
imap_logger.setLevel(log_level)
imap_logger.addHandler(fh)
if config.get("aprsd.web.enabled", default=False):
qh = logging.handlers.QueueHandler(threads.logging_queue)
q_log_formatter = logging.Formatter(
fmt=aprsd_config.QUEUE_LOG_FORMAT,
datefmt=aprsd_config.QUEUE_DATE_FORMAT,
)
qh.setFormatter(q_log_formatter)
LOG.addHandler(qh)
if not quiet:
sh = logging.StreamHandler(sys.stdout)
sh.setFormatter(log_formatter)
@@ -219,11 +215,11 @@ def setup_logging(config, loglevel, quiet):
"--config",
"config_file",
show_default=True,
default=utils.DEFAULT_CONFIG_FILE,
default=aprsd_config.DEFAULT_CONFIG_FILE,
help="The aprsd config file to use for options.",
)
def check_version(loglevel, config_file):
config = utils.parse_config(config_file)
config = aprsd_config.parse_config(config_file)
setup_logging(config, loglevel, False)
level, msg = utils._check_version()
@@ -236,7 +232,7 @@ def check_version(loglevel, config_file):
@main.command()
def sample_config():
"""This dumps the config to stdout."""
click.echo(utils.dump_default_cfg())
click.echo(aprsd_config.dump_default_cfg())
@main.command()
@@ -257,7 +253,7 @@ def sample_config():
"--config",
"config_file",
show_default=True,
default=utils.DEFAULT_CONFIG_FILE,
default=aprsd_config.DEFAULT_CONFIG_FILE,
help="The aprsd config file to use for options.",
)
@click.option(
@@ -297,7 +293,7 @@ def send_message(
"""Send a message to a callsign via APRS_IS."""
global got_ack, got_response
config = utils.parse_config(config_file)
config = aprsd_config.parse_config(config_file)
if not aprs_login:
click.echo("Must set --aprs_login or APRS_LOGIN")
return
@@ -356,11 +352,14 @@ def send_message(
sys.exit(0)
try:
cl = client.Client(config)
cl.setup_connection()
client.ClientFactory.setup(config)
client.factory.create().client
except LoginError:
sys.exit(-1)
packets.PacketList(config=config)
packets.WatchList(config=config)
# Send a message
# then we setup a consumer to rx messages
# We should get an ack back as well as a new message
@@ -381,7 +380,7 @@ def send_message(
# This will register a packet consumer with aprslib
# When new packets come in the consumer will process
# the packet
aprs_client = client.get_client()
aprs_client = client.factory.create().client
aprs_client.consumer(rx_packet, raw=False)
except aprslib.exceptions.ConnectionDrop:
LOG.error("Connection dropped, reconnecting")
@@ -389,7 +388,7 @@ def send_message(
# Force the deletion of the client object connected to aprs
# This will cause a reconnect, next time client.get_client()
# is called
cl.reset()
aprs_client.reset()
# main() ###
@@ -411,7 +410,7 @@ def send_message(
"--config",
"config_file",
show_default=True,
default=utils.DEFAULT_CONFIG_FILE,
default=aprsd_config.DEFAULT_CONFIG_FILE,
help="The aprsd config file to use for options.",
)
@click.option(
@@ -432,11 +431,12 @@ def server(
"""Start the aprsd server process."""
global flask_enabled
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
if not quiet:
click.echo("Load config")
config = utils.parse_config(config_file)
config = aprsd_config.parse_config(config_file)
setup_logging(config, loglevel, quiet)
level, msg = utils._check_version()
@@ -458,54 +458,55 @@ def server(
trace.setup_tracing(["method", "api"])
stats.APRSDStats(config)
try:
cl = client.Client(config)
cl.client
except LoginError:
# 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)
# Create the initial PM singleton and Register plugins
plugin_manager = plugin.PluginManager(config)
plugin_manager.setup_plugins()
# Creates the client object
LOG.info("Creating client connection")
client.factory.create().client
# Now load the msgTrack from disk if any
packets.PacketList(config=config)
if flush:
LOG.debug("Deleting saved MsgTrack.")
messaging.MsgTrack().flush()
messaging.MsgTrack(config=config).flush()
packets.WatchList(config=config)
packets.SeenList(config=config)
else:
# Try and load saved MsgTrack list
LOG.debug("Loading saved MsgTrack object.")
messaging.MsgTrack().load()
messaging.MsgTrack(config=config).load()
packets.WatchList(config=config).load()
packets.SeenList(config=config).load()
packets.PacketList(config=config)
# Create the initial PM singleton and Register plugins
LOG.info("Loading Plugin Manager and registering plugins")
plugin_manager = plugin.PluginManager(config)
plugin_manager.setup_plugins()
rx_thread = threads.APRSDRXThread(
msg_queues=threads.msg_queues,
config=config,
)
rx_thread.start()
if "watch_list" in config["aprsd"] and config["aprsd"]["watch_list"].get(
"enabled",
True,
):
packets.WatchList(config=config)
messaging.MsgTrack().restart()
keepalive = threads.KeepAliveThread(config=config)
keepalive.start()
try:
web_enabled = utils.check_config_option(config, ["aprsd", "web", "enabled"])
except Exception:
web_enabled = False
web_enabled = config.get("aprsd.web.enabled", default=False)
if web_enabled:
flask_enabled = True
app = flask.init_flask(config, loglevel, quiet)
app.run(
(socketio, app) = flask.init_flask(config, loglevel, quiet)
socketio.run(
app,
host=config["aprsd"]["web"]["host"],
port=config["aprsd"]["web"]["port"],
)
+63 -86
View File
@@ -2,14 +2,11 @@ import abc
import datetime
import logging
from multiprocessing import RawValue
import os
import pathlib
import pickle
import re
import threading
import time
from aprsd import client, packets, stats, threads, trace, utils
from aprsd import client, objectstore, packets, stats, threads
LOG = logging.getLogger("APRSD")
@@ -19,7 +16,7 @@ LOG = logging.getLogger("APRSD")
NULL_MESSAGE = -1
class MsgTrack:
class MsgTrack(objectstore.ObjectStoreMixin):
"""Class to keep track of outstanding text messages.
This is a thread safe class that keeps track of active
@@ -30,20 +27,13 @@ class MsgTrack:
automatically adds itself to this class. When the ack is
recieved from the radio, the message object is removed from
this class.
# TODO(hemna)
When aprsd is asked to quit this class should be serialized and
saved to disk/db to keep track of the state of outstanding messages.
When aprsd is started, it should try and fetch the saved state,
and reloaded to a live state.
"""
_instance = None
_start_time = None
lock = None
track = {}
data = {}
total_messages_tracked = 0
def __new__(cls, *args, **kwargs):
@@ -52,93 +42,65 @@ class MsgTrack:
cls._instance.track = {}
cls._instance._start_time = datetime.datetime.now()
cls._instance.lock = threading.Lock()
cls._instance.config = kwargs["config"]
cls._instance._init_store()
return cls._instance
def __getitem__(self, name):
with self.lock:
return self.track[name]
return self.data[name]
def __iter__(self):
with self.lock:
return iter(self.track)
return iter(self.data)
def keys(self):
with self.lock:
return self.track.keys()
return self.data.keys()
def items(self):
with self.lock:
return self.track.items()
return self.data.items()
def values(self):
with self.lock:
return self.track.values()
return self.data.values()
def __len__(self):
with self.lock:
return len(self.track)
return len(self.data)
def __str__(self):
with self.lock:
result = "{"
for key in self.track.keys():
result += f"{key}: {str(self.track[key])}, "
for key in self.data.keys():
result += f"{key}: {str(self.data[key])}, "
result += "}"
return result
def add(self, msg):
with self.lock:
key = int(msg.id)
self.track[key] = msg
self.data[key] = msg
stats.APRSDStats().msgs_tracked_inc()
self.total_messages_tracked += 1
def get(self, id):
with self.lock:
if id in self.track:
return self.track[id]
if id in self.data:
return self.data[id]
def remove(self, id):
with self.lock:
key = int(id)
if key in self.track.keys():
del self.track[key]
def save(self):
"""Save any queued to disk?"""
LOG.debug(f"Save tracker to disk? {len(self)}")
if len(self) > 0:
LOG.info(f"Saving {len(self)} tracking messages to disk")
pickle.dump(self.dump(), open(utils.DEFAULT_SAVE_FILE, "wb+"))
else:
LOG.debug(
"Nothing to save, flushing old save file '{}'".format(
utils.DEFAULT_SAVE_FILE,
),
)
self.flush()
def dump(self):
dump = {}
with self.lock:
for key in self.track.keys():
dump[key] = self.track[key]
return dump
def load(self):
if os.path.exists(utils.DEFAULT_SAVE_FILE):
raw = pickle.load(open(utils.DEFAULT_SAVE_FILE, "rb"))
if raw:
self.track = raw
LOG.debug("Loaded MsgTrack dict from disk.")
LOG.debug(self)
if key in self.data.keys():
del self.data[key]
def restart(self):
"""Walk the list of messages and restart them if any."""
for key in self.track.keys():
msg = self.track[key]
for key in self.data.keys():
msg = self.data[key]
if msg.last_send_attempt < msg.retry_count:
msg.send()
@@ -150,14 +112,14 @@ class MsgTrack:
"""Walk the list of delayed messages and restart them if any."""
if not count:
# Send all the delayed messages
for key in self.track.keys():
msg = self.track[key]
for key in self.data.keys():
msg = self.data[key]
if msg.last_send_attempt == msg.retry_count:
self._resend(msg)
else:
# They want to resend <count> delayed messages
tmp = sorted(
self.track.items(),
self.data.items(),
reverse=most_recent,
key=lambda x: x[1].last_send_time,
)
@@ -165,13 +127,6 @@ class MsgTrack:
for (_key, msg) in msg_list:
self._resend(msg)
def flush(self):
"""Nuke the old pickle file that stored the old results from last aprsd run."""
if os.path.exists(utils.DEFAULT_SAVE_FILE):
pathlib.Path(utils.DEFAULT_SAVE_FILE).unlink()
with self.lock:
self.track = {}
class MessageCounter:
"""
@@ -228,7 +183,14 @@ class Message(metaclass=abc.ABCMeta):
last_send_time = 0
last_send_attempt = 0
def __init__(self, fromcall, tocall, msg_id=None):
transport = None
def __init__(
self,
fromcall,
tocall,
msg_id=None,
):
self.fromcall = fromcall
self.tocall = tocall
if not msg_id:
@@ -280,9 +242,9 @@ class RawMessage(Message):
thread = SendMessageThread(message=self)
thread.start()
def send_direct(self):
def send_direct(self, aprsis_client=None):
"""Send a message without a separate thread."""
cl = client.get_client()
cl = client.factory.create().client
log_message(
"Sending Message Direct",
str(self).rstrip("\n"),
@@ -290,8 +252,8 @@ class RawMessage(Message):
tocall=self.tocall,
fromcall=self.fromcall,
)
cl.sendall(str(self))
stats.APRSDStats().msgs_sent_inc()
cl.send(self)
stats.APRSDStats().msgs_tx_inc()
class TextMessage(Message):
@@ -299,7 +261,14 @@ class TextMessage(Message):
message = None
def __init__(self, fromcall, tocall, message, msg_id=None, allow_delay=True):
def __init__(
self,
fromcall,
tocall,
message,
msg_id=None,
allow_delay=True,
):
super().__init__(fromcall, tocall, msg_id)
self.message = message
# do we try and save this message for later if we don't get
@@ -352,9 +321,12 @@ class TextMessage(Message):
thread = SendMessageThread(message=self)
thread.start()
def send_direct(self):
def send_direct(self, aprsis_client=None):
"""Send a message without a separate thread."""
cl = client.get_client()
if aprsis_client:
cl = aprsis_client
else:
cl = client.factory.create().client
log_message(
"Sending Message Direct",
str(self).rstrip("\n"),
@@ -362,8 +334,9 @@ class TextMessage(Message):
tocall=self.tocall,
fromcall=self.fromcall,
)
cl.sendall(str(self))
cl.send(self)
stats.APRSDStats().msgs_tx_inc()
packets.PacketList().add(self.dict())
class SendMessageThread(threads.APRSDThread):
@@ -382,7 +355,6 @@ class SendMessageThread(threads.APRSDThread):
last send attempt is old enough.
"""
cl = client.get_client()
tracker = MsgTrack()
# lets see if the message is still in the tracking queue
msg = tracker.get(self.msg.id)
@@ -422,7 +394,8 @@ class SendMessageThread(threads.APRSDThread):
retry_number=msg.last_send_attempt,
msg_num=msg.id,
)
cl.sendall(str(msg))
cl = client.factory.create().client
cl.send(msg)
stats.APRSDStats().msgs_tx_inc()
packets.PacketList().add(msg.dict())
msg.last_send_time = datetime.datetime.now()
@@ -463,14 +436,20 @@ class AckMessage(Message):
self.id,
)
def _filter_for_send(self):
return f"ack{self.id}"
def send(self):
LOG.debug(f"Send ACK({self.tocall}:{self.id}) to radio.")
thread = SendAckThread(self)
thread.start()
def send_direct(self):
def send_direct(self, aprsis_client=None):
"""Send an ack message without a separate thread."""
cl = client.get_client()
if aprsis_client:
cl = aprsis_client
else:
cl = client.factory.create().client
log_message(
"Sending ack",
str(self).rstrip("\n"),
@@ -479,7 +458,7 @@ class AckMessage(Message):
tocall=self.tocall,
fromcall=self.fromcall,
)
cl.sendall(str(self))
cl.send(self)
class SendAckThread(threads.APRSDThread):
@@ -487,10 +466,8 @@ class SendAckThread(threads.APRSDThread):
self.ack = ack
super().__init__(f"SendAck-{self.ack.id}")
@trace.trace
def loop(self):
"""Separate thread to send acks with retries."""
LOG.debug("SendAckThread loop start")
send_now = False
if self.ack.last_send_attempt == self.ack.retry_count:
# we reached the send limit, don't send again
@@ -515,7 +492,7 @@ class SendAckThread(threads.APRSDThread):
send_now = True
if send_now:
cl = client.get_client()
cl = client.factory.create().client
log_message(
"Sending ack",
str(self.ack).rstrip("\n"),
@@ -524,7 +501,7 @@ class SendAckThread(threads.APRSDThread):
tocall=self.ack.tocall,
retry_number=self.ack.last_send_attempt,
)
cl.sendall(str(self.ack))
cl.send(self.ack)
stats.APRSDStats().ack_tx_inc()
packets.PacketList().add(self.ack.dict())
self.ack.last_send_attempt += 1
+96
View File
@@ -0,0 +1,96 @@
import logging
import os
import pathlib
import pickle
from aprsd import config as aprsd_config
LOG = logging.getLogger("APRSD")
class ObjectStoreMixin:
"""Class 'MIXIN' intended to save/load object data.
The asumption of how this mixin is used:
The using class has to have a:
* data in self.data as a dictionary
* a self.lock thread lock
* Class must specify self.save_file as the location.
When APRSD quits, it calls save()
When APRSD Starts, it calls load()
aprsd server -f (flush) will wipe all saved objects.
"""
def __len__(self):
return len(self.data)
def get_all(self):
with self.lock:
return self.data
def get(self, id):
with self.lock:
return self.data[id]
def _init_store(self):
sl = self._save_location()
if not os.path.exists(sl):
LOG.warning(f"Save location {sl} doesn't exist")
try:
os.makedirs(sl)
except Exception as ex:
LOG.exception(ex)
def _save_location(self):
save_location = self.config.get("aprsd.save_location", None)
if not save_location:
save_location = aprsd_config.DEFAULT_CONFIG_DIR
return save_location
def _save_filename(self):
save_location = self._save_location()
return "{}/{}.p".format(
save_location,
self.__class__.__name__.lower(),
)
def _dump(self):
dump = {}
with self.lock:
for key in self.data.keys():
dump[key] = self.data[key]
return dump
def save(self):
"""Save any queued to disk?"""
if len(self) > 0:
LOG.info(f"{self.__class__.__name__}::Saving {len(self)} entries to disk at {self._save_location()}")
pickle.dump(self._dump(), open(self._save_filename(), "wb+"))
else:
LOG.debug(
"{} Nothing to save, flushing old save file '{}'".format(
self.__class__.__name__,
self._save_filename(),
),
)
self.flush()
def load(self):
if os.path.exists(self._save_filename()):
raw = pickle.load(open(self._save_filename(), "rb"))
if raw:
self.data = raw
LOG.debug(f"{self.__class__.__name__}::Loaded {len(self)} entries from disk.")
LOG.debug(f"{self.data}")
def flush(self):
"""Nuke the old pickle file that stored the old results from last aprsd run."""
if os.path.exists(self._save_filename()):
pathlib.Path(self._save_filename()).unlink()
with self.lock:
self.data = {}
+52 -12
View File
@@ -3,7 +3,7 @@ import logging
import threading
import time
from aprsd import utils
from aprsd import objectstore, utils
LOG = logging.getLogger("APRSD")
@@ -29,6 +29,7 @@ class PacketList:
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
def __init__(self, config=None):
@@ -50,6 +51,7 @@ class PacketList:
else:
self.total_recv += 1
self.packet_list.append(packet)
SeenList().update_seen(packet)
def get(self):
with self.lock:
@@ -64,17 +66,20 @@ class PacketList:
return self.total_tx
class WatchList:
class WatchList(objectstore.ObjectStoreMixin):
"""Global watch list and info for callsigns."""
_instance = None
callsigns = {}
data = {}
config = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.lock = threading.Lock()
cls.callsigns = {}
cls._instance.config = kwargs["config"]
cls._instance.data = {}
cls._instance._init_store()
return cls._instance
def __init__(self, config=None):
@@ -89,7 +94,7 @@ class WatchList:
# a beacon from a callsign or some other mechanism to find
# last time a message was seen by aprs-is. For now this
# is all we can do.
self.callsigns[call] = {
self.data[call] = {
"last": datetime.datetime.now(),
"packets": utils.RingBuffer(
ring_size,
@@ -97,23 +102,24 @@ class WatchList:
}
def is_enabled(self):
if "watch_list" in self.config["aprsd"]:
if self.config and "watch_list" in self.config["aprsd"]:
return self.config["aprsd"]["watch_list"].get("enabled", False)
else:
return False
def callsign_in_watchlist(self, callsign):
return callsign in self.callsigns
return callsign in self.data
def update_seen(self, packet):
callsign = packet["from"]
if self.callsign_in_watchlist(callsign):
self.callsigns[callsign]["last"] = datetime.datetime.now()
self.callsigns[callsign]["packets"].append(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)
def last_seen(self, callsign):
if self.callsign_in_watchlist(callsign):
return self.callsigns[callsign]["last"]
return self.data[callsign]["last"]
def age(self, callsign):
now = datetime.datetime.now()
@@ -148,6 +154,40 @@ class WatchList:
return False
class SeenList(objectstore.ObjectStoreMixin):
"""Global callsign seen list."""
_instance = None
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"]
cls._instance.data = {}
cls._instance._init_store()
return cls._instance
def update_seen(self, packet):
callsign = None
if "fromcall" in packet:
callsign = packet["fromcall"]
elif "from" in packet:
callsign = packet["from"]
else:
LOG.warning(f"Can't find FROM in packet {packet}")
return
if callsign not in self.data:
self.data[callsign] = {
"last": None,
"count": 0,
}
self.data[callsign]["last"] = str(datetime.datetime.now())
self.data[callsign]["count"] += 1
def get_packet_type(packet):
"""Decode the packet type from the packet."""
+134 -28
View File
@@ -6,6 +6,7 @@ import inspect
import logging
import os
import re
import textwrap
import threading
import pluggy
@@ -17,9 +18,6 @@ from aprsd import client, messaging, packets, threads
# setup the global logger
LOG = logging.getLogger("APRSD")
hookspec = pluggy.HookspecMarker("aprsd")
hookimpl = pluggy.HookimplMarker("aprsd")
CORE_MESSAGE_PLUGINS = [
"aprsd.plugins.email.EmailPlugin",
"aprsd.plugins.fortune.FortunePlugin",
@@ -36,8 +34,11 @@ CORE_NOTIFY_PLUGINS = [
"aprsd.plugins.notify.NotifySeenPlugin",
]
hookspec = pluggy.HookspecMarker("aprsd")
hookimpl = pluggy.HookimplMarker("aprsd")
class APRSDCommandSpec:
class APRSDPluginSpec:
"""A hook specification namespace."""
@hookspec
@@ -55,17 +56,18 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
# Holds the list of APRSDThreads that the plugin creates
threads = []
# Set this in setup()
enabled = False
def __init__(self, config):
self.config = config
self.message_counter = 0
self.setup()
self.threads = self.create_threads()
if self.threads:
self.start_threads()
self.threads = self.create_threads() or []
self.start_threads()
def start_threads(self):
if self.threads:
if self.enabled and self.threads:
if not isinstance(self.threads, list):
self.threads = [self.threads]
@@ -92,13 +94,13 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
def message_count(self):
return self.message_counter
@property
def version(self):
"""Version"""
raise NotImplementedError
def help(self):
return "Help!"
@abc.abstractmethod
def setup(self):
"""Do any plugin setup here."""
self.enabled = True
def create_threads(self):
"""Gives the plugin writer the ability start a background thread."""
@@ -116,7 +118,6 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
if isinstance(thread, threads.APRSDThread):
thread.stop()
@hookimpl
@abc.abstractmethod
def filter(self, packet):
pass
@@ -137,7 +138,6 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
by a particular HAM callsign, write a plugin based off of
this class.
"""
enabled = False
def setup(self):
# if we have a watch list enabled, we need to add filtering
@@ -153,22 +153,33 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
)
# make sure the timeout is set or this doesn't work
if watch_list:
aprs_client = client.get_client()
aprs_client = client.factory.create().client
filter_str = "b/{}".format("/".join(watch_list))
aprs_client.set_filter(filter_str)
else:
LOG.warning("Watch list enabled, but no callsigns set.")
@hookimpl
def filter(self, packet):
wl = packets.WatchList()
result = messaging.NULL_MESSAGE
if wl.callsign_in_watchlist(packet["from"]):
# packet is from a callsign in the watch list
self.rx_inc()
result = self.process()
if result:
self.tx_inc()
wl.update_seen(packet)
if self.enabled:
wl = packets.WatchList()
if wl.callsign_in_watchlist(packet["from"]):
# packet is from a callsign in the watch list
self.rx_inc()
try:
result = self.process(packet)
except Exception as ex:
LOG.error(
"Plugin {} failed to process packet {}".format(
self.__class__, ex,
),
)
if result:
self.tx_inc()
wl.update_seen(packet)
else:
LOG.warning(f"{self.__class__} plugin is not enabled")
return result
@@ -191,6 +202,16 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
"""The regex to match from the caller"""
raise NotImplementedError
def help(self):
return "{}: {}".format(
self.command_name.lower(),
self.command_regex,
)
def setup(self):
"""Do any plugin setup here."""
self.enabled = True
@hookimpl
def filter(self, packet):
result = None
@@ -208,13 +229,84 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
):
if re.search(self.command_regex, message):
self.rx_inc()
result = self.process(packet)
if result:
self.tx_inc()
if self.enabled:
try:
result = self.process(packet)
except Exception as ex:
LOG.error(
"Plugin {} failed to process packet {}".format(
self.__class__, ex,
),
)
LOG.exception(ex)
if result:
self.tx_inc()
else:
LOG.warning(f"{self.__class__} isn't enabled.")
return result
class HelpPlugin(APRSDRegexCommandPluginBase):
"""Help Plugin that is always enabled.
This plugin is in this file to prevent a circular import.
"""
version = "1.0"
command_regex = "^[hH]"
command_name = "help"
def help(self):
return "Help: send APRS help or help <plugin>"
def process(self, packet):
LOG.info("HelpPlugin")
# fromcall = packet.get("from")
message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
a = re.search(r"^.*\s+(.*)", message)
command_name = None
if a is not None:
command_name = a.group(1).lower()
pm = PluginManager()
if command_name and "?" not in command_name:
# user wants help for a specific plugin
reply = None
for p in pm.get_plugins():
if (
p.enabled and isinstance(p, APRSDRegexCommandPluginBase)
and p.command_name.lower() == command_name
):
reply = p.help()
if reply:
return reply
list = []
for p in pm.get_plugins():
LOG.debug(p)
if p.enabled and isinstance(p, APRSDRegexCommandPluginBase):
name = p.command_name.lower()
if name not in list and "help" not in name:
list.append(name)
list.sort()
reply = " ".join(list)
lines = textwrap.wrap(reply, 60)
replies = ["Send APRS MSG of 'help' or 'help <plugin>'"]
for line in lines:
replies.append(f"plugins: {line}")
for entry in replies:
LOG.debug(f"{len(entry)} {entry}")
LOG.debug(f"{replies}")
return replies
class PluginManager:
# The singleton instance object for this class
_instance = None
@@ -240,6 +332,10 @@ class PluginManager:
if config:
self.config = config
def _init(self):
self._pluggy_pm = pluggy.PluginManager("aprsd")
self._pluggy_pm.add_hookspecs(APRSDPluginSpec)
def load_plugins_from_path(self, module_path):
if not os.path.exists(module_path):
LOG.error(f"plugin path '{module_path}' doesn't exist.")
@@ -340,9 +436,12 @@ class PluginManager:
"""Create the plugin manager and register plugins."""
LOG.info("Loading APRSD Plugins")
self._init()
# Help plugin is always enabled.
_help = HelpPlugin(self.config)
self._pluggy_pm.register(_help)
enabled_plugins = self.config["aprsd"].get("enabled_plugins", None)
self._pluggy_pm = pluggy.PluginManager("aprsd")
self._pluggy_pm.add_hookspecs(APRSDCommandSpec)
if enabled_plugins:
for p_name in enabled_plugins:
self._load_plugin(p_name)
@@ -368,6 +467,13 @@ class PluginManager:
with self.lock:
return self._pluggy_pm.hook.filter(packet=packet)
def stop(self):
"""Stop all threads created by all plugins."""
with self.lock:
for p in self.get_plugins():
if hasattr(p, "stop_threads"):
p.stop_threads()
def register_msg(self, obj):
"""Register the plugin."""
self._pluggy_pm.register(obj)
+88 -65
View File
@@ -5,6 +5,7 @@ import imaplib
import logging
import re
import smtplib
import threading
import time
import imapclient
@@ -15,9 +16,44 @@ from aprsd import messaging, plugin, stats, threads, trace
LOG = logging.getLogger("APRSD")
# This gets forced set from main.py prior to being used internally
CONFIG = {}
check_email_delay = 60
class EmailInfo:
"""A singleton thread safe mechanism for the global check_email_delay.
This has to be done because we have 2 separate threads that access
the delay value.
1) when EmailPlugin runs from a user message and
2) when the background EmailThread runs to check email.
Access the check email delay with
EmailInfo().delay
Set it with
EmailInfo().delay = 100
or
EmailInfo().delay += 10
"""
_instance = None
def __new__(cls, *args, **kwargs):
"""This magic turns this into a singleton."""
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.lock = threading.Lock()
cls._instance._delay = 60
return cls._instance
@property
def delay(self):
with self.lock:
return self._delay
@delay.setter
def delay(self, val):
with self.lock:
self._delay = val
class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
@@ -34,8 +70,6 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
def setup(self):
"""Ensure that email is enabled and start the thread."""
global CONFIG
CONFIG = self.config
email_enabled = self.config["aprsd"]["email"].get("enabled", False)
validation = self.config["aprsd"]["email"].get("validate", False)
@@ -81,7 +115,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
r = re.search("^-([0-9])[0-9]*$", message)
if r is not None:
LOG.debug("RESEND EMAIL")
resend_email(r.group(1), fromcall)
resend_email(self.config, r.group(1), fromcall)
reply = messaging.NULL_MESSAGE
# -user@address.com body of email
elif re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message):
@@ -91,7 +125,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
to_addr = a.group(1)
content = a.group(2)
email_address = get_email_from_shortcut(to_addr)
email_address = get_email_from_shortcut(self.config, to_addr)
if not email_address:
reply = "Bad email address"
return reply
@@ -114,7 +148,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
too_soon = 1
if not too_soon or ack == 0:
LOG.info(f"Send email '{content}'")
send_result = email.send_email(to_addr, content)
send_result = send_email(self.config, to_addr, content)
reply = messaging.NULL_MESSAGE
if send_result != 0:
reply = f"-{to_addr} failed"
@@ -143,11 +177,9 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
return reply
@trace.trace
def _imap_connect():
global CONFIG
imap_port = CONFIG["aprsd"]["email"]["imap"].get("port", 143)
use_ssl = CONFIG["aprsd"]["email"]["imap"].get("use_ssl", False)
def _imap_connect(config):
imap_port = config["aprsd"]["email"]["imap"].get("port", 143)
use_ssl = config["aprsd"]["email"]["imap"].get("use_ssl", False)
# host = CONFIG["aprsd"]["email"]["imap"]["host"]
# msg = "{}{}:{}".format("TLS " if use_ssl else "", host, imap_port)
# LOG.debug("Connect to IMAP host {} with user '{}'".
@@ -155,7 +187,7 @@ def _imap_connect():
try:
server = imapclient.IMAPClient(
CONFIG["aprsd"]["email"]["imap"]["host"],
config["aprsd"]["email"]["imap"]["host"],
port=imap_port,
use_uid=True,
ssl=use_ssl,
@@ -167,8 +199,8 @@ def _imap_connect():
try:
server.login(
CONFIG["aprsd"]["email"]["imap"]["login"],
CONFIG["aprsd"]["email"]["imap"]["password"],
config["aprsd"]["email"]["imap"]["login"],
config["aprsd"]["email"]["imap"]["password"],
)
except (imaplib.IMAP4.error, Exception) as e:
msg = getattr(e, "message", repr(e))
@@ -184,16 +216,15 @@ def _imap_connect():
return server
@trace.trace
def _smtp_connect():
host = CONFIG["aprsd"]["email"]["smtp"]["host"]
smtp_port = CONFIG["aprsd"]["email"]["smtp"]["port"]
use_ssl = CONFIG["aprsd"]["email"]["smtp"].get("use_ssl", False)
def _smtp_connect(config):
host = config["aprsd"]["email"]["smtp"]["host"]
smtp_port = config["aprsd"]["email"]["smtp"]["port"]
use_ssl = config["aprsd"]["email"]["smtp"].get("use_ssl", False)
msg = "{}{}:{}".format("SSL " if use_ssl else "", host, smtp_port)
LOG.debug(
"Connect to SMTP host {} with user '{}'".format(
msg,
CONFIG["aprsd"]["email"]["imap"]["login"],
config["aprsd"]["email"]["imap"]["login"],
),
)
@@ -216,15 +247,15 @@ def _smtp_connect():
LOG.debug(f"Connected to smtp host {msg}")
debug = CONFIG["aprsd"]["email"]["smtp"].get("debug", False)
debug = config["aprsd"]["email"]["smtp"].get("debug", False)
if debug:
server.set_debuglevel(5)
server.sendmail = trace.trace(server.sendmail)
try:
server.login(
CONFIG["aprsd"]["email"]["smtp"]["login"],
CONFIG["aprsd"]["email"]["smtp"]["password"],
config["aprsd"]["email"]["smtp"]["login"],
config["aprsd"]["email"]["smtp"]["password"],
)
except Exception:
LOG.error("Couldn't connect to SMTP Server")
@@ -248,14 +279,14 @@ def validate_shortcuts(config):
LOG.info(f"Validating {key}:{shortcuts[key]}")
is_valid = validate_email(
email_address=shortcuts[key],
check_regex=True,
check_mx=False,
from_address=config["aprsd"]["email"]["smtp"]["login"],
helo_host=config["aprsd"]["email"]["smtp"]["host"],
check_format=True,
check_dns=True,
check_smtp=True,
smtp_from_address=config["aprsd"]["email"]["smtp"]["login"],
smtp_helo_host=config["aprsd"]["email"]["smtp"]["host"],
smtp_timeout=10,
dns_timeout=10,
use_blacklist=True,
debug=False,
smtp_debug=False,
)
if not is_valid:
LOG.error(
@@ -275,9 +306,9 @@ def validate_shortcuts(config):
)
def get_email_from_shortcut(addr):
if CONFIG["aprsd"]["email"].get("shortcuts", False):
return CONFIG["aprsd"]["email"]["shortcuts"].get(addr, addr)
def get_email_from_shortcut(config, addr):
if config["aprsd"]["email"].get("shortcuts", False):
return config["aprsd"]["email"]["shortcuts"].get(addr, addr)
else:
return addr
@@ -288,9 +319,9 @@ def validate_email_config(config, disable_validation=False):
This helps with failing early during startup.
"""
LOG.info("Checking IMAP configuration")
imap_server = _imap_connect()
imap_server = _imap_connect(config)
LOG.info("Checking SMTP configuration")
smtp_server = _smtp_connect()
smtp_server = _smtp_connect(config)
# Now validate and flag any shortcuts as invalid
if not disable_validation:
@@ -400,34 +431,32 @@ def parse_email(msgid, data, server):
@trace.trace
def send_email(to_addr, content):
global check_email_delay
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
email_address = get_email_from_shortcut(to_addr)
def send_email(config, to_addr, content):
shortcuts = config["aprsd"]["email"]["shortcuts"]
email_address = get_email_from_shortcut(config, to_addr)
LOG.info("Sending Email_________________")
if to_addr in shortcuts:
LOG.info("To : " + to_addr)
to_addr = email_address
LOG.info(" (" + to_addr + ")")
subject = CONFIG["ham"]["callsign"]
subject = config["ham"]["callsign"]
# content = content + "\n\n(NOTE: reply with one line)"
LOG.info("Subject : " + subject)
LOG.info("Body : " + content)
# check email more often since there's activity right now
check_email_delay = 60
EmailInfo().delay = 60
msg = MIMEText(content)
msg["Subject"] = subject
msg["From"] = CONFIG["aprsd"]["email"]["smtp"]["login"]
msg["From"] = config["aprsd"]["email"]["smtp"]["login"]
msg["To"] = to_addr
server = _smtp_connect()
server = _smtp_connect(config)
if server:
try:
server.sendmail(
CONFIG["aprsd"]["email"]["smtp"]["login"],
config["aprsd"]["email"]["smtp"]["login"],
[to_addr],
msg.as_string(),
)
@@ -442,20 +471,19 @@ def send_email(to_addr, content):
@trace.trace
def resend_email(count, fromcall):
global check_email_delay
def resend_email(config, count, fromcall):
date = datetime.datetime.now()
month = date.strftime("%B")[:3] # Nov, Mar, Apr
day = date.day
year = date.year
today = f"{day}-{month}-{year}"
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
shortcuts = config["aprsd"]["email"]["shortcuts"]
# swap key/value
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
try:
server = _imap_connect()
server = _imap_connect(config)
except Exception as e:
LOG.exception("Failed to Connect to IMAP. Cannot resend email ", e)
return
@@ -495,7 +523,7 @@ def resend_email(count, fromcall):
reply = "-" + from_addr + " * " + body.decode(errors="ignore")
# messaging.send_message(fromcall, reply)
msg = messaging.TextMessage(
CONFIG["aprs"]["login"],
config["aprs"]["login"],
fromcall,
reply,
)
@@ -517,11 +545,11 @@ def resend_email(count, fromcall):
str(s).zfill(2),
)
# messaging.send_message(fromcall, reply)
msg = messaging.TextMessage(CONFIG["aprs"]["login"], fromcall, reply)
msg = messaging.TextMessage(config["aprs"]["login"], fromcall, reply)
msg.send()
# check email more often since we're resending one now
check_email_delay = 60
EmailInfo().delay = 60
server.logout()
# end resend_email()
@@ -535,29 +563,24 @@ class APRSDEmailThread(threads.APRSDThread):
self.past = datetime.datetime.now()
def loop(self):
global check_email_delay
LOG.debug("Starting Loop")
check_email_delay = 60
time.sleep(5)
stats.APRSDStats().email_thread_update()
# always sleep for 5 seconds and see if we need to check email
# This allows CTRL-C to stop the execution of this loop sooner
# than check_email_delay time
now = datetime.datetime.now()
if now - self.past > datetime.timedelta(seconds=check_email_delay):
if now - self.past > datetime.timedelta(seconds=EmailInfo().delay):
# It's time to check email
# slowly increase delay every iteration, max out at 300 seconds
# any send/receive/resend activity will reset this to 60 seconds
if check_email_delay < 300:
check_email_delay += 1
if EmailInfo().delay < 300:
EmailInfo().delay += 10
LOG.debug(
"check_email_delay is " + str(check_email_delay) + " seconds",
f"check_email_delay is {EmailInfo().delay} seconds ",
)
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
shortcuts = self.config["aprsd"]["email"]["shortcuts"]
# swap key/value
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
@@ -568,7 +591,7 @@ class APRSDEmailThread(threads.APRSDThread):
today = f"{day}-{month}-{year}"
try:
server = _imap_connect()
server = _imap_connect(self.config)
except Exception as e:
LOG.exception("IMAP failed to connect.", e)
return True
@@ -662,7 +685,7 @@ class APRSDEmailThread(threads.APRSDThread):
LOG.exception("Couldn't remove seen flag from email", e)
# check email more often since we just received an email
check_email_delay = 60
EmailInfo().delay = 60
# reset clock
LOG.debug("Done looping over Server.fetch, logging out.")
+2 -2
View File
@@ -2,7 +2,7 @@ import logging
import re
import time
from aprsd import plugin, plugin_utils, trace, utils
from aprsd import plugin, plugin_utils, trace
LOG = logging.getLogger("APRSD")
@@ -24,7 +24,7 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase):
# get last location of a callsign, get descriptive name from weather service
try:
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
self.config.check_option(["services", "aprs.fi", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found"
+2 -7
View File
@@ -1,6 +1,6 @@
import logging
from aprsd import messaging, packets, plugin
from aprsd import packets, plugin
LOG = logging.getLogger("APRSD")
@@ -17,12 +17,8 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
version = "1.0"
def __init__(self, config):
"""The aprsd config object is stored."""
super().__init__(config)
def process(self, packet):
LOG.info("BaseNotifyPlugin")
LOG.info("NotifySeenPlugin")
notify_callsign = self.config["aprsd"]["watch_list"]["alert_callsign"]
fromcall = packet.get("from")
@@ -50,4 +46,3 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
wl.max_delta(),
),
)
return messaging.NULL_MESSAGE
+7 -8
View File
@@ -5,7 +5,7 @@ import time
from opencage.geocoder import OpenCageGeocode
import pytz
from aprsd import fuzzyclock, plugin, plugin_utils, trace, utils
from aprsd import fuzzyclock, plugin, plugin_utils, trace
LOG = logging.getLogger("APRSD")
@@ -54,7 +54,7 @@ class TimeOpenCageDataPlugin(TimePlugin):
version = "1.0"
command_regex = "^[tT]"
command_name = "Time"
command_name = "time"
@trace.trace
def process(self, packet):
@@ -64,7 +64,7 @@ class TimeOpenCageDataPlugin(TimePlugin):
# get last location of a callsign, get descriptive name from weather service
try:
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found"
@@ -95,7 +95,7 @@ class TimeOpenCageDataPlugin(TimePlugin):
lon = aprs_data["entries"][0]["lng"]
try:
utils.check_config_option(self.config, "opencagedata", "apiKey")
self.config.exists("opencagedata.apiKey")
except Exception as ex:
LOG.error(f"Failed to find config opencage:apiKey {ex}")
return "No opencage apiKey found"
@@ -120,7 +120,7 @@ class TimeOWMPlugin(TimePlugin):
version = "1.0"
command_regex = "^[tT]"
command_name = "Time"
command_name = "time"
@trace.trace
def process(self, packet):
@@ -130,7 +130,7 @@ class TimeOWMPlugin(TimePlugin):
# get last location of a callsign, get descriptive name from weather service
try:
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found"
@@ -160,8 +160,7 @@ class TimeOWMPlugin(TimePlugin):
lon = aprs_data["entries"][0]["lng"]
try:
utils.check_config_option(
self.config,
self.config.exists(
["services", "openweathermap", "apiKey"],
)
except Exception as ex:
+33 -21
View File
@@ -4,7 +4,7 @@ import re
import requests
from aprsd import plugin, plugin_utils, trace, utils
from aprsd import plugin, plugin_utils, trace
LOG = logging.getLogger("APRSD")
@@ -25,7 +25,7 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
version = "1.0"
command_regex = "^[wW]"
command_name = "weather"
command_name = "USWeather"
@trace.trace
def process(self, packet):
@@ -34,7 +34,7 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
# message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
try:
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found"
@@ -88,7 +88,7 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase):
version = "1.0"
command_regex = "^[metar]"
command_name = "Metar"
command_name = "USMetar"
@trace.trace
def process(self, packet):
@@ -115,10 +115,7 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase):
fromcall = fromcall
try:
utils.check_config_option(
self.config,
["services", "aprs.fi", "apiKey"],
)
self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found"
@@ -183,7 +180,16 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
version = "1.0"
command_regex = "^[wW]"
command_name = "Weather"
command_name = "OpenWeatherMap"
def help(self):
_help = [
"openweathermap: Send {} to get weather "
"from your location".format(self.command_regex),
"openweathermap: Send {} <callsign> to get "
"weather from <callsign>".format(self.command_regex),
]
return _help
@trace.trace
def process(self, packet):
@@ -199,7 +205,7 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
searchcall = fromcall
try:
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found"
@@ -220,16 +226,13 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
lon = aprs_data["entries"][0]["lng"]
try:
utils.check_config_option(
self.config,
["services", "openweathermap", "apiKey"],
)
self.config.exists(["services", "openweathermap", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config openweathermap:apiKey {ex}")
return "No openweathermap apiKey found"
try:
utils.check_config_option(self.config, ["aprsd", "units"])
self.config.exists(["aprsd", "units"])
except Exception:
LOG.debug("Couldn't find untis in aprsd:services:units")
units = "metric"
@@ -306,15 +309,24 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
"""
version = "1.0"
command_regex = "^[metar]"
command_name = "Weather"
command_regex = "^[mM]"
command_name = "AVWXWeather"
def help(self):
_help = [
"avwxweather: Send {} to get weather "
"from your location".format(self.command_regex),
"avwxweather: Send {} <callsign> to get "
"weather from <callsign>".format(self.command_regex),
]
return _help
@trace.trace
def process(self, packet):
fromcall = packet.get("from")
message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
LOG.info(f"OWMWeather Plugin '{message}'")
LOG.info(f"AVWXWeather Plugin '{message}'")
a = re.search(r"^.*\s+(.*)", message)
if a is not None:
searchcall = a.group(1)
@@ -323,7 +335,7 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
searchcall = fromcall
try:
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found"
@@ -344,13 +356,13 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
lon = aprs_data["entries"][0]["lng"]
try:
utils.check_config_option(self.config, ["services", "avwx", "apiKey"])
self.config.exists(["services", "avwx", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config avwx:apiKey {ex}")
return "No avwx apiKey found"
try:
utils.check_config_option(self.config, ["services", "avwx", "base_url"])
self.config.exists(self.config, ["services", "avwx", "base_url"])
except Exception as ex:
LOG.debug(f"Didn't find avwx:base_url {ex}")
base_url = "https://avwx.rest"
+5 -1
View File
@@ -194,11 +194,14 @@ class APRSDStats:
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()
stats = {
"aprsd": {
@@ -208,7 +211,8 @@ class APRSDStats:
"memory_current_str": utils.human_size(self.memory),
"memory_peak": self.memory_peak,
"memory_peak_str": utils.human_size(self.memory_peak),
"watch_list": wl.callsigns,
"watch_list": wl.get_all(),
"seen_list": sl.get_all(),
},
"aprs-is": {
"server": self.aprsis_server,
+41 -33
View File
@@ -17,6 +17,7 @@ RX_THREAD = "RX"
EMAIL_THREAD = "Email"
rx_msg_queue = queue.Queue(maxsize=20)
logging_queue = queue.Queue()
msg_queues = {
"rx": rx_msg_queue,
}
@@ -88,9 +89,11 @@ class KeepAliveThread(APRSDThread):
tracemalloc.start()
super().__init__("KeepAlive")
self.config = config
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
self.max_delta = datetime.timedelta(**max_timeout)
def loop(self):
if self.cntr % 6 == 0:
if self.cntr % 60 == 0:
tracker = messaging.MsgTrack()
stats_obj = stats.APRSDStats()
pl = packets.PacketList()
@@ -125,6 +128,19 @@ class KeepAliveThread(APRSDThread):
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):
@@ -142,13 +158,13 @@ class APRSDRXThread(APRSDThread):
super().__init__("RX_MSG")
self.msg_queues = msg_queues
self.config = config
self._client = client.factory.create()
def stop(self):
self.thread_stop = True
client.get_client().stop()
client.factory.create().client.stop()
def loop(self):
aprs_client = client.get_client()
# setup the consumer of messages and block until a messages
try:
@@ -161,7 +177,9 @@ class APRSDRXThread(APRSDThread):
# and the aprslib developer didn't want to allow a PR to add
# kwargs. :(
# https://github.com/rossengeorgiev/aprs-python/pull/56
aprs_client.consumer(self.process_packet, raw=False, blocking=False)
self._client.client.consumer(
self.process_packet, raw=False, blocking=False,
)
except aprslib.exceptions.ConnectionDrop:
LOG.error("Connection dropped, reconnecting")
@@ -169,11 +187,12 @@ class APRSDRXThread(APRSDThread):
# Force the deletion of the client object connected to aprs
# This will cause a reconnect, next time client.get_client()
# is called
client.Client().reset()
self._client.reset()
# Continue to loop
return True
def process_packet(self, packet):
def process_packet(self, *args, **kwargs):
packet = self._client.decode_packet(*args, **kwargs)
thread = APRSDProcessPacketThread(packet=packet, config=self.config)
thread.start()
@@ -250,14 +269,20 @@ class APRSDProcessPacketThread(APRSDThread):
replied = True
for subreply in reply:
LOG.debug(f"Sending '{subreply}'")
msg = messaging.TextMessage(
self.config["aprs"]["login"],
fromcall,
subreply,
)
msg.send()
if isinstance(subreply, messaging.Message):
subreply.send()
else:
msg = messaging.TextMessage(
self.config["aprs"]["login"],
fromcall,
subreply,
)
msg.send()
elif isinstance(reply, messaging.Message):
# We have a message based object.
LOG.debug(f"Sending '{reply}'")
reply.send()
replied = True
else:
replied = True
# A plugin can return a null message flag which signals
@@ -277,12 +302,11 @@ 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:
reply = "Usage: weather, locate [call], time, fortune, ping"
LOG.warning("Sending help!")
msg = messaging.TextMessage(
self.config["aprs"]["login"],
fromcall,
reply,
"Unknown command! Send 'help' message for help",
)
msg.send()
except Exception as ex:
@@ -298,19 +322,3 @@ class APRSDProcessPacketThread(APRSDThread):
msg.send()
LOG.debug("Packet processing complete")
class APRSDTXThread(APRSDThread):
def __init__(self, msg_queues, config):
super().__init__("TX_MSG")
self.msg_queues = msg_queues
self.config = config
def loop(self):
try:
msg = self.msg_queues["tx"].get(timeout=1)
msg.send()
except queue.Empty:
pass
# Continue to loop
return True
-327
View File
@@ -3,110 +3,13 @@
import collections
import errno
import functools
import logging
import os
from pathlib import Path
import re
import sys
import threading
import click
import update_checker
import yaml
import aprsd
from aprsd import plugin
LOG_LEVELS = {
"CRITICAL": logging.CRITICAL,
"ERROR": logging.ERROR,
"WARNING": logging.WARNING,
"INFO": logging.INFO,
"DEBUG": logging.DEBUG,
}
DEFAULT_LOG_FORMAT = (
"[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s]"
" %(message)s - [%(pathname)s:%(lineno)d]"
)
DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
# an example of what should be in the ~/.aprsd/config.yml
DEFAULT_CONFIG_DICT = {
"ham": {"callsign": "NOCALL"},
"aprs": {
"login": "NOCALL",
"password": "00000",
"host": "rotate.aprs2.net",
"port": 14580,
},
"aprsd": {
"logfile": "/tmp/aprsd.log",
"logformat": DEFAULT_LOG_FORMAT,
"dateformat": DEFAULT_DATE_FORMAT,
"trace": False,
"enabled_plugins": plugin.CORE_MESSAGE_PLUGINS,
"units": "imperial",
"watch_list": {
"enabled": False,
# Who gets the alert?
"alert_callsign": "NOCALL",
# 43200 is 12 hours
"alert_time_seconds": 43200,
# How many packets to save in a ring Buffer
# for a particular callsign
"packet_keep_count": 10,
"callsigns": [],
"enabled_plugins": plugin.CORE_NOTIFY_PLUGINS,
},
"web": {
"enabled": True,
"logging_enabled": True,
"host": "0.0.0.0",
"port": 8001,
"users": {
"admin": "password-here",
},
},
"email": {
"enabled": True,
"shortcuts": {
"aa": "5551239999@vtext.com",
"cl": "craiglamparter@somedomain.org",
"wb": "555309@vtext.com",
},
"smtp": {
"login": "SMTP_USERNAME",
"password": "SMTP_PASSWORD",
"host": "smtp.gmail.com",
"port": 465,
"use_ssl": False,
"debug": False,
},
"imap": {
"login": "IMAP_USERNAME",
"password": "IMAP_PASSWORD",
"host": "imap.gmail.com",
"port": 993,
"use_ssl": True,
"debug": False,
},
},
},
"services": {
"aprs.fi": {"apiKey": "APIKEYVALUE"},
"openweathermap": {"apiKey": "APIKEYVALUE"},
"opencagedata": {"apiKey": "APIKEYVALUE"},
"avwx": {"base_url": "http://host:port", "apiKey": "APIKEYVALUE"},
},
}
home = str(Path.home())
DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/"
DEFAULT_SAVE_FILE = f"{home}/.config/aprsd/aprsd.p"
DEFAULT_CONFIG_FILE = f"{home}/.config/aprsd/aprsd.yml"
def synchronized(wrapped):
@@ -157,236 +60,6 @@ def end_substr(original, substr):
return idx
def dump_default_cfg():
return add_config_comments(
yaml.dump(
DEFAULT_CONFIG_DICT,
indent=4,
),
)
def add_config_comments(raw_yaml):
end_idx = end_substr(raw_yaml, "aprs:")
if end_idx != -1:
# lets insert a comment
raw_yaml = insert_str(
raw_yaml,
"\n # Get the passcode for your callsign here: "
"\n # https://apps.magicbug.co.uk/passcode",
end_idx,
)
end_idx = end_substr(raw_yaml, "aprs.fi:")
if end_idx != -1:
# lets insert a comment
raw_yaml = insert_str(
raw_yaml,
"\n # Get the apiKey from your aprs.fi account here: "
"\n # http://aprs.fi/account",
end_idx,
)
end_idx = end_substr(raw_yaml, "opencagedata:")
if end_idx != -1:
# lets insert a comment
raw_yaml = insert_str(
raw_yaml,
"\n # (Optional for TimeOpenCageDataPlugin) "
"\n # Get the apiKey from your opencagedata account here: "
"\n # https://opencagedata.com/dashboard#api-keys",
end_idx,
)
end_idx = end_substr(raw_yaml, "openweathermap:")
if end_idx != -1:
# lets insert a comment
raw_yaml = insert_str(
raw_yaml,
"\n # (Optional for OWMWeatherPlugin) "
"\n # Get the apiKey from your "
"\n # openweathermap account here: "
"\n # https://home.openweathermap.org/api_keys",
end_idx,
)
end_idx = end_substr(raw_yaml, "avwx:")
if end_idx != -1:
# lets insert a comment
raw_yaml = insert_str(
raw_yaml,
"\n # (Optional for AVWXWeatherPlugin) "
"\n # Use hosted avwx-api here: https://avwx.rest "
"\n # or deploy your own from here: "
"\n # https://github.com/avwx-rest/avwx-api",
end_idx,
)
return raw_yaml
def create_default_config():
"""Create a default config file."""
# make sure the directory location exists
config_file_expanded = os.path.expanduser(DEFAULT_CONFIG_FILE)
config_dir = os.path.dirname(config_file_expanded)
if not os.path.exists(config_dir):
click.echo(f"Config dir '{config_dir}' doesn't exist, creating.")
mkdir_p(config_dir)
with open(config_file_expanded, "w+") as cf:
cf.write(dump_default_cfg())
def get_config(config_file):
"""This tries to read the yaml config from <config_file>."""
config_file_expanded = os.path.expanduser(config_file)
if os.path.exists(config_file_expanded):
with open(config_file_expanded) as stream:
config = yaml.load(stream, Loader=yaml.FullLoader)
return config
else:
if config_file == DEFAULT_CONFIG_FILE:
click.echo(
f"{config_file_expanded} is missing, creating config file",
)
create_default_config()
msg = (
"Default config file created at {}. Please edit with your "
"settings.".format(config_file)
)
click.echo(msg)
else:
# The user provided a config file path different from the
# Default, so we won't try and create it, just bitch and bail.
msg = f"Custom config file '{config_file}' is missing."
click.echo(msg)
sys.exit(-1)
def conf_option_exists(conf, chain):
_key = chain.pop(0)
if _key in conf:
return conf_option_exists(conf[_key], chain) if chain else conf[_key]
def check_config_option(config, chain, default_fail=None):
result = conf_option_exists(config, chain.copy())
if not result:
raise Exception(
"'{}' was not in config file".format(
chain,
),
)
else:
if default_fail:
if result == default_fail:
# We have to fail and bail if the user hasn't edited
# this config option.
raise Exception(
"Config file needs to be edited from provided defaults for {}.".format(
chain,
),
)
else:
return config
# This method tries to parse the config yaml file
# and consume the settings.
# If the required params don't exist,
# it will look in the environment
def parse_config(config_file):
# for now we still use globals....ugh
global CONFIG
def fail(msg):
click.echo(msg)
sys.exit(-1)
def check_option(config, chain, default_fail=None):
try:
config = check_config_option(config, chain, default_fail=default_fail)
except Exception as ex:
fail(repr(ex))
else:
return config
config = get_config(config_file)
# special check here to make sure user has edited the config file
# and changed the ham callsign
check_option(
config,
[
"ham",
"callsign",
],
default_fail=DEFAULT_CONFIG_DICT["ham"]["callsign"],
)
check_option(
config,
["services", "aprs.fi", "apiKey"],
default_fail=DEFAULT_CONFIG_DICT["services"]["aprs.fi"]["apiKey"],
)
check_option(
config,
["aprs", "login"],
default_fail=DEFAULT_CONFIG_DICT["aprs"]["login"],
)
check_option(
config,
["aprs", "password"],
default_fail=DEFAULT_CONFIG_DICT["aprs"]["password"],
)
# Ensure they change the admin password
if config["aprsd"]["web"]["enabled"] is True:
check_option(
config,
["aprsd", "web", "users", "admin"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["web"]["users"]["admin"],
)
if config["aprsd"]["watch_list"]["enabled"] is True:
check_option(
config,
["aprsd", "watch_list", "alert_callsign"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["watch_list"]["alert_callsign"],
)
if config["aprsd"]["email"]["enabled"] is True:
# Check IMAP server settings
check_option(config, ["aprsd", "email", "imap", "host"])
check_option(config, ["aprsd", "email", "imap", "port"])
check_option(
config,
["aprsd", "email", "imap", "login"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["login"],
)
check_option(
config,
["aprsd", "email", "imap", "password"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["password"],
)
# Check SMTP server settings
check_option(config, ["aprsd", "email", "smtp", "host"])
check_option(config, ["aprsd", "email", "smtp", "port"])
check_option(
config,
["aprsd", "email", "smtp", "login"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["login"],
)
check_option(
config,
["aprsd", "email", "smtp", "password"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["password"],
)
return config
def human_size(bytes, units=None):
"""Returns a human readable string representation of bytes"""
if not units:
+189
View File
@@ -0,0 +1,189 @@
/* PrismJS 1.24.1
https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+log&plugins=show-language+toolbar */
/**
* prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML
* Based on https://github.com/chriskempson/tomorrow-theme
* @author Rose Pritchard
*/
code[class*="language-"],
pre[class*="language-"] {
color: #ccc;
background: none;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
font-size: 1em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #2d2d2d;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.block-comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #999;
}
.token.punctuation {
color: #ccc;
}
.token.tag,
.token.attr-name,
.token.namespace,
.token.deleted {
color: #e2777a;
}
.token.function-name {
color: #6196cc;
}
.token.boolean,
.token.number,
.token.function {
color: #f08d49;
}
.token.property,
.token.class-name,
.token.constant,
.token.symbol {
color: #f8c555;
}
.token.selector,
.token.important,
.token.atrule,
.token.keyword,
.token.builtin {
color: #cc99cd;
}
.token.string,
.token.char,
.token.attr-value,
.token.regex,
.token.variable {
color: #7ec699;
}
.token.operator,
.token.entity,
.token.url {
color: #67cdcc;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.token.inserted {
color: green;
}
div.code-toolbar {
position: relative;
}
div.code-toolbar > .toolbar {
position: absolute;
top: .3em;
right: .2em;
transition: opacity 0.3s ease-in-out;
opacity: 0;
}
div.code-toolbar:hover > .toolbar {
opacity: 1;
}
/* Separate line b/c rules are thrown out if selector is invalid.
IE11 and old Edge versions don't support :focus-within. */
div.code-toolbar:focus-within > .toolbar {
opacity: 1;
}
div.code-toolbar > .toolbar > .toolbar-item {
display: inline-block;
}
div.code-toolbar > .toolbar > .toolbar-item > a {
cursor: pointer;
}
div.code-toolbar > .toolbar > .toolbar-item > button {
background: none;
border: 0;
color: inherit;
font: inherit;
line-height: normal;
overflow: visible;
padding: 0;
-webkit-user-select: none; /* for button */
-moz-user-select: none;
-ms-user-select: none;
}
div.code-toolbar > .toolbar > .toolbar-item > a,
div.code-toolbar > .toolbar > .toolbar-item > button,
div.code-toolbar > .toolbar > .toolbar-item > span {
color: #bbb;
font-size: .8em;
padding: 0 .5em;
background: #f5f2f0;
background: rgba(224, 224, 224, 0.2);
box-shadow: 0 2px 0 0 rgba(0,0,0,0.2);
border-radius: .5em;
}
div.code-toolbar > .toolbar > .toolbar-item > a:hover,
div.code-toolbar > .toolbar > .toolbar-item > a:focus,
div.code-toolbar > .toolbar > .toolbar-item > button:hover,
div.code-toolbar > .toolbar > .toolbar-item > button:focus,
div.code-toolbar > .toolbar > .toolbar-item > span:hover,
div.code-toolbar > .toolbar > .toolbar-item > span:focus {
color: inherit;
text-decoration: none;
}
+1 -1
View File
@@ -220,7 +220,7 @@ function updateQuadData(chart, label, first, second, third, fourth) {
function update_stats( data ) {
$("#version").text( data["stats"]["aprsd"]["version"] );
$("#aprsis").html( "APRS-IS Server: <a href='http://status.aprs2.net' >" + data["stats"]["aprs-is"]["server"] + "</a>" );
$("#aprs_connection").html( data["aprs_connection"] );
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] );
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
$("#jsonstats").html(html_pretty);
+26
View File
@@ -0,0 +1,26 @@
function init_logs() {
const socket = io("/logs");
socket.on('connect', function () {
console.log("Connected to logs socketio");
});
socket.on('connected', function(msg) {
console.log("Connected to /logs");
console.log(msg);
});
socket.on('log_entry', function(data) {
update_logs(data);
});
};
function update_logs(data) {
var code_block = $('#logtext')
entry = data["message"]
const html_pretty = Prism.highlight(entry, Prism.languages.log, 'log');
code_block.append(html_pretty + "<br>");
var div = document.getElementById('logContainer');
div.scrollTop = div.scrollHeight;
}
+28 -2
View File
@@ -58,9 +58,32 @@ function update_watchlist_from_packet(callsign, val) {
//console.log(watchlist)
}
function update_seenlist( data ) {
var seendiv = $("#seenDiv");
var html_str = '<table class="ui celled striped table">'
html_str += '<thead><tr><th>HAM Callsign</th><th>Age since last seen by APRSD</th>'
html_str += '<th>Number of packets RX</th></tr></thead><tbody>'
seendiv.html('')
var seen_list = data["stats"]["aprsd"]["seen_list"]
var len = Object.keys(seen_list).length
$('#seen_count').html(len)
jQuery.each(seen_list, function(i, val) {
html_str += '<tr><td class="collapsing">'
html_str += '<img id="callsign_'+i+'" class="aprsd_1"></img>' + i + '</td>'
html_str += '<td>' + val["last"] + '</td>'
html_str += '<td>' + val["count"] + '</td></tr>'
});
html_str += "</tbody></table>";
seendiv.append(html_str);
}
function update_plugins( data ) {
var plugindiv = $("#pluginDiv");
var html_str = '<table class="ui celled striped table"><thead><tr><th>Plugin Name</th><th>Processed Packets</th><th>Sent Packets</th></tr></thead><tbody>'
var html_str = '<table class="ui celled striped table"><thead><tr>'
html_str += '<th>Plugin Name</th><th>Plugin Enabled?</th>'
html_str += '<th>Processed Packets</th><th>Sent Packets</th>'
html_str += '<th>Version</th>'
html_str += '</tr></thead><tbody>'
plugindiv.html('')
var plugins = data["stats"]["plugins"];
@@ -69,7 +92,9 @@ function update_plugins( data ) {
for (var i=0; i<keys.length; i++) { // now lets iterate in sort order
var key = keys[i];
var val = plugins[key];
html_str += '<tr><td class="collapsing">' + key + '</td><td>' + val["rx"] + '</td><td>' + val["tx"] + '</td></tr>';
html_str += '<tr><td class="collapsing">' + key + '</td>';
html_str += '<td>' + val["enabled"] + '</td><td>' + val["rx"] + '</td>';
html_str += '<td>' + val["tx"] + '</td><td>' + val["version"] +'</td></tr>';
}
html_str += "</tbody></table>";
plugindiv.append(html_str);
@@ -137,6 +162,7 @@ function start_update() {
success: function(data) {
update_stats(data);
update_watchlist(data);
update_seenlist(data);
update_plugins(data);
},
complete: function() {
File diff suppressed because it is too large Load Diff
+144
View File
@@ -0,0 +1,144 @@
var cleared = false;
function size_dict(d){c=0; for (i in d) ++c; return c}
function init_messages() {
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 = $("#msgsDiv");
msgsdiv.html('')
cleared = true
}
add_msg(msg);
});
socket.on("ack", function(msg) {
update_msg(msg);
});
socket.on("reply", function(msg) {
update_msg(msg);
});
$("#sendform").submit(function(event) {
event.preventDefault();
var $checkboxes = $(this).find('input[type=checkbox]');
//loop through the checkboxes and change to hidden fields
$checkboxes.each(function() {
if ($(this)[0].checked) {
$(this).attr('type', 'hidden');
$(this).val(1);
} else {
$(this).attr('type', 'hidden');
$(this).val(0);
}
});
msg = {'from': $('#from').val(),
'password': $('#password').val(),
'to': $('#to').val(),
'message': $('#message').val(),
'wait_reply': $('#wait_reply').val(),
}
socket.emit("send", msg);
//loop through the checkboxes and change to hidden fields
$checkboxes.each(function() {
$(this).attr('type', 'checkbox');
});
});
}
function add_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
ts_str = msg["ts"].toString();
ts = ts_str.split(".")[0]*1000;
var d = new Date(ts).toLocaleDateString("en-US")
var t = new Date(ts).toLocaleTimeString("en-US")
from = msg['from']
title_id = 'title_tx'
var from_to = d + " " + t + "&nbsp;&nbsp;&nbsp;&nbsp;" + from + " > "
if (msg.hasOwnProperty('to')) {
from_to = from_to + msg['to']
}
from_to = from_to + "&nbsp;&nbsp;-&nbsp;&nbsp;" + msg['message']
id = ts_str.split('.')[0]
pretty_id = "pretty_" + id
loader_id = "loader_" + id
ack_id = "ack_" + id
reply_id = "reply_" + id
span_id = "span_" + id
json_pretty = Prism.highlight(JSON.stringify(msg, null, '\t'), Prism.languages.json, 'json');
msg_html = '<div class="ui title" id="' + title_id + '"><i class="dropdown icon"></i>';
msg_html += '<div class="ui active inline loader" id="' + loader_id +'" data-content="Waiting for Ack"></div>&nbsp;';
msg_html += '<i class="thumbs down outline icon" id="' + ack_id + '" data-content="Waiting for ACK"></i>&nbsp;';
msg_html += '<i class="thumbs down outline icon" id="' + reply_id + '" data-content="Waiting for Reply"></i>&nbsp;';
msg_html += '<span id="' + span_id + '">' + from_to +'</span></div>';
msg_html += '<div class="content"><p class="transition hidden"><pre id="' + pretty_id + '" class="language-json">' + json_pretty + '</p></p></div>'
msgsdiv.prepend(msg_html);
$('.ui.accordion').accordion('refresh');
}
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
reply_id = "reply_" + 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');
}
if (msg['reply'] !== null) {
var reply_div = $('#' + reply_id);
reply_div.removeClass("thumbs down outline icon");
reply_div.addClass('reply icon');
reply_div.attr('data-content', 'Got Reply');
var d = new Date(ts).toLocaleDateString("en-US")
var t = new Date(ts).toLocaleTimeString("en-US")
var from_to = d + " " + t + "&nbsp;&nbsp;&nbsp;&nbsp;" + from + " > "
if (msg.hasOwnProperty('to')) {
from_to = from_to + msg['to']
}
from_to = from_to + "&nbsp;&nbsp;-&nbsp;&nbsp;" + msg['message']
from_to += "&nbsp;&nbsp; ===> " + msg["reply"]["message_text"]
var span_div = $('#' + span_id);
span_div.html(from_to);
}
var pretty_pre = $("#" + pretty_id);
pretty_pre.html('');
json_pretty = Prism.highlight(JSON.stringify(msg, null, '\t'), Prism.languages.json, 'json');
pretty_pre.html(json_pretty);
$('.ui.accordion').accordion('refresh');
}
+51 -5
View File
@@ -3,10 +3,8 @@
<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://cdn.jsdelivr.net/npm/prismjs@1.23.0/prism.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/components/prism-json.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/themes/prism-tomorrow.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.bundle.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
@@ -14,12 +12,16 @@
<link rel="stylesheet" href="/static/css/index.css">
<link rel="stylesheet" href="/static/css/tabs.css">
<link rel="stylesheet" href="/static/css/prism.css">
<script src="/static/js/prism.js"></script>
<script src="/static/js/main.js"></script>
<script src="/static/js/charts.js"></script>
<script src="/static/js/tabs.js"></script>
<script src="/static/js/send-message.js"></script>
<script src="/static/js/logs.js"></script>
<script type="text/javascript"">
<script type="text/javascript">
var initial_stats = {{ initial_stats|tojson|safe }};
var memory_chart = null
@@ -30,6 +32,8 @@
console.log(initial_stats);
start_update();
start_charts();
init_messages();
init_logs();
$("#toggleStats").click(function() {
$("#jsonstats").fadeToggle(1000);
@@ -43,6 +47,10 @@
$("#configjson").html(html_pretty);
$("#jsonstats").fadeToggle(1000);
//var log_text_pretty = $('#logtext').text();
//const log_pretty = Prism.highlight( log_text_pretty, Prism.languages.log, 'log');
//$('#logtext').html(log_pretty);
$('.ui.accordion').accordion({exclusive: false});
$('.menu .item').tab('change tab', 'charts-tab');
});
@@ -58,7 +66,7 @@
<div class='left floated ten wide column'>
<span style='color: green'>{{ callsign }}</span>
connected to
<span style='color: blue' id='aprsis'>NONE</span>
<span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
</div>
<div class='right floated four wide column'>
@@ -70,9 +78,12 @@
<div class="ui top attached tabular menu">
<div class="active item" data-tab="charts-tab">Charts</div>
<div class="item" data-tab="msgs-tab">Messages</div>
<div class="item" data-tab="seen-tab">Seen List</div>
<div class="item" data-tab="watch-tab">Watch List</div>
<div class="item" data-tab="plugin-tab">Plugins</div>
<div class="item" data-tab="config-tab">Config</div>
<div class="item" data-tab="send-tab">Send Message</div>
<div class="item" data-tab="log-tab">LogFile</div>
<div class="item" data-tab="raw-tab">Raw JSON</div>
</div>
@@ -121,6 +132,13 @@
</div>
</div>
<div class="ui bottom attached tab segment" data-tab="seen-tab">
<h3 class="ui dividing header">
Callsign Seen List (<span id="seen_count">{{ seen_count }}</span>)
</h3>
<div id="seenDiv" class="ui mini text">Loading</div>
</div>
<div class="ui bottom attached tab segment" data-tab="watch-tab">
<h3 class="ui dividing header">
Callsign Watch List (<span id="watch_count">{{ watch_count }}</span>)
@@ -142,6 +160,34 @@
<pre id="configjson" class="language-json">{{ config_json|safe }}</pre>
</div>
<div class="ui bottom attached tab segment" data-tab="send-tab">
<h3 class="ui dividing header">Send Message</h3>
<div id="sendMsgDiv" class="ui mini text">
<form id="sendform" name="sendmsg" action="">
<p><label for="from_call">From Callsign:</label>
<input type="text" name="from_call" id="from"></p>
<p><label for="from_call_password">Password:</label>
<input type="password" name="from_call_password" id='password'></p>
<p><label for="to_call">To Callsign:</label>
<input type="text" name="to_call" id="to" ></p>
<p><label for="message">Message:</label>
<input type="text" name="message" id="message" ></p>
<p><label for="wait">Wait for Reply?</label>
<input type="checkbox" name="wait_reply" id="wait_reply" value="off" checked>
</p>
<input type="submit" name="submit" class="button" id="send_msg" value="Send" />
</form>
<div class="ui styled fluid accordion" id="accordion">
<div id="sendMsgsDiv" class="ui mini text">Messages</div>
</div>
</div>
</div>
<div class="ui bottom attached tab segment" data-tab="log-tab">
<h3 class="ui dividing header">LOGFILE</h3>
<pre id="logContainer" style="height: 600px;overflow-y:auto;overflow-x:auto;"><code id="logtext" class="language-log" ></code></pre>
</div>
<div class="ui bottom attached tab segment" data-tab="raw-tab">
<h3 class="ui dividing header">Raw JSON</h3>
<pre id="jsonstats" class="language-json">{{ stats|safe }}</pre>
+74
View File
@@ -0,0 +1,74 @@
<html>
<head>
<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.jsdelivr.net/npm/jquery-simple-websocket@1.1.4/src/jquery.simple.websocket.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://cdn.jsdelivr.net/npm/prismjs@1.23.0/prism.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/components/prism-json.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/themes/prism-tomorrow.css">
<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/send-message.js"></script>
<script language="JavaScript">
$(document).ready(function() {
init_messages();
});
</script>
</head>
<body>
<div class='ui text container'>
<h1 class='ui dividing header'>APRSD {{ 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='aprsis'>NONE</span>
</div>
<div class='right floated four wide column'>
<span id='uptime'>NONE</span>
</div>
</div>
<h3 class="ui dividing header">Send Message Form</h3>
<form id="sendform" name="sendmsg" action="">
<p><label for="from_call">From Callsign:</label>
<input type="text" name="from_call" id="from" value="WB4BOR"></p>
<p><label for="from_call_password">Password:</label>
<input type="password" name="from_call_password" id='password' value="24496"></p>
<p><label for="to_call">To Callsign:</label>
<input type="text" name="to_call" id="to" value="WB4BOR-11"></p>
<p><label for="message">Message:</label>
<input type="text" name="message" id="message" value="ping"></p>
<p><label for="wait">Wait for Reply?</label>
<input type="checkbox" name="wait_reply" id="wait_reply" value="off" checked>
</p>
<input type="submit" name="submit" class="button" id="send_msg" value="Send" />
</form>
<h3 class="ui dividing header">Messages (<span id="msgs_count">0</span>)</h3>
<div class="ui styled fluid accordion" id="accordion">
<div id="msgsDiv" class="ui mini text">Messages</div>
</div>
</body>
</html>
+2 -2
View File
@@ -1,12 +1,12 @@
flake8
isort
mypy
pytest
pytest-cov
pep8-naming
Sphinx
tox
twine
pre-commit
pip-tools
pytest
pytest-cov
gray
+61 -55
View File
@@ -1,5 +1,5 @@
#
# This file is autogenerated by pip-compile
# This file is autogenerated by pip-compile with python 3.8
# To update, run:
#
# pip-compile dev-requirements.in
@@ -9,10 +9,8 @@ add-trailing-comma==2.1.0
alabaster==0.7.12
# via sphinx
appdirs==1.4.4
# via
# black
# virtualenv
attrs==20.3.0
# via black
attrs==21.2.0
# via
# jsonschema
# pytest
@@ -20,17 +18,19 @@ 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==3.3.0
bleach==4.1.0
# via readme-renderer
certifi==2020.12.5
certifi==2021.5.30
# via requests
cfgv==3.2.0
cfgv==3.3.1
# via pre-commit
chardet==4.0.0
charset-normalizer==2.0.4
# via requests
click==7.1.2
click==8.0.1
# via
# black
# pip-tools
@@ -42,9 +42,9 @@ configargparse==1.5.2
# via gray
coverage==5.5
# via pytest-cov
distlib==0.3.1
distlib==0.3.2
# via virtualenv
docutils==0.16
docutils==0.17.1
# via
# readme-renderer
# sphinx
@@ -56,22 +56,23 @@ filelock==3.0.12
# virtualenv
fixit==0.1.4
# via gray
flake8-polyfill==1.0.2
# via pep8-naming
flake8==3.9.1
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.4
identify==2.2.13
# via pre-commit
idna==2.10
idna==3.2
# via requests
imagesize==1.2.0
# via sphinx
importlib-metadata==4.0.1
importlib-metadata==4.7.1
# via
# keyring
# twine
@@ -79,52 +80,54 @@ importlib-resources==5.2.2
# via fixit
iniconfig==1.1.1
# via pytest
isort==5.8.0
isort==5.9.3
# via
# -r dev-requirements.in
# gray
jinja2==2.11.3
jinja2==3.0.1
# via sphinx
jsonschema==3.2.0
# via fixit
keyring==23.0.1
keyring==23.1.0
# via twine
libcst==0.3.20
# via fixit
markupsafe==1.1.1
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
mypy==0.812
# via -r dev-requirements.in
nodeenv==1.6.0
# via pre-commit
packaging==20.9
packaging==21.0
# via
# bleach
# pytest
# sphinx
# tox
pathspec==0.8.1
pathspec==0.9.0
# via black
pep517==0.10.0
pep517==0.11.0
# via pip-tools
pep8-naming==0.11.1
pep8-naming==0.12.1
# via -r dev-requirements.in
pip-tools==6.1.0
pip-tools==6.2.0
# via -r dev-requirements.in
pkginfo==1.7.0
pkginfo==1.7.1
# via twine
pluggy==0.13.1
platformdirs==2.2.0
# via virtualenv
pluggy==1.0.0
# via
# pytest
# tox
pre-commit==2.12.1
pre-commit==2.14.0
# via -r dev-requirements.in
prettylog==0.3.0
# via gray
@@ -138,7 +141,7 @@ pyflakes==2.3.1
# via
# autoflake
# flake8
pygments==2.9.0
pygments==2.10.0
# via
# readme-renderer
# sphinx
@@ -146,12 +149,12 @@ pyparsing==2.4.7
# via packaging
pyrsistent==0.18.0
# via jsonschema
pytest-cov==2.11.1
# via -r dev-requirements.in
pytest==6.2.3
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
@@ -163,18 +166,18 @@ pyyaml==5.4.1
# pre-commit
readme-renderer==29.0
# via twine
regex==2021.4.4
regex==2021.8.27
# via black
requests-toolbelt==0.9.1
# via twine
requests==2.25.1
requests==2.26.0
# via
# requests-toolbelt
# sphinx
# twine
rfc3986==1.4.0
requests-toolbelt==0.9.1
# via twine
six==1.15.0
rfc3986==1.5.0
# via twine
six==1.16.0
# via
# bleach
# jsonschema
@@ -183,19 +186,19 @@ six==1.15.0
# virtualenv
snowballstemmer==2.1.0
# via sphinx
sphinx==3.5.4
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==1.0.3
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.4
sphinxcontrib-serializinghtml==1.1.5
# via sphinx
tokenize-rt==4.1.0
# via
@@ -203,20 +206,21 @@ tokenize-rt==4.1.0
# pyupgrade
toml==0.10.2
# via
# pep517
# mypy
# pre-commit
# pytest
# pytest-cov
# tox
tomli==1.2.1
# via black
tox==3.23.0
# via
# black
# pep517
tox==3.24.3
# via -r dev-requirements.in
tqdm==4.60.0
tqdm==4.62.2
# via twine
twine==3.4.1
twine==3.4.2
# via -r dev-requirements.in
typed-ast==1.4.3
# via mypy
typing-extensions==3.10.0.0
# via
# libcst
@@ -230,15 +234,17 @@ unify==0.5
# via gray
untokenize==0.1.1
# via unify
urllib3==1.26.5
urllib3==1.26.6
# via requests
virtualenv==20.4.4
virtualenv==20.7.2
# via
# pre-commit
# tox
webencodings==0.5.1
# via bleach
zipp==3.4.1
wheel==0.37.0
# via pip-tools
zipp==3.5.0
# via
# importlib-metadata
# importlib-resources
+2 -2
View File
@@ -1,4 +1,4 @@
FROM python:3.8-slim as aprsd
FROM python:3-bullseye as aprsd
# Dockerfile for building a container during aprsd development.
@@ -28,7 +28,7 @@ RUN addgroup --gid $GID $APRS_USER
RUN useradd -m -u $UID -g $APRS_USER $APRS_USER
# Install aprsd
RUN /usr/local/bin/pip3 install aprsd==1.6.0
RUN /usr/local/bin/pip3 install aprsd==2.3.1
# Ensure /config is there with a default config file
USER root
+2 -2
View File
@@ -1,14 +1,14 @@
FROM python:3.8-slim as aprsd
# Dockerfile for building a container during aprsd development.
ARG BRANCH
ARG branch
ARG UID
ARG GID
ENV APRS_USER=aprs
ENV HOME=/home/aprs
ENV APRSD=http://github.com/craigerl/aprsd.git
ENV APRSD_BRANCH=${BRANCH:-master}
ENV APRSD_BRANCH=${branch:-master}
ENV VIRTUAL_ENV=$HOME/.venv3
ENV UID=${UID:-1000}
ENV GID=${GID:-1000}
+20 -8
View File
@@ -15,14 +15,18 @@ EOF
ALL_PLATFORMS=0
DEV=0
TAG="master"
TAG="latest"
BRANCH="master"
while getopts “t:da” OPTION
while getopts “t:dab:” OPTION
do
case $OPTION in
t)
TAG=$OPTARG
;;
b)
BRANCH=$OPTARG
;;
a)
ALL_PLATFORMS=1
;;
@@ -36,7 +40,7 @@ do
esac
done
VERSION="1.6.0"
VERSION="2.3.1"
if [ $ALL_PLATFORMS -eq 1 ]
then
@@ -45,20 +49,28 @@ else
PLATFORMS="linux/amd64"
fi
echo "Build with tag=${TAG} BRANCH=${BRANCH} dev?=${DEV} platforms?=${PLATFORMS}"
echo "Destroying old multiarch build container"
docker buildx rm multiarch
echo "Creating new buildx container"
docker buildx create --name multiarch --platform linux/arm/v7,linux/arm/v6,linux/arm64,linux/amd64 --config ./buildkit.toml --use --driver-opt image=moby/buildkit:master
if [ $DEV -eq 1 ]
then
echo "Build -DEV- with tag=${TAG} BRANCH=${BRANCH} platforms?=${PLATFORMS}"
# Use this script to locally build the docker image
docker buildx build --push --platform $PLATFORMS \
-t harbor.hemna.com/hemna6969/aprsd:$TAG \
-f Dockerfile-dev --no-cache .
-f Dockerfile-dev --build-arg branch=$BRANCH --no-cache .
else
# Use this script to locally build the docker image
echo "Build with tag=${TAG} BRANCH=${BRANCH} platforms?=${PLATFORMS}"
docker buildx build --push --platform $PLATFORMS \
-t hemna6969/aprsd:$VERSION \
-t hemna6969/aprsd:latest \
-t harbor.hemna.com/hemna6969/aprsd:latest \
-t hemna6969/aprsd:$TAG \
-t harbor.hemna.com/hemna6969/aprsd:$TAG \
-t harbor.hemna.com/hemna6969/aprsd:$VERSION \
-f Dockerfile .
fi
+8 -8
View File
@@ -28,14 +28,6 @@ aprsd.dev module
:undoc-members:
:show-inheritance:
aprsd.email module
------------------
.. automodule:: aprsd.email
:members:
:undoc-members:
:show-inheritance:
aprsd.fake\_aprs module
-----------------------
@@ -68,6 +60,14 @@ aprsd.healthcheck module
:undoc-members:
:show-inheritance:
aprsd.kissclient module
-----------------------
.. automodule:: aprsd.kissclient
:members:
:undoc-members:
:show-inheritance:
aprsd.listen module
-------------------
+4 -1
View File
@@ -1,3 +1,4 @@
aioax25>=0.0.10
aprslib
click
click-completion
@@ -11,10 +12,12 @@ pbr
pyyaml
# Allowing a newer version can lead to a conflict with
# requests.
py3-validate-email==0.2.16
py3-validate-email
pytz
requests
six
thesmuggler
yfinance
update_checker
flask-socketio
eventlet
+55 -29
View File
@@ -1,86 +1,107 @@
#
# This file is autogenerated by pip-compile
# This file is autogenerated by pip-compile with python 3.8
# To update, run:
#
# pip-compile requirements.in
#
aioax25==0.0.10
# via -r requirements.in
aprslib==0.6.47
# via -r requirements.in
backoff==1.10.0
backoff==1.11.1
# via opencage
certifi==2020.12.5
bidict==0.21.2
# via python-socketio
certifi==2021.5.30
# via requests
cffi==1.14.5
cffi==1.14.6
# via cryptography
chardet==4.0.0
charset-normalizer==2.0.4
# via requests
click-completion==0.5.2
# via -r requirements.in
click==7.1.2
click==8.0.1
# via
# -r requirements.in
# click-completion
# flask
click-completion==0.5.2
# via -r requirements.in
contexter==0.1.4
# via signalslot
cryptography==3.4.7
# via pyopenssl
dnspython==2.1.0
# via py3-validate-email
# via
# eventlet
# py3-validate-email
eventlet==0.32.0
# via -r requirements.in
filelock==3.0.12
# via py3-validate-email
flask-classful==0.14.2
# via -r requirements.in
flask-httpauth==4.3.0
# via -r requirements.in
flask==1.1.2
flask==2.0.1
# via
# -r requirements.in
# flask-classful
# flask-httpauth
idna==2.10
# 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==1.1.0
itsdangerous==2.0.1
# via flask
jinja2==2.11.3
jinja2==3.0.1
# via
# click-completion
# flask
lxml==4.6.3
# via yfinance
markupsafe==1.1.1
markupsafe==2.0.1
# via jinja2
multitasking==0.0.9
# via yfinance
numpy==1.20.2
numpy==1.21.2
# via
# pandas
# yfinance
opencage==1.2.2
opencage==2.0.0
# via -r requirements.in
pandas==1.2.4
pandas==1.3.2
# via yfinance
pbr==5.6.0
# via -r requirements.in
pluggy==0.13.1
pluggy==1.0.0
# via -r requirements.in
py3-validate-email==0.2.16
py3-validate-email==1.0.1
# via -r requirements.in
pycparser==2.20
# via cffi
pyopenssl==20.0.1
# via opencage
pyserial==3.5
# via aioax25
python-dateutil==2.8.1
# via pandas
python-engineio==4.2.1
# via python-socketio
python-socketio==5.4.0
# via flask-socketio
pytz==2021.1
# via
# -r requirements.in
# pandas
pyyaml==5.4.1
# via -r requirements.in
requests==2.25.1
requests==2.26.0
# via
# -r requirements.in
# opencage
@@ -88,21 +109,26 @@ requests==2.25.1
# yfinance
shellingham==1.4.0
# via click-completion
six==1.15.0
signalslot==0.1.2
# via aioax25
six==1.16.0
# via
# -r requirements.in
# click-completion
# eventlet
# imapclient
# opencage
# pyopenssl
# python-dateutil
# signalslot
thesmuggler==1.0.1
# via -r requirements.in
update-checker==0.18.0
# via -r requirements.in
urllib3==1.26.5
urllib3==1.26.6
# via requests
werkzeug==1.0.1
weakrefmethod==1.0.3
# via signalslot
werkzeug==2.0.0
# via flask
yfinance==0.1.59
yfinance==0.1.63
# via -r requirements.in
+6
View File
@@ -30,6 +30,9 @@ def fake_packet(
class FakeBaseNoThreadsPlugin(plugin.APRSDPluginBase):
version = "1.0"
def setup(self):
self.enabled = True
def filter(self, packet):
return None
@@ -48,6 +51,9 @@ class FakeThread(threads.APRSDThread):
class FakeBaseThreadsPlugin(plugin.APRSDPluginBase):
version = "1.0"
def setup(self):
self.enabled = True
def filter(self, packet):
return None
+8 -8
View File
@@ -5,21 +5,21 @@ from aprsd.plugins import email
class TestEmail(unittest.TestCase):
def test_get_email_from_shortcut(self):
email.CONFIG = {"aprsd": {"email": {"shortcuts": {}}}}
config = {"aprsd": {"email": {"shortcuts": {}}}}
email_address = "something@something.com"
addr = f"-{email_address}"
actual = email.get_email_from_shortcut(addr)
actual = email.get_email_from_shortcut(config, addr)
self.assertEqual(addr, actual)
email.CONFIG = {"aprsd": {"email": {"nothing": "nothing"}}}
actual = email.get_email_from_shortcut(addr)
config = {"aprsd": {"email": {"nothing": "nothing"}}}
actual = email.get_email_from_shortcut(config, addr)
self.assertEqual(addr, actual)
email.CONFIG = {"aprsd": {"email": {"shortcuts": {"not_used": "empty"}}}}
actual = email.get_email_from_shortcut(addr)
config = {"aprsd": {"email": {"shortcuts": {"not_used": "empty"}}}}
actual = email.get_email_from_shortcut(config, addr)
self.assertEqual(addr, actual)
email.CONFIG = {"aprsd": {"email": {"shortcuts": {"-wb": email_address}}}}
config = {"aprsd": {"email": {"shortcuts": {"-wb": email_address}}}}
short = "-wb"
actual = email.get_email_from_shortcut(short)
actual = email.get_email_from_shortcut(config, short)
self.assertEqual(email_address, actual)
+6 -1
View File
@@ -6,9 +6,14 @@ from aprsd import messaging
class TestMessageTrack(unittest.TestCase):
def setUp(self) -> None:
config = {}
messaging.MsgTrack(config=config)
def _clean_track(self):
track = messaging.MsgTrack()
track.track = {}
track.data = {}
track.total_messages_tracked = 0
return track
+6 -3
View File
@@ -4,7 +4,7 @@ from unittest import mock
import pytz
import aprsd
from aprsd import messaging, packets, stats, utils
from aprsd import config, messaging, packets, stats
from aprsd.fuzzyclock import fuzzy
from aprsd.plugins import fortune as fortune_plugin
from aprsd.plugins import ping as ping_plugin
@@ -19,11 +19,14 @@ class TestPlugin(unittest.TestCase):
def setUp(self):
self.fromcall = fake.FAKE_FROM_CALLSIGN
self.ack = 1
self.config = utils.DEFAULT_CONFIG_DICT
self.config = config.DEFAULT_CONFIG_DICT
self.config["ham"]["callsign"] = self.fromcall
self.config["aprs"]["login"] = fake.FAKE_TO_CALLSIGN
# Inintialize the stats object with the config
stats.APRSDStats(self.config)
packets.WatchList(config=self.config)
packets.SeenList(config=self.config)
messaging.MsgTrack(config=self.config)
@mock.patch.object(fake.FakeBaseNoThreadsPlugin, "process")
def test_base_plugin_no_threads(self, mock_process):
@@ -159,7 +162,7 @@ class TestQueryPlugin(TestPlugin):
@mock.patch("aprsd.messaging.MsgTrack.restart_delayed")
def test_query_restart_delayed(self, mock_restart):
track = messaging.MsgTrack()
track.track = {}
track.data = {}
packet = fake.fake_packet(message="!4")
query = query_plugin.QueryPlugin(self.config)