1
0
mirror of https://github.com/craigerl/aprsd.git synced 2026-08-19 01:53:58 -04:00

Compare commits

..

87 Commits

Author SHA1 Message Date
hemna 2089b2575e v2.6.1 2022-12-16 15:56:48 -05:00
hemna 9571b0bb38 Fixed position report for webchat beacon
With more testing of the webchat beaconing, found a problem
with the packet format for the beacon.  This patch fixes the
packet format of the beacon.

Also added a timeout when trying to get the GPS location in the browser,
otherwise it could never come back.
2022-12-16 12:01:35 -05:00
hemna 87cbcaa47f Try and fix broken 32bit qemu builds on 64bit system
This patch adds a 'fix' for trying to build on armv7 32bit system
from a 64bit system.  qemu seems broken in this case.
2022-12-15 13:05:22 -05:00
hemna 19e5cfa9cc Add unit tests for webchat 2022-12-14 08:26:12 -05:00
hemna 24b16a29e8 Merge pull request #105 from craigerl/collections-fix
Collections fix
2022-12-13 09:55:02 -05:00
hemna 321c5a2c25 remove armv7 build RUST sucks 2022-12-13 08:37:31 -05:00
hemna 9d19502dd8 Fix for Collections change in 3.10
python 3.10 moved a module in the collections package
breaking backwards compatibility.  this patch puts a fix in
to account for it.
2022-12-12 20:45:19 -05:00
hemna a6015adecc Update workflow again
to include tag latest
2022-12-12 19:49:51 -05:00
hemna 4fe99c35b5 Update Dockerfile to 22.04 2022-12-12 19:37:55 -05:00
hemna c1db238719 Update Dockerfile and build.sh
This fixes a problem with the github workflow
2022-12-12 19:30:30 -05:00
hemna 40f23dcb48 Update workflow 2022-12-12 14:56:35 -05:00
hemna 5891c71483 Prep for 2.6.0 release 2022-12-12 14:45:35 -05:00
hemna 68472b0d84 Update requirements 2022-12-12 13:07:45 -05:00
hemna 935f820271 Removed Makefile comment. 2022-12-07 14:23:35 -05:00
hemna 576301ca20 Update Makefile for dev vs. run environments
This patch updates the Makefile to allow for creating
development vs runtime python virtual environments.

If you only want to run aprsd commands
make run

If you want to work on aprsd code
make dev
2022-12-07 14:19:42 -05:00
hemna 6d34d9c514 Added pyopenssl for https for webchat
In order for webchat to support fetching the GPS location in the
browser, the conenction from the browser needs to be https://
2022-12-07 14:03:25 -05:00
hemna deeee71f8f Merge pull request #103 from craigerl/user-agents
change from device-detector to user-agents
2022-12-07 13:59:26 -05:00
hemna f2b1ad35f9 change from device-detector to user-agents
the device detector was taking 1 minute on a raspi to parse out the
user-agent string from the browser.  user-agents takes 2 seconds,
which still isn't great, but 'doable' for the webchat interface.
2022-12-07 13:40:08 -05:00
hemna 1e65af2dea Merge pull request #102 from craigerl/remove-twine
Remove twine from dev-requirements
2022-12-05 17:10:27 -05:00
hemna 83370689b9 Remove twine from dev-requirements
twine is only used for building a distribution and uploading
to pypi.  Unfortunately it has a dependency that pulls in
cryptography which is painful on rpi systems as it requires
the latest version of rustc and cargo.
2022-12-05 16:13:39 -05:00
hemna e4f93a2ab4 Update to latest Makefile.venv
This patch updates the Makefile.venv to the latest upstream.
2022-12-02 17:08:10 -05:00
hemna acecba27e8 Merge pull request #101 from craigerl/webchat_mobile
Add support for mobile browsers for webchat
2022-12-02 17:06:17 -05:00
hemna 51b80cd4ea Refactored threads a bit
This patch refactors the rx threads a bit to reuse some code
responsible for processing acks when packets are received.

This also eliminates a custom thread in the webchat command for
processing received packets now that there is common code in the base
classes.
2022-12-02 16:26:48 -05:00
hemna 480094b0d4 Mark packets as acked in MsgTracker
This patch updates webchat to track the msgs recieved as tracked
and acked, so the TX thread can stop trying to send.
2022-12-02 14:58:32 -05:00
hemna 726c8f4f2f remove dev setting for template 2022-12-02 14:20:52 -05:00
hemna ee96108324 Add GPS beacon to mobile page
This patch adds the GPS beacon button to the mobile layout.
2022-11-30 15:17:28 -05:00
hemna 5067f745ca Allow werkzeug for admin interface.
This patch enables werkzeug for socketio for the admin interface
2022-11-30 14:31:44 -05:00
hemna 98fe9daac5 Allow werkzeug for admin interface.
This patch enables werkzeug for socketio for the admin interface
2022-11-30 14:28:31 -05:00
hemna f9e7195e25 Add support for mobile browsers for webchat
This patch adds initial support for changing the UI for webchat
based if the browser is on a mobile device.
2022-11-30 14:14:51 -05:00
hemna 44696fbc56 Ignore callsign case while processing packets
This patch fixes an issue where aprsd was deciding if it was
supposed to process a packet destined for itself or not.  It was
making a case sensitive comparison.  This patch makes that comparison
case insensitive for the callsign itself.
2022-11-30 13:57:25 -05:00
hemna 78329f79f4 Merge pull request #100 from craigerl/webchat_gps
Send GPS Beacon from webchat interface
2022-11-30 11:25:34 -05:00
hemna 5add0f958d remove linux/arm/v7 for official builds for now 2022-11-26 18:34:32 -05:00
hemna d40927d1c3 added workflow for building specific version 2022-11-26 18:28:14 -05:00
hemna d5e56b553e Allow passing in version to the Dockerfile
This patch allows setting the version from pypi.org to use
when building the container.

Currently defaults to 2.5.9.
2022-11-26 18:23:07 -05:00
hemna 1a1d00242b Send GPS Beacon from webchat interface
This patchset allow getting the GPS coordinates from the browser's
geolocation API (which can be denied by user), then send's the GPS
coordinates to aprsd via socketio and then aprsd sends a beacon.

This allows the APRS network to know the location of the person running
the webchat app via browser so packets can get routed back to it.
2022-11-25 13:25:09 -05:00
hemna 19f804bf68 Merge pull request #99 from craigerl/remove-email-validation
Remove email validation
2022-11-25 11:38:13 -05:00
hemna 4111d16aaf specify Dockerfile-dev 2022-11-25 11:21:10 -05:00
hemna d1a0a988f2 Fixed build.sh
This patch fixes passing the branch to the build script
2022-11-25 10:05:16 -05:00
hemna d9b39734e6 Build on the source not released aprsd 2022-11-25 10:03:34 -05:00
hemna d4bf0f1e3c Remove email validation
The package/library being used for email validation is basically
defunct now.
2022-11-25 09:29:41 -05:00
hemna 117f81f55f Add support for building linux/arm/v7
This patch adds support for the github workflow for building
the raspi architecture
2022-11-24 09:44:32 -05:00
hemna b41e4a9ef3 Remove python 3.7 from docker build github
This patch removes the testing of python 3.7 during the
github action workflow for building the docker image
2022-11-23 15:41:27 -05:00
hemna e66dc344b8 Merge pull request #91 from craigerl/small_refactor
Small refactor
2022-11-23 13:33:23 -05:00
hemna 5acddbd466 Fixed failing unit tests
This patch re-adds in the pytz lib for the generic time plugins.
2022-11-23 13:28:38 -05:00
hemna 17e784629e change github workflow
remove python 3.7
2022-11-23 13:06:33 -05:00
hemna 528bdb99e7 Removed TimeOpenCageDataPlugin
This patch removes the TimeOpenCageDataPlugin as it's been superceded
by the aprsd-timeopencage-plugin
2022-11-23 13:02:46 -05:00
hemna fc1ca52593 Dump config with aprsd dev test-plugin
This patch adds the dumping of the config read for the
aprsd dev test-plugin command
2022-11-23 13:02:46 -05:00
hemna 075078b520 Updated requirements 2022-11-23 13:02:44 -05:00
hemna 7d970cbe70 Got webchat working with KISS tcp
This patch reworks the KISS client to get rid of
aioax25 as it was too difficult to work with due to
heavy use of asyncio.

Switched to the kiss3 pypi library.
2022-11-23 13:01:43 -05:00
hemna d717a22717 Added click auto_envvar_prefix
This allows setting environment variables that are
prefixed with APRSD_
2022-11-23 13:01:06 -05:00
hemna 9b0c626b59 Update aprsd thread base class to use queue
This patch updates the main aprsd threads class to use
a shared queue to notify all aprsd thread classes they need
to exit.  This ensures any closing down of sockets, etc happens from
inside the context of the thread itself, not the MainThread that
calls stop.
2022-11-23 13:01:06 -05:00
hemna 967959e7b3 Update packets to use wrapt
This patch updates the aprsd/packets.py to use wrapt for it's method
lock synchornization.
2022-11-23 13:01:06 -05:00
hemna e5f60b5ce1 Add remving existing requirements
This patch updates the Makefile to do an rm on the requirements.txt
when updating the requirements files.
2022-11-23 13:01:06 -05:00
hemna 2ce50d8861 Try sending raw APRSFrames to aioax25
This seems to work sending out, but still getting
third-party dropped packets as response from the local repeater.
2022-11-23 13:01:06 -05:00
hemna ad79ed1261 Use new aprsd.callsign as the main callsign
This patch changes how aprsd identifies itself when connected to
any client, which is not relying on the login for each client.
There are 3 supported clients currently
aprsis,
tcpkiss
serialkiss.

Each client has their own potential login/callsign to connect
to the remote.  This patch tells aprsd to use the new config option
aprsd.callsign as a means to identify itself.  It will accept
packets as <aprsd.callsign> and reply as <aprsd.callsign> regardless
of which client object is being used to connect to the remote.

Note: this breaks backwards compatibility.  This patch now requires
the new config option
aprsd:
  callsign: <callsign>
2022-11-23 13:01:01 -05:00
hemna 5f28788180 Fixed access to threads refactor 2022-11-23 13:00:37 -05:00
hemna 585d55f10d Added webchat command
This patch adds the new aprsd webchat command which shows
a new webpage that allows you to aprsd chat with multiple
callsigns
2022-11-23 13:00:36 -05:00
hemna 1ccb2f7695 Moved log.py to logging
Also renamed logging/logging.py to logging/rich.py
2022-11-23 13:00:36 -05:00
hemna a62843920a Moved trace.py to utils
This patch moves trace.py to the utils directory
2022-11-23 13:00:36 -05:00
hemna 29b84b453b Fixed pep8 errors 2022-11-23 13:00:36 -05:00
hemna 347a6d69f7 Refactored threads.py
This patch creates a threads directory and separates out
the contents of threads.py into separate files in the
threads directory to make it easier to find and maintain.
2022-11-23 13:00:36 -05:00
hemna bed060f1c5 Refactor utils to directory
This patch moves the utils.py to utils/__init__.py
and fuzzyclock.py to utils
and separates the ring_buffer to it's own file in utils
2022-11-23 13:00:36 -05:00
hemna ab6583666f remove arm build for now 2022-11-04 14:06:03 -04:00
hemna 3580425ca3 Added rustc and cargo to Dockerfile
This is an attempt to fix the failing docker image build for
linux/arm/v7
2022-11-04 11:34:27 -04:00
hemna 358aa59042 remove linux/arm/v6 from docker platform build 2022-11-04 10:41:25 -04:00
hemna 9671dacb1c Only tag master build as master 2022-11-04 10:32:22 -04:00
hemna f9d3bc433f Remove docker build from test
This patch removes the container build from the main python.yml
github action that is only supposed to test tox results for commits
2022-11-04 10:30:34 -04:00
hemna 1383352e75 create master-build.yml
This patch adds the tox and docker image build for the latest container image on every push to master branch
2022-11-04 10:27:43 -04:00
hemna b50f343440 Added container build action 2022-11-04 09:04:14 -04:00
hemna 4c7c90b947 Merge pull request #98 from ranguli/ranguli-patch-1
Update docs on using Docker
2022-11-02 10:47:04 -04:00
ranguli bb09296efa Update docs on using Docker 2022-11-01 21:49:58 -02:30
hemna 7db2242060 Update dev-requirements pip-tools
This patch updates the pip-tools version to prevent the bug when
trying to run make update-requirements failing.
2022-11-01 14:16:24 -04:00
hemna 61655a0a85 Merge pull request #89 from wildeyedskies/update-eventlet
Bump dependencies to fix python 3.10
2022-11-01 14:02:13 -04:00
hemna fdc8bfafc0 Merge pull request #96 from ranguli/fix-pypi-scraping
Fix #92 (PyPI scraping)
2022-11-01 14:01:37 -04:00
hemna 0e5f7aa211 Merge pull request #93 from ranguli/fix-readme-formatting
README formatting fixes
2022-11-01 13:59:28 -04:00
hemna c16886263f Merge pull request #94 from ranguli/fix-exception-typo
Fix typo on exception
2022-11-01 13:51:41 -04:00
hemna eb4b67d9b8 Merge pull request #97 from ranguli/patch-1
Fix plugins not installing via docker-compose
2022-11-01 13:47:59 -04:00
ranguli 389304c3f2 Fix typo in docker-compose.yml 2022-10-28 10:35:39 -02:30
ranguli 9ffd320353 Fix PyPI scraping 2022-10-27 12:33:31 -02:30
hemna 74e4e2c4f5 Merge pull request #95 from ranguli/patch-1 2022-10-26 22:19:20 -04:00
ranguli b1db08a08c Allow web interface when running in Docker 2022-10-26 20:03:42 -02:30
ranguli cc2918377e Fix typo on exception 2022-10-26 16:46:50 -02:30
ranguli f339ee3ebf README formatting fixes 2022-10-26 16:01:51 -02:30
Zoe Moore 9d39b030fb Bump dependencies to fix python 3.10 2022-05-31 15:53:26 -07:00
hemna 1c052a63c0 Fixed up config option checking for KISS
This patch updates the config option checking for
required fields in the config yaml file.  Specifically
for the existence of the aprsd: section
and the required fields for the 3 supported client types
apris,
kiss serial,
kiss tcp
2022-02-21 16:04:33 -05:00
hemna e739441268 Fix logging issue with log messages
This patch changes the base Message class to
ensure that all printing of the message class only
outputs the message in the truncated and bad word filtering
enabled in the log.
2022-02-11 10:03:02 -05:00
hemna 03a20ebb5c for 2.5.9 2022-01-26 14:59:46 -05:00
98 changed files with 3230 additions and 1243 deletions
+62
View File
@@ -0,0 +1,62 @@
name: Test and Build Latest Container Image
on:
schedule:
- cron: "0 10 * * *"
push:
branches:
- "**"
tags:
- "v*.*.*"
pull_request:
branches:
- "master"
jobs:
tox:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Test with tox
run: tox
build:
needs: tox
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Get Branch Name
id: branch-name
uses: tj-actions/branch-names@v6
- name: Setup QEMU
uses: docker/setup-qemu-action@v2
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker HUB
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build the Docker image
uses: docker/build-push-action@v3
with:
context: "{{defaultContext}}:docker"
platforms: linux/amd64,linux/arm64
file: ./Dockerfile-dev
build-args: |
BRANCH=${{ steps.branch-name.outputs.current_branch }}
BUILDX_QEMU_ENV=true
push: true
tags: |
hemna6969/aprsd:${{ steps.branch-name.outputs.current_branch }}
+2 -2
View File
@@ -1,4 +1,4 @@
name: python
name: TOX Test
on: [push, pull_request]
@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8, 3.9]
python-version: ["3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
+50
View File
@@ -0,0 +1,50 @@
name: Build specific version
on:
workflow_dispatch:
inputs:
aprsd_version:
required: true
options:
- 2.5.9
- 2.6.0
logLevel:
description: 'Log level'
required: true
default: 'warning'
type: choice
options:
- info
- warning
- debug
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Get Branch Name
id: branch-name
uses: tj-actions/branch-names@v6
- name: Setup QEMU
uses: docker/setup-qemu-action@v2
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker HUB
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build the Docker image
uses: docker/build-push-action@v3
with:
context: "{{defaultContext}}:docker"
platforms: linux/amd64,linux/arm64,linux/arm/v7
file: ./Dockerfile
build-args: |
VERSION=${{ inputs.aprsd_version }}
BUILDX_QEMU_ENV=true
push: true
tags: |
hemna6969/aprsd:v${{ inputs.aprsd_version }}
hemna6969/aprsd:latest
+88
View File
@@ -1,9 +1,97 @@
CHANGES
=======
v2.6.1
------
* Fixed position report for webchat beacon
* Try and fix broken 32bit qemu builds on 64bit system
* Add unit tests for webchat
* remove armv7 build RUST sucks
* Fix for Collections change in 3.10
v2.6.0
------
* Update workflow again
* Update Dockerfile to 22.04
* Update Dockerfile and build.sh
* Update workflow
* Prep for 2.6.0 release
* Update requirements
* Removed Makefile comment
* Update Makefile for dev vs. run environments
* Added pyopenssl for https for webchat
* change from device-detector to user-agents
* Remove twine from dev-requirements
* Update to latest Makefile.venv
* Refactored threads a bit
* Mark packets as acked in MsgTracker
* remove dev setting for template
* Add GPS beacon to mobile page
* Allow werkzeug for admin interface
* Allow werkzeug for admin interface
* Add support for mobile browsers for webchat
* Ignore callsign case while processing packets
* remove linux/arm/v7 for official builds for now
* added workflow for building specific version
* Allow passing in version to the Dockerfile
* Send GPS Beacon from webchat interface
* specify Dockerfile-dev
* Fixed build.sh
* Build on the source not released aprsd
* Remove email validation
* Add support for building linux/arm/v7
* Remove python 3.7 from docker build github
* Fixed failing unit tests
* change github workflow
* Removed TimeOpenCageDataPlugin
* Dump config with aprsd dev test-plugin
* Updated requirements
* Got webchat working with KISS tcp
* Added click auto\_envvar\_prefix
* Update aprsd thread base class to use queue
* Update packets to use wrapt
* Add remving existing requirements
* Try sending raw APRSFrames to aioax25
* Use new aprsd.callsign as the main callsign
* Fixed access to threads refactor
* Added webchat command
* Moved log.py to logging
* Moved trace.py to utils
* Fixed pep8 errors
* Refactored threads.py
* Refactor utils to directory
* remove arm build for now
* Added rustc and cargo to Dockerfile
* remove linux/arm/v6 from docker platform build
* Only tag master build as master
* Remove docker build from test
* create master-build.yml
* Added container build action
* Update docs on using Docker
* Update dev-requirements pip-tools
* Fix typo in docker-compose.yml
* Fix PyPI scraping
* Allow web interface when running in Docker
* Fix typo on exception
* README formatting fixes
* Bump dependencies to fix python 3.10
* Fixed up config option checking for KISS
* Fix logging issue with log messages
* for 2.5.9
v2.5.9
------
* FIX: logging exceptions
* Updated build and run for rich lib
* update build for 2.5.8
v2.5.8
------
* For 2.5.8
* Removed debug code
* Updated list-plugins
* Renamed virtualenv dir to .aprsd-venv
+21 -10
View File
@@ -1,4 +1,3 @@
REQUIREMENTS_TXT ?= requirements.txt dev-requirements.txt
WORKDIR?=.
VENVDIR ?= $(WORKDIR)/.aprsd-venv
@@ -9,23 +8,26 @@ VENVDIR ?= $(WORKDIR)/.aprsd-venv
include Makefile.venv
Makefile.venv:
curl \
-o Makefile.fetched \
-L "https://github.com/sio/Makefile.venv/raw/v2020.08.14/Makefile.venv"
echo "5afbcf51a82f629cd65ff23185acde90ebe4dec889ef80bbdc12562fbd0b2611 *Makefile.fetched" \
| sha256sum --check - \
&& mv Makefile.fetched Makefile.venv
-o Makefile.fetched \
-L "https://github.com/sio/Makefile.venv/raw/v2022.07.20/Makefile.venv"
echo "147b164f0cbbbe4a2740dcca6c9adb6e9d8d15b895be3998697aa6a821a277d8 *Makefile.fetched" \
| sha256sum --check - \
&& mv Makefile.fetched Makefile.venv
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}'
dev: venv ## Create the virtualenv with all the requirements installed
dev: REQUIREMENTS_TXT = requirements.txt dev-requirements.txt
dev: venv ## Create a python virtual environment for development of aprsd
run: venv ## Create a virtual environment for running aprsd commands
docs: build
cp README.rst docs/readme.rst
cp Changelog docs/changelog.rst
tox -edocs
clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
clean: clean-build clean-pyc clean-test clean-dev ## remove all build, test, coverage and Python artifacts
clean-build: ## remove build artifacts
rm -fr build/
@@ -46,10 +48,15 @@ clean-test: ## remove test and coverage artifacts
rm -fr htmlcov/
rm -fr .pytest_cache
clean-dev:
rm -rf $(VENVDIR)
rm Makefile.venv
test: dev ## Run all the tox tests
tox -p all
build: test ## Make the build artifact prior to doing an upload
$(VENV)/pip install twine
$(VENV)/python3 setup.py sdist bdist_wheel
$(VENV)/twine check dist/*
@@ -73,5 +80,9 @@ docker-dev: test ## Make a development docker container tagged with hemna6969/a
docker build -t hemna6969/aprsd:master -f docker/Dockerfile-dev docker
update-requirements: dev ## Update the requirements.txt and dev-requirements.txt files
$(VENV)/pip-compile requirements.in
$(VENV)/pip-compile dev-requirements.in
rm requirements.txt
rm dev-requirements.txt
touch requirements.txt
touch dev-requirements.txt
$(VENV)/pip-compile --resolver backtracking --annotation-style line requirements.in
$(VENV)/pip-compile --resolver backtracking --annotation-style line dev-requirements.in
+76 -61
View File
@@ -28,7 +28,7 @@ Please `read the docs`_ to learn more!
APRSD Overview Diagram
----------------------
======================
.. image:: https://raw.githubusercontent.com/craigerl/aprsd/master/docs/_static/aprsd_overview.svg?sanitize=true
@@ -46,7 +46,7 @@ callsigns to look out for. The watch list can notify you when a HAM callsign
in the list is seen and now available to message on the APRS network.
Current List of built-in plugins:
Current list of built-in plugins
======================================
::
@@ -86,18 +86,20 @@ Current List of built-in plugins:
│ 📂 aprsd-slack-plugin │ Amateur radio APRS daemon which listens for messages and responds │ 1.0.4 │ Jan 15, 2021 │ No │
└──────────────────────────┴────────────────────────────────────────────────────────────────────┴─────────┴──────────────┴────────────┘
installation:
Installation
=============
pip install aprsd
To install ``aprsd``, use Pip:
Example usage:
``pip install aprsd``
Example usage
==============
aprsd -h
``aprsd -h``
Help
====
----
::
└─[$] > aprsd -h
@@ -122,17 +124,15 @@ Help
Commands
========
--------
Configuration
=============
^^^^^^^^^^^^^
This command outputs a sample config yml formatted block that you can edit
and use to pass in to aprsd with -c. By default aprsd looks in ~/.config/aprsd/aprsd.yml
and use to pass in to ``aprsd`` with ``-c``. By default aprsd looks in ``~/.config/aprsd/aprsd.yml``
aprsd sample-config
``aprsd sample-config``
Output
======
::
└─> aprsd sample-config
@@ -237,7 +237,7 @@ Output
apiKey: APIKEYVALUE
server
======
^^^^^^
This is the main server command that will listen to APRS-IS servers and
look for incomming commands to the callsign configured in the config file
@@ -277,7 +277,7 @@ look for incomming commands to the callsign configured in the config file
send-message
============
^^^^^^^^^^^^
This command is typically used for development to send another aprsd instance
test messages
@@ -309,12 +309,8 @@ test messages
-h, --help Show this message and exit.
Example output:
===============
SEND EMAIL (radio to smtp server)
=================================
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
::
@@ -336,7 +332,7 @@ SEND EMAIL (radio to smtp server)
RECEIVE EMAIL (imap server to radio)
====================================
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
::
@@ -353,7 +349,7 @@ RECEIVE EMAIL (imap server to radio)
LOCATION
========
^^^^^^^^
::
@@ -383,56 +379,66 @@ AND... ping, fortune, time.....
Development
===========
* git clone git@github.com:craigerl/aprsd.git
* cd aprsd
* make
* ``git clone git@github.com:craigerl/aprsd.git``
* ``cd aprsd``
* ``make``
Workflow
========
--------
While working aprsd, The workflow is as follows
While working aprsd, The workflow is as follows:
* Checkout a new branch to work on by running
``git checkout -b mybranch``
* Make your changes to the code
* Run Tox with the following options:
- ``tox -epep8``
- ``tox -efmt``
- ``tox -p``
* Commit your changes. This will run the pre-commit hooks which does checks too
``git commit``
* checkout a new branch to work on
* git checkout -b mybranch
* Edit code
* run tox -epep8
* run tox -efmt
* run tox -p
* git commit ( This will run the pre-commit hooks which does checks too )
* Once you are done with all of your commits, then push up the branch to
github
* git push -u origin mybranch
github with:
``git push -u origin mybranch``
* Create a pull request from your branch so github tests can run and we can do
a code review.
Release
=======
-------
To do release to pypi:
* Tag release with
* Tag release with:
git tag -v1.XX -m "New release"
``git tag -v1.XX -m "New release"``
* push release tag up
* Push release tag:
git push origin master --tags
``git push origin master --tags``
* Do a test build and verify build is valid
* Do a test build and verify build is valid by running:
make build
``make build``
* Once twine is happy, upload release to pypi
* Once twine is happy, upload release to pypi:
make upload
``make upload``
Docker Container
================
----------------
Building
========
^^^^^^^^
There are 2 versions of the container Dockerfile that can be used.
The main Dockerfile, which is for building the official release container
@@ -441,29 +447,38 @@ which is used for building a container based off of a git branch of
the repo.
Official Build
==============
^^^^^^^^^^^^^^
docker build -t hemna6969/aprsd:latest .
``docker build -t hemna6969/aprsd:latest .``
Development Build
=================
^^^^^^^^^^^^^^^^^
docker build -t hemna6969/aprsd:latest -f Dockerfile-dev .
``docker build -t hemna6969/aprsd:latest -f Dockerfile-dev .``
Running the container
=====================
^^^^^^^^^^^^^^^^^^^^^
There is a docker-compose.yml file that can be used to run your container.
There are 2 volumes defined that can be used to store your configuration
and the plugins directory: /config and /plugins
There is a ``docker-compose.yml`` file in the ``docker/`` directory
that can be used to run your container. To provide the container
an ``aprsd.conf`` configuration file, change your
``docker-compose.yml`` as shown below:
If you want to install plugins at container start time, then use the
environment var in docker-compose.yml specified as APRS_PLUGINS
Provide a csv list of pypi installable plugins. Then make sure the plugin
python file is in your /plugins volume and the plugin will be installed at
container startup. The plugin may have dependencies that are required.
The plugin file should be copied to /plugins for loading by aprsd
::
volumes:
- $HOME/.config/aprsd:/config
To install plugins at container start time, pass in a list of
comma-separated list of plugins on PyPI using the ``APRSD_PLUGINS``
environment variable in the ``docker-compose.yml`` file. Note that
version constraints may also be provided. For example:
::
environment:
- APRSD_PLUGINS=aprsd-slack-plugin>=1.0.2,aprsd-twitter-plugin
.. badges
+2 -2
View File
@@ -68,9 +68,9 @@ def main():
# The commands themselves live in the cmds directory
from .cmds import ( # noqa
completion, dev, healthcheck, list_plugins, listen, send_message,
server,
server, webchat,
)
cli()
cli(auto_envvar_prefix="APRSD")
def signal_handler(sig, frame):
+6 -2
View File
@@ -4,7 +4,8 @@ import typing as t
import click
from aprsd import config as aprsd_config
from aprsd import log
from aprsd.logging import log
from aprsd.utils import trace
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
@@ -55,9 +56,12 @@ def process_standard_options(f: F) -> F:
ctx.obj["quiet"] = kwargs["quiet"]
ctx.obj["config"] = aprsd_config.parse_config(kwargs["config_file"])
log.setup_logging(
ctx.obj["config"], ctx.obj["loglevel"],
ctx.obj["config"],
ctx.obj["loglevel"],
ctx.obj["quiet"],
)
if ctx.obj["config"]["aprsd"].get("trace", False):
trace.setup_tracing(["method", "api"])
del kwargs["loglevel"]
del kwargs["config_file"]
+64 -7
View File
@@ -5,8 +5,10 @@ import time
import aprslib
from aprslib.exceptions import LoginError
from aprsd import trace
from aprsd import config as aprsd_config
from aprsd import exception
from aprsd.clients import aprsis, kiss
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")
@@ -50,7 +52,8 @@ class Client:
def reset(self):
"""Call this to force a rebuild/reconnect."""
del self._client
if self._client:
del self._client
@abc.abstractmethod
def setup_connection(self):
@@ -76,7 +79,21 @@ class APRSISClient(Client):
@staticmethod
def is_enabled(config):
# Defaults to True if the enabled flag is non existent
return config["aprs"].get("enabled", True)
try:
return config["aprs"].get("enabled", True)
except KeyError:
return False
@staticmethod
def is_configured(config):
if APRSISClient.is_enabled(config):
# Ensure that the config vars are correctly set
config.check_option("aprs.login")
config.check_option("aprs.password")
config.check_option("aprs.host")
return True
return True
@staticmethod
def transport(config):
@@ -114,6 +131,7 @@ class APRSISClient(Client):
backoff = backoff * 2
continue
LOG.debug(f"Logging in to APRS-IS with user '{user}'")
self._client = aprs_client
return aprs_client
@@ -133,6 +151,26 @@ class KISSClient(Client):
return False
@staticmethod
def is_configured(config):
# Ensure that the config vars are correctly set
if KISSClient.is_enabled(config):
config.check_option(
"aprsd.callsign",
default_fail=aprsd_config.DEFAULT_CONFIG_DICT["aprsd"]["callsign"],
)
transport = KISSClient.transport(config)
if transport == TRANSPORT_SERIALKISS:
config.check_option("kiss.serial")
config.check_option("kiss.serial.device")
elif transport == TRANSPORT_TCPKISS:
config.check_option("kiss.tcp")
config.check_option("kiss.tcp.host")
config.check_option("kiss.tcp.port")
return True
return True
@staticmethod
def transport(config):
if config.get("kiss.serial.enabled", default=False):
@@ -157,8 +195,8 @@ class KISSClient(Client):
@trace.trace
def setup_connection(self):
ax25client = kiss.Aioax25Client(self.config)
return ax25client
client = kiss.KISS3Client(self.config)
return client
class ClientFactory:
@@ -185,7 +223,7 @@ class ClientFactory:
elif KISSClient.is_enabled(self.config):
key = KISSClient.transport(self.config)
LOG.debug(f"GET client {key}")
LOG.debug(f"GET client '{key}'")
builder = self._builders.get(key)
if not builder:
raise ValueError(key)
@@ -195,7 +233,26 @@ class ClientFactory:
"""Make sure at least one client is enabled."""
enabled = False
for key in self._builders.keys():
enabled |= self._builders[key].is_enabled(self.config)
try:
enabled |= self._builders[key].is_enabled(self.config)
except KeyError:
pass
return enabled
def is_client_configured(self):
enabled = False
for key in self._builders.keys():
try:
enabled |= self._builders[key].is_configured(self.config)
except KeyError:
pass
except exception.MissingConfigOptionException as ex:
LOG.error(ex.message)
return False
except exception.ConfigOptionBogusDefaultException as ex:
LOG.error(ex.message)
return False
return enabled
+22
View File
@@ -1,5 +1,7 @@
import logging
import select
import socket
import threading
import aprslib
from aprslib import is_py3
@@ -7,6 +9,7 @@ from aprslib.exceptions import (
ConnectionDrop, ConnectionError, GenericError, LoginError, ParseError,
UnknownFormat,
)
import wrapt
import aprsd
from aprsd import stats
@@ -23,11 +26,30 @@ class Aprsdis(aprslib.IS):
# timeout in seconds
select_timeout = 1
lock = threading.Lock()
def stop(self):
self.thread_stop = True
LOG.info("Shutdown Aprsdis client.")
def is_socket_closed(self, sock: socket.socket) -> bool:
try:
# this will try to read bytes without blocking and also without removing them from buffer (peek only)
data = sock.recv(16, socket.MSG_DONTWAIT | socket.MSG_PEEK)
if len(data) == 0:
return True
except BlockingIOError:
return False # socket is open and reading from it would block
except ConnectionResetError:
return True # socket was closed for some other reason
except Exception:
self.logger.exception(
"unexpected exception when checking if a socket is closed",
)
return False
return False
@wrapt.synchronized(lock)
def send(self, msg):
"""Send an APRS Message object."""
line = str(msg)
+75 -50
View File
@@ -1,20 +1,19 @@
import asyncio
import logging
from aioax25 import interface
from aioax25 import kiss as kiss
from aioax25.aprs import APRSInterface
import aprslib
from ax253 import Frame
import kiss
from aprsd import messaging
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")
class Aioax25Client:
class KISS3Client:
def __init__(self, config):
self.config = config
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
self.loop = asyncio.get_event_loop()
self.setup()
def setup(self):
@@ -24,71 +23,97 @@ class Aioax25Client:
False,
):
LOG.debug(
"Setting up Serial KISS connection to {}".format(
"KISS({}) Serial connection to {}".format(
kiss.__version__,
self.config["kiss"]["serial"]["device"],
),
)
self.kissdev = kiss.SerialKISSDevice(
device=self.config["kiss"]["serial"]["device"],
baudrate=self.config["kiss"]["serial"].get("baudrate", 9600),
loop=self.loop,
self.kiss = kiss.SerialKISS(
port=self.config["kiss"]["serial"]["device"],
speed=self.config["kiss"]["serial"].get("baudrate", 9600),
strip_df_start=True,
)
elif "tcp" in self.config["kiss"] and self.config["kiss"]["tcp"].get(
"enabled",
False,
):
LOG.debug(
"Setting up KISSTCP Connection to {}:{}".format(
"KISS({}) TCP Connection to {}:{}".format(
kiss.__version__,
self.config["kiss"]["tcp"]["host"],
self.config["kiss"]["tcp"]["port"],
),
)
self.kissdev = kiss.TCPKISSDevice(
self.config["kiss"]["tcp"]["host"],
self.config["kiss"]["tcp"]["port"],
loop=self.loop,
log=LOG,
self.kiss = kiss.TCPKISS(
host=self.config["kiss"]["tcp"]["host"],
port=int(self.config["kiss"]["tcp"]["port"]),
strip_df_start=True,
)
self.kissdev.open()
self.kissport0 = self.kissdev[0]
LOG.debug("Creating AX25Interface")
self.ax25int = interface.AX25Interface(kissport=self.kissport0, loop=self.loop)
LOG.debug("Creating APRSInterface")
self.aprsint = APRSInterface(
ax25int=self.ax25int,
mycall=self.config["kiss"]["callsign"],
log=LOG,
)
LOG.debug("Starting KISS interface connection")
self.kiss.start()
@trace.trace
def stop(self):
LOG.debug(self.kissdev)
self.kissdev._close()
self.loop.stop()
try:
self.kiss.stop()
self.kiss.loop.call_soon_threadsafe(
self.kiss.protocol.transport.close,
)
except Exception as ex:
LOG.exception(ex)
def set_filter(self, filter):
# This does nothing right now.
pass
def consumer(self, callback, blocking=True, immortal=False, raw=False):
callsign = self.config["kiss"]["callsign"]
call = callsign.split("-")
if len(call) > 1:
callsign = call[0]
ssid = int(call[1])
else:
ssid = 0
self.aprsint.bind(callback=callback, callsign=callsign, ssid=ssid, regex=False)
self.loop.run_forever()
def parse_frame(self, frame_bytes):
frame = Frame.from_bytes(frame_bytes)
# Now parse it with aprslib
packet = aprslib.parse(str(frame))
kwargs = {
"frame": str(frame),
"packet": packet,
}
self._parse_callback(**kwargs)
def consumer(self, callback, blocking=False, immortal=False, raw=False):
LOG.debug("Start blocking KISS consumer")
self._parse_callback = callback
self.kiss.read(callback=self.parse_frame, min_frames=None)
LOG.debug("END blocking KISS consumer")
def send(self, msg):
"""Send an APRS Message object."""
payload = f"{msg._filter_for_send()}"
self.aprsint.send_message(
addressee=msg.tocall,
message=payload,
path=["WIDE1-1", "WIDE2-1"],
oneshot=True,
# payload = (':%-9s:%s' % (
# msg.tocall,
# payload
# )).encode('US-ASCII'),
# payload = str(msg).encode('US-ASCII')
payload = None
path = ["WIDE1-1", "WIDE2-1"]
if isinstance(msg, messaging.AckMessage):
msg_payload = f"ack{msg.id}"
elif isinstance(msg, messaging.RawMessage):
payload = msg.message.encode("US-ASCII")
path = ["WIDE2-1"]
else:
msg_payload = f"{msg.message}{{{str(msg.id)}"
if not payload:
payload = (
":{:<9}:{}".format(
msg.tocall,
msg_payload,
)
).encode("US-ASCII")
LOG.debug(f"Send '{payload}' TO KISS")
frame = Frame.ui(
destination=msg.tocall,
source=msg.fromcall,
path=path,
info=payload,
)
self.kiss.write(frame)
+10 -1
View File
@@ -8,8 +8,9 @@ import logging
import click
# local imports here
from aprsd import cli_helper, client, messaging, packets, plugin, stats, trace
from aprsd import cli_helper, client, messaging, packets, plugin, stats, utils
from aprsd.aprsd import cli
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")
@@ -69,6 +70,14 @@ def test_plugin(
"""Test an individual APRSD plugin given a python path."""
config = ctx.obj["config"]
flat_config = utils.flatten_dict(config)
LOG.info("Using CONFIG values:")
for x in flat_config:
if "password" in x or "aprsd.web.users.admin" in x:
LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX")
else:
LOG.info(f"{x} = {flat_config[x]}")
if not aprs_login:
if not config.exists("aprs.login"):
click.echo("Must set --aprs_login or APRS_LOGIN")
+2 -2
View File
@@ -174,7 +174,7 @@ def show_pypi_plugins(installed_plugins, console):
link = urljoin(api_url, snippet.get("href"))
package = re.sub(r"\s+", " ", snippet.select_one('span[class*="name"]').text.strip())
version = re.sub(r"\s+", " ", snippet.select_one('span[class*="version"]').text.strip())
released = re.sub(r"\s+", " ", snippet.select_one('span[class*="released"]').text.strip())
created = re.sub(r"\s+", " ", snippet.select_one('span[class*="created"]').text.strip())
description = re.sub(r"\s+", " ", snippet.select_one('p[class*="description"]').text.strip())
emoji = ":open_file_folder:"
@@ -189,7 +189,7 @@ def show_pypi_plugins(installed_plugins, console):
table.add_row(
f"[link={link}]{emoji}[/link] {package}",
description, version, released, installed,
description, version, created, installed,
)
console.print("\n")
+12 -8
View File
@@ -14,10 +14,9 @@ from rich.console import Console
# local imports here
import aprsd
from aprsd import (
cli_helper, client, messaging, packets, stats, threads, trace, utils,
)
from aprsd import cli_helper, client, messaging, packets, stats, threads, utils
from aprsd.aprsd import cli
from aprsd.utils import trace
# setup the global logger
@@ -140,19 +139,24 @@ def listen(
# Creates the client object
LOG.info("Creating client connection")
client.factory.create().client
aprs_client = client.factory.create().client
aprs_client = client.factory.create()
console.log(aprs_client)
LOG.debug(f"Filter by '{filter}'")
aprs_client.set_filter(filter)
aprs_client.client.set_filter(filter)
packets.PacketList(config=config)
keepalive = threads.KeepAliveThread(config=config)
keepalive.start()
while True:
try:
# This will register a packet consumer with aprslib
# When new packets come in the consumer will process
# the packet
with console.status("Listening for packets"):
aprs_client.consumer(rx_packet, raw=False)
# with console.status("Listening for packets"):
aprs_client.client.consumer(rx_packet, raw=False)
except aprslib.exceptions.ConnectionDrop:
LOG.error("Connection dropped, reconnecting")
time.sleep(5)
+8 -4
View File
@@ -7,10 +7,11 @@ import click
import aprsd
from aprsd import (
cli_helper, client, flask, messaging, packets, plugin, stats, threads,
trace, utils,
utils,
)
from aprsd import aprsd as aprsd_main
from aprsd.aprsd import cli
from aprsd.threads import rx
LOG = logging.getLogger("APRSD")
@@ -58,8 +59,6 @@ def server(ctx, flush):
else:
LOG.info(f"{x} = {flat_config[x]}")
if config["aprsd"].get("trace", False):
trace.setup_tracing(["method", "api"])
stats.APRSDStats(config)
# Initialize the client factory and create
@@ -70,6 +69,10 @@ def server(ctx, flush):
LOG.error("No Clients are enabled in config.")
sys.exit(-1)
if not client.factory.is_client_configured():
LOG.error("APRS client is not properly configured in config file.")
sys.exit(-1)
# Creates the client object
LOG.info("Creating client connection")
client.factory.create().client
@@ -93,7 +96,7 @@ def server(ctx, flush):
plugin_manager = plugin.PluginManager(config)
plugin_manager.setup_plugins()
rx_thread = threads.APRSDRXThread(
rx_thread = rx.APRSDPluginRXThread(
msg_queues=threads.msg_queues,
config=config,
)
@@ -111,6 +114,7 @@ def server(ctx, flush):
(socketio, app) = flask.init_flask(config, loglevel, quiet)
socketio.run(
app,
allow_unsafe_werkzeug=True,
host=config["aprsd"]["web"]["host"],
port=config["aprsd"]["web"]["port"],
)
+560
View File
@@ -0,0 +1,560 @@
import datetime
import json
import logging
from logging.handlers import RotatingFileHandler
import signal
import sys
import threading
import time
from aprslib import util as aprslib_util
import click
import flask
from flask import request
from flask.logging import default_handler
import flask_classful
from flask_httpauth import HTTPBasicAuth
from flask_socketio import Namespace, SocketIO
from user_agents import parse as ua_parse
from werkzeug.security import check_password_hash, generate_password_hash
import wrapt
import aprsd
from aprsd import cli_helper, client
from aprsd import config as aprsd_config
from aprsd import messaging, packets, stats, threads, utils
from aprsd.aprsd import cli
from aprsd.logging import rich as aprsd_logging
from aprsd.threads import rx
from aprsd.utils import objectstore, trace
LOG = logging.getLogger("APRSD")
auth = HTTPBasicAuth()
users = None
def signal_handler(sig, frame):
click.echo("signal_handler: called")
LOG.info(
f"Ctrl+C, Sending all threads({len(threads.APRSDThreadList())}) exit! "
f"Can take up to 10 seconds {datetime.datetime.now()}",
)
threads.APRSDThreadList().stop_all()
if "subprocess" not in str(frame):
time.sleep(1.5)
# messaging.MsgTrack().save()
# packets.WatchList().save()
# packets.SeenList().save()
LOG.info(stats.APRSDStats())
LOG.info("Telling flask to bail.")
signal.signal(signal.SIGTERM, sys.exit(0))
sys.exit(0)
class SentMessages(objectstore.ObjectStoreMixin):
_instance = None
lock = threading.Lock()
data = {}
def __new__(cls, *args, **kwargs):
"""This magic turns this into a singleton."""
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
@wrapt.synchronized(lock)
def add(self, msg):
self.data[msg.id] = self.create(msg.id)
self.data[msg.id]["from"] = msg.fromcall
self.data[msg.id]["to"] = msg.tocall
self.data[msg.id]["message"] = msg.message.rstrip("\n")
self.data[msg.id]["raw"] = str(msg).rstrip("\n")
def create(self, id):
return {
"id": id,
"ts": time.time(),
"ack": False,
"from": None,
"to": None,
"raw": None,
"message": None,
"status": None,
"last_update": None,
"reply": None,
}
@wrapt.synchronized(lock)
def __len__(self):
return len(self.data.keys())
@wrapt.synchronized(lock)
def get(self, id):
if id in self.data:
return self.data[id]
@wrapt.synchronized(lock)
def get_all(self):
return self.data
@wrapt.synchronized(lock)
def set_status(self, id, status):
if id in self.data:
self.data[id]["last_update"] = str(datetime.datetime.now())
self.data[id]["status"] = status
@wrapt.synchronized(lock)
def ack(self, id):
"""The message got an ack!"""
if id in self.data:
self.data[id]["last_update"] = str(datetime.datetime.now())
self.data[id]["ack"] = True
@wrapt.synchronized(lock)
def reply(self, id, packet):
"""We got a packet back from the sent message."""
if id in self.data:
self.data[id]["reply"] = packet
# HTTPBasicAuth doesn't work on a class method.
# This has to be out here. Rely on the APRSDFlask
# class to initialize the users from the config
@auth.verify_password
def verify_password(username, password):
global users
if username in users and check_password_hash(users.get(username), password):
return username
class WebChatRXThread(rx.APRSDRXThread):
"""Class that connects to APRISIS/kiss and waits for messages.
After the packet is received from APRSIS/KISS, the packet is
sent to processing in the WebChatProcessPacketThread.
"""
def __init__(self, config, socketio):
super().__init__(None, config)
self.socketio = socketio
self.connected = False
def connected(self, connected=True):
self.connected = connected
def process_packet(self, *args, **kwargs):
# packet = self._client.decode_packet(*args, **kwargs)
if "packet" in kwargs:
packet = kwargs["packet"]
else:
packet = self._client.decode_packet(*args, **kwargs)
LOG.debug(f"GOT Packet {packet}")
thread = WebChatProcessPacketThread(
config=self.config,
packet=packet,
socketio=self.socketio,
)
thread.start()
class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
"""Class that handles packets being sent to us."""
def __init__(self, config, packet, socketio):
self.socketio = socketio
self.connected = False
super().__init__(config, packet)
def process_ack_packet(self, packet):
super().process_ack_packet(packet)
ack_num = packet.get("msgNo")
SentMessages().ack(int(ack_num))
self.socketio.emit(
"ack", SentMessages().get(int(ack_num)),
namespace="/sendmsg",
)
self.got_ack = True
def process_non_ack_packet(self, packet):
LOG.info(f"process non ack PACKET {packet}")
packet.get("addresse", None)
fromcall = packet["from"]
packets.PacketList().add(packet)
stats.APRSDStats().msgs_rx_inc()
message = packet.get("message_text", None)
msg = {
"id": 0,
"ts": time.time(),
"ack": False,
"from": fromcall,
"to": packet["to"],
"raw": packet["raw"],
"message": message,
"status": None,
"last_update": None,
"reply": None,
}
self.socketio.emit(
"new", msg,
namespace="/sendmsg",
)
class WebChatFlask(flask_classful.FlaskView):
config = None
def set_config(self, config):
global users
self.config = config
self.users = {}
for user in self.config["aprsd"]["web"]["users"]:
self.users[user] = generate_password_hash(
self.config["aprsd"]["web"]["users"][user],
)
users = self.users
def _get_transport(self, stats):
if self.config["aprs"].get("enabled", True):
transport = "aprs-is"
aprs_connection = (
"APRS-IS Server: <a href='http://status.aprs2.net' >"
"{}</a>".format(stats["stats"]["aprs-is"]["server"])
)
else:
# We might be connected to a KISS socket?
if client.KISSClient.is_enabled(self.config):
transport = client.KISSClient.transport(self.config)
if transport == client.TRANSPORT_TCPKISS:
aprs_connection = (
"TCPKISS://{}:{}".format(
self.config["kiss"]["tcp"]["host"],
self.config["kiss"]["tcp"]["port"],
)
)
elif transport == client.TRANSPORT_SERIALKISS:
# for pep8 violation
kiss_default = aprsd_config.DEFAULT_DATE_FORMAT["kiss"]
default_baudrate = kiss_default["serial"]["baudrate"]
aprs_connection = (
"SerialKISS://{}@{} baud".format(
self.config["kiss"]["serial"]["device"],
self.config["kiss"]["serial"].get(
"baudrate",
default_baudrate,
),
)
)
return transport, aprs_connection
@auth.login_required
def index(self):
ua_str = request.headers.get("User-Agent")
# this takes about 2 seconds :(
user_agent = ua_parse(ua_str)
LOG.debug(f"Is mobile? {user_agent.is_mobile}")
stats = self._stats()
if user_agent.is_mobile:
html_template = "mobile.html"
else:
html_template = "index.html"
# For development
# html_template = "mobile.html"
LOG.debug(f"Template {html_template}")
transport, aprs_connection = self._get_transport(stats)
LOG.debug(f"transport {transport} aprs_connection {aprs_connection}")
stats["transport"] = transport
stats["aprs_connection"] = aprs_connection
LOG.debug(f"initial stats = {stats}")
return flask.render_template(
html_template,
initial_stats=stats,
aprs_connection=aprs_connection,
callsign=self.config["aprsd"]["callsign"],
version=aprsd.__version__,
)
@auth.login_required
def send_message_status(self):
LOG.debug(request)
msgs = SentMessages()
info = msgs.get_all()
return json.dumps(info)
def _stats(self):
stats_obj = stats.APRSDStats()
now = datetime.datetime.now()
time_format = "%m-%d-%Y %H:%M:%S"
stats_dict = stats_obj.stats()
# Webchat doesnt need these
del stats_dict["aprsd"]["watch_list"]
del stats_dict["aprsd"]["seen_list"]
# del stats_dict["email"]
# del stats_dict["plugins"]
# del stats_dict["messages"]
result = {
"time": now.strftime(time_format),
"stats": stats_dict,
}
return result
def stats(self):
return json.dumps(self._stats())
class SendMessageNamespace(Namespace):
"""Class to handle the socketio interactions."""
_config = None
got_ack = False
reply_sent = False
msg = None
request = None
def __init__(self, namespace=None, config=None):
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
data["from"] = self._config["aprs"]["login"]
msg = messaging.TextMessage(
data["from"],
data["to"].upper(),
data["message"],
)
self.msg = msg
msgs = SentMessages()
msgs.add(msg)
msgs.set_status(msg.id, "Sending")
obj = msgs.get(self.msg.id)
socketio.emit(
"sent", obj,
namespace="/sendmsg",
)
msg.send()
def on_gps(self, data):
LOG.debug(f"WS on_GPS: {data}")
lat = aprslib_util.latitude_to_ddm(data["latitude"])
long = aprslib_util.longitude_to_ddm(data["longitude"])
LOG.debug(f"Lat DDM {lat}")
LOG.debug(f"Long DDM {long}")
local_datetime = datetime.datetime.now()
utc_offset_timedelta = datetime.datetime.utcnow() - local_datetime
result_utc_datetime = local_datetime + utc_offset_timedelta
time_zulu = result_utc_datetime.strftime("%d%H%M")
# now construct a beacon to send over the client connection
txt = (
f"{self._config['aprs']['login']}>APZ100,WIDE2-1"
f":@{time_zulu}z{lat}/{long}l APRSD WebChat Beacon"
)
beacon_msg = messaging.RawMessage(txt)
beacon_msg.fromcall = self._config["aprs"]["login"]
beacon_msg.tocall = "APDW16"
beacon_msg.send_direct()
def handle_message(self, data):
LOG.debug(f"WS Data {data}")
def handle_json(self, data):
LOG.debug(f"WS json {data}")
def setup_logging(config, flask_app, loglevel, quiet):
flask_log = logging.getLogger("werkzeug")
flask_app.logger.removeHandler(default_handler)
flask_log.removeHandler(default_handler)
log_level = aprsd_config.LOG_LEVELS[loglevel]
flask_log.setLevel(log_level)
date_format = config["aprsd"].get(
"dateformat",
aprsd_config.DEFAULT_DATE_FORMAT,
)
if not config["aprsd"]["web"].get("logging_enabled", False):
# disable web logging
flask_log.disabled = True
flask_app.logger.disabled = True
# return
if config["aprsd"].get("rich_logging", False) and not quiet:
log_format = "%(message)s"
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
rh = aprsd_logging.APRSDRichHandler(
show_thread=True, thread_width=15,
rich_tracebacks=True, omit_repeated_times=False,
)
rh.setFormatter(log_formatter)
flask_log.addHandler(rh)
log_file = config["aprsd"].get("logfile", None)
if log_file:
log_format = config["aprsd"].get(
"logformat",
aprsd_config.DEFAULT_LOG_FORMAT,
)
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
fh = RotatingFileHandler(
log_file, maxBytes=(10248576 * 5),
backupCount=4,
)
fh.setFormatter(log_formatter)
flask_log.addHandler(fh)
@trace.trace
def init_flask(config, loglevel, quiet):
global socketio
flask_app = flask.Flask(
"aprsd",
static_url_path="/static",
static_folder="web/chat/static",
template_folder="web/chat/templates",
)
setup_logging(config, flask_app, loglevel, quiet)
server = WebChatFlask()
server.set_config(config)
flask_app.route("/", methods=["GET"])(server.index)
flask_app.route("/stats", methods=["GET"])(server.stats)
# flask_app.route("/send-message", methods=["GET"])(server.send_message)
flask_app.route("/send-message-status", methods=["GET"])(server.send_message_status)
socketio = SocketIO(
flask_app, logger=False, engineio_logger=False,
async_mode="threading",
)
# async_mode="gevent",
# async_mode="eventlet",
# import eventlet
# eventlet.monkey_patch()
socketio.on_namespace(
SendMessageNamespace(
"/sendmsg", config=config,
),
)
return socketio, flask_app
# main() ###
@cli.command()
@cli_helper.add_options(cli_helper.common_options)
@click.option(
"-f",
"--flush",
"flush",
is_flag=True,
show_default=True,
default=False,
help="Flush out all old aged messages on disk.",
)
@click.option(
"-p",
"--port",
"port",
show_default=True,
default=80,
help="Port to listen to web requests",
)
@click.pass_context
@cli_helper.process_standard_options
def webchat(ctx, flush, port):
"""Web based HAM Radio chat program!"""
ctx.obj["config_file"]
loglevel = ctx.obj["loglevel"]
quiet = ctx.obj["quiet"]
config = ctx.obj["config"]
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
if not quiet:
click.echo("Load config")
level, msg = utils._check_version()
if level:
LOG.warning(msg)
else:
LOG.info(msg)
LOG.info(f"APRSD Started version: {aprsd.__version__}")
flat_config = utils.flatten_dict(config)
LOG.info("Using CONFIG values:")
for x in flat_config:
if "password" in x or "aprsd.web.users.admin" in x:
LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX")
else:
LOG.info(f"{x} = {flat_config[x]}")
stats.APRSDStats(config)
# Initialize the client factory and create
# The correct client object ready for use
client.ClientFactory.setup(config)
# Make sure we have 1 client transport enabled
if not client.factory.is_client_enabled():
LOG.error("No Clients are enabled in config.")
sys.exit(-1)
if not client.factory.is_client_configured():
LOG.error("APRS client is not properly configured in config file.")
sys.exit(-1)
packets.PacketList(config=config)
messaging.MsgTrack(config=config)
packets.WatchList(config=config)
packets.SeenList(config=config)
(socketio, app) = init_flask(config, loglevel, quiet)
rx_thread = WebChatRXThread(
config=config,
socketio=socketio,
)
LOG.info("Start RX Thread")
rx_thread.start()
keepalive = threads.KeepAliveThread(config=config)
LOG.info("Start KeepAliveThread")
keepalive.start()
LOG.info("Start socketio.run()")
socketio.run(
app,
ssl_context="adhoc",
host=config["aprsd"]["web"]["host"],
port=port,
)
LOG.info("WebChat exiting!!!! Bye.")
+31 -16
View File
@@ -7,7 +7,7 @@ import sys
import click
import yaml
from aprsd import utils
from aprsd import exception, utils
home = str(Path.home())
@@ -57,6 +57,7 @@ DEFAULT_CONFIG_DICT = {
"ham": {"callsign": "NOCALL"},
"aprs": {
"enabled": True,
# Only used as the login for aprsis.
"login": "CALLSIGN",
"password": "00000",
"host": "rotate.aprs2.net",
@@ -75,11 +76,14 @@ DEFAULT_CONFIG_DICT = {
},
},
"aprsd": {
# Callsign to use for all packets to/from aprsd instance
# regardless of the client (aprsis vs kiss)
"callsign": "NOCALL",
"logfile": "/tmp/aprsd.log",
"logformat": DEFAULT_LOG_FORMAT,
"dateformat": DEFAULT_DATE_FORMAT,
"save_location": DEFAULT_CONFIG_DIR,
"rich_logging": False,
"rich_logging": True,
"trace": False,
"enabled_plugins": CORE_MESSAGE_PLUGINS,
"units": "imperial",
@@ -174,25 +178,37 @@ class Config(collections.UserDict):
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,
),
)
if type(path) is list:
path = ".".join(path)
raise exception.MissingConfigOptionException(path)
val = self.get(path)
if val == default_fail:
# We have to fail and bail if the user hasn't edited
# this config option.
raise Exception(
"Config file needs to be changed from provided"
" defaults for '{}'".format(
path,
),
raise exception.ConfigOptionBogusDefaultException(
path, default_fail,
)
def add_config_comments(raw_yaml):
end_idx = utils.end_substr(raw_yaml, "ham:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # Callsign that owns this instance of APRSD.",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "aprsd:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # Callsign to use for all APRSD Packets as the to/from."
"\n # regardless of client type (aprsis vs tcpkiss vs serial)",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "aprs:")
if end_idx != -1:
# lets insert a comment
@@ -330,13 +346,12 @@ def parse_config(config_file):
)
check_option(
config,
"aprs.login",
default_fail=DEFAULT_CONFIG_DICT["aprs"]["login"],
["aprsd"],
)
check_option(
config,
["aprs", "password"],
default_fail=DEFAULT_CONFIG_DICT["aprs"]["password"],
"aprsd.callsign",
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["callsign"],
)
# Ensure they change the admin password
+13
View File
@@ -0,0 +1,13 @@
class MissingConfigOptionException(Exception):
"""Missing a config option."""
def __init__(self, config_option):
self.message = f"Option '{config_option}' was not in config file"
class ConfigOptionBogusDefaultException(Exception):
"""Missing a config option."""
def __init__(self, config_option, default_fail):
self.message = (
f"Config file option '{config_option}' needs to be "
f"changed from provided default of '{default_fail}'"
)
+5 -4
View File
@@ -18,9 +18,10 @@ from werkzeug.security import check_password_hash, generate_password_hash
import aprsd
from aprsd import client
from aprsd import config as aprsd_config
from aprsd import log, messaging, packets, plugin, stats, threads, utils
from aprsd import messaging, packets, plugin, stats, threads, utils
from aprsd.clients import aprsis
from aprsd.logging import logging as aprsd_logging
from aprsd.logging import log
from aprsd.logging import rich as aprsd_logging
LOG = logging.getLogger("APRSD")
@@ -600,8 +601,8 @@ def init_flask(config, loglevel, quiet):
flask_app = flask.Flask(
"aprsd",
static_url_path="/static",
static_folder="web/static",
template_folder="web/templates",
static_folder="web/admin/static",
template_folder="web/admin/templates",
)
setup_logging(config, flask_app, loglevel, quiet)
server = APRSDFlask()
+3 -3
View File
@@ -5,7 +5,7 @@ import queue
import sys
from aprsd import config as aprsd_config
from aprsd.logging import logging as aprsd_logging
from aprsd.logging import rich as aprsd_logging
LOG = logging.getLogger("APRSD")
@@ -33,10 +33,10 @@ def setup_logging(config, loglevel, quiet):
rich_logging = True
log_file = config["aprsd"].get("logfile", None)
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
if log_file:
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
fh.setFormatter(log_formatter)
LOG.addHandler(fh)
+40 -35
View File
@@ -6,7 +6,8 @@ import re
import threading
import time
from aprsd import client, objectstore, packets, stats, threads
from aprsd import client, packets, stats, threads
from aprsd.utils import objectstore
LOG = logging.getLogger("APRSD")
@@ -184,6 +185,7 @@ class Message(metaclass=abc.ABCMeta):
last_send_attempt = 0
transport = None
_raw_message = None
def __init__(
self,
@@ -208,6 +210,23 @@ class Message(metaclass=abc.ABCMeta):
def send(self):
"""Child class must declare."""
def _filter_for_send(self):
"""Filter and format message string for FCC."""
# max? ftm400 displays 64, raw msg shows 74
# and ftm400-send is max 64. setting this to
# 67 displays 64 on the ftm400. (+3 {01 suffix)
# feature req: break long ones into two msgs
message = self._raw_message[:67]
# We all miss George Carlin
return re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message)
@property
def message(self):
return self._filter_for_send().rstrip("\n")
def __str__(self):
return self.message
class RawMessage(Message):
"""Send a raw message.
@@ -217,11 +236,14 @@ class RawMessage(Message):
"""
message = None
last_send_age = last_send_time = None
def __init__(self, message, allow_delay=True):
super().__init__(fromcall=None, tocall=None, msg_id=None, allow_delay=allow_delay)
self.message = message
super().__init__(
fromcall=None, tocall=None, msg_id=None,
allow_delay=allow_delay,
)
self._raw_message = message
def dict(self):
now = datetime.datetime.now()
@@ -230,17 +252,14 @@ class RawMessage(Message):
last_send_age = str(now - self.last_send_time)
return {
"type": "raw",
"message": self.message.rstrip("\n"),
"raw": self.message.rstrip("\n"),
"message": self.message,
"raw": str(self),
"retry_count": self.retry_count,
"last_send_attempt": self.last_send_attempt,
"last_send_time": str(self.last_send_time),
"last_send_age": last_send_age,
}
def __str__(self):
return self.message
def send(self):
tracker = MsgTrack()
tracker.add(self)
@@ -252,7 +271,7 @@ class RawMessage(Message):
cl = client.factory.create().client
log_message(
"Sending Message Direct",
str(self).rstrip("\n"),
str(self),
self.message,
tocall=self.tocall,
fromcall=self.fromcall,
@@ -264,21 +283,17 @@ class RawMessage(Message):
class TextMessage(Message):
"""Send regular ARPS text/command messages/replies."""
message = None
last_send_time = last_send_age = None
def __init__(
self,
fromcall,
tocall,
message,
msg_id=None,
allow_delay=True,
self, fromcall, tocall, message,
msg_id=None, allow_delay=True,
):
super().__init__(
fromcall=fromcall, tocall=tocall,
msg_id=msg_id, allow_delay=allow_delay,
)
self.message = message
self._raw_message = message
def dict(self):
now = datetime.datetime.now()
@@ -292,8 +307,8 @@ class TextMessage(Message):
"type": "text-message",
"fromcall": self.fromcall,
"tocall": self.tocall,
"message": self.message.rstrip("\n"),
"raw": str(self).rstrip("\n"),
"message": self.message,
"raw": str(self),
"retry_count": self.retry_count,
"last_send_attempt": self.last_send_attempt,
"last_send_time": str(self.last_send_time),
@@ -305,20 +320,10 @@ class TextMessage(Message):
return "{}>APZ100::{}:{}{{{}\n".format(
self.fromcall,
self.tocall.ljust(9),
self._filter_for_send(),
self.message,
str(self.id),
)
def _filter_for_send(self):
"""Filter and format message string for FCC."""
# max? ftm400 displays 64, raw msg shows 74
# and ftm400-send is max 64. setting this to
# 67 displays 64 on the ftm400. (+3 {01 suffix)
# feature req: break long ones into two msgs
message = self.message[:67]
# We all miss George Carlin
return re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message)
def send(self):
tracker = MsgTrack()
tracker.add(self)
@@ -334,7 +339,7 @@ class TextMessage(Message):
cl = client.factory.create().client
log_message(
"Sending Message Direct",
str(self).rstrip("\n"),
str(self),
self.message,
tocall=self.tocall,
fromcall=self.fromcall,
@@ -347,8 +352,8 @@ class TextMessage(Message):
class SendMessageThread(threads.APRSDThread):
def __init__(self, message):
self.msg = message
name = self.msg.message[:5]
super().__init__(f"SendMessage-{self.msg.id}-{name}")
name = self.msg._raw_message[:5]
super().__init__(f"TXPKT-{self.msg.id}-{name}")
def loop(self):
"""Loop until a message is acked or it gets delayed.
@@ -395,7 +400,7 @@ class SendMessageThread(threads.APRSDThread):
# tracking the time.
log_message(
"Sending Message",
str(msg).rstrip("\n"),
str(msg),
msg.message,
tocall=self.msg.tocall,
retry_number=msg.last_send_attempt,
+38 -32
View File
@@ -3,7 +3,10 @@ import logging
import threading
import time
from aprsd import objectstore, utils
import wrapt
from aprsd import utils
from aprsd.utils import objectstore
LOG = logging.getLogger("APRSD")
@@ -17,6 +20,7 @@ class PacketList:
"""Class to track all of the packets rx'd and tx'd by aprsd."""
_instance = None
lock = threading.Lock()
config = None
packet_list = {}
@@ -28,7 +32,6 @@ class PacketList:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.packet_list = utils.RingBuffer(1000)
cls._instance.lock = threading.Lock()
cls._instance.config = kwargs["config"]
return cls._instance
@@ -36,50 +39,51 @@ class PacketList:
if config:
self.config = config
@wrapt.synchronized(lock)
def __iter__(self):
with self.lock:
return iter(self.packet_list)
return iter(self.packet_list)
@wrapt.synchronized(lock)
def add(self, packet):
with self.lock:
packet["ts"] = time.time()
if (
"fromcall" in packet
and packet["fromcall"] == self.config["aprs"]["login"]
):
self.total_tx += 1
else:
self.total_recv += 1
self.packet_list.append(packet)
SeenList().update_seen(packet)
packet["ts"] = time.time()
if (
"fromcall" in packet
and packet["fromcall"] == self.config["aprs"]["login"]
):
self.total_tx += 1
else:
self.total_recv += 1
self.packet_list.append(packet)
SeenList().update_seen(packet)
@wrapt.synchronized(lock)
def get(self):
with self.lock:
return self.packet_list.get()
return self.packet_list.get()
@wrapt.synchronized(lock)
def total_received(self):
with self.lock:
return self.total_recv
return self.total_recv
@wrapt.synchronized(lock)
def total_sent(self):
with self.lock:
return self.total_tx
return self.total_tx
class WatchList(objectstore.ObjectStoreMixin):
"""Global watch list and info for callsigns."""
_instance = None
lock = threading.Lock()
data = {}
config = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.lock = threading.Lock()
cls._instance.config = kwargs["config"]
if "config" in kwargs:
cls._instance.config = kwargs["config"]
cls._instance._init_store()
cls._instance.data = {}
cls._instance._init_store()
return cls._instance
def __init__(self, config=None):
@@ -110,12 +114,12 @@ class WatchList(objectstore.ObjectStoreMixin):
def callsign_in_watchlist(self, callsign):
return callsign in self.data
@wrapt.synchronized(lock)
def update_seen(self, packet):
with self.lock:
callsign = packet["from"]
if self.callsign_in_watchlist(callsign):
self.data[callsign]["last"] = datetime.datetime.now()
self.data[callsign]["packets"].append(packet)
callsign = packet["from"]
if self.callsign_in_watchlist(callsign):
self.data[callsign]["last"] = datetime.datetime.now()
self.data[callsign]["packets"].append(packet)
def last_seen(self, callsign):
if self.callsign_in_watchlist(callsign):
@@ -158,18 +162,20 @@ class SeenList(objectstore.ObjectStoreMixin):
"""Global callsign seen list."""
_instance = None
lock = threading.Lock()
data = {}
config = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.lock = threading.Lock()
cls._instance.config = kwargs["config"]
if "config" in kwargs:
cls._instance.config = kwargs["config"]
cls._instance._init_store()
cls._instance.data = {}
cls._instance._init_store()
return cls._instance
@wrapt.synchronized(lock)
def update_seen(self, packet):
callsign = None
if "fromcall" in packet:
+2 -1
View File
@@ -492,4 +492,5 @@ class PluginManager:
self._pluggy_pm.register(obj)
def get_plugins(self):
return self._pluggy_pm.get_plugins()
if self._pluggy_pm:
return self._pluggy_pm.get_plugins()
+3 -57
View File
@@ -9,9 +9,9 @@ import threading
import time
import imapclient
from validate_email import validate_email
from aprsd import messaging, plugin, stats, threads, trace
from aprsd import messaging, plugin, stats, threads
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")
@@ -72,15 +72,8 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
"""Ensure that email is enabled and start the thread."""
email_enabled = self.config["aprsd"]["email"].get("enabled", False)
validation = self.config["aprsd"]["email"].get("validate", False)
if email_enabled:
valid = validate_email_config(self.config, validation)
if not valid:
LOG.error("Failed to validate email config options.")
LOG.error("EmailPlugin DISABLED!!!!")
else:
self.enabled = True
self.enabled = True
else:
LOG.info("Email services not enabled.")
@@ -265,47 +258,6 @@ def _smtp_connect(config):
return server
def validate_shortcuts(config):
shortcuts = config["aprsd"]["email"].get("shortcuts", None)
if not shortcuts:
return
LOG.info(
"Validating {} Email shortcuts. This can take up to 10 seconds"
" per shortcut".format(len(shortcuts)),
)
delete_keys = []
for key in shortcuts:
LOG.info(f"Validating {key}:{shortcuts[key]}")
is_valid = validate_email(
email_address=shortcuts[key],
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,
smtp_debug=False,
)
if not is_valid:
LOG.error(
"'{}' is an invalid email address. Removing shortcut".format(
shortcuts[key],
),
)
delete_keys.append(key)
for key in delete_keys:
del config["aprsd"]["email"]["shortcuts"][key]
LOG.info(
"Available shortcuts: {}".format(
config["aprsd"]["email"]["shortcuts"],
),
)
def get_email_from_shortcut(config, addr):
if config["aprsd"]["email"].get("shortcuts", False):
return config["aprsd"]["email"]["shortcuts"].get(addr, addr)
@@ -323,12 +275,6 @@ def validate_email_config(config, disable_validation=False):
LOG.info("Checking SMTP configuration")
smtp_server = _smtp_connect(config)
# Now validate and flag any shortcuts as invalid
if not disable_validation:
validate_shortcuts(config)
else:
LOG.info("Shortcuts email validation is Disabled!!, you were warned.")
if imap_server and smtp_server:
return True
else:
+2 -1
View File
@@ -2,7 +2,8 @@ import logging
import shutil
import subprocess
from aprsd import plugin, trace
from aprsd import plugin
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")
+2 -1
View File
@@ -2,7 +2,8 @@ import logging
import re
import time
from aprsd import plugin, plugin_utils, trace
from aprsd import plugin, plugin_utils
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")
+2 -1
View File
@@ -1,6 +1,7 @@
import logging
from aprsd import messaging, packets, plugin, trace
from aprsd import messaging, packets, plugin
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")
+2 -1
View File
@@ -1,7 +1,8 @@
import logging
import time
from aprsd import plugin, trace
from aprsd import plugin
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")
+2 -1
View File
@@ -2,7 +2,8 @@ import datetime
import logging
import re
from aprsd import messaging, plugin, trace
from aprsd import messaging, plugin
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")
+3 -65
View File
@@ -2,10 +2,10 @@ import logging
import re
import time
from opencage.geocoder import OpenCageGeocode
import pytz
from aprsd import fuzzyclock, plugin, plugin_utils, trace
from aprsd import plugin, plugin_utils
from aprsd.utils import fuzzy, trace
LOG = logging.getLogger("APRSD")
@@ -32,7 +32,7 @@ class TimePlugin(plugin.APRSDRegexCommandPluginBase):
local_short_str = local_t.strftime("%H:%M %Z")
local_hour = local_t.strftime("%H")
local_min = local_t.strftime("%M")
cur_time = fuzzyclock.fuzzy(int(local_hour), int(local_min), 1)
cur_time = fuzzy(int(local_hour), int(local_min), 1)
reply = "{} ({})".format(
cur_time,
@@ -49,68 +49,6 @@ class TimePlugin(plugin.APRSDRegexCommandPluginBase):
return self.build_date_str(localzone)
class TimeOpenCageDataPlugin(TimePlugin, plugin.APRSFIKEYMixin):
"""geocage based timezone fetching."""
command_regex = "^[tT]"
command_name = "time"
short_description = "Current time of GPS beacon timezone. Uses OpenCage"
def setup(self):
self.ensure_aprs_fi_key()
@trace.trace
def process(self, packet):
fromcall = packet.get("from")
message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
api_key = self.config["services"]["aprs.fi"]["apiKey"]
# optional second argument is a callsign to search
a = re.search(r"^.*\s+(.*)", message)
if a is not None:
searchcall = a.group(1)
searchcall = searchcall.upper()
else:
# if no second argument, search for calling station
searchcall = fromcall
try:
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
except Exception as ex:
LOG.error(f"Failed to fetch aprs.fi data {ex}")
return "Failed to fetch location"
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
if not len(aprs_data["entries"]):
LOG.error("Didn't get any entries from aprs.fi")
return "Failed to fetch aprs.fi location"
lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"]
try:
self.config.exists("opencagedata.apiKey")
except Exception as ex:
LOG.error(f"Failed to find config opencage:apiKey {ex}")
return "No opencage apiKey found"
try:
opencage_key = self.config["opencagedata"]["apiKey"]
geocoder = OpenCageGeocode(opencage_key)
results = geocoder.reverse_geocode(lat, lon)
except Exception as ex:
LOG.error(f"Couldn't fetch opencagedata api '{ex}'")
# Default to UTC instead
localzone = pytz.timezone("UTC")
else:
tzone = results[0]["annotations"]["timezone"]["name"]
localzone = pytz.timezone(tzone)
return self.build_date_str(localzone)
class TimeOWMPlugin(TimePlugin, plugin.APRSFIKEYMixin):
"""OpenWeatherMap based timezone fetching."""
+2 -1
View File
@@ -1,7 +1,8 @@
import logging
import aprsd
from aprsd import plugin, stats, trace
from aprsd import plugin, stats
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")
+2 -1
View File
@@ -4,7 +4,8 @@ import re
import requests
from aprsd import plugin, plugin_utils, trace
from aprsd import plugin, plugin_utils
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")
+82 -80
View File
@@ -2,6 +2,8 @@ import datetime
import logging
import threading
import wrapt
import aprsd
from aprsd import packets, plugin, utils
@@ -12,7 +14,7 @@ LOG = logging.getLogger("APRSD")
class APRSDStats:
_instance = None
lock = None
lock = threading.Lock()
config = None
start_time = None
@@ -39,7 +41,6 @@ class APRSDStats:
if cls._instance is None:
cls._instance = super().__new__(cls)
# any initializetion here
cls._instance.lock = threading.Lock()
cls._instance.start_time = datetime.datetime.now()
cls._instance._aprsis_keepalive = datetime.datetime.now()
return cls._instance
@@ -48,128 +49,129 @@ class APRSDStats:
if config:
self.config = config
@wrapt.synchronized(lock)
@property
def uptime(self):
with self.lock:
return datetime.datetime.now() - self.start_time
return datetime.datetime.now() - self.start_time
@wrapt.synchronized(lock)
@property
def memory(self):
with self.lock:
return self._mem_current
return self._mem_current
@wrapt.synchronized(lock)
def set_memory(self, memory):
with self.lock:
self._mem_current = memory
self._mem_current = memory
@wrapt.synchronized(lock)
@property
def memory_peak(self):
with self.lock:
return self._mem_peak
return self._mem_peak
@wrapt.synchronized(lock)
def set_memory_peak(self, memory):
with self.lock:
self._mem_peak = memory
self._mem_peak = memory
@wrapt.synchronized(lock)
@property
def aprsis_server(self):
with self.lock:
return self._aprsis_server
return self._aprsis_server
@wrapt.synchronized(lock)
def set_aprsis_server(self, server):
with self.lock:
self._aprsis_server = server
self._aprsis_server = server
@wrapt.synchronized(lock)
@property
def aprsis_keepalive(self):
with self.lock:
return self._aprsis_keepalive
return self._aprsis_keepalive
@wrapt.synchronized(lock)
def set_aprsis_keepalive(self):
with self.lock:
self._aprsis_keepalive = datetime.datetime.now()
self._aprsis_keepalive = datetime.datetime.now()
@wrapt.synchronized(lock)
@property
def msgs_tx(self):
with self.lock:
return self._msgs_tx
return self._msgs_tx
@wrapt.synchronized(lock)
def msgs_tx_inc(self):
with self.lock:
self._msgs_tx += 1
self._msgs_tx += 1
@wrapt.synchronized(lock)
@property
def msgs_rx(self):
with self.lock:
return self._msgs_rx
return self._msgs_rx
@wrapt.synchronized(lock)
def msgs_rx_inc(self):
with self.lock:
self._msgs_rx += 1
self._msgs_rx += 1
@wrapt.synchronized(lock)
@property
def msgs_mice_rx(self):
with self.lock:
return self._msgs_mice_rx
return self._msgs_mice_rx
@wrapt.synchronized(lock)
def msgs_mice_inc(self):
with self.lock:
self._msgs_mice_rx += 1
self._msgs_mice_rx += 1
@wrapt.synchronized(lock)
@property
def ack_tx(self):
with self.lock:
return self._ack_tx
return self._ack_tx
@wrapt.synchronized(lock)
def ack_tx_inc(self):
with self.lock:
self._ack_tx += 1
self._ack_tx += 1
@wrapt.synchronized(lock)
@property
def ack_rx(self):
with self.lock:
return self._ack_rx
return self._ack_rx
@wrapt.synchronized(lock)
def ack_rx_inc(self):
with self.lock:
self._ack_rx += 1
self._ack_rx += 1
@wrapt.synchronized(lock)
@property
def msgs_tracked(self):
with self.lock:
return self._msgs_tracked
return self._msgs_tracked
@wrapt.synchronized(lock)
def msgs_tracked_inc(self):
with self.lock:
self._msgs_tracked += 1
self._msgs_tracked += 1
@wrapt.synchronized(lock)
@property
def email_tx(self):
with self.lock:
return self._email_tx
return self._email_tx
@wrapt.synchronized(lock)
def email_tx_inc(self):
with self.lock:
self._email_tx += 1
self._email_tx += 1
@wrapt.synchronized(lock)
@property
def email_rx(self):
with self.lock:
return self._email_rx
return self._email_rx
@wrapt.synchronized(lock)
def email_rx_inc(self):
with self.lock:
self._email_rx += 1
self._email_rx += 1
@wrapt.synchronized(lock)
@property
def email_thread_time(self):
with self.lock:
return self._email_thread_last_time
return self._email_thread_last_time
@wrapt.synchronized(lock)
def email_thread_update(self):
with self.lock:
self._email_thread_last_time = datetime.datetime.now()
self._email_thread_last_time = datetime.datetime.now()
@wrapt.synchronized(lock)
def stats(self):
now = datetime.datetime.now()
if self._email_thread_last_time:
@@ -185,20 +187,20 @@ class APRSDStats:
pm = plugin.PluginManager()
plugins = pm.get_plugins()
plugin_stats = {}
if plugins:
def full_name_with_qualname(obj):
return "{}.{}".format(
obj.__class__.__module__,
obj.__class__.__qualname__,
)
def full_name_with_qualname(obj):
return "{}.{}".format(
obj.__class__.__module__,
obj.__class__.__qualname__,
)
for p in plugins:
plugin_stats[full_name_with_qualname(p)] = {
"enabled": p.enabled,
"rx": p.rx_count,
"tx": p.tx_count,
"version": p.version,
}
for p in plugins:
plugin_stats[full_name_with_qualname(p)] = {
"enabled": p.enabled,
"rx": p.rx_count,
"tx": p.tx_count,
"version": p.version,
}
wl = packets.WatchList()
sl = packets.SeenList()
@@ -207,30 +209,30 @@ class APRSDStats:
"aprsd": {
"version": aprsd.__version__,
"uptime": utils.strfdelta(self.uptime),
"memory_current": self.memory,
"memory_current": int(self.memory),
"memory_current_str": utils.human_size(self.memory),
"memory_peak": self.memory_peak,
"memory_peak": int(self.memory_peak),
"memory_peak_str": utils.human_size(self.memory_peak),
"watch_list": wl.get_all(),
"seen_list": sl.get_all(),
},
"aprs-is": {
"server": self.aprsis_server,
"server": str(self.aprsis_server),
"callsign": self.config["aprs"]["login"],
"last_update": last_aprsis_keepalive,
},
"messages": {
"tracked": self.msgs_tracked,
"sent": self.msgs_tx,
"recieved": self.msgs_rx,
"ack_sent": self.ack_tx,
"ack_recieved": self.ack_rx,
"mic-e recieved": self.msgs_mice_rx,
"tracked": int(self.msgs_tracked),
"sent": int(self.msgs_tx),
"recieved": int(self.msgs_rx),
"ack_sent": int(self.ack_tx),
"ack_recieved": int(self.ack_rx),
"mic-e recieved": int(self.msgs_mice_rx),
},
"email": {
"enabled": self.config["aprsd"]["email"]["enabled"],
"sent": self._email_tx,
"recieved": self._email_rx,
"sent": int(self._email_tx),
"recieved": int(self._email_rx),
"thread_last_update": last_update,
},
"plugins": plugin_stats,
-326
View File
@@ -1,326 +0,0 @@
import abc
import datetime
import logging
import queue
import threading
import time
import tracemalloc
import aprslib
from aprsd import client, messaging, packets, plugin, stats, utils
LOG = logging.getLogger("APRSD")
RX_THREAD = "RX"
EMAIL_THREAD = "Email"
rx_msg_queue = queue.Queue(maxsize=20)
msg_queues = {
"rx": rx_msg_queue,
}
class APRSDThreadList:
"""Singleton class that keeps track of application wide threads."""
_instance = None
threads_list = []
lock = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls.lock = threading.Lock()
cls.threads_list = []
return cls._instance
def add(self, thread_obj):
with self.lock:
self.threads_list.append(thread_obj)
def remove(self, thread_obj):
with self.lock:
self.threads_list.remove(thread_obj)
def stop_all(self):
"""Iterate over all threads and call stop on them."""
with self.lock:
for th in self.threads_list:
LOG.debug(f"Stopping Thread {th.name}")
th.stop()
def __len__(self):
with self.lock:
return len(self.threads_list)
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
def __init__(self, name):
super().__init__(name=name)
self.thread_stop = False
APRSDThreadList().add(self)
def stop(self):
self.thread_stop = True
@abc.abstractmethod
def loop(self):
pass
def run(self):
LOG.debug("Starting")
while not self.thread_stop:
can_loop = self.loop()
if not can_loop:
self.stop()
APRSDThreadList().remove(self)
LOG.debug("Exiting")
class KeepAliveThread(APRSDThread):
cntr = 0
checker_time = datetime.datetime.now()
def __init__(self, config):
tracemalloc.start()
super().__init__("KeepAlive")
self.config = config
max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0}
self.max_delta = datetime.timedelta(**max_timeout)
def loop(self):
if self.cntr % 60 == 0:
tracker = messaging.MsgTrack()
stats_obj = stats.APRSDStats()
pl = packets.PacketList()
thread_list = APRSDThreadList()
now = datetime.datetime.now()
last_email = stats_obj.email_thread_time
if last_email:
email_thread_time = utils.strfdelta(now - last_email)
else:
email_thread_time = "N/A"
last_msg_time = utils.strfdelta(now - stats_obj.aprsis_keepalive)
current, peak = tracemalloc.get_traced_memory()
stats_obj.set_memory(current)
stats_obj.set_memory_peak(peak)
keepalive = (
"{} - Uptime {} RX:{} TX:{} Tracker:{} Msgs TX:{} RX:{} "
"Last:{} Email: {} - RAM Current:{} Peak:{} Threads:{}"
).format(
self.config["aprs"]["login"],
utils.strfdelta(stats_obj.uptime),
pl.total_recv,
pl.total_tx,
len(tracker),
stats_obj.msgs_tx,
stats_obj.msgs_rx,
last_msg_time,
email_thread_time,
utils.human_size(current),
utils.human_size(peak),
len(thread_list),
)
LOG.info(keepalive)
# See if we should reset the aprs-is client
# Due to losing a keepalive from them
delta_dict = utils.parse_delta_str(last_msg_time)
delta = datetime.timedelta(**delta_dict)
if delta > self.max_delta:
# We haven't gotten a keepalive from aprs-is in a while
# reset the connection.a
if not client.KISSClient.is_enabled(self.config):
LOG.warning("Resetting connection to APRS-IS.")
client.factory.create().reset()
# Check version every hour
delta = now - self.checker_time
if delta > datetime.timedelta(hours=1):
self.checker_time = now
level, msg = utils._check_version()
if level:
LOG.warning(msg)
self.cntr += 1
time.sleep(1)
return True
class APRSDRXThread(APRSDThread):
def __init__(self, msg_queues, config):
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.factory.create().client.stop()
def loop(self):
# setup the consumer of messages and block until a messages
try:
# This will register a packet consumer with aprslib
# When new packets come in the consumer will process
# the packet
# Do a partial here because the consumer signature doesn't allow
# For kwargs to be passed in to the consumer func we declare
# and the aprslib developer didn't want to allow a PR to add
# kwargs. :(
# https://github.com/rossengeorgiev/aprs-python/pull/56
self._client.client.consumer(
self.process_packet, raw=False, blocking=False,
)
except aprslib.exceptions.ConnectionDrop:
LOG.error("Connection dropped, reconnecting")
time.sleep(5)
# Force the deletion of the client object connected to aprs
# This will cause a reconnect, next time client.get_client()
# is called
self._client.reset()
# Continue to loop
return True
def process_packet(self, *args, **kwargs):
packet = self._client.decode_packet(*args, **kwargs)
thread = APRSDProcessPacketThread(packet=packet, config=self.config)
thread.start()
class APRSDProcessPacketThread(APRSDThread):
def __init__(self, packet, config):
self.packet = packet
self.config = config
name = self.packet["raw"][:10]
super().__init__(f"RXPKT-{name}")
def process_ack_packet(self, packet):
ack_num = packet.get("msgNo")
LOG.info(f"Got ack for message {ack_num}")
messaging.log_message(
"RXACK",
packet["raw"],
None,
ack=ack_num,
fromcall=packet["from"],
)
tracker = messaging.MsgTrack()
tracker.remove(ack_num)
stats.APRSDStats().ack_rx_inc()
return
def loop(self):
"""Process a packet recieved from aprs-is server."""
packet = self.packet
packets.PacketList().add(packet)
fromcall = packet["from"]
tocall = packet.get("addresse", None)
msg = packet.get("message_text", None)
msg_id = packet.get("msgNo", "0")
msg_response = packet.get("response", None)
# LOG.debug(f"Got packet from '{fromcall}' - {packet}")
# We don't put ack packets destined for us through the
# plugins.
if tocall == self.config["aprs"]["login"] and msg_response == "ack":
self.process_ack_packet(packet)
else:
# It's not an ACK for us, so lets run it through
# the plugins.
messaging.log_message(
"Received Message",
packet["raw"],
msg,
fromcall=fromcall,
msg_num=msg_id,
)
# Only ack messages that were sent directly to us
if tocall == self.config["aprs"]["login"]:
stats.APRSDStats().msgs_rx_inc()
# let any threads do their thing, then ack
# send an ack last
ack = messaging.AckMessage(
self.config["aprs"]["login"],
fromcall,
msg_id=msg_id,
)
ack.send()
pm = plugin.PluginManager()
try:
results = pm.run(packet)
wl = packets.WatchList()
wl.update_seen(packet)
replied = False
for reply in results:
if isinstance(reply, list):
# one of the plugins wants to send multiple messages
replied = True
for subreply in reply:
LOG.debug(f"Sending '{subreply}'")
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
# us that they processed the message correctly, but have
# nothing to reply with, so we avoid replying with a
# usage string
if reply is not messaging.NULL_MESSAGE:
LOG.debug(f"Sending '{reply}'")
msg = messaging.TextMessage(
self.config["aprs"]["login"],
fromcall,
reply,
)
msg.send()
# 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:
LOG.warning("Sending help!")
msg = messaging.TextMessage(
self.config["aprs"]["login"],
fromcall,
"Unknown command! Send 'help' message for help",
)
msg.send()
except Exception as ex:
LOG.error("Plugin failed!!!")
LOG.exception(ex)
# Do we need to send a reply?
if tocall == self.config["aprs"]["login"]:
reply = "A Plugin failed! try again?"
msg = messaging.TextMessage(
self.config["aprs"]["login"],
fromcall,
reply,
)
msg.send()
LOG.debug("Packet processing complete")
+13
View File
@@ -0,0 +1,13 @@
import queue
# Make these available to anyone importing
# aprsd.threads
from .aprsd import APRSDThread, APRSDThreadList # noqa: F401
from .keep_alive import KeepAliveThread # noqa: F401
from .rx import APRSDRXThread # noqa: F401
rx_msg_queue = queue.Queue(maxsize=20)
msg_queues = {
"rx": rx_msg_queue,
}
+94
View File
@@ -0,0 +1,94 @@
import abc
import logging
from queue import Queue
import threading
import wrapt
LOG = logging.getLogger("APRSD")
class APRSDThreadList:
"""Singleton class that keeps track of application wide threads."""
_instance = None
threads_list = []
lock = threading.Lock()
global_queue = Queue()
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls.threads_list = []
return cls._instance
@wrapt.synchronized(lock)
def add(self, thread_obj):
thread_obj.set_global_queue(self.global_queue)
self.threads_list.append(thread_obj)
@wrapt.synchronized(lock)
def remove(self, thread_obj):
self.threads_list.remove(thread_obj)
@wrapt.synchronized(lock)
def stop_all(self):
self.global_queue.put_nowait({"quit": True})
"""Iterate over all threads and call stop on them."""
for th in self.threads_list:
LOG.info(f"Stopping Thread {th.name}")
th.stop()
@wrapt.synchronized(lock)
def __len__(self):
return len(self.threads_list)
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
global_queue = None
def __init__(self, name):
super().__init__(name=name)
self.thread_stop = False
APRSDThreadList().add(self)
def set_global_queue(self, global_queue):
self.global_queue = global_queue
def _should_quit(self):
""" see if we have a quit message from the global queue."""
if self.thread_stop:
return True
if self.global_queue.empty():
return False
msg = self.global_queue.get(timeout=1)
if not msg:
return False
if "quit" in msg and msg["quit"] is True:
# put the message back on the queue for others
self.global_queue.put_nowait(msg)
self.thread_stop = True
return True
def stop(self):
self.thread_stop = True
@abc.abstractmethod
def loop(self):
pass
def _cleanup(self):
"""Add code to subclass to do any cleanup"""
def run(self):
LOG.debug("Starting")
while not self._should_quit():
can_loop = self.loop()
if not can_loop:
self.stop()
self._cleanup()
APRSDThreadList().remove(self)
LOG.debug("Exiting")
+88
View File
@@ -0,0 +1,88 @@
import datetime
import logging
import time
import tracemalloc
from aprsd import client, messaging, packets, stats, utils
from aprsd.threads import APRSDThread, APRSDThreadList
LOG = logging.getLogger("APRSD")
class KeepAliveThread(APRSDThread):
cntr = 0
checker_time = datetime.datetime.now()
def __init__(self, config):
tracemalloc.start()
super().__init__("KeepAlive")
self.config = config
max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0}
self.max_delta = datetime.timedelta(**max_timeout)
def loop(self):
if self.cntr % 60 == 0:
tracker = messaging.MsgTrack()
stats_obj = stats.APRSDStats()
pl = packets.PacketList()
thread_list = APRSDThreadList()
now = datetime.datetime.now()
last_email = stats_obj.email_thread_time
if last_email:
email_thread_time = utils.strfdelta(now - last_email)
else:
email_thread_time = "N/A"
last_msg_time = utils.strfdelta(now - stats_obj.aprsis_keepalive)
current, peak = tracemalloc.get_traced_memory()
stats_obj.set_memory(current)
stats_obj.set_memory_peak(peak)
try:
login = self.config["aprsd"]["callsign"]
except KeyError:
login = self.config["ham"]["callsign"]
keepalive = (
"{} - Uptime {} RX:{} TX:{} Tracker:{} Msgs TX:{} RX:{} "
"Last:{} Email: {} - RAM Current:{} Peak:{} Threads:{}"
).format(
login,
utils.strfdelta(stats_obj.uptime),
pl.total_recv,
pl.total_tx,
len(tracker),
stats_obj.msgs_tx,
stats_obj.msgs_rx,
last_msg_time,
email_thread_time,
utils.human_size(current),
utils.human_size(peak),
len(thread_list),
)
LOG.info(keepalive)
# See if we should reset the aprs-is client
# Due to losing a keepalive from them
delta_dict = utils.parse_delta_str(last_msg_time)
delta = datetime.timedelta(**delta_dict)
if delta > self.max_delta:
# We haven't gotten a keepalive from aprs-is in a while
# reset the connection.a
if not client.KISSClient.is_enabled(self.config):
LOG.warning(f"Resetting connection to APRS-IS {delta}")
client.factory.create().reset()
# Check version every hour
delta = now - self.checker_time
if delta > datetime.timedelta(hours=1):
self.checker_time = now
level, msg = utils._check_version()
if level:
LOG.warning(msg)
self.cntr += 1
time.sleep(1)
return True
+236
View File
@@ -0,0 +1,236 @@
import abc
import logging
import time
import aprslib
from aprsd import client, messaging, packets, plugin, stats
from aprsd.threads import APRSDThread
LOG = logging.getLogger("APRSD")
class APRSDRXThread(APRSDThread):
def __init__(self, msg_queues, config):
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.factory.create().client.stop()
def loop(self):
# setup the consumer of messages and block until a messages
try:
# This will register a packet consumer with aprslib
# When new packets come in the consumer will process
# the packet
# Do a partial here because the consumer signature doesn't allow
# For kwargs to be passed in to the consumer func we declare
# and the aprslib developer didn't want to allow a PR to add
# kwargs. :(
# https://github.com/rossengeorgiev/aprs-python/pull/56
self._client.client.consumer(
self.process_packet, raw=False, blocking=False,
)
except (
aprslib.exceptions.ConnectionDrop,
aprslib.exceptions.ConnectionError,
):
LOG.error("Connection dropped, reconnecting")
time.sleep(5)
# Force the deletion of the client object connected to aprs
# This will cause a reconnect, next time client.get_client()
# is called
self._client.reset()
# Continue to loop
return True
@abc.abstractmethod
def process_packet(self, *args, **kwargs):
pass
class APRSDPluginRXThread(APRSDRXThread):
"""Process received packets.
This is the main APRSD Server command thread that
receives packets from APRIS and then sends them for
processing in the PluginProcessPacketThread.
"""
def process_packet(self, *args, **kwargs):
packet = self._client.decode_packet(*args, **kwargs)
thread = APRSDPluginProcessPacketThread(
config=self.config,
packet=packet,
)
thread.start()
class APRSDProcessPacketThread(APRSDThread):
"""Base class for processing received packets.
This is the base class for processing packets coming from
the consumer. This base class handles sending ack packets and
will ack a message before sending the packet to the subclass
for processing."""
def __init__(self, config, packet):
self.config = config
self.packet = packet
name = self.packet["raw"][:10]
super().__init__(f"RXPKT-{name}")
def process_ack_packet(self, packet):
ack_num = packet.get("msgNo")
LOG.info(f"Got ack for message {ack_num}")
messaging.log_message(
"RXACK",
packet["raw"],
None,
ack=ack_num,
fromcall=packet["from"],
)
tracker = messaging.MsgTrack()
tracker.remove(ack_num)
stats.APRSDStats().ack_rx_inc()
return
def loop(self):
"""Process a packet received from aprs-is server."""
packet = self.packet
packets.PacketList().add(packet)
fromcall = packet["from"]
tocall = packet.get("addresse", None)
msg = packet.get("message_text", None)
msg_id = packet.get("msgNo", "0")
msg_response = packet.get("response", None)
# LOG.debug(f"Got packet from '{fromcall}' - {packet}")
# We don't put ack packets destined for us through the
# plugins.
if (
tocall
and tocall.lower() == self.config["aprsd"]["callsign"].lower()
and msg_response == "ack"
):
self.process_ack_packet(packet)
else:
# It's not an ACK for us, so lets run it through
# the plugins.
messaging.log_message(
"Received Message",
packet["raw"],
msg,
fromcall=fromcall,
msg_num=msg_id,
)
# Only ack messages that were sent directly to us
if (
tocall
and tocall.lower() == self.config["aprsd"]["callsign"].lower()
):
stats.APRSDStats().msgs_rx_inc()
# let any threads do their thing, then ack
# send an ack last
ack = messaging.AckMessage(
self.config["aprsd"]["callsign"],
fromcall,
msg_id=msg_id,
)
ack.send()
self.process_non_ack_packet(packet)
else:
LOG.info("Packet was not for us.")
LOG.debug("Packet processing complete")
@abc.abstractmethod
def process_non_ack_packet(self, *args, **kwargs):
"""Ack packets already dealt with here."""
class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
"""Process the packet through the plugin manager.
This is the main aprsd server plugin processing thread."""
def process_non_ack_packet(self, packet):
"""Send the packet through the plugins."""
fromcall = packet["from"]
tocall = packet.get("addresse", None)
msg = packet.get("message_text", None)
packet.get("msgNo", "0")
packet.get("response", None)
pm = plugin.PluginManager()
try:
results = pm.run(packet)
wl = packets.WatchList()
wl.update_seen(packet)
replied = False
for reply in results:
if isinstance(reply, list):
# one of the plugins wants to send multiple messages
replied = True
for subreply in reply:
LOG.debug(f"Sending '{subreply}'")
if isinstance(subreply, messaging.Message):
subreply.send()
else:
msg = messaging.TextMessage(
self.config["aprsd"]["callsign"],
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
# us that they processed the message correctly, but have
# nothing to reply with, so we avoid replying with a
# usage string
if reply is not messaging.NULL_MESSAGE:
LOG.debug(f"Sending '{reply}'")
msg = messaging.TextMessage(
self.config["aprsd"]["callsign"],
fromcall,
reply,
)
msg.send()
# If the message was for us and we didn't have a
# response, then we send a usage statement.
if tocall == self.config["aprsd"]["callsign"] and not replied:
LOG.warning("Sending help!")
msg = messaging.TextMessage(
self.config["aprsd"]["callsign"],
fromcall,
"Unknown command! Send 'help' message for help",
)
msg.send()
except Exception as ex:
LOG.error("Plugin failed!!!")
LOG.exception(ex)
# Do we need to send a reply?
if tocall == self.config["aprsd"]["callsign"]:
reply = "A Plugin failed! try again?"
msg = messaging.TextMessage(
self.config["aprsd"]["callsign"],
fromcall,
reply,
)
msg.send()
+10 -51
View File
@@ -1,26 +1,24 @@
"""Utilities and helper functions."""
import collections
import errno
import functools
import os
import re
import threading
import sys
import update_checker
import aprsd
from .fuzzyclock import fuzzy # noqa: F401
# Make these available by anyone importing
# aprsd.utils
from .ring_buffer import RingBuffer # noqa: F401
def synchronized(wrapped):
lock = threading.Lock()
@functools.wraps(wrapped)
def _wrap(*args, **kwargs):
with lock:
return wrapped(*args, **kwargs)
return _wrap
if sys.version_info.major == 3 and sys.version_info.minor >= 3:
from collections.abc import MutableMapping
else:
from collections import MutableMapping
def env(*vars, **kwargs):
@@ -113,7 +111,7 @@ def flatten_dict(d, parent_key="", sep="."):
items = []
for k, v in d.items():
new_key = parent_key + sep + k if parent_key else k
if isinstance(v, collections.MutableMapping):
if isinstance(v, MutableMapping):
items.extend(flatten_dict(v, new_key, sep=sep).items())
else:
items.append((new_key, v))
@@ -129,42 +127,3 @@ def parse_delta_str(s):
else:
m = re.match(r"(?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d[\.\d+]*)", s)
return {key: float(val) for key, val in m.groupdict().items()}
class RingBuffer:
"""class that implements a not-yet-full buffer"""
def __init__(self, size_max):
self.max = size_max
self.data = []
class __Full:
"""class that implements a full buffer"""
def append(self, x):
"""Append an element overwriting the oldest one."""
self.data[self.cur] = x
self.cur = (self.cur + 1) % self.max
def get(self):
"""return list of elements in correct order"""
return self.data[self.cur :] + self.data[: self.cur]
def __len__(self):
return len(self.data)
def append(self, x):
"""append an element at the end of the buffer"""
self.data.append(x)
if len(self.data) == self.max:
self.cur = 0
# Permanently change self's class from non-full to full
self.__class__ = self.__Full
def get(self):
"""Return a list of elements from the oldest to the newest."""
return self.data
def __len__(self):
return len(self.data)
+37
View File
@@ -0,0 +1,37 @@
class RingBuffer:
"""class that implements a not-yet-full buffer"""
def __init__(self, size_max):
self.max = size_max
self.data = []
class __Full:
"""class that implements a full buffer"""
def append(self, x):
"""Append an element overwriting the oldest one."""
self.data[self.cur] = x
self.cur = (self.cur + 1) % self.max
def get(self):
"""return list of elements in correct order"""
return self.data[self.cur :] + self.data[: self.cur]
def __len__(self):
return len(self.data)
def append(self, x):
"""append an element at the end of the buffer"""
self.data.append(x)
if len(self.data) == self.max:
self.cur = 0
# Permanently change self's class from non-full to full
self.__class__ = self.__Full
def get(self):
"""Return a list of elements from the oldest to the newest."""
return self.data
def __len__(self):
return len(self.data)
View File
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

+94
View File
@@ -0,0 +1,94 @@
body {
background: #eeeeee;
margin: 2em;
text-align: center;
font-family: system-ui, sans-serif;
}
footer {
padding: 2em;
text-align: center;
height: 10vh;
}
.ui.segment {
background: #eeeeee;
}
ul.list {
list-style-type: disc;
}
ul.list li {
list-style-position: outside;
}
#left {
margin-right: 2px;
height: 300px;
}
#right {
height: 300px;
}
#center {
height: 300px;
}
#title {
font-size: 4em;
}
#version{
font-size: .5em;
}
#uptime, #aprsis {
font-size: 1em;
}
#callsign {
font-size: 1.4em;
color: #00F;
padding-top: 8px;
margin:10px;
}
#title_rx {
background-color: darkseagreen;
text-align: left;
}
#title_tx {
background-color: lightcoral;
text-align: left;
}
.aprsd_1 {
background-image: url(/static/images/aprs-symbols-16-0.png);
background-repeat: no-repeat;
background-position: -160px -48px;
width: 16px;
height: 16px;
}
#msgsTabsDiv .ui.tab {
margin:0px;
padding:0px;
display: block;
}
#msgsTabsDiv .header, .tiny.text, .content, .break,
.thumbs.down.outline.icon,
.phone.volume.icon
{
display: inline-block;
float: left;
position: relative;
}
#msgsTabsDiv .tiny.text {
width:100px;
}
#msgsTabsDiv .tiny.header {
width:100px;
text-align: left;
}
#msgsTabsDiv .break {
margin: 2px;
text-align: left;
}
File diff suppressed because one or more lines are too long
+41
View File
@@ -0,0 +1,41 @@
* {box-sizing: border-box}
/* Style the tab */
.tab {
border: 1px solid #ccc;
background-color: #f1f1f1;
height: 450px;
}
/* Style the buttons inside the tab */
.tab div {
display: block;
background-color: inherit;
color: black;
padding: 10px;
width: 100%;
border: none;
outline: none;
text-align: left;
cursor: pointer;
transition: 0.3s;
font-size: 17px;
}
/* Change background color of buttons on hover */
.tab div:hover {
background-color: #ddd;
}
/* Create an active/current "tab button" class */
.tab div.active {
background-color: #ccc;
}
/* Style the tab content */
.tabcontent {
border: 1px solid #ccc;
height: 450px;
overflow-y: scroll;
background-color: white;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+73
View File
@@ -0,0 +1,73 @@
function init_gps() {
console.log("init_gps Called.")
$("#send_beacon").click(function() {
console.log("Send a beacon!")
getLocation();
});
}
function getLocation() {
if (navigator.geolocation) {
console.log("getCurrentPosition");
try {
navigator.geolocation.getCurrentPosition(
showPosition, showError,
{timeout:3000});
} catch(err) {
console.log("Failed to getCurrentPosition");
console.log(err);
}
} else {
var msg = "Geolocation is not supported by this browser."
console.log(msg);
alert(msg)
}
}
function showError(error) {
console.log("showError");
console.log(error);
var msg = "";
switch(error.code) {
case error.PERMISSION_DENIED:
msg = "User denied the request for Geolocation."
break;
case error.POSITION_UNAVAILABLE:
msg = "Location information is unavailable."
break;
case error.TIMEOUT:
msg = "The location fix timed out."
break;
case error.UNKNOWN_ERROR:
msg = "An unknown error occurred."
break;
}
console.log(msg);
$.toast({
title: 'GPS Error',
class: 'warning',
position: 'middle center',
message: msg,
showProgress: 'top',
classProgress: 'blue',
});
}
function showPosition(position) {
console.log("showPosition Called");
msg = {
'latitude': position.coords.latitude,
'longitude': position.coords.longitude
}
console.log(msg);
$.toast({
title: 'Sending GPS Beacon',
message: "Latitude: "+position.coords.latitude+"<br>Longitude: "+position.coords.longitude,
showProgress: 'bottom',
classProgress: 'red'
});
console.log("Sending GPS msg")
socket.emit("gps", msg);
}
+44
View File
@@ -0,0 +1,44 @@
function aprs_img(item, x_offset, y_offset) {
var x = x_offset * -16;
if (y_offset > 5) {
y_offset = 5;
}
var y = y_offset * -16;
var loc = x + 'px '+ y + 'px'
item.css('background-position', loc);
}
function show_aprs_icon(item, symbol) {
var offset = ord(symbol) - 33;
var col = Math.floor(offset / 16);
var row = offset % 16;
//console.log("'" + symbol+"' off: "+offset+" row: "+ row + " col: " + col)
aprs_img(item, row, col);
}
function ord(str){return str.charCodeAt(0);}
function update_stats( data ) {
$("#version").text( data["stats"]["aprsd"]["version"] );
$("#aprs_connection").html( data["aprs_connection"] );
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] );
short_time = data["time"].split(/\s(.+)/)[1];
}
function start_update() {
(function statsworker() {
$.ajax({
url: "/stats",
type: 'GET',
dataType: 'json',
success: function(data) {
update_stats(data);
},
complete: function() {
setTimeout(statsworker, 10000);
}
});
})();
}
@@ -0,0 +1,223 @@
var cleared = false;
var callsign_list = {};
var message_list = {};
function size_dict(d){c=0; for (i in d) ++c; return c}
function init_chat() {
const socket = io("/sendmsg");
socket.on('connect', function () {
console.log("Connected to socketio");
});
socket.on('connected', function(msg) {
console.log("Connected!");
console.log(msg);
});
socket.on("sent", function(msg) {
if (cleared == false) {
var msgsdiv = $("#msgsTabsDiv");
msgsdiv.html('')
cleared = true
}
sent_msg(msg);
});
socket.on("ack", function(msg) {
update_msg(msg);
});
socket.on("new", function(msg) {
if (cleared == false) {
var msgsdiv = $("#msgsTabsDiv");
msgsdiv.html('')
cleared = true
}
from_msg(msg);
});
$("#sendform").submit(function(event) {
event.preventDefault();
msg = {'to': $('#to_call').val().toUpperCase(),
'message': $('#message').val(),
}
socket.emit("send", msg);
$('#message').val('');
$('#to_call').val('');
});
init_gps();
}
function add_callsign(callsign) {
/* Ensure a callsign exists in the left hand nav */
dropdown = $('#callsign_dropdown')
if (callsign in callsign_list) {
console.log(callsign+' already in list.')
return false
}
var callsignTabs = $("#callsignTabs");
tab_name = tab_string(callsign);
tab_content = tab_content_name(callsign);
divname = content_divname(callsign);
item_html = '<div class="active item" id="'+tab_name+'" onclick="openCallsign(event, \''+callsign+'\');">'+callsign+'</div>';
callsignTabs.append(item_html);
callsign_list[callsign] = {'name': callsign, 'value': callsign, 'text': callsign}
return true
}
function append_message(callsign, msg, msg_html) {
console.log('append_message');
new_callsign = false
if (!message_list.hasOwnProperty(callsign)) {
message_list[callsign] = new Array();
}
message_list[callsign].push(msg);
// Find the right div to place the html
new_callsign = add_callsign(callsign);
append_message_html(callsign, msg_html, new_callsign);
if (new_callsign) {
//click on the new tab
click_div = '#'+tab_string(callsign);
console.log("Click on "+click_div);
$(click_div).click();
}
}
function tab_string(callsign) {
return "msgs"+callsign;
}
function tab_content_name(callsign) {
return tab_string(callsign)+"Content";
}
function content_divname(callsign) {
return "#"+tab_content_name(callsign);
}
function append_message_html(callsign, msg_html, new_callsign) {
var msgsTabs = $('#msgsTabsDiv');
divname_str = tab_content_name(callsign);
divname = content_divname(callsign);
if (new_callsign) {
// we have to add a new DIV
msg_div_html = '<div class="tabcontent" id="'+divname_str+'" style="height:450px;">'+msg_html+'</div>';
msgsTabs.append(msg_div_html);
} else {
var msgDiv = $(divname);
msgDiv.append(msg_html);
}
$(divname).animate({scrollTop: $(divname)[0].scrollHeight}, "slow");
}
function create_message_html(time, from, to, message, ack) {
msg_html = '<div class="item">';
msg_html += '<div class="tiny text">'+time+'</div>';
msg_html += '<div class="middle aligned content">';
msg_html += '<div class="tiny red header">'+from+'</div>';
if (ack) {
msg_html += '<i class="thumbs down outline icon" id="' + ack_id + '" data-content="Waiting for ACK"></i>';
} else {
msg_html += '<i class="phone volume icon" data-content="Recieved Message"></i>';
}
msg_html += '<div class="middle aligned content">>&nbsp;&nbsp;&nbsp;</div>';
msg_html += '</div>';
msg_html += '<div class="middle aligned content">'+message+'</div>';
msg_html += '</div><br>';
return msg_html
}
function sent_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
ts_str = msg["ts"].toString();
ts = ts_str.split(".")[0]*1000;
id = ts_str.split('.')[0]
ack_id = "ack_" + id
var d = new Date(ts).toLocaleDateString("en-US")
var t = new Date(ts).toLocaleTimeString("en-US")
msg_html = create_message_html(t, msg['from'], msg['to'], msg['message'], ack_id);
append_message(msg['to'], msg, msg_html);
}
function from_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
// We have an existing entry
ts_str = msg["ts"].toString();
ts = ts_str.split(".")[0]*1000;
id = ts_str.split('.')[0]
ack_id = "ack_" + id
var d = new Date(ts).toLocaleDateString("en-US")
var t = new Date(ts).toLocaleTimeString("en-US")
from = msg['from']
msg_html = create_message_html(t, from, false, msg['message'], false);
append_message(from, msg, msg_html);
}
function update_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
// We have an existing entry
ts_str = msg["ts"].toString();
id = ts_str.split('.')[0]
pretty_id = "pretty_" + id
loader_id = "loader_" + id
ack_id = "ack_" + id
span_id = "span_" + id
if (msg['ack'] == true) {
var loader_div = $('#' + loader_id);
var ack_div = $('#' + ack_id);
loader_div.removeClass('ui active inline loader');
loader_div.addClass('ui disabled loader');
ack_div.removeClass('thumbs up outline icon');
ack_div.addClass('thumbs up outline icon');
}
$('.ui.accordion').accordion('refresh');
}
function callsign_select(callsign) {
var tocall = $("#to_call");
tocall.val(callsign);
}
function reset_Tabs() {
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
}
function openCallsign(evt, callsign) {
var i, tabcontent, tablinks;
tab_content = tab_content_name(callsign);
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(tab_content).style.display = "block";
evt.target.className += " active";
callsign_select(callsign);
}
+216
View File
@@ -0,0 +1,216 @@
var cleared = false;
var callsign_list = {};
var message_list = {};
const socket = io("/sendmsg");
function size_dict(d){c=0; for (i in d) ++c; return c}
function init_chat() {
socket.on('connect', function () {
console.log("Connected to socketio");
});
socket.on('connected', function(msg) {
console.log("Connected!");
console.log(msg);
});
socket.on("sent", function(msg) {
if (cleared == false) {
var msgsdiv = $("#msgsTabsDiv");
msgsdiv.html('')
cleared = true
}
sent_msg(msg);
});
socket.on("ack", function(msg) {
update_msg(msg);
});
socket.on("new", function(msg) {
if (cleared == false) {
var msgsdiv = $("#msgsTabsDiv");
msgsdiv.html('')
cleared = true
}
from_msg(msg);
});
$("#sendform").submit(function(event) {
event.preventDefault();
msg = {'to': $('#to_call').val(),
'message': $('#message').val(),
}
socket.emit("send", msg);
$('#message').val('');
});
init_gps();
}
function add_callsign(callsign) {
/* Ensure a callsign exists in the left hand nav */
if (callsign in callsign_list) {
return false
}
var callsignTabs = $("#callsignTabs");
tab_name = tab_string(callsign);
tab_content = tab_content_name(callsign);
divname = content_divname(callsign);
item_html = '<div class="tablinks" id="'+tab_name+'" onclick="openCallsign(event, \''+callsign+'\');">'+callsign+'</div>';
callsignTabs.append(item_html);
callsign_list[callsign] = true;
return true
}
function append_message(callsign, msg, msg_html) {
new_callsign = false
if (!message_list.hasOwnProperty(callsign)) {
message_list[callsign] = new Array();
}
message_list[callsign].push(msg);
// Find the right div to place the html
new_callsign = add_callsign(callsign);
append_message_html(callsign, msg_html, new_callsign);
if (new_callsign) {
//click on the new tab
click_div = '#'+tab_string(callsign);
$(click_div).click();
}
}
function tab_string(callsign) {
return "msgs"+callsign;
}
function tab_content_name(callsign) {
return tab_string(callsign)+"Content";
}
function content_divname(callsign) {
return "#"+tab_content_name(callsign);
}
function append_message_html(callsign, msg_html, new_callsign) {
var msgsTabs = $('#msgsTabsDiv');
divname_str = tab_content_name(callsign);
divname = content_divname(callsign);
if (new_callsign) {
// we have to add a new DIV
msg_div_html = '<div class="tabcontent" id="'+divname_str+'" style="height:450px;">'+msg_html+'</div>';
msgsTabs.append(msg_div_html);
} else {
var msgDiv = $(divname);
msgDiv.append(msg_html);
}
$(divname).animate({scrollTop: $(divname)[0].scrollHeight}, "slow");
}
function create_message_html(time, from, to, message, ack) {
msg_html = '<div class="item">';
msg_html += '<div class="tiny text">'+time+'</div>';
msg_html += '<div class="middle aligned content">';
msg_html += '<div class="tiny red header">'+from+'</div>';
if (ack) {
msg_html += '<i class="thumbs down outline icon" id="' + ack_id + '" data-content="Waiting for ACK"></i>';
} else {
msg_html += '<i class="phone volume icon" data-content="Recieved Message"></i>';
}
msg_html += '<div class="middle aligned content">>&nbsp;&nbsp;&nbsp;</div>';
msg_html += '</div>';
msg_html += '<div class="middle aligned content">'+message+'</div>';
msg_html += '</div><br>';
return msg_html
}
function sent_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
ts_str = msg["ts"].toString();
ts = ts_str.split(".")[0]*1000;
id = ts_str.split('.')[0]
ack_id = "ack_" + id
var d = new Date(ts).toLocaleDateString("en-US")
var t = new Date(ts).toLocaleTimeString("en-US")
msg_html = create_message_html(t, msg['from'], msg['to'], msg['message'], ack_id);
append_message(msg['to'], msg, msg_html);
}
function from_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
// We have an existing entry
ts_str = msg["ts"].toString();
ts = ts_str.split(".")[0]*1000;
id = ts_str.split('.')[0]
ack_id = "ack_" + id
var d = new Date(ts).toLocaleDateString("en-US")
var t = new Date(ts).toLocaleTimeString("en-US")
from = msg['from']
msg_html = create_message_html(t, from, false, msg['message'], false);
append_message(from, msg, msg_html);
}
function update_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
// We have an existing entry
ts_str = msg["ts"].toString();
id = ts_str.split('.')[0]
pretty_id = "pretty_" + id
loader_id = "loader_" + id
ack_id = "ack_" + id
span_id = "span_" + id
if (msg['ack'] == true) {
var loader_div = $('#' + loader_id);
var ack_div = $('#' + ack_id);
loader_div.removeClass('ui active inline loader');
loader_div.addClass('ui disabled loader');
ack_div.removeClass('thumbs up outline icon');
ack_div.addClass('thumbs up outline icon');
}
$('.ui.accordion').accordion('refresh');
}
function callsign_select(callsign) {
var tocall = $("#to_call");
tocall.val(callsign);
}
function reset_Tabs() {
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
}
function openCallsign(evt, callsign) {
var i, tabcontent, tablinks;
tab_content = tab_content_name(callsign);
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(tab_content).style.display = "block";
evt.target.className += " active";
callsign_select(callsign);
}
+28
View File
@@ -0,0 +1,28 @@
function openTab(evt, tabName) {
// Declare all variables
var i, tabcontent, tablinks;
if (typeof tabName == 'undefined') {
return
}
// Get all elements with class="tabcontent" and hide them
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
// Get all elements with class="tablinks" and remove the class "active"
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
// Show the current tab, and add an "active" class to the button that opened the tab
document.getElementById(tabName).style.display = "block";
if (typeof evt.currentTarget == 'undefined') {
return
} else {
evt.currentTarget.className += " active";
}
}
@@ -0,0 +1,57 @@
/* Root element */
.json-document {
padding: 1em 2em;
}
/* Syntax highlighting for JSON objects */
ul.json-dict, ol.json-array {
list-style-type: none;
margin: 0 0 0 1px;
border-left: 1px dotted #ccc;
padding-left: 2em;
}
.json-string {
color: #0B7500;
}
.json-literal {
color: #1A01CC;
font-weight: bold;
}
/* Toggle button */
a.json-toggle {
position: relative;
color: inherit;
text-decoration: none;
}
a.json-toggle:focus {
outline: none;
}
a.json-toggle:before {
font-size: 1.1em;
color: #c0c0c0;
content: "\25BC"; /* down arrow */
position: absolute;
display: inline-block;
width: 1em;
text-align: center;
line-height: 1em;
left: -1.2em;
}
a.json-toggle:hover:before {
color: #aaa;
}
a.json-toggle.collapsed:before {
/* Use rotated down arrow, prevents right arrow appearing smaller than down arrow in some browsers */
transform: rotate(-90deg);
}
/* Collapsable placeholder links */
a.json-placeholder {
color: #aaa;
padding: 0 1em;
text-decoration: none;
}
a.json-placeholder:hover {
text-decoration: underline;
}
@@ -0,0 +1,158 @@
/**
* jQuery json-viewer
* @author: Alexandre Bodelot <alexandre.bodelot@gmail.com>
* @link: https://github.com/abodelot/jquery.json-viewer
*/
(function($) {
/**
* Check if arg is either an array with at least 1 element, or a dict with at least 1 key
* @return boolean
*/
function isCollapsable(arg) {
return arg instanceof Object && Object.keys(arg).length > 0;
}
/**
* Check if a string represents a valid url
* @return boolean
*/
function isUrl(string) {
var urlRegexp = /^(https?:\/\/|ftps?:\/\/)?([a-z0-9%-]+\.){1,}([a-z0-9-]+)?(:(\d{1,5}))?(\/([a-z0-9\-._~:/?#[\]@!$&'()*+,;=%]+)?)?$/i;
return urlRegexp.test(string);
}
/**
* Transform a json object into html representation
* @return string
*/
function json2html(json, options) {
var html = '';
if (typeof json === 'string') {
// Escape tags and quotes
json = json
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/'/g, '&apos;')
.replace(/"/g, '&quot;');
if (options.withLinks && isUrl(json)) {
html += '<a href="' + json + '" class="json-string" target="_blank">' + json + '</a>';
} else {
// Escape double quotes in the rendered non-URL string.
json = json.replace(/&quot;/g, '\\&quot;');
html += '<span class="json-string">"' + json + '"</span>';
}
} else if (typeof json === 'number') {
html += '<span class="json-literal">' + json + '</span>';
} else if (typeof json === 'boolean') {
html += '<span class="json-literal">' + json + '</span>';
} else if (json === null) {
html += '<span class="json-literal">null</span>';
} else if (json instanceof Array) {
if (json.length > 0) {
html += '[<ol class="json-array">';
for (var i = 0; i < json.length; ++i) {
html += '<li>';
// Add toggle button if item is collapsable
if (isCollapsable(json[i])) {
html += '<a href class="json-toggle"></a>';
}
html += json2html(json[i], options);
// Add comma if item is not last
if (i < json.length - 1) {
html += ',';
}
html += '</li>';
}
html += '</ol>]';
} else {
html += '[]';
}
} else if (typeof json === 'object') {
var keyCount = Object.keys(json).length;
if (keyCount > 0) {
html += '{<ul class="json-dict">';
for (var key in json) {
if (Object.prototype.hasOwnProperty.call(json, key)) {
html += '<li>';
var keyRepr = options.withQuotes ?
'<span class="json-string">"' + key + '"</span>' : key;
// Add toggle button if item is collapsable
if (isCollapsable(json[key])) {
html += '<a href class="json-toggle">' + keyRepr + '</a>';
} else {
html += keyRepr;
}
html += ': ' + json2html(json[key], options);
// Add comma if item is not last
if (--keyCount > 0) {
html += ',';
}
html += '</li>';
}
}
html += '</ul>}';
} else {
html += '{}';
}
}
return html;
}
/**
* jQuery plugin method
* @param json: a javascript object
* @param options: an optional options hash
*/
$.fn.jsonViewer = function(json, options) {
// Merge user options with default options
options = Object.assign({}, {
collapsed: false,
rootCollapsable: true,
withQuotes: false,
withLinks: true
}, options);
// jQuery chaining
return this.each(function() {
// Transform to HTML
var html = json2html(json, options);
if (options.rootCollapsable && isCollapsable(json)) {
html = '<a href class="json-toggle"></a>' + html;
}
// Insert HTML in target DOM element
$(this).html(html);
$(this).addClass('json-document');
// Bind click on toggle buttons
$(this).off('click');
$(this).on('click', 'a.json-toggle', function() {
var target = $(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array');
target.toggle();
if (target.is(':visible')) {
target.siblings('.json-placeholder').remove();
} else {
var count = target.children('li').length;
var placeholder = count + (count > 1 ? ' items' : ' item');
target.after('<a href class="json-placeholder">' + placeholder + '</a>');
}
return false;
});
// Simulate click on toggle button when placeholder is clicked
$(this).on('click', 'a.json-placeholder', function() {
$(this).siblings('a.json-toggle').click();
return false;
});
if (options.collapsed == true) {
// Trigger click to collapse all nodes
$(this).find('a.json-toggle').click();
}
});
};
})(jQuery);
+85
View File
@@ -0,0 +1,85 @@
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.js"></script>
<link rel="stylesheet" href="/static/css/index.css">
<link rel="stylesheet" href="/static/css/tabs.css">
<script src="/static/js/main.js"></script>
<script src="/static/js/gps.js"></script>
<script src="/static/js/send-message.js"></script>
<script type="text/javascript">
var initial_stats = {{ initial_stats|tojson|safe }};
var memory_chart = null
var message_chart = null
$(document).ready(function() {
console.log(initial_stats);
start_update();
init_chat();
reset_Tabs();
});
</script>
</head>
<body>
<div class='ui text container'>
<h1 class='ui dividing header'>APRSD WebChat {{ version }}</h1>
</div>
<div class='ui grid text container'>
<div class='left floated ten wide column'>
<span style='color: green'>{{ callsign }}</span>
connected to
<span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
</div>
<div class='right floated four wide column'>
<span id='uptime'>NONE</span>
</div>
</div>
<div class="ui container">
<h3 class="ui dividing header">Send Message</h3>
<div id="sendMsgDiv" class="ui mini text">
<form id="sendform" name="sendmsg" action="">
<div class="ui corner labeled input">
<label for="to_call" class="ui label">Callsign</label>
<input type="text" name="to_call" id="to_call" placeholder="To Callsign" size="11" maxlength="9">
<div class="ui corner label">
<i class="asterisk icon"></i>
</div>
</div>
<div class="ui labeled input">
<label for="message" class="ui label">Message</label>
<input type="text" name="message" id="message" size="40" maxlength="40">
</div>
<input type="submit" name="submit" class="ui button" id="send_msg" value="Send" />
<button type="button" class="ui button" id="send_beacon" value="Send GPS Beacon">Send GPS Beacon</button>
</form>
</div>
</div>
<div class="ui grid">
<div class="three wide column">
<div class="tab" id="callsignTabs"></div>
</div>
<div class="ten wide column ui raised segment" id="msgsTabsDiv" style="height:450px;padding:0px;">
&nbsp;
</div>
</div>
<div class="ui text container" style="padding-top: 40px">
<a href="https://badge.fury.io/py/aprsd"><img src="https://badge.fury.io/py/aprsd.svg" alt="PyPI version" height="18"></a>
<a href="https://github.com/craigerl/aprsd"><img src="https://img.shields.io/badge/Made%20with-Python-1f425f.svg" height="18"></a>
</div>
</body>
</html>
+82
View File
@@ -0,0 +1,82 @@
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.js"></script>
<link rel="stylesheet" href="/static/css/index.css">
<script src="/static/js/main.js"></script>
<script src="/static/js/gps.js"></script>
<script src="/static/js/send-message-mobile.js"></script>
<script type="text/javascript">
var initial_stats = {{ initial_stats|tojson|safe }};
var memory_chart = null
var message_chart = null
$(document).ready(function() {
console.log(initial_stats);
start_update();
init_chat();
});
</script>
</head>
<body>
<div class='ui text container'>
<h1 class='ui dividing header'>APRSD WebChat {{ version }}</h1>
</div>
<div class='ui grid text container' style="padding-bottom: 5px;">
<div class='left floated twelve wide column'>
<span style='color: green'>{{ callsign }}</span>
connected to
<span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
</div>
<div class='right floated four wide column'>
<span id='uptime'>NONE</span>
</div>
</div>
<div id="sendMsgDiv" class="ui grid" align="left" style="padding-top: 2px;">
<h3 class="sixteen wide column ui dividing header">Send Message</h3>
<form id="sendform" name="sendmsg" action="">
<div class="sixteen wide column ui left labeled icon input">
<div class="ui label">Callsign</div>
<input type="text" name="to_call" id="to_call" placeholder="To Callsign" size="11" maxlength="9">
<i class="users icon"></i>
</div>
<div class="sixteen wide column ui left labeled icon input" style="padding-bottom: 5px;">
<label for="message" class="ui label">Message</label>
<input type="text" name="message" id="message" maxlength="40" placeholder="Message">
<i class="comment icon"></i>
</div>
<div class="right floated column">
<input type="submit" name="submit" class="ui button" id="send_msg" value="Send" />
<button type="button" class="ui button" id="send_beacon" value="Send GPS Beacon">Send GPS Beacon</button>
</div>
</form>
</div>
<div class="ui grid">
<div class="ui top attached tabular raised menu" id="callsignTabs">
</div>
<div class="sixteen wide column ui bottom attached raised tab segment" id="msgsTabsDiv" style="height:250px;padding:5px;">
&nbsp;
</div>
</div>
<div class="ui text container" style="padding-top: 40px">
<a href="https://badge.fury.io/py/aprsd"><img src="https://badge.fury.io/py/aprsd.svg" alt="PyPI version" height="18"></a>
<a href="https://github.com/craigerl/aprsd"><img src="https://img.shields.io/badge/Made%20with-Python-1f425f.svg" height="18"></a>
</div>
</body>
</html>
+6 -2
View File
@@ -4,9 +4,13 @@ mypy
pep8-naming
Sphinx
tox
twine
# Twine is used for uploading packages to pypi
# but it induces an install of cryptography
# This is sucky for rpi systems.
# twine
pre-commit
pip-tools
pytest
pytest-cov
gray
pip
pip-tools
+82 -247
View File
@@ -1,253 +1,88 @@
#
# This file is autogenerated by pip-compile with python 3.8
# To update, run:
# This file is autogenerated by pip-compile with Python 3.9
# by the following command:
#
# pip-compile dev-requirements.in
# pip-compile --annotation-style=line --resolver=backtracking dev-requirements.in
#
add-trailing-comma==2.1.0
# via gray
alabaster==0.7.12
# via sphinx
appdirs==1.4.4
# via black
attrs==21.2.0
# via
# jsonschema
# pytest
autoflake==1.4
# via gray
babel==2.9.1
# via sphinx
backports.entry-points-selectable==1.1.0
# via virtualenv
black==21.7b0
# via gray
bleach==4.1.0
# via readme-renderer
certifi==2021.5.30
# via requests
cfgv==3.3.1
# via pre-commit
charset-normalizer==2.0.4
# via requests
click==8.0.1
# via
# black
# pip-tools
colorama==0.4.4
# via twine
colorlog==6.4.1
# via prettylog
configargparse==1.5.2
# via gray
coverage==5.5
# via pytest-cov
distlib==0.3.2
# via virtualenv
docutils==0.17.1
# via
# readme-renderer
# sphinx
fast-json==0.3.2
# via prettylog
filelock==3.0.12
# via
# tox
# virtualenv
fixit==0.1.4
# via gray
flake8==3.9.2
# via
# -r dev-requirements.in
# fixit
# flake8-polyfill
# pep8-naming
flake8-polyfill==1.0.2
# via pep8-naming
gray==0.10.1
# via -r dev-requirements.in
identify==2.2.13
# via pre-commit
idna==3.2
# via requests
imagesize==1.2.0
# via sphinx
importlib-metadata==4.7.1
# via
# keyring
# twine
importlib-resources==5.2.2
# via fixit
iniconfig==1.1.1
# via pytest
isort==5.9.3
# via
# -r dev-requirements.in
# gray
jinja2==3.0.1
# via sphinx
jsonschema==3.2.0
# via fixit
keyring==23.1.0
# via twine
libcst==0.3.20
# via fixit
markupsafe==2.0.1
# via jinja2
mccabe==0.6.1
# via flake8
mypy==0.910
# via -r dev-requirements.in
mypy-extensions==0.4.3
# via
# black
# mypy
# typing-inspect
nodeenv==1.6.0
# via pre-commit
packaging==21.0
# via
# bleach
# pytest
# sphinx
# tox
pathspec==0.9.0
# via black
pep517==0.11.0
# via pip-tools
pep8-naming==0.12.1
# via -r dev-requirements.in
pip-tools==6.2.0
# via -r dev-requirements.in
pkginfo==1.7.1
# via twine
platformdirs==2.2.0
# via virtualenv
pluggy==1.0.0
# via
# pytest
# tox
pre-commit==2.14.0
# via -r dev-requirements.in
prettylog==0.3.0
# via gray
py==1.10.0
# via
# pytest
# tox
pycodestyle==2.7.0
# via flake8
pyflakes==2.3.1
# via
# autoflake
# flake8
pygments==2.10.0
# via
# readme-renderer
# sphinx
pyparsing==2.4.7
# via packaging
pyrsistent==0.18.0
# via jsonschema
pytest==6.2.5
# via
# -r dev-requirements.in
# pytest-cov
pytest-cov==2.12.1
# via -r dev-requirements.in
pytz==2021.1
# via babel
pyupgrade==2.24.0
# via gray
pyyaml==5.4.1
# via
# fixit
# libcst
# pre-commit
readme-renderer==29.0
# via twine
regex==2021.8.27
# via black
requests==2.26.0
# via
# requests-toolbelt
# sphinx
# twine
requests-toolbelt==0.9.1
# via twine
rfc3986==1.5.0
# via twine
six==1.16.0
# via
# bleach
# jsonschema
# readme-renderer
# tox
# virtualenv
snowballstemmer==2.1.0
# via sphinx
sphinx==4.1.2
# via -r dev-requirements.in
sphinxcontrib-applehelp==1.0.2
# via sphinx
sphinxcontrib-devhelp==1.0.2
# via sphinx
sphinxcontrib-htmlhelp==2.0.0
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
sphinxcontrib-qthelp==1.0.3
# via sphinx
sphinxcontrib-serializinghtml==1.1.5
# via sphinx
tokenize-rt==4.1.0
# via
# add-trailing-comma
# pyupgrade
toml==0.10.2
# via
# mypy
# pre-commit
# pytest
# pytest-cov
# tox
tomli==1.2.1
# via
# black
# pep517
tox==3.24.3
# via -r dev-requirements.in
tqdm==4.62.2
# via twine
twine==3.4.2
# via -r dev-requirements.in
typing-extensions==3.10.0.0
# via
# libcst
# mypy
# typing-inspect
typing-inspect==0.7.1
# via libcst
ujson==4.1.0
# via fast-json
unify==0.5
# via gray
untokenize==0.1.1
# via unify
urllib3==1.26.6
# via requests
virtualenv==20.7.2
# via
# pre-commit
# tox
webencodings==0.5.1
# via bleach
wheel==0.37.0
# via pip-tools
zipp==3.5.0
# via
# importlib-metadata
# importlib-resources
add-trailing-comma==2.3.0 # via gray
alabaster==0.7.12 # via sphinx
attrs==22.1.0 # via jsonschema, pytest
autoflake==1.5.3 # via gray
babel==2.11.0 # via sphinx
black==22.12.0 # via gray
build==0.9.0 # via pip-tools
cachetools==5.2.0 # via tox
certifi==2022.12.7 # via requests
cfgv==3.3.1 # via pre-commit
chardet==5.1.0 # via tox
charset-normalizer==2.1.1 # via requests
click==8.1.3 # via black, pip-tools
colorama==0.4.6 # via tox
commonmark==0.9.1 # via rich
configargparse==1.5.3 # via gray
coverage[toml]==6.5.0 # via pytest-cov
distlib==0.3.6 # via virtualenv
docutils==0.19 # via sphinx
exceptiongroup==1.0.4 # via pytest
filelock==3.8.2 # via tox, virtualenv
fixit==0.1.4 # via gray
flake8==6.0.0 # via -r dev-requirements.in, fixit, pep8-naming
gray==0.13.0 # via -r dev-requirements.in
identify==2.5.9 # via pre-commit
idna==3.4 # via requests
imagesize==1.4.1 # via sphinx
importlib-metadata==5.1.0 # via sphinx
importlib-resources==5.10.1 # via fixit
iniconfig==1.1.1 # via pytest
isort==5.10.1 # via -r dev-requirements.in, gray
jinja2==3.1.2 # via sphinx
jsonschema==4.17.3 # via fixit
libcst==0.4.9 # via fixit
markupsafe==2.1.1 # via jinja2
mccabe==0.7.0 # via flake8
mypy==0.991 # via -r dev-requirements.in
mypy-extensions==0.4.3 # via black, mypy, typing-inspect
nodeenv==1.7.0 # via pre-commit
packaging==22.0 # via build, pyproject-api, pytest, sphinx, tox
pathspec==0.10.3 # via black
pep517==0.13.0 # via build
pep8-naming==0.13.2 # via -r dev-requirements.in
pip-tools==6.11.0 # via -r dev-requirements.in
platformdirs==2.6.0 # via black, tox, virtualenv
pluggy==1.0.0 # via pytest, tox
pre-commit==2.20.0 # via -r dev-requirements.in
pycodestyle==2.10.0 # via flake8
pyflakes==3.0.1 # via autoflake, flake8
pygments==2.13.0 # via rich, sphinx
pyproject-api==1.2.1 # via tox
pyrsistent==0.19.2 # via jsonschema
pytest==7.2.0 # via -r dev-requirements.in, pytest-cov
pytest-cov==4.0.0 # via -r dev-requirements.in
pytz==2022.6 # via babel
pyupgrade==3.3.1 # via gray
pyyaml==6.0 # via fixit, libcst, pre-commit
requests==2.28.1 # via sphinx
rich==12.6.0 # via gray
snowballstemmer==2.2.0 # via sphinx
sphinx==5.3.0 # via -r dev-requirements.in
sphinxcontrib-applehelp==1.0.2 # via sphinx
sphinxcontrib-devhelp==1.0.2 # via sphinx
sphinxcontrib-htmlhelp==2.0.0 # via sphinx
sphinxcontrib-jsmath==1.0.1 # via sphinx
sphinxcontrib-qthelp==1.0.3 # via sphinx
sphinxcontrib-serializinghtml==1.1.5 # via sphinx
tokenize-rt==5.0.0 # via add-trailing-comma, pyupgrade
toml==0.10.2 # via autoflake, pre-commit
tomli==2.0.1 # via black, build, coverage, mypy, pep517, pyproject-api, pytest, tox
tox==4.0.8 # via -r dev-requirements.in
typing-extensions==4.4.0 # via black, libcst, mypy, typing-inspect
typing-inspect==0.8.0 # via libcst
unify==0.5 # via gray
untokenize==0.1.1 # via unify
urllib3==1.26.13 # via requests
virtualenv==20.17.1 # via pre-commit, tox
wheel==0.38.4 # via pip-tools
zipp==3.11.0 # via importlib-metadata, importlib-resources
# The following packages are considered to be unsafe in a requirements file:
# pip
+14 -2
View File
@@ -1,11 +1,13 @@
#FROM python:3-bullseye as aprsd
FROM ubuntu:focal as aprsd
FROM ubuntu:22.04 as aprsd
# Dockerfile for building a container during aprsd development.
ARG UID
ARG GID
ARG TZ
ARG VERSION=2.6.0
ARG BUILDX_QEMU_ENV
ENV APRS_USER=aprs
ENV HOME=/home/aprs
ENV TZ=${TZ:-US/Eastern}
@@ -13,6 +15,8 @@ ENV UID=${UID:-1000}
ENV GID=${GID:-1000}
ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8
ENV APRSD_PIP_VERSION=${VERSION}
ENV DEBIAN_FRONTEND=noninteractive
RUN apt update
@@ -23,8 +27,16 @@ RUN apt install -y python3 python3-pip python3-dev python3-lxml
RUN addgroup --gid $GID $APRS_USER
RUN useradd -m -u $UID -g $APRS_USER $APRS_USER
# Handle an extremely specific issue when building the cryptography package for
# 32-bit architectures within QEMU running on a 64-bit host (issue #30).
RUN if [ "${BUILDX_QEMU_ENV}" = "true" -a "$(getconf LONG_BIT)" = "32" ]; then \
pip3 install -U cryptography==3.3.2; \
else \
pip3 install cryptography ;\
fi
# Install aprsd
RUN pip install aprsd
RUN pip install aprsd==$APRSD_PIP_VERSION
# Ensure /config is there with a default config file
USER root
+12 -3
View File
@@ -2,14 +2,16 @@
FROM ubuntu:focal as aprsd
# Dockerfile for building a container during aprsd development.
ARG branch
ARG BRANCH=master
ARG UID
ARG GID
ARG BUILDX_QEMU_ENV
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}
@@ -39,8 +41,15 @@ RUN cat $HOME/.bashrc
USER root
WORKDIR $HOME
# Handle an extremely specific issue when building the cryptography package for
# 32-bit architectures within QEMU running on a 64-bit host (issue #30).
RUN if [ "${BUILDX_QEMU_ENV}" = "true" -a "$(getconf LONG_BIT)" = "32" ]; then \
pip3 install -U cryptography==3.3.2; \
else \
pip3 install cryptography ;\
fi
RUN mkdir $INSTALL
RUN git clone -b $APRSD_BRANCH $APRSD $INSTALL/aprsd
RUN git clone -b $BRANCH $APRSD $INSTALL/aprsd
RUN cd $INSTALL/aprsd && pip3 install -v .
RUN ls -al /usr/local/bin
RUN ls -al /usr/bin
+35 -17
View File
@@ -11,18 +11,23 @@ cat << EOF
usage: $0 options
OPTIONS:
-h Show help
-t The tag/version (${TAG}) (default = master)
-d Use Dockerfile-dev for a git clone build
-b Branch to use (default = master)
-r Destroy and rebuild the buildx environment
EOF
}
ALL_PLATFORMS=0
DEV=0
REBUILD_BUILDX=0
TAG="latest"
BRANCH="master"
BRANCH=${BRANCH:-master}
VERSION="2.6.0"
while getoptst:dab:” OPTION
while getoptshdart:b:v:” OPTION
do
case $OPTION in
t)
@@ -34,17 +39,26 @@ do
a)
ALL_PLATFORMS=1
;;
r)
REBUILD_BUILDX=1
;;
d)
DEV=1
;;
v)
VERSION=$OPTARG
;;
h)
usage
exit 0
;;
?)
usage
exit
exit -1
;;
esac
done
VERSION="2.5.8"
if [ $ALL_PLATFORMS -eq 1 ]
then
@@ -54,15 +68,19 @@ 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
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
echo "Creating new buildx container"
docker buildx create --name multiarch --driver docker-container --use --platform linux/arm/v7,linux/arm/v6,linux/arm64,linux/amd64 --config ./buildkit.toml --use --driver-opt image=moby/buildkit:master
docker buildx inspect --bootstrap
if [ $REBUILD_BUILDX -eq 1 ]
then
echo "Destroying old multiarch build container"
docker buildx rm multiarch
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
echo "Creating new buildx container"
docker buildx create --name multiarch --driver docker-container --use \
--config ./buildkit.toml --use \
--driver-opt image=moby/buildkit:master
docker buildx inspect --bootstrap
fi
if [ $DEV -eq 1 ]
then
@@ -70,17 +88,17 @@ then
# 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 --build-arg branch=$BRANCH --no-cache .
-f Dockerfile-dev --build-arg branch=$BRANCH \
--build-arg BUILDX_QEMU_ENV=true \
--no-cache .
else
# Use this script to locally build the docker image
echo "Build with tag=${TAG} BRANCH=${BRANCH} platforms?=${PLATFORMS}"
echo "Build with tag=${TAG} BRANCH=${BRANCH} dev?=${DEV} platforms?=${PLATFORMS} VERSION=${VERSION}"
docker buildx build --push --platform $PLATFORMS \
--allow security.insecure \
--build-arg VERSION=$VERSION \
--build-arg BUILDX_QEMU_ENV=true \
-t hemna6969/aprsd:$VERSION \
-t hemna6969/aprsd:$TAG \
-t hemna6969/aprsd:latest \
-t harbor.hemna.com/hemna6969/aprsd:$TAG \
-t harbor.hemna.com/hemna6969/aprsd:$VERSION \
-t harbor.hemna.com/hemna6969/aprsd:latest \
-f Dockerfile .
fi
+3 -1
View File
@@ -3,10 +3,12 @@ services:
aprsd:
image: hemna6969/aprsd:latest
container_name: aprsd
ports:
- "8001:8001"
volumes:
- $HOME/.config/aprsd:/config
restart: unless-stopped
environment:
- TZ=America/New_York
- APRS_PLUGINS=aprsd-slack-plugin>=1.0.2
- APRSD_PLUGINS=aprsd-slack-plugin>=1.0.2
- LOG_LEVEL=ERROR
+10 -7
View File
@@ -1,20 +1,16 @@
aioax25>=0.0.10
aprslib>=0.7.0
click
click-completion
flask
flask==2.1.2
werkzeug==2.1.2
flask-classful
flask-httpauth
imapclient
opencage
pluggy
pbr
pyyaml
# Allowing a newer version can lead to a conflict with
# requests.
py3-validate-email
pytz
requests
pytz
six
thesmuggler
update_checker
@@ -24,3 +20,10 @@ tabulate
rich
# For the list-plugins pypi.org search scraping
beautifulsoup4
wrapt
# kiss3 uses attrs
kiss3
attrs==22.1.0
# for mobile checking
user-agents
pyopenssl
+55 -127
View File
@@ -1,130 +1,58 @@
#
# This file is autogenerated by pip-compile with python 3.8
# To update, run:
# This file is autogenerated by pip-compile with Python 3.9
# by the following command:
#
# pip-compile requirements.in
# pip-compile --annotation-style=line --resolver=backtracking requirements.in
#
aioax25==0.0.10
# via -r requirements.in
aprslib==0.7.0
# via -r requirements.in
backoff==1.11.1
# via opencage
beautifulsoup4==4.10.0
# via -r requirements.in
bidict==0.21.2
# via python-socketio
certifi==2021.5.30
# via requests
cffi==1.14.6
# via cryptography
charset-normalizer==2.0.4
# via requests
click==8.0.1
# via
# -r requirements.in
# click-completion
# flask
click-completion==0.5.2
# via -r requirements.in
colorama==0.4.4
# via rich
commonmark==0.9.1
# via rich
contexter==0.1.4
# via signalslot
cryptography==3.4.7
# via pyopenssl
dnspython==2.1.0
# via
# eventlet
# py3-validate-email
eventlet==0.32.0
# via -r requirements.in
filelock==3.0.12
# via py3-validate-email
flask==2.0.1
# via
# -r requirements.in
# flask-classful
# flask-httpauth
# flask-socketio
flask-classful==0.14.2
# via -r requirements.in
flask-httpauth==4.4.0
# via -r requirements.in
flask-socketio==5.1.1
# via -r requirements.in
greenlet==1.1.1
# via eventlet
idna==3.2
# via
# py3-validate-email
# requests
imapclient==2.2.0
# via -r requirements.in
itsdangerous==2.0.1
# via flask
jinja2==3.0.1
# via
# click-completion
# flask
markupsafe==2.0.1
# via jinja2
opencage==2.0.0
# via -r requirements.in
pbr==5.6.0
# via -r requirements.in
pluggy==1.0.0
# via -r requirements.in
py3-validate-email==1.0.1
# via -r requirements.in
pycparser==2.20
# via cffi
pygments==2.10.0
# via rich
pyopenssl==20.0.1
# via opencage
pyserial==3.5
# via aioax25
python-engineio==4.2.1
# via python-socketio
python-socketio==5.4.0
# via flask-socketio
pytz==2021.1
# via -r requirements.in
pyyaml==5.4.1
# via -r requirements.in
requests==2.26.0
# via
# -r requirements.in
# opencage
# update-checker
rich==10.15.2
# via -r requirements.in
shellingham==1.4.0
# via click-completion
signalslot==0.1.2
# via aioax25
six==1.16.0
# via
# -r requirements.in
# click-completion
# eventlet
# imapclient
# pyopenssl
# signalslot
soupsieve==2.3.1
# via beautifulsoup4
tabulate==0.8.9
# via -r requirements.in
thesmuggler==1.0.1
# via -r requirements.in
update-checker==0.18.0
# via -r requirements.in
urllib3==1.26.6
# via requests
weakrefmethod==1.0.3
# via signalslot
werkzeug==2.0.0
# via flask
aprslib==0.7.2 # via -r requirements.in
attrs==22.1.0 # via -r requirements.in, ax253, kiss3
ax253==0.1.5.post1 # via kiss3
beautifulsoup4==4.11.1 # via -r requirements.in
bidict==0.22.0 # via python-socketio
bitarray==2.6.0 # via ax253, kiss3
certifi==2022.12.7 # via requests
cffi==1.15.1 # via cryptography
charset-normalizer==2.1.1 # via requests
click==8.1.3 # via -r requirements.in, click-completion, flask
click-completion==0.5.2 # via -r requirements.in
commonmark==0.9.1 # via rich
cryptography==38.0.4 # via pyopenssl
dnspython==2.2.1 # via eventlet
eventlet==0.33.2 # via -r requirements.in
flask==2.1.2 # via -r requirements.in, flask-classful, flask-httpauth, flask-socketio
flask-classful==0.14.2 # via -r requirements.in
flask-httpauth==4.7.0 # via -r requirements.in
flask-socketio==5.3.2 # via -r requirements.in
greenlet==2.0.1 # via eventlet
idna==3.4 # via requests
imapclient==2.3.1 # via -r requirements.in
importlib-metadata==5.1.0 # via ax253, flask, kiss3
itsdangerous==2.1.2 # via flask
jinja2==3.1.2 # via click-completion, flask
kiss3==8.0.0 # via -r requirements.in
markupsafe==2.1.1 # via jinja2
pbr==5.11.0 # via -r requirements.in
pluggy==1.0.0 # via -r requirements.in
pycparser==2.21 # via cffi
pygments==2.13.0 # via rich
pyopenssl==22.1.0 # via -r requirements.in
pyserial==3.5 # via pyserial-asyncio
pyserial-asyncio==0.6 # via kiss3
python-engineio==4.3.4 # via python-socketio
python-socketio==5.7.2 # via flask-socketio
pytz==2022.6 # via -r requirements.in
pyyaml==6.0 # via -r requirements.in
requests==2.28.1 # via -r requirements.in, update-checker
rich==12.6.0 # via -r requirements.in
shellingham==1.5.0 # via click-completion
six==1.16.0 # via -r requirements.in, click-completion, eventlet, imapclient
soupsieve==2.3.2.post1 # via beautifulsoup4
tabulate==0.9.0 # via -r requirements.in
thesmuggler==1.0.1 # via -r requirements.in
ua-parser==0.16.1 # via user-agents
update-checker==0.18.0 # via -r requirements.in
urllib3==1.26.13 # via requests
user-agents==2.2.0 # via -r requirements.in
werkzeug==2.1.2 # via -r requirements.in, flask
wrapt==1.14.1 # via -r requirements.in
zipp==3.11.0 # via importlib-metadata
+6 -3
View File
@@ -15,7 +15,10 @@ F = t.TypeVar("F", bound=t.Callable[..., t.Any])
class TestDevTestPluginCommand(unittest.TestCase):
def _build_config(self, login=None, password=None):
config = {"aprs": {}}
config = {
"aprs": {},
"aprsd": {"trace": False},
}
if login:
config["aprs"]["login"] = login
@@ -25,7 +28,7 @@ class TestDevTestPluginCommand(unittest.TestCase):
return aprsd_config.Config(config)
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.log.setup_logging")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_login(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no login and config."""
@@ -43,7 +46,7 @@ class TestDevTestPluginCommand(unittest.TestCase):
assert "Must set --aprs_login or APRS_LOGIN" in result.output
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.log.setup_logging")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_plugin_arg(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no login and config."""
+8 -5
View File
@@ -15,7 +15,10 @@ F = t.TypeVar("F", bound=t.Callable[..., t.Any])
class TestSendMessageCommand(unittest.TestCase):
def _build_config(self, login=None, password=None):
config = {"aprs": {}}
config = {
"aprs": {},
"aprsd": {"trace": False},
}
if login:
config["aprs"]["login"] = login
@@ -25,7 +28,7 @@ class TestSendMessageCommand(unittest.TestCase):
return aprsd_config.Config(config)
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.log.setup_logging")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_login(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no login and config."""
@@ -43,7 +46,7 @@ class TestSendMessageCommand(unittest.TestCase):
assert "Must set --aprs_login or APRS_LOGIN" in result.output
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.log.setup_logging")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_password(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no password and config."""
@@ -58,7 +61,7 @@ class TestSendMessageCommand(unittest.TestCase):
assert "Must set --aprs-password or APRS_PASSWORD" in result.output
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.log.setup_logging")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_tocallsign(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no tocallsign."""
@@ -76,7 +79,7 @@ class TestSendMessageCommand(unittest.TestCase):
assert "Error: Missing argument 'TOCALLSIGN'" in result.output
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.log.setup_logging")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_command(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no command."""
+117
View File
@@ -0,0 +1,117 @@
import typing as t
import unittest
from unittest import mock
from click.testing import CliRunner
import flask
import flask_socketio
from aprsd import config as aprsd_config
from aprsd import messaging, packets
from aprsd.cmds import webchat # noqa
from .. import fake
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
class TestSendMessageCommand(unittest.TestCase):
def _build_config(self, login=None, password=None):
config = {
"aprs": {},
"aprsd": {
"trace": False,
"web": {
"users": {"admin": "password"},
},
"watch_list": {"packet_keep_count": 1},
},
}
if login:
config["aprs"]["login"] = login
if password:
config["aprs"]["password"] = password
return aprsd_config.Config(config)
@mock.patch("aprsd.config.parse_config")
def test_missing_config(self, mock_parse_config):
CliRunner()
cfg = self._build_config()
del cfg["aprsd"]["web"]["users"]
mock_parse_config.return_value = cfg
server = webchat.WebChatFlask()
self.assertRaises(
KeyError,
server.set_config, cfg,
)
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.logging.log.setup_logging")
def test_init_flask(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no login and config."""
CliRunner()
cfg = self._build_config()
mock_parse_config.return_value = cfg
socketio, flask_app = webchat.init_flask(cfg, "DEBUG", False)
self.assertIsInstance(socketio, flask_socketio.SocketIO)
self.assertIsInstance(flask_app, flask.Flask)
@mock.patch("aprsd.messaging.log_message")
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.messaging.MsgTrack.remove")
@mock.patch("aprsd.cmds.webchat.socketio.emit")
def test_process_ack_packet(
self, mock_parse_config, mock_log_message,
mock_remove, mock_emit,
):
config = self._build_config()
mock_parse_config.return_value = config
packet = fake.fake_packet(
message="blah",
msg_number=1,
message_format=packets.PACKET_TYPE_ACK,
)
socketio = mock.MagicMock()
packets.PacketList(config=config)
messaging.MsgTrack(config=config)
packets.WatchList(config=config)
packets.SeenList(config=config)
wcp = webchat.WebChatProcessPacketThread(config, packet, socketio)
wcp.process_ack_packet(packet)
mock_log_message.called_once()
mock_remove.called_once()
mock_emit.called_once()
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.packets.PacketList.add")
@mock.patch("aprsd.cmds.webchat.socketio.emit")
def test_process_non_ack_packet(
self, mock_parse_config,
mock_packet_add,
mock_emit,
):
config = self._build_config()
mock_parse_config.return_value = config
packet = fake.fake_packet(
message="blah",
msg_number=1,
message_format=packets.PACKET_TYPE_MESSAGE,
)
socketio = mock.MagicMock()
packets.PacketList(config=config)
messaging.MsgTrack(config=config)
packets.WatchList(config=config)
packets.SeenList(config=config)
wcp = webchat.WebChatProcessPacketThread(config, packet, socketio)
wcp.process_non_ack_packet(packet)
mock_packet_add.called_once()
mock_emit.called_once()
+2
View File
@@ -16,7 +16,9 @@ def fake_packet(
packet = {
"from": fromcall,
"addresse": tocall,
"to": tocall,
"format": message_format,
"raw": "",
}
if message:
packet["message_text"] = message
+1 -1
View File
@@ -2,8 +2,8 @@ from unittest import mock
import pytz
from aprsd.fuzzyclock import fuzzy
from aprsd.plugins import time as time_plugin
from aprsd.utils import fuzzy
from .. import fake, test_plugin
+5 -2
View File
@@ -2,7 +2,10 @@
minversion = 2.9.0
skipdist = True
skip_missing_interpreters = true
envlist = pre-commit,pep8,py{36,37,38,39}
envlist = pep8,py{38,39}
#requires = tox-pipenv
# pip==22.0.4
# pip-tools==5.4.0
# Activate isolated build environment. tox will use a virtual environment
# to build a source distribution from the source tree. For build tools and
@@ -61,7 +64,7 @@ commands =
[flake8]
max-line-length = 99
show-source = True
ignore = E713,E501,W503
ignore = E713,E501,W503,N818
extend-ignore = E203,W503
extend-exclude = venv
exclude = .venv,.git,.tox,dist,doc,.ropeproject