mirror of
https://github.com/craigerl/aprsd.git
synced 2025-07-30 20:32:27 -04:00
Merge pull request #33 from craigerl/craiger-stable
add null reply for send_email
This commit is contained in:
commit
278e258648
@ -1,18 +1,47 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v3.2.0
|
rev: v3.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
- repo: https://github.com/psf/black
|
- id: detect-private-key
|
||||||
rev: 19.3b0
|
- id: check-merge-conflict
|
||||||
hooks:
|
- id: check-case-conflict
|
||||||
- id: black
|
- id: check-docstring-first
|
||||||
|
- id: check-builtin-literals
|
||||||
|
|
||||||
- repo: https://gitlab.com/pycqa/flake8
|
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||||
rev: 3.8.1
|
rev: v1.16.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: setup-cfg-fmt
|
||||||
additional_dependencies: [flake8-bugbear]
|
|
||||||
|
- repo: https://github.com/asottile/add-trailing-comma
|
||||||
|
rev: v2.0.2
|
||||||
|
hooks:
|
||||||
|
- id: add-trailing-comma
|
||||||
|
args: [--py36-plus]
|
||||||
|
|
||||||
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
|
rev: v2.7.4
|
||||||
|
hooks:
|
||||||
|
- id: pyupgrade
|
||||||
|
args:
|
||||||
|
- --py3-plus
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-isort
|
||||||
|
rev: v5.7.0
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 20.8b1
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
|
||||||
|
- repo: https://gitlab.com/pycqa/flake8
|
||||||
|
rev: 3.8.4
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
additional_dependencies: [flake8-bugbear]
|
||||||
|
175
LICENSE
Normal file
175
LICENSE
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
45
Makefile
45
Makefile
@ -1,25 +1,54 @@
|
|||||||
.PHONY: virtual install build-requirements black isort flake8
|
.PHONY: virtual dev build-requirements black isort flake8
|
||||||
|
|
||||||
|
all: pip dev
|
||||||
|
|
||||||
virtual: .venv/bin/pip # Creates an isolated python 3 environment
|
virtual: .venv/bin/pip # Creates an isolated python 3 environment
|
||||||
|
|
||||||
.venv/bin/pip:
|
.venv/bin/pip:
|
||||||
virtualenv -p /usr/bin/python3 .venv
|
virtualenv -p /usr/bin/python3 .venv
|
||||||
|
|
||||||
install:
|
.venv/bin/aprsd: virtual
|
||||||
|
test -s .venv/bin/aprsd || .venv/bin/pip install -q -e .
|
||||||
|
|
||||||
|
install: .venv/bin/aprsd
|
||||||
.venv/bin/pip install -Ur requirements.txt
|
.venv/bin/pip install -Ur requirements.txt
|
||||||
|
|
||||||
dev: virtual
|
dev-pre-commit:
|
||||||
.venv/bin/pip install -e .
|
test -s .git/hooks/pre-commit || .venv/bin/pre-commit install
|
||||||
.venv/bin/pre-commit install
|
|
||||||
|
dev-requirements:
|
||||||
|
test -s .venv/bin/twine || .venv/bin/pip install -q -r dev-requirements.txt
|
||||||
|
|
||||||
|
pip: virtual
|
||||||
|
.venv/bin/pip install -q -U pip
|
||||||
|
|
||||||
|
dev: pip .venv/bin/aprsd dev-requirements dev-pre-commit
|
||||||
|
|
||||||
|
pip-tools:
|
||||||
|
test -s .venv/bin/pip-compile || .venv/bin/pip install pip-tools
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf dist/*
|
||||||
|
rm -rf .venv
|
||||||
|
|
||||||
test: dev
|
test: dev
|
||||||
|
.venv/bin/pre-commit run --all-files
|
||||||
tox -p
|
tox -p
|
||||||
|
|
||||||
update-requirements: install
|
build: test
|
||||||
.venv/bin/pip freeze > requirements.txt
|
rm -rf dist/*
|
||||||
|
.venv/bin/python3 setup.py sdist bdist_wheel
|
||||||
|
.venv/bin/twine check dist/*
|
||||||
|
|
||||||
|
upload: build
|
||||||
|
.venv/bin/twine upload dist/*
|
||||||
|
|
||||||
|
update-requirements: dev pip-tools
|
||||||
|
.venv/bin/pip-compile -q -U requirements.in
|
||||||
|
.venv/bin/pip-compile -q -U dev-requirements.in
|
||||||
|
|
||||||
.venv/bin/tox: # install tox
|
.venv/bin/tox: # install tox
|
||||||
.venv/bin/pip install -U tox
|
test -s .venv/bin/tox || .venv/bin/pip install -q -U tox
|
||||||
|
|
||||||
check: .venv/bin/tox # Code format check with isort and black
|
check: .venv/bin/tox # Code format check with isort and black
|
||||||
tox -efmt-check
|
tox -efmt-check
|
||||||
|
137
README.rst
137
README.rst
@ -2,6 +2,9 @@
|
|||||||
APRSD
|
APRSD
|
||||||
=====
|
=====
|
||||||
|
|
||||||
|
.. image:: https://badge.fury.io/py/aprsd.svg
|
||||||
|
:target: https://badge.fury.io/py/aprsd
|
||||||
|
|
||||||
.. image:: https://github.com/craigerl/aprsd/workflows/python/badge.svg
|
.. image:: https://github.com/craigerl/aprsd/workflows/python/badge.svg
|
||||||
:target: https://github.com/craigerl/aprsd/actions
|
:target: https://github.com/craigerl/aprsd/actions
|
||||||
|
|
||||||
@ -11,9 +14,21 @@ APRSD
|
|||||||
.. image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336
|
.. image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336
|
||||||
:target: https://timothycrosley.github.io/isort/
|
:target: https://timothycrosley.github.io/isort/
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/github/issues/craigerl/aprsd
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/github/last-commit/craigerl/aprsd
|
||||||
|
|
||||||
|
.. image:: https://static.pepy.tech/personalized-badge/aprsd?period=month&units=international_system&left_color=black&right_color=orange&left_text=Downloads
|
||||||
|
:target: https://pepy.tech/project/aprsd
|
||||||
|
|
||||||
.. contents:: :local:
|
.. contents:: :local:
|
||||||
|
|
||||||
Listen on amateur radio aprs-is network for messages and respond to them.
|
`APRSD <http://github.com/craigerl/aprsd>`_ is a Ham radio `APRS <http://aprs.org>`_ message command gateway built on python.
|
||||||
|
|
||||||
|
APRSD listens on amateur radio aprs-is network for messages and respond to them.
|
||||||
|
It has a plugin architecture for extensibility. Users of APRSD can write their own
|
||||||
|
plugins that can respond to APRS-IS messages.
|
||||||
|
|
||||||
You must have an amateur radio callsign to use this software. APRSD gets
|
You must have an amateur radio callsign to use this software. APRSD gets
|
||||||
messages for the configured HAM callsign, and sends those messages to a
|
messages for the configured HAM callsign, and sends those messages to a
|
||||||
list of plugins for processing. There are a set of core plugins that
|
list of plugins for processing. There are a set of core plugins that
|
||||||
@ -21,20 +36,26 @@ provide responding to messages to check email, get location, ping,
|
|||||||
time of day, get weather, and fortune telling as well as version information
|
time of day, get weather, and fortune telling as well as version information
|
||||||
of aprsd itself.
|
of aprsd itself.
|
||||||
|
|
||||||
|
|
||||||
|
APRSD Overview Diagram
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. image:: https://raw.githubusercontent.com/craigerl/aprsd/crager-stable/docs/_static/aprsd_overview.svg?sanitize=true
|
||||||
|
|
||||||
|
|
||||||
Typical use case
|
Typical use case
|
||||||
================
|
================
|
||||||
|
|
||||||
Ham radio operator using an APRS enabled HAM radio sends a message to check
|
Ham radio operator using an APRS enabled HAM radio sends a message to check
|
||||||
the weather. an APRS message is sent, and then picked up by APRSD. The
|
the weather. an APRS message is sent, and then picked up by APRSD. The
|
||||||
APRS packet is decoded, and the message is sent through the list of plugins
|
APRS packet is decoded, and the message is sent through the list of plugins
|
||||||
for processing. The WeatherPlugin picks up the message, fetches the weather
|
for processing. For example, the WeatherPlugin picks up the message, fetches the weather
|
||||||
for the area around the user who sent the request, and then responds with
|
for the area around the user who sent the request, and then responds with
|
||||||
the weather conditions in that area.
|
the weather conditions in that area.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
APRSD Capabilities
|
APRSD Capabilities
|
||||||
------------------
|
==================
|
||||||
|
|
||||||
* server - The main aprsd server processor. Send/Rx APRS messages to HAM callsign
|
* server - The main aprsd server processor. Send/Rx APRS messages to HAM callsign
|
||||||
* send-message - use aprsd to send a command/message to aprsd server. Used for development testing
|
* send-message - use aprsd to send a command/message to aprsd server. Used for development testing
|
||||||
@ -52,6 +73,7 @@ If it matches, the plugin runs. IF the regex doesn't match, the plugin is skipp
|
|||||||
* FortunePlugin - Replies with old unix fortune random fortune!
|
* FortunePlugin - Replies with old unix fortune random fortune!
|
||||||
* LocationPlugin - Checks location of ham operator
|
* LocationPlugin - Checks location of ham operator
|
||||||
* PingPlugin - Sends pong with timestamp
|
* PingPlugin - Sends pong with timestamp
|
||||||
|
* QueryPlugin - Allows querying the list of delayed messages that were not ACK'd by radio
|
||||||
* TimePlugin - Current time of day
|
* TimePlugin - Current time of day
|
||||||
* WeatherPlugin - Get weather conditions for current location of HAM callsign
|
* WeatherPlugin - Get weather conditions for current location of HAM callsign
|
||||||
* VersionPlugin - Reports the version information for aprsd
|
* VersionPlugin - Reports the version information for aprsd
|
||||||
@ -72,6 +94,7 @@ Current messages this will respond to:
|
|||||||
-email_addr email text = send an email, say "mapme" to send a current position/map
|
-email_addr email text = send an email, say "mapme" to send a current position/map
|
||||||
-2 = resend the last 2 emails from your imap inbox to this radio
|
-2 = resend the last 2 emails from your imap inbox to this radio
|
||||||
p(ing) = respond with Pong!/time
|
p(ing) = respond with Pong!/time
|
||||||
|
v(ersion) = Respond with current APRSD Version string
|
||||||
anything else = respond with usage
|
anything else = respond with usage
|
||||||
|
|
||||||
|
|
||||||
@ -86,7 +109,7 @@ email server, and associated logins, passwords. search for "yourdomain",
|
|||||||
|
|
||||||
|
|
||||||
Installation:
|
Installation:
|
||||||
-------------
|
=============
|
||||||
|
|
||||||
pip install aprsd
|
pip install aprsd
|
||||||
|
|
||||||
@ -118,13 +141,14 @@ Help
|
|||||||
show Show the click-completion-command completion code
|
show Show the click-completion-command completion code
|
||||||
|
|
||||||
|
|
||||||
Commands
|
|
||||||
--------
|
|
||||||
|
|
||||||
sample-config
|
Commands
|
||||||
|
========
|
||||||
|
|
||||||
|
Configuration
|
||||||
=============
|
=============
|
||||||
This command outputs a sample config yml formatted block that you can edit
|
This command outputs a sample config yml formatted block that you can edit
|
||||||
and use to pass in to aprsd with -c.
|
and use to pass in to aprsd with -c. By default aprsd looks in ~/.config/aprsd/aprsd.yml
|
||||||
|
|
||||||
aprsd sample-config
|
aprsd sample-config
|
||||||
|
|
||||||
@ -235,8 +259,9 @@ test messages
|
|||||||
|
|
||||||
-h, --help Show this message and exit.
|
-h, --help Show this message and exit.
|
||||||
|
|
||||||
|
|
||||||
Example output:
|
Example output:
|
||||||
---------------
|
===============
|
||||||
|
|
||||||
|
|
||||||
SEND EMAIL (radio to smtp server)
|
SEND EMAIL (radio to smtp server)
|
||||||
@ -278,60 +303,30 @@ RECEIVE EMAIL (imap server to radio)
|
|||||||
Msg number : 0
|
Msg number : 0
|
||||||
|
|
||||||
|
|
||||||
WEATHER
|
|
||||||
=======
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Received message______________
|
|
||||||
Raw : KM6XXX>APY400,WIDE1-1,qAO,KM6XXX-1::KM6XXX-9 :weather{27
|
|
||||||
From : KM6XXX
|
|
||||||
Message : weather
|
|
||||||
Msg number : 27
|
|
||||||
|
|
||||||
Sending message_______________ 6(Tx3)
|
|
||||||
Raw : KM6XXX-9>APRS::KM6XXX :58F(58F/46F) Partly cloudy. Tonight, Heavy Rain.{6
|
|
||||||
To : KM6XXX
|
|
||||||
Message : 58F(58F/46F) Party Cloudy. Tonight, Heavy Rain.
|
|
||||||
|
|
||||||
Sending ack __________________ Tx(3)
|
|
||||||
Raw : KM6XXX-9>APRS::KM6XXX :ack27
|
|
||||||
To : KM6XXX
|
|
||||||
Ack number : 27
|
|
||||||
|
|
||||||
Received message______________
|
|
||||||
Raw : KM6XXX>APY400,WIDE1-1,qAO,KM6XXX-1::KM6XXX-9 :ack6
|
|
||||||
From : KM6XXX
|
|
||||||
Message : ack6
|
|
||||||
Msg number : 0
|
|
||||||
|
|
||||||
|
|
||||||
LOCATION
|
LOCATION
|
||||||
========
|
========
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
Received message______________
|
Received Message _______________
|
||||||
Raw : KM6XXX>APY400,WIDE1-1,qAO,KM6XXX-1::KM6XXX-9 :location{28
|
Raw : KM6XXX-6>APRS,TCPIP*,qAC,T2CAEAST::KM6XXX-14:location{2
|
||||||
From : KM6XXX
|
From : KM6XXX-6
|
||||||
Message : location
|
Message : location
|
||||||
Msg number : 28
|
Msg number : 2
|
||||||
|
Received Message _______________ Complete
|
||||||
|
|
||||||
Sending message_______________ 7(Tx3)
|
Sending Message _______________
|
||||||
Raw : KM6XXX-9>APRS::KM6XXX :8 Miles NE Auburn CA 1673' 39.91150,-120.93450 0.1h ago{7
|
Raw : KM6XXX-14>APRS::KM6XXX-6 :KM6XXX-6: 8 Miles E Auburn CA 0' 0,-120.93584 1873.7h ago{2
|
||||||
To : KM6XXX
|
To : KM6XXX-6
|
||||||
Message : 8 Miles E Auburn CA 1673' 38.91150,-120.93450 0.1h ago
|
Message : KM6XXX-6: 8 Miles E Auburn CA 0' 0,-120.93584 1873.7h ago
|
||||||
|
Msg number : 2
|
||||||
|
Sending Message _______________ Complete
|
||||||
|
|
||||||
Sending ack __________________ Tx(3)
|
Sending ack _______________
|
||||||
Raw : KM6XXX-9>APRS::KM6XXX :ack28
|
Raw : KM6XXX-14>APRS::KM6XXX-6 :ack2
|
||||||
To : KM6XXX
|
To : KM6XXX-6
|
||||||
Ack number : 28
|
Ack : 2
|
||||||
|
Sending ack _______________ Complete
|
||||||
Received message______________
|
|
||||||
Raw : KM6XXX>APY400,WIDE1-1,qAO,KM6XXX-1::KM6XXX-9 :ack7
|
|
||||||
From : KM6XXX
|
|
||||||
Message : ack7
|
|
||||||
Msg number : 0
|
|
||||||
|
|
||||||
AND... ping, fortune, time.....
|
AND... ping, fortune, time.....
|
||||||
|
|
||||||
@ -341,13 +336,10 @@ Development
|
|||||||
|
|
||||||
* git clone git@github.com:craigerl/aprsd.git
|
* git clone git@github.com:craigerl/aprsd.git
|
||||||
* cd aprsd
|
* cd aprsd
|
||||||
* virtualenv .venv
|
* make
|
||||||
* source .venv/bin/activate
|
|
||||||
* pip install -e .
|
|
||||||
* pre-commit install
|
|
||||||
|
|
||||||
Workflow
|
Workflow
|
||||||
--------
|
========
|
||||||
|
|
||||||
While working aprsd, The workflow is as follows
|
While working aprsd, The workflow is as follows
|
||||||
|
|
||||||
@ -366,7 +358,7 @@ While working aprsd, The workflow is as follows
|
|||||||
|
|
||||||
|
|
||||||
Release
|
Release
|
||||||
-------
|
=======
|
||||||
|
|
||||||
To do release to pypi:
|
To do release to pypi:
|
||||||
|
|
||||||
@ -378,25 +370,20 @@ To do release to pypi:
|
|||||||
|
|
||||||
git push origin master --tags
|
git push origin master --tags
|
||||||
|
|
||||||
* Build dist and wheel
|
* Do a test build and verify build is valid
|
||||||
|
|
||||||
python setup.py sdist bdist_wheel
|
make build
|
||||||
|
|
||||||
* Verify build is valid for pypi (need twine installed )
|
|
||||||
|
|
||||||
pip install twine
|
|
||||||
twine check dist/*
|
|
||||||
|
|
||||||
* Once twine is happy, upload release to pypi
|
* Once twine is happy, upload release to pypi
|
||||||
|
|
||||||
twine upload dist/*
|
make upload
|
||||||
|
|
||||||
|
|
||||||
Docker Container
|
Docker Container
|
||||||
----------------
|
================
|
||||||
|
|
||||||
Building
|
Building
|
||||||
--------
|
========
|
||||||
|
|
||||||
There are 2 versions of the container Dockerfile that can be used.
|
There are 2 versions of the container Dockerfile that can be used.
|
||||||
The main Dockerfile, which is for building the official release container
|
The main Dockerfile, which is for building the official release container
|
||||||
@ -405,18 +392,18 @@ which is used for building a container based off of a git branch of
|
|||||||
the repo.
|
the repo.
|
||||||
|
|
||||||
Official Build
|
Official Build
|
||||||
--------------
|
==============
|
||||||
|
|
||||||
docker build -t hemna6969/aprsd:latest .
|
docker build -t hemna6969/aprsd:latest .
|
||||||
|
|
||||||
Development Build
|
Development Build
|
||||||
-----------------
|
=================
|
||||||
|
|
||||||
docker build -t hemna6969/aprsd:latest -f Dockerfile-dev .
|
docker build -t hemna6969/aprsd:latest -f Dockerfile-dev .
|
||||||
|
|
||||||
|
|
||||||
Running the container
|
Running the container
|
||||||
---------------------
|
=====================
|
||||||
|
|
||||||
There is a docker-compose.yml file that can be used to run your 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
|
There are 2 volumes defined that can be used to store your configuration
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
# not use this file except in compliance with the License. You may obtain
|
# not use this file except in compliance with the License. You may obtain
|
||||||
# a copy of the License at
|
# a copy of the License at
|
||||||
|
@ -1,24 +1,28 @@
|
|||||||
import logging
|
import logging
|
||||||
import select
|
import select
|
||||||
import socket
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
import aprsd
|
||||||
import aprslib
|
import aprslib
|
||||||
|
from aprslib import is_py3
|
||||||
|
from aprslib.exceptions import LoginError
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class Client(object):
|
class Client:
|
||||||
"""Singleton client class that constructs the aprslib connection."""
|
"""Singleton client class that constructs the aprslib connection."""
|
||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
aprs_client = None
|
aprs_client = None
|
||||||
config = None
|
config = None
|
||||||
|
|
||||||
|
connected = False
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
"""This magic turns this into a singleton."""
|
"""This magic turns this into a singleton."""
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super(Client, cls).__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
# Put any initialization here.
|
# Put any initialization here.
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
@ -53,6 +57,10 @@ class Client(object):
|
|||||||
aprs_client.connect()
|
aprs_client.connect()
|
||||||
connected = True
|
connected = True
|
||||||
backoff = 1
|
backoff = 1
|
||||||
|
except LoginError as e:
|
||||||
|
LOG.error("Failed to login to APRS-IS Server '{}'".format(e))
|
||||||
|
connected = False
|
||||||
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error("Unable to connect to APRS-IS server. '{}' ".format(e))
|
LOG.error("Unable to connect to APRS-IS server. '{}' ".format(e))
|
||||||
time.sleep(backoff)
|
time.sleep(backoff)
|
||||||
@ -81,7 +89,7 @@ class Aprsdis(aprslib.IS):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.sock.setblocking(0)
|
self.sock.setblocking(0)
|
||||||
except socket.error as e:
|
except OSError as e:
|
||||||
self.logger.error("socket error when setblocking(0): %s" % str(e))
|
self.logger.error("socket error when setblocking(0): %s" % str(e))
|
||||||
raise aprslib.ConnectionDrop("connection dropped")
|
raise aprslib.ConnectionDrop("connection dropped")
|
||||||
|
|
||||||
@ -92,7 +100,10 @@ class Aprsdis(aprslib.IS):
|
|||||||
# set a select timeout, so we get a chance to exit
|
# set a select timeout, so we get a chance to exit
|
||||||
# when user hits CTRL-C
|
# when user hits CTRL-C
|
||||||
readable, writable, exceptional = select.select(
|
readable, writable, exceptional = select.select(
|
||||||
[self.sock], [], [], self.select_timeout
|
[self.sock],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
self.select_timeout,
|
||||||
)
|
)
|
||||||
if not readable:
|
if not readable:
|
||||||
continue
|
continue
|
||||||
@ -104,7 +115,7 @@ class Aprsdis(aprslib.IS):
|
|||||||
if not short_buf:
|
if not short_buf:
|
||||||
self.logger.error("socket.recv(): returned empty")
|
self.logger.error("socket.recv(): returned empty")
|
||||||
raise aprslib.ConnectionDrop("connection dropped")
|
raise aprslib.ConnectionDrop("connection dropped")
|
||||||
except socket.error as e:
|
except OSError as e:
|
||||||
# self.logger.error("socket error on recv(): %s" % str(e))
|
# self.logger.error("socket error on recv(): %s" % str(e))
|
||||||
if "Resource temporarily unavailable" in str(e):
|
if "Resource temporarily unavailable" in str(e):
|
||||||
if not blocking:
|
if not blocking:
|
||||||
@ -118,6 +129,53 @@ class Aprsdis(aprslib.IS):
|
|||||||
|
|
||||||
yield line
|
yield line
|
||||||
|
|
||||||
|
def _send_login(self):
|
||||||
|
"""
|
||||||
|
Sends login string to server
|
||||||
|
"""
|
||||||
|
login_str = "user {0} pass {1} vers github.com/craigerl/aprsd {3}{2}\r\n"
|
||||||
|
login_str = login_str.format(
|
||||||
|
self.callsign,
|
||||||
|
self.passwd,
|
||||||
|
(" filter " + self.filter) if self.filter != "" else "",
|
||||||
|
aprsd.__version__,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.logger.info("Sending login information")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._sendall(login_str)
|
||||||
|
self.sock.settimeout(5)
|
||||||
|
test = self.sock.recv(len(login_str) + 100)
|
||||||
|
if is_py3:
|
||||||
|
test = test.decode("latin-1")
|
||||||
|
test = test.rstrip()
|
||||||
|
|
||||||
|
self.logger.debug("Server: %s", test)
|
||||||
|
|
||||||
|
_, _, callsign, status, _ = test.split(" ", 4)
|
||||||
|
|
||||||
|
if callsign == "":
|
||||||
|
raise LoginError("Server responded with empty callsign???")
|
||||||
|
if callsign != self.callsign:
|
||||||
|
raise LoginError("Server: %s" % test)
|
||||||
|
if status != "verified," and self.passwd != "-1":
|
||||||
|
raise LoginError("Password is incorrect")
|
||||||
|
|
||||||
|
if self.passwd == "-1":
|
||||||
|
self.logger.info("Login successful (receive only)")
|
||||||
|
else:
|
||||||
|
self.logger.info("Login successful")
|
||||||
|
|
||||||
|
except LoginError as e:
|
||||||
|
self.logger.error(str(e))
|
||||||
|
self.close()
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
self.close()
|
||||||
|
self.logger.error("Failed to login")
|
||||||
|
raise LoginError("Failed to login")
|
||||||
|
|
||||||
|
|
||||||
def get_client():
|
def get_client():
|
||||||
cl = Client()
|
cl = Client()
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import email
|
import email
|
||||||
|
from email.mime.text import MIMEText
|
||||||
import imaplib
|
import imaplib
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import smtplib
|
import smtplib
|
||||||
import time
|
import time
|
||||||
from email.mime.text import MIMEText
|
|
||||||
|
|
||||||
import imapclient
|
|
||||||
import six
|
|
||||||
from validate_email import validate_email
|
|
||||||
|
|
||||||
from aprsd import messaging, threads
|
from aprsd import messaging, threads
|
||||||
|
import imapclient
|
||||||
|
from validate_email import validate_email
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
@ -29,7 +27,10 @@ def _imap_connect():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
server = imapclient.IMAPClient(
|
server = imapclient.IMAPClient(
|
||||||
CONFIG["imap"]["host"], port=imap_port, use_uid=True, ssl=use_ssl
|
CONFIG["imap"]["host"],
|
||||||
|
port=imap_port,
|
||||||
|
use_uid=True,
|
||||||
|
ssl=use_ssl,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
LOG.error("Failed to connect IMAP server")
|
LOG.error("Failed to connect IMAP server")
|
||||||
@ -52,7 +53,7 @@ def _smtp_connect():
|
|||||||
use_ssl = CONFIG["smtp"].get("use_ssl", False)
|
use_ssl = CONFIG["smtp"].get("use_ssl", False)
|
||||||
msg = "{}{}:{}".format("SSL " if use_ssl else "", host, smtp_port)
|
msg = "{}{}:{}".format("SSL " if use_ssl else "", host, smtp_port)
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
"Connect to SMTP host {} with user '{}'".format(msg, CONFIG["imap"]["login"])
|
"Connect to SMTP host {} with user '{}'".format(msg, CONFIG["imap"]["login"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -83,26 +84,27 @@ def validate_shortcuts(config):
|
|||||||
|
|
||||||
LOG.info(
|
LOG.info(
|
||||||
"Validating {} Email shortcuts. This can take up to 10 seconds"
|
"Validating {} Email shortcuts. This can take up to 10 seconds"
|
||||||
" per shortcut".format(len(shortcuts))
|
" per shortcut".format(len(shortcuts)),
|
||||||
)
|
)
|
||||||
delete_keys = []
|
delete_keys = []
|
||||||
for key in shortcuts:
|
for key in shortcuts:
|
||||||
|
LOG.info("Validating {}:{}".format(key, shortcuts[key]))
|
||||||
is_valid = validate_email(
|
is_valid = validate_email(
|
||||||
email_address=shortcuts[key],
|
email_address=shortcuts[key],
|
||||||
check_regex=True,
|
check_regex=True,
|
||||||
check_mx=True,
|
check_mx=False,
|
||||||
from_address=config["smtp"]["login"],
|
from_address=config["smtp"]["login"],
|
||||||
helo_host=config["smtp"]["host"],
|
helo_host=config["smtp"]["host"],
|
||||||
smtp_timeout=10,
|
smtp_timeout=10,
|
||||||
dns_timeout=10,
|
dns_timeout=10,
|
||||||
use_blacklist=False,
|
use_blacklist=True,
|
||||||
debug=False,
|
debug=False,
|
||||||
)
|
)
|
||||||
if not is_valid:
|
if not is_valid:
|
||||||
LOG.error(
|
LOG.error(
|
||||||
"'{}' is an invalid email address. Removing shortcut".format(
|
"'{}' is an invalid email address. Removing shortcut".format(
|
||||||
shortcuts[key]
|
shortcuts[key],
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
delete_keys.append(key)
|
delete_keys.append(key)
|
||||||
|
|
||||||
@ -112,9 +114,11 @@ def validate_shortcuts(config):
|
|||||||
LOG.info("Available shortcuts: {}".format(config["shortcuts"]))
|
LOG.info("Available shortcuts: {}".format(config["shortcuts"]))
|
||||||
|
|
||||||
|
|
||||||
def get_email_from_shortcut(shortcut):
|
def get_email_from_shortcut(addr):
|
||||||
if shortcut in CONFIG.get("shortcuts", None):
|
if CONFIG.get("shortcuts", False):
|
||||||
return CONFIG["shortcuts"].get(shortcut, None)
|
return CONFIG["shortcuts"].get(addr, addr)
|
||||||
|
else:
|
||||||
|
return addr
|
||||||
|
|
||||||
|
|
||||||
def validate_email_config(config, disable_validation=False):
|
def validate_email_config(config, disable_validation=False):
|
||||||
@ -170,14 +174,18 @@ def parse_email(msgid, data, server):
|
|||||||
|
|
||||||
if part.get_content_type() == "text/plain":
|
if part.get_content_type() == "text/plain":
|
||||||
LOG.debug("Email got text/plain")
|
LOG.debug("Email got text/plain")
|
||||||
text = six.text_type(
|
text = str(
|
||||||
part.get_payload(decode=True), str(charset), "ignore"
|
part.get_payload(decode=True),
|
||||||
|
str(charset),
|
||||||
|
"ignore",
|
||||||
).encode("utf8", "replace")
|
).encode("utf8", "replace")
|
||||||
|
|
||||||
if part.get_content_type() == "text/html":
|
if part.get_content_type() == "text/html":
|
||||||
LOG.debug("Email got text/html")
|
LOG.debug("Email got text/html")
|
||||||
html = six.text_type(
|
html = str(
|
||||||
part.get_payload(decode=True), str(charset), "ignore"
|
part.get_payload(decode=True),
|
||||||
|
str(charset),
|
||||||
|
"ignore",
|
||||||
).encode("utf8", "replace")
|
).encode("utf8", "replace")
|
||||||
|
|
||||||
if text is not None:
|
if text is not None:
|
||||||
@ -189,12 +197,15 @@ def parse_email(msgid, data, server):
|
|||||||
# email.uscc.net sends no charset, blows up unicode function below
|
# email.uscc.net sends no charset, blows up unicode function below
|
||||||
LOG.debug("Email is not multipart")
|
LOG.debug("Email is not multipart")
|
||||||
if msg.get_content_charset() is None:
|
if msg.get_content_charset() is None:
|
||||||
text = six.text_type(
|
text = str(msg.get_payload(decode=True), "US-ASCII", "ignore").encode(
|
||||||
msg.get_payload(decode=True), "US-ASCII", "ignore"
|
"utf8",
|
||||||
).encode("utf8", "replace")
|
"replace",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
text = six.text_type(
|
text = str(
|
||||||
msg.get_payload(decode=True), msg.get_content_charset(), "ignore"
|
msg.get_payload(decode=True),
|
||||||
|
msg.get_content_charset(),
|
||||||
|
"ignore",
|
||||||
).encode("utf8", "replace")
|
).encode("utf8", "replace")
|
||||||
body = text.strip()
|
body = text.strip()
|
||||||
|
|
||||||
@ -263,11 +274,11 @@ def resend_email(count, fromcall):
|
|||||||
month = date.strftime("%B")[:3] # Nov, Mar, Apr
|
month = date.strftime("%B")[:3] # Nov, Mar, Apr
|
||||||
day = date.day
|
day = date.day
|
||||||
year = date.year
|
year = date.year
|
||||||
today = "%s-%s-%s" % (day, month, year)
|
today = "{}-{}-{}".format(day, month, year)
|
||||||
|
|
||||||
shortcuts = CONFIG["shortcuts"]
|
shortcuts = CONFIG["shortcuts"]
|
||||||
# swap key/value
|
# swap key/value
|
||||||
shortcuts_inverted = dict([[v, k] for k, v in shortcuts.items()])
|
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
server = _imap_connect()
|
server = _imap_connect()
|
||||||
@ -307,7 +318,7 @@ def resend_email(count, fromcall):
|
|||||||
# thinking this is a duplicate message.
|
# thinking this is a duplicate message.
|
||||||
# The FT1XDR pretty much ignores the aprs message number in this
|
# The FT1XDR pretty much ignores the aprs message number in this
|
||||||
# regard. The FTM400 gets it right.
|
# regard. The FTM400 gets it right.
|
||||||
reply = "No new msg %s:%s:%s" % (
|
reply = "No new msg {}:{}:{}".format(
|
||||||
str(h).zfill(2),
|
str(h).zfill(2),
|
||||||
str(m).zfill(2),
|
str(m).zfill(2),
|
||||||
str(s).zfill(2),
|
str(s).zfill(2),
|
||||||
@ -325,13 +336,15 @@ def resend_email(count, fromcall):
|
|||||||
|
|
||||||
class APRSDEmailThread(threads.APRSDThread):
|
class APRSDEmailThread(threads.APRSDThread):
|
||||||
def __init__(self, msg_queues, config):
|
def __init__(self, msg_queues, config):
|
||||||
super(APRSDEmailThread, self).__init__("EmailThread")
|
super().__init__("EmailThread")
|
||||||
self.msg_queues = msg_queues
|
self.msg_queues = msg_queues
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
global check_email_delay
|
global check_email_delay
|
||||||
|
|
||||||
|
LOG.debug("Starting")
|
||||||
|
|
||||||
check_email_delay = 60
|
check_email_delay = 60
|
||||||
past = datetime.datetime.now()
|
past = datetime.datetime.now()
|
||||||
while not self.thread_stop:
|
while not self.thread_stop:
|
||||||
@ -351,13 +364,13 @@ class APRSDEmailThread(threads.APRSDThread):
|
|||||||
|
|
||||||
shortcuts = CONFIG["shortcuts"]
|
shortcuts = CONFIG["shortcuts"]
|
||||||
# swap key/value
|
# swap key/value
|
||||||
shortcuts_inverted = dict([[v, k] for k, v in shortcuts.items()])
|
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
|
||||||
|
|
||||||
date = datetime.datetime.now()
|
date = datetime.datetime.now()
|
||||||
month = date.strftime("%B")[:3] # Nov, Mar, Apr
|
month = date.strftime("%B")[:3] # Nov, Mar, Apr
|
||||||
day = date.day
|
day = date.day
|
||||||
year = date.year
|
year = date.year
|
||||||
today = "%s-%s-%s" % (day, month, year)
|
today = "{}-{}-{}".format(day, month, year)
|
||||||
|
|
||||||
server = None
|
server = None
|
||||||
try:
|
try:
|
||||||
@ -369,13 +382,14 @@ class APRSDEmailThread(threads.APRSDThread):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
messages = server.search(["SINCE", today])
|
messages = server.search(["SINCE", today])
|
||||||
# LOG.debug("{} messages received today".format(len(messages)))
|
LOG.debug("{} messages received today".format(len(messages)))
|
||||||
|
|
||||||
for msgid, data in server.fetch(messages, ["ENVELOPE"]).items():
|
for msgid, data in server.fetch(messages, ["ENVELOPE"]).items():
|
||||||
envelope = data[b"ENVELOPE"]
|
envelope = data[b"ENVELOPE"]
|
||||||
# LOG.debug('ID:%d "%s" (%s)' % (msgid, envelope.subject.decode(), envelope.date))
|
# LOG.debug('ID:%d "%s" (%s)' % (msgid, envelope.subject.decode(), envelope.date))
|
||||||
f = re.search(
|
f = re.search(
|
||||||
r"'([[A-a][0-9]_-]+@[[A-a][0-9]_-\.]+)", str(envelope.from_[0])
|
r"'([[A-a][0-9]_-]+@[[A-a][0-9]_-\.]+)",
|
||||||
|
str(envelope.from_[0]),
|
||||||
)
|
)
|
||||||
if f is not None:
|
if f is not None:
|
||||||
from_addr = f.group(1)
|
from_addr = f.group(1)
|
||||||
@ -401,7 +415,6 @@ class APRSDEmailThread(threads.APRSDThread):
|
|||||||
from_addr = shortcuts_inverted[from_addr]
|
from_addr = shortcuts_inverted[from_addr]
|
||||||
|
|
||||||
reply = "-" + from_addr + " " + body.decode(errors="ignore")
|
reply = "-" + from_addr + " " + body.decode(errors="ignore")
|
||||||
# messaging.send_message(CONFIG["ham"]["callsign"], reply)
|
|
||||||
msg = messaging.TextMessage(
|
msg = messaging.TextMessage(
|
||||||
self.config["aprs"]["login"],
|
self.config["aprs"]["login"],
|
||||||
self.config["ham"]["callsign"],
|
self.config["ham"]["callsign"],
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
import socketserver
|
import socketserver
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from logging.handlers import RotatingFileHandler
|
|
||||||
|
|
||||||
from aprsd import utils
|
from aprsd import utils
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ def main():
|
|||||||
|
|
||||||
ip = CONFIG["aprs"]["host"]
|
ip = CONFIG["aprs"]["host"]
|
||||||
port = CONFIG["aprs"]["port"]
|
port = CONFIG["aprs"]["port"]
|
||||||
LOG.info("Start server listening on %s:%s" % (args.ip, args.port))
|
LOG.info("Start server listening on {}:{}".format(args.ip, args.port))
|
||||||
|
|
||||||
with socketserver.TCPServer((ip, port), MyAPRSTCPHandler) as server:
|
with socketserver.TCPServer((ip, port), MyAPRSTCPHandler) as server:
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
|
128
aprsd/main.py
128
aprsd/main.py
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Listen on amateur radio aprs-is network for messages and respond to them.
|
# Listen on amateur radio aprs-is network for messages and respond to them.
|
||||||
# You must have an amateur radio callsign to use this software. You must
|
# You must have an amateur radio callsign to use this software. You must
|
||||||
@ -22,23 +21,23 @@
|
|||||||
|
|
||||||
# python included libs
|
# python included libs
|
||||||
import logging
|
import logging
|
||||||
|
from logging import NullHandler
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from logging import NullHandler
|
|
||||||
from logging.handlers import RotatingFileHandler
|
|
||||||
|
|
||||||
import aprslib
|
|
||||||
import click
|
|
||||||
import click_completion
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
# local imports here
|
# local imports here
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import client, email, messaging, plugin, threads, utils
|
from aprsd import client, email, messaging, plugin, threads, utils
|
||||||
|
import aprslib
|
||||||
|
from aprslib.exceptions import LoginError
|
||||||
|
import click
|
||||||
|
import click_completion
|
||||||
|
import yaml
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||||
@ -99,7 +98,9 @@ def main():
|
|||||||
|
|
||||||
@main.command()
|
@main.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
"-i", "--case-insensitive/--no-case-insensitive", help="Case insensitive completion"
|
"-i",
|
||||||
|
"--case-insensitive/--no-case-insensitive",
|
||||||
|
help="Case insensitive completion",
|
||||||
)
|
)
|
||||||
@click.argument(
|
@click.argument(
|
||||||
"shell",
|
"shell",
|
||||||
@ -118,10 +119,14 @@ def show(shell, case_insensitive):
|
|||||||
|
|
||||||
@main.command()
|
@main.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
"--append/--overwrite", help="Append the completion code to the file", default=None
|
"--append/--overwrite",
|
||||||
|
help="Append the completion code to the file",
|
||||||
|
default=None,
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"-i", "--case-insensitive/--no-case-insensitive", help="Case insensitive completion"
|
"-i",
|
||||||
|
"--case-insensitive/--no-case-insensitive",
|
||||||
|
help="Case insensitive completion",
|
||||||
)
|
)
|
||||||
@click.argument(
|
@click.argument(
|
||||||
"shell",
|
"shell",
|
||||||
@ -137,19 +142,24 @@ def install(append, case_insensitive, shell, path):
|
|||||||
else {}
|
else {}
|
||||||
)
|
)
|
||||||
shell, path = click_completion.core.install(
|
shell, path = click_completion.core.install(
|
||||||
shell=shell, path=path, append=append, extra_env=extra_env
|
shell=shell,
|
||||||
|
path=path,
|
||||||
|
append=append,
|
||||||
|
extra_env=extra_env,
|
||||||
)
|
)
|
||||||
click.echo("%s completion installed in %s" % (shell, path))
|
click.echo("{} completion installed in {}".format(shell, path))
|
||||||
|
|
||||||
|
|
||||||
def signal_handler(signal, frame):
|
def signal_handler(sig, frame):
|
||||||
global server_vent
|
global server_vent
|
||||||
|
|
||||||
LOG.info(
|
LOG.info(
|
||||||
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds to exit all threads"
|
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds to exit all threads",
|
||||||
)
|
)
|
||||||
threads.APRSDThreadList().stop_all()
|
threads.APRSDThreadList().stop_all()
|
||||||
server_event.set()
|
server_event.set()
|
||||||
|
time.sleep(1)
|
||||||
|
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||||
|
|
||||||
|
|
||||||
# end signal_handler
|
# end signal_handler
|
||||||
@ -182,7 +192,7 @@ def setup_logging(config, loglevel, quiet):
|
|||||||
@main.command()
|
@main.command()
|
||||||
def sample_config():
|
def sample_config():
|
||||||
"""This dumps the config to stdout."""
|
"""This dumps the config to stdout."""
|
||||||
click.echo(yaml.dump(utils.DEFAULT_CONFIG_DICT))
|
click.echo(utils.add_config_comments(yaml.dump(utils.DEFAULT_CONFIG_DICT)))
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
@main.command()
|
||||||
@ -191,7 +201,8 @@ def sample_config():
|
|||||||
default="DEBUG",
|
default="DEBUG",
|
||||||
show_default=True,
|
show_default=True,
|
||||||
type=click.Choice(
|
type=click.Choice(
|
||||||
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], case_sensitive=False
|
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||||
|
case_sensitive=False,
|
||||||
),
|
),
|
||||||
show_choices=True,
|
show_choices=True,
|
||||||
help="The log level to use for aprsd.log",
|
help="The log level to use for aprsd.log",
|
||||||
@ -217,17 +228,31 @@ def sample_config():
|
|||||||
show_envvar=True,
|
show_envvar=True,
|
||||||
help="the APRS-IS password for APRS_LOGIN",
|
help="the APRS-IS password for APRS_LOGIN",
|
||||||
)
|
)
|
||||||
@click.argument("tocallsign")
|
@click.option(
|
||||||
@click.argument("command", nargs=-1)
|
"--no-ack",
|
||||||
|
"-n",
|
||||||
|
is_flag=True,
|
||||||
|
show_default=True,
|
||||||
|
default=False,
|
||||||
|
help="Don't wait for an ack, just sent it to APRS-IS and bail.",
|
||||||
|
)
|
||||||
|
@click.option("--raw", default=None, help="Send a raw message. Implies --no-ack")
|
||||||
|
@click.argument("tocallsign", required=False)
|
||||||
|
@click.argument("command", nargs=-1, required=False)
|
||||||
def send_message(
|
def send_message(
|
||||||
loglevel, quiet, config_file, aprs_login, aprs_password, tocallsign, command
|
loglevel,
|
||||||
|
quiet,
|
||||||
|
config_file,
|
||||||
|
aprs_login,
|
||||||
|
aprs_password,
|
||||||
|
no_ack,
|
||||||
|
raw,
|
||||||
|
tocallsign,
|
||||||
|
command,
|
||||||
):
|
):
|
||||||
"""Send a message to a callsign via APRS_IS."""
|
"""Send a message to a callsign via APRS_IS."""
|
||||||
global got_ack, got_response
|
global got_ack, got_response
|
||||||
|
|
||||||
click.echo("{} {} {} {}".format(aprs_login, aprs_password, tocallsign, command))
|
|
||||||
|
|
||||||
click.echo("Load config")
|
|
||||||
config = utils.parse_config(config_file)
|
config = utils.parse_config(config_file)
|
||||||
if not aprs_login:
|
if not aprs_login:
|
||||||
click.echo("Must set --aprs_login or APRS_LOGIN")
|
click.echo("Must set --aprs_login or APRS_LOGIN")
|
||||||
@ -245,7 +270,11 @@ def send_message(
|
|||||||
LOG.info("APRSD Started version: {}".format(aprsd.__version__))
|
LOG.info("APRSD Started version: {}".format(aprsd.__version__))
|
||||||
if type(command) is tuple:
|
if type(command) is tuple:
|
||||||
command = " ".join(command)
|
command = " ".join(command)
|
||||||
LOG.info("Sending Command '{}'".format(command))
|
if not quiet:
|
||||||
|
if raw:
|
||||||
|
LOG.info("L'{}' R'{}'".format(aprs_login, raw))
|
||||||
|
else:
|
||||||
|
LOG.info("L'{}' To'{}' C'{}'".format(aprs_login, tocallsign, command))
|
||||||
|
|
||||||
got_ack = False
|
got_ack = False
|
||||||
got_response = False
|
got_response = False
|
||||||
@ -273,23 +302,37 @@ def send_message(
|
|||||||
got_response = True
|
got_response = True
|
||||||
# Send the ack back?
|
# Send the ack back?
|
||||||
ack = messaging.AckMessage(
|
ack = messaging.AckMessage(
|
||||||
config["aprs"]["login"], fromcall, msg_id=msg_number
|
config["aprs"]["login"],
|
||||||
|
fromcall,
|
||||||
|
msg_id=msg_number,
|
||||||
)
|
)
|
||||||
ack.send_direct()
|
ack.send_direct()
|
||||||
|
|
||||||
if got_ack and got_response:
|
if got_ack and got_response:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
cl = client.Client(config)
|
try:
|
||||||
|
cl = client.Client(config)
|
||||||
|
cl.setup_connection()
|
||||||
|
except LoginError:
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
# Send a message
|
# Send a message
|
||||||
# then we setup a consumer to rx messages
|
# then we setup a consumer to rx messages
|
||||||
# We should get an ack back as well as a new message
|
# We should get an ack back as well as a new message
|
||||||
# we should bail after we get the ack and send an ack back for the
|
# we should bail after we get the ack and send an ack back for the
|
||||||
# message
|
# message
|
||||||
msg = messaging.TextMessage(aprs_login, tocallsign, command)
|
if raw:
|
||||||
|
msg = messaging.RawMessage(raw)
|
||||||
|
msg.send_direct()
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
msg = messaging.TextMessage(aprs_login, tocallsign, command)
|
||||||
msg.send_direct()
|
msg.send_direct()
|
||||||
|
|
||||||
|
if no_ack:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# This will register a packet consumer with aprslib
|
# This will register a packet consumer with aprslib
|
||||||
# When new packets come in the consumer will process
|
# When new packets come in the consumer will process
|
||||||
@ -309,10 +352,11 @@ def send_message(
|
|||||||
@main.command()
|
@main.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
"--loglevel",
|
"--loglevel",
|
||||||
default="DEBUG",
|
default="INFO",
|
||||||
show_default=True,
|
show_default=True,
|
||||||
type=click.Choice(
|
type=click.Choice(
|
||||||
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], case_sensitive=False
|
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||||
|
case_sensitive=False,
|
||||||
),
|
),
|
||||||
show_choices=True,
|
show_choices=True,
|
||||||
help="The log level to use for aprsd.log",
|
help="The log level to use for aprsd.log",
|
||||||
@ -341,14 +385,28 @@ def send_message(
|
|||||||
default=False,
|
default=False,
|
||||||
help="Flush out all old aged messages on disk.",
|
help="Flush out all old aged messages on disk.",
|
||||||
)
|
)
|
||||||
def server(loglevel, quiet, disable_validation, config_file, flush):
|
@click.option(
|
||||||
|
"--stats-server",
|
||||||
|
is_flag=True,
|
||||||
|
default=False,
|
||||||
|
help="Run a stats web server on port 5001?",
|
||||||
|
)
|
||||||
|
def server(
|
||||||
|
loglevel,
|
||||||
|
quiet,
|
||||||
|
disable_validation,
|
||||||
|
config_file,
|
||||||
|
flush,
|
||||||
|
stats_server,
|
||||||
|
):
|
||||||
"""Start the aprsd server process."""
|
"""Start the aprsd server process."""
|
||||||
global event
|
global event
|
||||||
|
|
||||||
event = threading.Event()
|
event = threading.Event()
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
|
||||||
click.echo("Load config")
|
if not quiet:
|
||||||
|
click.echo("Load config")
|
||||||
config = utils.parse_config(config_file)
|
config = utils.parse_config(config_file)
|
||||||
|
|
||||||
# Force setting the config to the modules that need it
|
# Force setting the config to the modules that need it
|
||||||
@ -370,7 +428,11 @@ def server(loglevel, quiet, disable_validation, config_file, flush):
|
|||||||
# Create the initial PM singleton and Register plugins
|
# Create the initial PM singleton and Register plugins
|
||||||
plugin_manager = plugin.PluginManager(config)
|
plugin_manager = plugin.PluginManager(config)
|
||||||
plugin_manager.setup_plugins()
|
plugin_manager.setup_plugins()
|
||||||
client.Client(config)
|
try:
|
||||||
|
cl = client.Client(config)
|
||||||
|
cl.client
|
||||||
|
except LoginError:
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
# Now load the msgTrack from disk if any
|
# Now load the msgTrack from disk if any
|
||||||
if flush:
|
if flush:
|
||||||
@ -397,7 +459,7 @@ def server(loglevel, quiet, disable_validation, config_file, flush):
|
|||||||
cntr = 0
|
cntr = 0
|
||||||
while not server_event.is_set():
|
while not server_event.is_set():
|
||||||
# to keep the log noise down
|
# to keep the log noise down
|
||||||
if cntr % 6 == 0:
|
if cntr % 12 == 0:
|
||||||
tracker = messaging.MsgTrack()
|
tracker = messaging.MsgTrack()
|
||||||
LOG.debug("KeepAlive Tracker({}): {}".format(len(tracker), str(tracker)))
|
LOG.debug("KeepAlive Tracker({}): {}".format(len(tracker), str(tracker)))
|
||||||
cntr += 1
|
cntr += 1
|
||||||
|
@ -1,29 +1,24 @@
|
|||||||
import abc
|
import abc
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
from multiprocessing import RawValue
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import pickle
|
import pickle
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from multiprocessing import RawValue
|
|
||||||
|
|
||||||
from aprsd import client, threads, utils
|
from aprsd import client, threads, utils
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
# message_nubmer:ack combos so we stop sending a message after an
|
|
||||||
# ack from radio {int:int}
|
|
||||||
# FIXME
|
|
||||||
ack_dict = {}
|
|
||||||
|
|
||||||
# What to return from a plugin if we have processed the message
|
# What to return from a plugin if we have processed the message
|
||||||
# and it's ok, but don't send a usage string back
|
# and it's ok, but don't send a usage string back
|
||||||
NULL_MESSAGE = -1
|
NULL_MESSAGE = -1
|
||||||
|
|
||||||
|
|
||||||
class MsgTrack(object):
|
class MsgTrack:
|
||||||
"""Class to keep track of outstanding text messages.
|
"""Class to keep track of outstanding text messages.
|
||||||
|
|
||||||
This is a thread safe class that keeps track of active
|
This is a thread safe class that keeps track of active
|
||||||
@ -44,6 +39,7 @@ class MsgTrack(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
|
_start_time = None
|
||||||
lock = None
|
lock = None
|
||||||
|
|
||||||
track = {}
|
track = {}
|
||||||
@ -51,8 +47,9 @@ class MsgTrack(object):
|
|||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super(MsgTrack, cls).__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
cls._instance.track = {}
|
cls._instance.track = {}
|
||||||
|
cls._start_time = datetime.datetime.now()
|
||||||
cls._instance.lock = threading.Lock()
|
cls._instance.lock = threading.Lock()
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
@ -117,13 +114,28 @@ class MsgTrack(object):
|
|||||||
if msg.last_send_attempt < msg.retry_count:
|
if msg.last_send_attempt < msg.retry_count:
|
||||||
msg.send()
|
msg.send()
|
||||||
|
|
||||||
def restart_delayed(self):
|
def _resend(self, msg):
|
||||||
|
msg.last_send_attempt = 0
|
||||||
|
msg.send()
|
||||||
|
|
||||||
|
def restart_delayed(self, count=None, most_recent=True):
|
||||||
"""Walk the list of delayed messages and restart them if any."""
|
"""Walk the list of delayed messages and restart them if any."""
|
||||||
for key in self.track.keys():
|
if not count:
|
||||||
msg = self.track[key]
|
# Send all the delayed messages
|
||||||
if msg.last_send_attempt == msg.retry_count:
|
for key in self.track.keys():
|
||||||
msg.last_send_attempt = 0
|
msg = self.track[key]
|
||||||
msg.send()
|
if msg.last_send_attempt == msg.retry_count:
|
||||||
|
self._resend(msg)
|
||||||
|
else:
|
||||||
|
# They want to resend <count> delayed messages
|
||||||
|
tmp = sorted(
|
||||||
|
self.track.items(),
|
||||||
|
reverse=most_recent,
|
||||||
|
key=lambda x: x[1].last_send_time,
|
||||||
|
)
|
||||||
|
msg_list = tmp[:count]
|
||||||
|
for (_key, msg) in msg_list:
|
||||||
|
self._resend(msg)
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
"""Nuke the old pickle file that stored the old results from last aprsd run."""
|
"""Nuke the old pickle file that stored the old results from last aprsd run."""
|
||||||
@ -133,7 +145,7 @@ class MsgTrack(object):
|
|||||||
self.track = {}
|
self.track = {}
|
||||||
|
|
||||||
|
|
||||||
class MessageCounter(object):
|
class MessageCounter:
|
||||||
"""
|
"""
|
||||||
Global message id counter class.
|
Global message id counter class.
|
||||||
|
|
||||||
@ -151,7 +163,7 @@ class MessageCounter(object):
|
|||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
"""Make this a singleton class."""
|
"""Make this a singleton class."""
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super(MessageCounter, cls).__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
cls._instance.val = RawValue("i", 1)
|
cls._instance.val = RawValue("i", 1)
|
||||||
cls._instance.lock = threading.Lock()
|
cls._instance.lock = threading.Lock()
|
||||||
return cls._instance
|
return cls._instance
|
||||||
@ -177,7 +189,7 @@ class MessageCounter(object):
|
|||||||
return str(self.val.value)
|
return str(self.val.value)
|
||||||
|
|
||||||
|
|
||||||
class Message(object, metaclass=abc.ABCMeta):
|
class Message(metaclass=abc.ABCMeta):
|
||||||
"""Base Message Class."""
|
"""Base Message Class."""
|
||||||
|
|
||||||
# The message id to send over the air
|
# The message id to send over the air
|
||||||
@ -202,13 +214,52 @@ class Message(object, metaclass=abc.ABCMeta):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RawMessage(Message):
|
||||||
|
"""Send a raw message.
|
||||||
|
|
||||||
|
This class is used for custom messages that contain the entire
|
||||||
|
contents of an APRS message in the message field.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
message = None
|
||||||
|
|
||||||
|
def __init__(self, message):
|
||||||
|
super().__init__(None, None, msg_id=None)
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
def send(self):
|
||||||
|
tracker = MsgTrack()
|
||||||
|
tracker.add(self)
|
||||||
|
thread = SendMessageThread(message=self)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def send_direct(self):
|
||||||
|
"""Send a message without a separate thread."""
|
||||||
|
cl = client.get_client()
|
||||||
|
log_message(
|
||||||
|
"Sending Message Direct",
|
||||||
|
repr(self).rstrip("\n"),
|
||||||
|
self.message,
|
||||||
|
tocall=self.tocall,
|
||||||
|
fromcall=self.fromcall,
|
||||||
|
)
|
||||||
|
cl.sendall(repr(self))
|
||||||
|
|
||||||
|
|
||||||
class TextMessage(Message):
|
class TextMessage(Message):
|
||||||
"""Send regular ARPS text/command messages/replies."""
|
"""Send regular ARPS text/command messages/replies."""
|
||||||
|
|
||||||
message = None
|
message = None
|
||||||
|
|
||||||
def __init__(self, fromcall, tocall, message, msg_id=None, allow_delay=True):
|
def __init__(self, fromcall, tocall, message, msg_id=None, allow_delay=True):
|
||||||
super(TextMessage, self).__init__(fromcall, tocall, msg_id)
|
super().__init__(fromcall, tocall, msg_id)
|
||||||
self.message = message
|
self.message = message
|
||||||
# do we try and save this message for later if we don't get
|
# do we try and save this message for later if we don't get
|
||||||
# an ack? Some messages we don't want to do this ever.
|
# an ack? Some messages we don't want to do this ever.
|
||||||
@ -217,7 +268,10 @@ class TextMessage(Message):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
"""Build raw string to send over the air."""
|
"""Build raw string to send over the air."""
|
||||||
return "{}>APRS::{}:{}{{{}\n".format(
|
return "{}>APRS::{}:{}{{{}\n".format(
|
||||||
self.fromcall, self.tocall.ljust(9), self._filter_for_send(), str(self.id)
|
self.fromcall,
|
||||||
|
self.tocall.ljust(9),
|
||||||
|
self._filter_for_send(),
|
||||||
|
str(self.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -226,7 +280,11 @@ class TextMessage(Message):
|
|||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
delta = now - self.last_send_time
|
delta = now - self.last_send_time
|
||||||
return "{}>{} Msg({})({}): '{}'".format(
|
return "{}>{} Msg({})({}): '{}'".format(
|
||||||
self.fromcall, self.tocall, self.id, delta, self.message
|
self.fromcall,
|
||||||
|
self.tocall,
|
||||||
|
self.id,
|
||||||
|
delta,
|
||||||
|
self.message,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _filter_for_send(self):
|
def _filter_for_send(self):
|
||||||
@ -240,8 +298,6 @@ class TextMessage(Message):
|
|||||||
return re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message)
|
return re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message)
|
||||||
|
|
||||||
def send(self):
|
def send(self):
|
||||||
global ack_dict
|
|
||||||
|
|
||||||
tracker = MsgTrack()
|
tracker = MsgTrack()
|
||||||
tracker.add(self)
|
tracker.add(self)
|
||||||
LOG.debug("Length of MsgTrack is {}".format(len(tracker)))
|
LOG.debug("Length of MsgTrack is {}".format(len(tracker)))
|
||||||
@ -265,9 +321,7 @@ class SendMessageThread(threads.APRSDThread):
|
|||||||
def __init__(self, message):
|
def __init__(self, message):
|
||||||
self.msg = message
|
self.msg = message
|
||||||
name = self.msg.message[:5]
|
name = self.msg.message[:5]
|
||||||
super(SendMessageThread, self).__init__(
|
super().__init__("SendMessage-{}-{}".format(self.msg.id, name))
|
||||||
"SendMessage-{}-{}".format(self.msg.id, name)
|
|
||||||
)
|
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
"""Loop until a message is acked or it gets delayed.
|
"""Loop until a message is acked or it gets delayed.
|
||||||
@ -332,11 +386,13 @@ class AckMessage(Message):
|
|||||||
"""Class for building Acks and sending them."""
|
"""Class for building Acks and sending them."""
|
||||||
|
|
||||||
def __init__(self, fromcall, tocall, msg_id):
|
def __init__(self, fromcall, tocall, msg_id):
|
||||||
super(AckMessage, self).__init__(fromcall, tocall, msg_id=msg_id)
|
super().__init__(fromcall, tocall, msg_id=msg_id)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "{}>APRS::{}:ack{}\n".format(
|
return "{}>APRS::{}:ack{}\n".format(
|
||||||
self.fromcall, self.tocall.ljust(9), self.id
|
self.fromcall,
|
||||||
|
self.tocall.ljust(9),
|
||||||
|
self.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -384,7 +440,7 @@ class AckMessage(Message):
|
|||||||
class SendAckThread(threads.APRSDThread):
|
class SendAckThread(threads.APRSDThread):
|
||||||
def __init__(self, ack):
|
def __init__(self, ack):
|
||||||
self.ack = ack
|
self.ack = ack
|
||||||
super(SendAckThread, self).__init__("SendAck-{}".format(self.ack.id))
|
super().__init__("SendAck-{}".format(self.ack.id))
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
"""Separate thread to send acks with retries."""
|
"""Separate thread to send acks with retries."""
|
||||||
|
459
aprsd/plugin.py
459
aprsd/plugin.py
@ -3,23 +3,13 @@ import abc
|
|||||||
import fnmatch
|
import fnmatch
|
||||||
import importlib
|
import importlib
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
|
|
||||||
import pluggy
|
import pluggy
|
||||||
import requests
|
|
||||||
import six
|
|
||||||
from thesmuggler import smuggle
|
from thesmuggler import smuggle
|
||||||
|
|
||||||
import aprsd
|
|
||||||
from aprsd import email, messaging
|
|
||||||
from aprsd.fuzzyclock import fuzzy
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
@ -27,18 +17,62 @@ hookspec = pluggy.HookspecMarker("aprsd")
|
|||||||
hookimpl = pluggy.HookimplMarker("aprsd")
|
hookimpl = pluggy.HookimplMarker("aprsd")
|
||||||
|
|
||||||
CORE_PLUGINS = [
|
CORE_PLUGINS = [
|
||||||
"aprsd.plugin.EmailPlugin",
|
"aprsd.plugins.email.EmailPlugin",
|
||||||
"aprsd.plugin.FortunePlugin",
|
"aprsd.plugins.fortune.FortunePlugin",
|
||||||
"aprsd.plugin.LocationPlugin",
|
"aprsd.plugins.location.LocationPlugin",
|
||||||
"aprsd.plugin.PingPlugin",
|
"aprsd.plugins.ping.PingPlugin",
|
||||||
"aprsd.plugin.QueryPlugin",
|
"aprsd.plugins.query.QueryPlugin",
|
||||||
"aprsd.plugin.TimePlugin",
|
"aprsd.plugins.time.TimePlugin",
|
||||||
"aprsd.plugin.WeatherPlugin",
|
"aprsd.plugins.weather.WeatherPlugin",
|
||||||
"aprsd.plugin.VersionPlugin",
|
"aprsd.plugins.version.VersionPlugin",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PluginManager(object):
|
class APRSDCommandSpec:
|
||||||
|
"""A hook specification namespace."""
|
||||||
|
|
||||||
|
@hookspec
|
||||||
|
def run(self, fromcall, message, ack):
|
||||||
|
"""My special little hook that you can customize."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class APRSDPluginBase(metaclass=abc.ABCMeta):
|
||||||
|
def __init__(self, config):
|
||||||
|
"""The aprsd config object is stored."""
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def command_name(self):
|
||||||
|
"""The usage string help."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def command_regex(self):
|
||||||
|
"""The regex to match from the caller"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version(self):
|
||||||
|
"""Version"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def run(self, fromcall, message, ack):
|
||||||
|
if re.search(self.command_regex, message):
|
||||||
|
return self.command(fromcall, message, ack)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def command(self, fromcall, message, ack):
|
||||||
|
"""This is the command that runs when the regex matches.
|
||||||
|
|
||||||
|
To reply with a message over the air, return a string
|
||||||
|
to send.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PluginManager:
|
||||||
# The singleton instance object for this class
|
# The singleton instance object for this class
|
||||||
_instance = None
|
_instance = None
|
||||||
|
|
||||||
@ -51,7 +85,7 @@ class PluginManager(object):
|
|||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
"""This magic turns this into a singleton."""
|
"""This magic turns this into a singleton."""
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super(PluginManager, cls).__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
# Put any initialization here.
|
# Put any initialization here.
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
@ -78,7 +112,7 @@ class PluginManager(object):
|
|||||||
for mem_name, obj in inspect.getmembers(module):
|
for mem_name, obj in inspect.getmembers(module):
|
||||||
if inspect.isclass(obj) and self.is_plugin(obj):
|
if inspect.isclass(obj) and self.is_plugin(obj):
|
||||||
self.obj_list.append(
|
self.obj_list.append(
|
||||||
{"name": mem_name, "obj": obj(self.config)}
|
{"name": mem_name, "obj": obj(self.config)},
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.obj_list
|
return self.obj_list
|
||||||
@ -93,7 +127,6 @@ class PluginManager(object):
|
|||||||
def _create_class(self, module_class_string, super_cls: type = None, **kwargs):
|
def _create_class(self, module_class_string, super_cls: type = None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Method to create a class from a fqn python string.
|
Method to create a class from a fqn python string.
|
||||||
|
|
||||||
:param module_class_string: full name of the class to create an object of
|
:param module_class_string: full name of the class to create an object of
|
||||||
:param super_cls: expected super class for validity, None if bypass
|
:param super_cls: expected super class for validity, None if bypass
|
||||||
:param kwargs: parameters to pass
|
:param kwargs: parameters to pass
|
||||||
@ -107,14 +140,16 @@ class PluginManager(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
assert hasattr(module, class_name), "class {} is not in {}".format(
|
assert hasattr(module, class_name), "class {} is not in {}".format(
|
||||||
class_name, module_name
|
class_name,
|
||||||
|
module_name,
|
||||||
)
|
)
|
||||||
# click.echo('reading class {} from module {}'.format(
|
# click.echo('reading class {} from module {}'.format(
|
||||||
# class_name, module_name))
|
# class_name, module_name))
|
||||||
cls = getattr(module, class_name)
|
cls = getattr(module, class_name)
|
||||||
if super_cls is not None:
|
if super_cls is not None:
|
||||||
assert issubclass(cls, super_cls), "class {} should inherit from {}".format(
|
assert issubclass(cls, super_cls), "class {} should inherit from {}".format(
|
||||||
class_name, super_cls.__name__
|
class_name,
|
||||||
|
super_cls.__name__,
|
||||||
)
|
)
|
||||||
# click.echo('initialising {} with params {}'.format(class_name, kwargs))
|
# click.echo('initialising {} with params {}'.format(class_name, kwargs))
|
||||||
obj = cls(**kwargs)
|
obj = cls(**kwargs)
|
||||||
@ -122,7 +157,6 @@ class PluginManager(object):
|
|||||||
|
|
||||||
def _load_plugin(self, plugin_name):
|
def _load_plugin(self, plugin_name):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Given a python fully qualified class path.name,
|
Given a python fully qualified class path.name,
|
||||||
Try importing the path, then creating the object,
|
Try importing the path, then creating the object,
|
||||||
then registering it as a aprsd Command Plugin
|
then registering it as a aprsd Command Plugin
|
||||||
@ -130,13 +164,17 @@ class PluginManager(object):
|
|||||||
plugin_obj = None
|
plugin_obj = None
|
||||||
try:
|
try:
|
||||||
plugin_obj = self._create_class(
|
plugin_obj = self._create_class(
|
||||||
plugin_name, APRSDPluginBase, config=self.config
|
plugin_name,
|
||||||
|
APRSDPluginBase,
|
||||||
|
config=self.config,
|
||||||
)
|
)
|
||||||
if plugin_obj:
|
if plugin_obj:
|
||||||
LOG.info(
|
LOG.info(
|
||||||
"Registering Command plugin '{}'({}) '{}'".format(
|
"Registering Command plugin '{}'({}) '{}'".format(
|
||||||
plugin_name, plugin_obj.version, plugin_obj.command_regex
|
plugin_name,
|
||||||
)
|
plugin_obj.version,
|
||||||
|
plugin_obj.command_regex,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
self._pluggy_pm.register(plugin_obj)
|
self._pluggy_pm.register(plugin_obj)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@ -172,8 +210,10 @@ class PluginManager(object):
|
|||||||
if plugin_obj:
|
if plugin_obj:
|
||||||
LOG.info(
|
LOG.info(
|
||||||
"Registering Command plugin '{}'({}) '{}'".format(
|
"Registering Command plugin '{}'({}) '{}'".format(
|
||||||
o["name"], o["obj"].version, o["obj"].command_regex
|
o["name"],
|
||||||
)
|
o["obj"].version,
|
||||||
|
o["obj"].command_regex,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
self._pluggy_pm.register(o["obj"])
|
self._pluggy_pm.register(o["obj"])
|
||||||
|
|
||||||
@ -191,362 +231,3 @@ class PluginManager(object):
|
|||||||
|
|
||||||
def get_plugins(self):
|
def get_plugins(self):
|
||||||
return self._pluggy_pm.get_plugins()
|
return self._pluggy_pm.get_plugins()
|
||||||
|
|
||||||
|
|
||||||
class APRSDCommandSpec:
|
|
||||||
"""A hook specification namespace."""
|
|
||||||
|
|
||||||
@hookspec
|
|
||||||
def run(self, fromcall, message, ack):
|
|
||||||
"""My special little hook that you can customize."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
|
||||||
class APRSDPluginBase(object):
|
|
||||||
def __init__(self, config):
|
|
||||||
"""The aprsd config object is stored."""
|
|
||||||
self.config = config
|
|
||||||
|
|
||||||
@property
|
|
||||||
def command_name(self):
|
|
||||||
"""The usage string help."""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@property
|
|
||||||
def command_regex(self):
|
|
||||||
"""The regex to match from the caller"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@property
|
|
||||||
def version(self):
|
|
||||||
"""Version"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@hookimpl
|
|
||||||
def run(self, fromcall, message, ack):
|
|
||||||
if re.search(self.command_regex, message):
|
|
||||||
return self.command(fromcall, message, ack)
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def command(self, fromcall, message, ack):
|
|
||||||
"""This is the command that runs when the regex matches.
|
|
||||||
|
|
||||||
To reply with a message over the air, return a string
|
|
||||||
to send.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FortunePlugin(APRSDPluginBase):
|
|
||||||
"""Fortune."""
|
|
||||||
|
|
||||||
version = "1.0"
|
|
||||||
command_regex = "^[fF]"
|
|
||||||
command_name = "fortune"
|
|
||||||
|
|
||||||
def command(self, fromcall, message, ack):
|
|
||||||
LOG.info("FortunePlugin")
|
|
||||||
reply = None
|
|
||||||
|
|
||||||
fortune_path = shutil.which("fortune")
|
|
||||||
if not fortune_path:
|
|
||||||
reply = "Fortune command not installed"
|
|
||||||
return reply
|
|
||||||
|
|
||||||
try:
|
|
||||||
process = subprocess.Popen(
|
|
||||||
[fortune_path, "-s", "-n 60"], stdout=subprocess.PIPE
|
|
||||||
)
|
|
||||||
reply = process.communicate()[0]
|
|
||||||
reply = reply.decode(errors="ignore").rstrip()
|
|
||||||
except Exception as ex:
|
|
||||||
reply = "Fortune command failed '{}'".format(ex)
|
|
||||||
LOG.error(reply)
|
|
||||||
|
|
||||||
return reply
|
|
||||||
|
|
||||||
|
|
||||||
class LocationPlugin(APRSDPluginBase):
|
|
||||||
"""Location!"""
|
|
||||||
|
|
||||||
version = "1.0"
|
|
||||||
command_regex = "^[lL]"
|
|
||||||
command_name = "location"
|
|
||||||
|
|
||||||
config_items = {"apikey": "aprs.fi api key here"}
|
|
||||||
|
|
||||||
def command(self, fromcall, message, ack):
|
|
||||||
LOG.info("Location Plugin")
|
|
||||||
# get last location of a callsign, get descriptive name from weather service
|
|
||||||
try:
|
|
||||||
# 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
|
|
||||||
url = (
|
|
||||||
"http://api.aprs.fi/api/get?name="
|
|
||||||
+ searchcall
|
|
||||||
+ "&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json"
|
|
||||||
)
|
|
||||||
response = requests.get(url)
|
|
||||||
# aprs_data = json.loads(response.read())
|
|
||||||
aprs_data = json.loads(response.text)
|
|
||||||
LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
|
|
||||||
lat = aprs_data["entries"][0]["lat"]
|
|
||||||
lon = aprs_data["entries"][0]["lng"]
|
|
||||||
try: # altitude not always provided
|
|
||||||
alt = aprs_data["entries"][0]["altitude"]
|
|
||||||
except Exception:
|
|
||||||
alt = 0
|
|
||||||
altfeet = int(alt * 3.28084)
|
|
||||||
aprs_lasttime_seconds = aprs_data["entries"][0]["lasttime"]
|
|
||||||
# aprs_lasttime_seconds = aprs_lasttime_seconds.encode(
|
|
||||||
# "ascii", errors="ignore"
|
|
||||||
# ) # unicode to ascii
|
|
||||||
delta_seconds = time.time() - int(aprs_lasttime_seconds)
|
|
||||||
delta_hours = delta_seconds / 60 / 60
|
|
||||||
url2 = (
|
|
||||||
"https://forecast.weather.gov/MapClick.php?lat="
|
|
||||||
+ str(lat)
|
|
||||||
+ "&lon="
|
|
||||||
+ str(lon)
|
|
||||||
+ "&FcstType=json"
|
|
||||||
)
|
|
||||||
response2 = requests.get(url2)
|
|
||||||
wx_data = json.loads(response2.text)
|
|
||||||
|
|
||||||
reply = "{}: {} {}' {},{} {}h ago".format(
|
|
||||||
searchcall,
|
|
||||||
wx_data["location"]["areaDescription"],
|
|
||||||
str(altfeet),
|
|
||||||
str(alt),
|
|
||||||
str(lon),
|
|
||||||
str("%.1f" % round(delta_hours, 1)),
|
|
||||||
).rstrip()
|
|
||||||
except Exception as e:
|
|
||||||
LOG.debug("Locate failed with: " + "%s" % str(e))
|
|
||||||
reply = "Unable to find station " + searchcall + ". Sending beacons?"
|
|
||||||
|
|
||||||
return reply
|
|
||||||
|
|
||||||
|
|
||||||
class PingPlugin(APRSDPluginBase):
|
|
||||||
"""Ping."""
|
|
||||||
|
|
||||||
version = "1.0"
|
|
||||||
command_regex = "^[pP]"
|
|
||||||
command_name = "ping"
|
|
||||||
|
|
||||||
def command(self, fromcall, message, ack):
|
|
||||||
LOG.info("PINGPlugin")
|
|
||||||
stm = time.localtime()
|
|
||||||
h = stm.tm_hour
|
|
||||||
m = stm.tm_min
|
|
||||||
s = stm.tm_sec
|
|
||||||
reply = (
|
|
||||||
"Pong! " + str(h).zfill(2) + ":" + str(m).zfill(2) + ":" + str(s).zfill(2)
|
|
||||||
)
|
|
||||||
return reply.rstrip()
|
|
||||||
|
|
||||||
|
|
||||||
class QueryPlugin(APRSDPluginBase):
|
|
||||||
"""Query command."""
|
|
||||||
|
|
||||||
version = "1.0"
|
|
||||||
command_regex = r"^\?.*"
|
|
||||||
command_name = "query"
|
|
||||||
|
|
||||||
def command(self, fromcall, message, ack):
|
|
||||||
LOG.info("Query COMMAND")
|
|
||||||
|
|
||||||
tracker = messaging.MsgTrack()
|
|
||||||
reply = "Pending Messages ({})".format(len(tracker))
|
|
||||||
|
|
||||||
searchstring = "^" + self.config["ham"]["callsign"] + ".*"
|
|
||||||
# only I can do admin commands
|
|
||||||
if re.search(searchstring, fromcall):
|
|
||||||
r = re.search(r"^\?-\*", message)
|
|
||||||
if r is not None:
|
|
||||||
if len(tracker) > 0:
|
|
||||||
reply = "Resend ALL Delayed msgs"
|
|
||||||
LOG.debug(reply)
|
|
||||||
tracker.restart_delayed()
|
|
||||||
else:
|
|
||||||
reply = "No Delayed Msgs"
|
|
||||||
LOG.debug(reply)
|
|
||||||
return reply
|
|
||||||
|
|
||||||
r = re.search(r"^\?-[fF]!", message)
|
|
||||||
if r is not None:
|
|
||||||
reply = "Deleting ALL Delayed msgs."
|
|
||||||
LOG.debug(reply)
|
|
||||||
tracker.flush()
|
|
||||||
return reply
|
|
||||||
|
|
||||||
return reply
|
|
||||||
|
|
||||||
|
|
||||||
class TimePlugin(APRSDPluginBase):
|
|
||||||
"""Time command."""
|
|
||||||
|
|
||||||
version = "1.0"
|
|
||||||
command_regex = "^[tT]"
|
|
||||||
command_name = "time"
|
|
||||||
|
|
||||||
def command(self, fromcall, message, ack):
|
|
||||||
LOG.info("TIME COMMAND")
|
|
||||||
stm = time.localtime()
|
|
||||||
h = stm.tm_hour
|
|
||||||
m = stm.tm_min
|
|
||||||
cur_time = fuzzy(h, m, 1)
|
|
||||||
reply = "{} ({}:{} PDT) ({})".format(
|
|
||||||
cur_time, str(h), str(m).rjust(2, "0"), message.rstrip()
|
|
||||||
)
|
|
||||||
return reply
|
|
||||||
|
|
||||||
|
|
||||||
class WeatherPlugin(APRSDPluginBase):
|
|
||||||
"""Weather Command"""
|
|
||||||
|
|
||||||
version = "1.0"
|
|
||||||
command_regex = "^[wW]"
|
|
||||||
command_name = "weather"
|
|
||||||
|
|
||||||
def command(self, fromcall, message, ack):
|
|
||||||
LOG.info("Weather Plugin")
|
|
||||||
try:
|
|
||||||
url = (
|
|
||||||
"http://api.aprs.fi/api/get?"
|
|
||||||
"&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json"
|
|
||||||
"&name=%s" % fromcall
|
|
||||||
)
|
|
||||||
response = requests.get(url)
|
|
||||||
# aprs_data = json.loads(response.read())
|
|
||||||
aprs_data = json.loads(response.text)
|
|
||||||
lat = aprs_data["entries"][0]["lat"]
|
|
||||||
lon = aprs_data["entries"][0]["lng"]
|
|
||||||
url2 = (
|
|
||||||
"https://forecast.weather.gov/MapClick.php?lat=%s"
|
|
||||||
"&lon=%s&FcstType=json" % (lat, lon)
|
|
||||||
)
|
|
||||||
response2 = requests.get(url2)
|
|
||||||
# wx_data = json.loads(response2.read())
|
|
||||||
wx_data = json.loads(response2.text)
|
|
||||||
reply = (
|
|
||||||
"%sF(%sF/%sF) %s. %s, %s."
|
|
||||||
% (
|
|
||||||
wx_data["currentobservation"]["Temp"],
|
|
||||||
wx_data["data"]["temperature"][0],
|
|
||||||
wx_data["data"]["temperature"][1],
|
|
||||||
wx_data["data"]["weather"][0],
|
|
||||||
wx_data["time"]["startPeriodName"][1],
|
|
||||||
wx_data["data"]["weather"][1],
|
|
||||||
)
|
|
||||||
).rstrip()
|
|
||||||
LOG.debug("reply: '{}' ".format(reply))
|
|
||||||
except Exception as e:
|
|
||||||
LOG.debug("Weather failed with: " + "%s" % str(e))
|
|
||||||
reply = "Unable to find you (send beacon?)"
|
|
||||||
|
|
||||||
return reply
|
|
||||||
|
|
||||||
|
|
||||||
class EmailPlugin(APRSDPluginBase):
|
|
||||||
"""Email Plugin."""
|
|
||||||
|
|
||||||
version = "1.0"
|
|
||||||
command_regex = "^-.*"
|
|
||||||
command_name = "email"
|
|
||||||
|
|
||||||
# message_number:time combos so we don't resend the same email in
|
|
||||||
# five mins {int:int}
|
|
||||||
email_sent_dict = {}
|
|
||||||
|
|
||||||
def command(self, fromcall, message, ack):
|
|
||||||
LOG.info("Email COMMAND")
|
|
||||||
reply = None
|
|
||||||
|
|
||||||
searchstring = "^" + self.config["ham"]["callsign"] + ".*"
|
|
||||||
# only I can do email
|
|
||||||
if re.search(searchstring, fromcall):
|
|
||||||
# digits only, first one is number of emails to resend
|
|
||||||
r = re.search("^-([0-9])[0-9]*$", message)
|
|
||||||
if r is not None:
|
|
||||||
LOG.debug("RESEND EMAIL")
|
|
||||||
email.resend_email(r.group(1), fromcall)
|
|
||||||
reply = messaging.NULL_MESSAGE
|
|
||||||
# -user@address.com body of email
|
|
||||||
elif re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message):
|
|
||||||
# (same search again)
|
|
||||||
a = re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message)
|
|
||||||
if a is not None:
|
|
||||||
to_addr = a.group(1)
|
|
||||||
content = a.group(2)
|
|
||||||
|
|
||||||
email_address = email.get_email_from_shortcut(to_addr)
|
|
||||||
if not email_address:
|
|
||||||
reply = "Bad email address"
|
|
||||||
return reply
|
|
||||||
|
|
||||||
# send recipient link to aprs.fi map
|
|
||||||
if content == "mapme":
|
|
||||||
content = "Click for my location: http://aprs.fi/{}".format(
|
|
||||||
self.config["ham"]["callsign"]
|
|
||||||
)
|
|
||||||
too_soon = 0
|
|
||||||
now = time.time()
|
|
||||||
# see if we sent this msg number recently
|
|
||||||
if ack in self.email_sent_dict:
|
|
||||||
# BUG(hemna) - when we get a 2 different email command
|
|
||||||
# with the same ack #, we don't send it.
|
|
||||||
timedelta = now - self.email_sent_dict[ack]
|
|
||||||
if timedelta < 300: # five minutes
|
|
||||||
too_soon = 1
|
|
||||||
if not too_soon or ack == 0:
|
|
||||||
LOG.info("Send email '{}'".format(content))
|
|
||||||
send_result = email.send_email(to_addr, content)
|
|
||||||
if send_result != 0:
|
|
||||||
reply = "-{} failed".format(to_addr)
|
|
||||||
# messaging.send_message(fromcall, "-" + to_addr + " failed")
|
|
||||||
else:
|
|
||||||
# clear email sent dictionary if somehow goes over 100
|
|
||||||
if len(self.email_sent_dict) > 98:
|
|
||||||
LOG.debug(
|
|
||||||
"DEBUG: email_sent_dict is big ("
|
|
||||||
+ str(len(self.email_sent_dict))
|
|
||||||
+ ") clearing out."
|
|
||||||
)
|
|
||||||
self.email_sent_dict.clear()
|
|
||||||
self.email_sent_dict[ack] = now
|
|
||||||
else:
|
|
||||||
LOG.info(
|
|
||||||
"Email for message number "
|
|
||||||
+ ack
|
|
||||||
+ " recently sent, not sending again."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
reply = "Bad email address"
|
|
||||||
# messaging.send_message(fromcall, "Bad email address")
|
|
||||||
|
|
||||||
return reply
|
|
||||||
|
|
||||||
|
|
||||||
class VersionPlugin(APRSDPluginBase):
|
|
||||||
"""Version of APRSD Plugin."""
|
|
||||||
|
|
||||||
version = "1.0"
|
|
||||||
command_regex = "^[vV]"
|
|
||||||
command_name = "version"
|
|
||||||
|
|
||||||
# message_number:time combos so we don't resend the same email in
|
|
||||||
# five mins {int:int}
|
|
||||||
email_sent_dict = {}
|
|
||||||
|
|
||||||
def command(self, fromcall, message, ack):
|
|
||||||
LOG.info("Version COMMAND")
|
|
||||||
return "APRSD version '{}'".format(aprsd.__version__)
|
|
||||||
|
0
aprsd/plugins/__init__.py
Normal file
0
aprsd/plugins/__init__.py
Normal file
88
aprsd/plugins/email.py
Normal file
88
aprsd/plugins/email.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
|
from aprsd import email, messaging, plugin
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class EmailPlugin(plugin.APRSDPluginBase):
|
||||||
|
"""Email Plugin."""
|
||||||
|
|
||||||
|
version = "1.0"
|
||||||
|
command_regex = "^-.*"
|
||||||
|
command_name = "email"
|
||||||
|
|
||||||
|
# message_number:time combos so we don't resend the same email in
|
||||||
|
# five mins {int:int}
|
||||||
|
email_sent_dict = {}
|
||||||
|
|
||||||
|
def command(self, fromcall, message, ack):
|
||||||
|
LOG.info("Email COMMAND")
|
||||||
|
reply = None
|
||||||
|
|
||||||
|
searchstring = "^" + self.config["ham"]["callsign"] + ".*"
|
||||||
|
# only I can do email
|
||||||
|
if re.search(searchstring, fromcall):
|
||||||
|
# digits only, first one is number of emails to resend
|
||||||
|
r = re.search("^-([0-9])[0-9]*$", message)
|
||||||
|
if r is not None:
|
||||||
|
LOG.debug("RESEND EMAIL")
|
||||||
|
email.resend_email(r.group(1), fromcall)
|
||||||
|
reply = messaging.NULL_MESSAGE
|
||||||
|
# -user@address.com body of email
|
||||||
|
elif re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message):
|
||||||
|
# (same search again)
|
||||||
|
a = re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message)
|
||||||
|
if a is not None:
|
||||||
|
to_addr = a.group(1)
|
||||||
|
content = a.group(2)
|
||||||
|
|
||||||
|
email_address = email.get_email_from_shortcut(to_addr)
|
||||||
|
if not email_address:
|
||||||
|
reply = "Bad email address"
|
||||||
|
return reply
|
||||||
|
|
||||||
|
# send recipient link to aprs.fi map
|
||||||
|
if content == "mapme":
|
||||||
|
content = "Click for my location: http://aprs.fi/{}".format(
|
||||||
|
self.config["ham"]["callsign"],
|
||||||
|
)
|
||||||
|
too_soon = 0
|
||||||
|
now = time.time()
|
||||||
|
# see if we sent this msg number recently
|
||||||
|
if ack in self.email_sent_dict:
|
||||||
|
# BUG(hemna) - when we get a 2 different email command
|
||||||
|
# with the same ack #, we don't send it.
|
||||||
|
timedelta = now - self.email_sent_dict[ack]
|
||||||
|
if timedelta < 300: # five minutes
|
||||||
|
too_soon = 1
|
||||||
|
if not too_soon or ack == 0:
|
||||||
|
LOG.info("Send email '{}'".format(content))
|
||||||
|
send_result = email.send_email(to_addr, content)
|
||||||
|
reply = messaging.NULL_MESSAGE
|
||||||
|
if send_result != 0:
|
||||||
|
reply = "-{} failed".format(to_addr)
|
||||||
|
# messaging.send_message(fromcall, "-" + to_addr + " failed")
|
||||||
|
else:
|
||||||
|
# clear email sent dictionary if somehow goes over 100
|
||||||
|
if len(self.email_sent_dict) > 98:
|
||||||
|
LOG.debug(
|
||||||
|
"DEBUG: email_sent_dict is big ("
|
||||||
|
+ str(len(self.email_sent_dict))
|
||||||
|
+ ") clearing out.",
|
||||||
|
)
|
||||||
|
self.email_sent_dict.clear()
|
||||||
|
self.email_sent_dict[ack] = now
|
||||||
|
else:
|
||||||
|
LOG.info(
|
||||||
|
"Email for message number "
|
||||||
|
+ ack
|
||||||
|
+ " recently sent, not sending again.",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
reply = "Bad email address"
|
||||||
|
# messaging.send_message(fromcall, "Bad email address")
|
||||||
|
|
||||||
|
return reply
|
40
aprsd/plugins/fortune.py
Normal file
40
aprsd/plugins/fortune.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from aprsd import plugin
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class FortunePlugin(plugin.APRSDPluginBase):
|
||||||
|
"""Fortune."""
|
||||||
|
|
||||||
|
version = "1.0"
|
||||||
|
command_regex = "^[fF]"
|
||||||
|
command_name = "fortune"
|
||||||
|
|
||||||
|
def command(self, fromcall, message, ack):
|
||||||
|
LOG.info("FortunePlugin")
|
||||||
|
reply = None
|
||||||
|
|
||||||
|
fortune_path = shutil.which("fortune")
|
||||||
|
if not fortune_path:
|
||||||
|
reply = "Fortune command not installed"
|
||||||
|
return reply
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmnd = [fortune_path, "-s", "-n 60"]
|
||||||
|
command = " ".join(cmnd)
|
||||||
|
output = subprocess.check_output(
|
||||||
|
command,
|
||||||
|
shell=True,
|
||||||
|
timeout=3,
|
||||||
|
universal_newlines=True,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as ex:
|
||||||
|
reply = "Fortune command failed '{}'".format(ex.output)
|
||||||
|
else:
|
||||||
|
reply = output
|
||||||
|
|
||||||
|
return reply
|
78
aprsd/plugins/location.py
Normal file
78
aprsd/plugins/location.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
|
from aprsd import plugin
|
||||||
|
import requests
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class LocationPlugin(plugin.APRSDPluginBase):
|
||||||
|
"""Location!"""
|
||||||
|
|
||||||
|
version = "1.0"
|
||||||
|
command_regex = "^[lL]"
|
||||||
|
command_name = "location"
|
||||||
|
|
||||||
|
config_items = {"apikey": "aprs.fi api key here"}
|
||||||
|
|
||||||
|
def command(self, fromcall, message, ack):
|
||||||
|
LOG.info("Location Plugin")
|
||||||
|
# get last location of a callsign, get descriptive name from weather service
|
||||||
|
api_key = self.config["aprs.fi"]["apiKey"]
|
||||||
|
try:
|
||||||
|
# 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
|
||||||
|
url = (
|
||||||
|
"http://api.aprs.fi/api/get?name="
|
||||||
|
+ searchcall
|
||||||
|
+ "&what=loc&apikey={}&format=json".format(api_key)
|
||||||
|
)
|
||||||
|
response = requests.get(url)
|
||||||
|
# aprs_data = json.loads(response.read())
|
||||||
|
aprs_data = json.loads(response.text)
|
||||||
|
LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
|
||||||
|
lat = aprs_data["entries"][0]["lat"]
|
||||||
|
lon = aprs_data["entries"][0]["lng"]
|
||||||
|
try: # altitude not always provided
|
||||||
|
alt = aprs_data["entries"][0]["altitude"]
|
||||||
|
except Exception:
|
||||||
|
alt = 0
|
||||||
|
altfeet = int(alt * 3.28084)
|
||||||
|
aprs_lasttime_seconds = aprs_data["entries"][0]["lasttime"]
|
||||||
|
# aprs_lasttime_seconds = aprs_lasttime_seconds.encode(
|
||||||
|
# "ascii", errors="ignore"
|
||||||
|
# ) # unicode to ascii
|
||||||
|
delta_seconds = time.time() - int(aprs_lasttime_seconds)
|
||||||
|
delta_hours = delta_seconds / 60 / 60
|
||||||
|
url2 = (
|
||||||
|
"https://forecast.weather.gov/MapClick.php?lat="
|
||||||
|
+ str(lat)
|
||||||
|
+ "&lon="
|
||||||
|
+ str(lon)
|
||||||
|
+ "&FcstType=json"
|
||||||
|
)
|
||||||
|
response2 = requests.get(url2)
|
||||||
|
wx_data = json.loads(response2.text)
|
||||||
|
|
||||||
|
reply = "{}: {} {}' {},{} {}h ago".format(
|
||||||
|
searchcall,
|
||||||
|
wx_data["location"]["areaDescription"],
|
||||||
|
str(altfeet),
|
||||||
|
str(lat),
|
||||||
|
str(lon),
|
||||||
|
str("%.1f" % round(delta_hours, 1)),
|
||||||
|
).rstrip()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.debug("Locate failed with: " + "%s" % str(e))
|
||||||
|
reply = "Unable to find station " + searchcall + ". Sending beacons?"
|
||||||
|
|
||||||
|
return reply
|
25
aprsd/plugins/ping.py
Normal file
25
aprsd/plugins/ping.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
from aprsd import plugin
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class PingPlugin(plugin.APRSDPluginBase):
|
||||||
|
"""Ping."""
|
||||||
|
|
||||||
|
version = "1.0"
|
||||||
|
command_regex = "^[pP]"
|
||||||
|
command_name = "ping"
|
||||||
|
|
||||||
|
def command(self, fromcall, message, ack):
|
||||||
|
LOG.info("PINGPlugin")
|
||||||
|
stm = time.localtime()
|
||||||
|
h = stm.tm_hour
|
||||||
|
m = stm.tm_min
|
||||||
|
s = stm.tm_sec
|
||||||
|
reply = (
|
||||||
|
"Pong! " + str(h).zfill(2) + ":" + str(m).zfill(2) + ":" + str(s).zfill(2)
|
||||||
|
)
|
||||||
|
return reply.rstrip()
|
64
aprsd/plugins/query.py
Normal file
64
aprsd/plugins/query.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from aprsd import messaging, plugin
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class QueryPlugin(plugin.APRSDPluginBase):
|
||||||
|
"""Query command."""
|
||||||
|
|
||||||
|
version = "1.0"
|
||||||
|
command_regex = r"^\?.*"
|
||||||
|
command_name = "query"
|
||||||
|
|
||||||
|
def command(self, fromcall, message, ack):
|
||||||
|
LOG.info("Query COMMAND")
|
||||||
|
|
||||||
|
tracker = messaging.MsgTrack()
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
reply = "Pending messages ({}) {}".format(
|
||||||
|
len(tracker),
|
||||||
|
now.strftime("%H:%M:%S"),
|
||||||
|
)
|
||||||
|
|
||||||
|
searchstring = "^" + self.config["ham"]["callsign"] + ".*"
|
||||||
|
# only I can do admin commands
|
||||||
|
if re.search(searchstring, fromcall):
|
||||||
|
|
||||||
|
# resend last N most recent: "?3"
|
||||||
|
r = re.search(r"^\?([0-9]).*", message)
|
||||||
|
if r is not None:
|
||||||
|
if len(tracker) > 0:
|
||||||
|
last_n = r.group(1)
|
||||||
|
reply = messaging.NULL_MESSAGE
|
||||||
|
LOG.debug(reply)
|
||||||
|
tracker.restart_delayed(count=int(last_n))
|
||||||
|
else:
|
||||||
|
reply = "No pending msgs to resend"
|
||||||
|
LOG.debug(reply)
|
||||||
|
return reply
|
||||||
|
|
||||||
|
# resend all: "?a"
|
||||||
|
r = re.search(r"^\?[aA].*", message)
|
||||||
|
if r is not None:
|
||||||
|
if len(tracker) > 0:
|
||||||
|
reply = messaging.NULL_MESSAGE
|
||||||
|
LOG.debug(reply)
|
||||||
|
tracker.restart_delayed()
|
||||||
|
else:
|
||||||
|
reply = "No pending msgs"
|
||||||
|
LOG.debug(reply)
|
||||||
|
return reply
|
||||||
|
|
||||||
|
# delete all: "?d"
|
||||||
|
r = re.search(r"^\?[dD].*", message)
|
||||||
|
if r is not None:
|
||||||
|
reply = "Deleted ALL pending msgs."
|
||||||
|
LOG.debug(reply)
|
||||||
|
tracker.flush()
|
||||||
|
return reply
|
||||||
|
|
||||||
|
return reply
|
28
aprsd/plugins/time.py
Normal file
28
aprsd/plugins/time.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
from aprsd import fuzzyclock, plugin
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class TimePlugin(plugin.APRSDPluginBase):
|
||||||
|
"""Time command."""
|
||||||
|
|
||||||
|
version = "1.0"
|
||||||
|
command_regex = "^[tT]"
|
||||||
|
command_name = "time"
|
||||||
|
|
||||||
|
def command(self, fromcall, message, ack):
|
||||||
|
LOG.info("TIME COMMAND")
|
||||||
|
stm = time.localtime()
|
||||||
|
h = stm.tm_hour
|
||||||
|
m = stm.tm_min
|
||||||
|
cur_time = fuzzyclock.fuzzy(h, m, 1)
|
||||||
|
reply = "{} ({}:{} PDT) ({})".format(
|
||||||
|
cur_time,
|
||||||
|
str(h),
|
||||||
|
str(m).rjust(2, "0"),
|
||||||
|
message.rstrip(),
|
||||||
|
)
|
||||||
|
return reply
|
22
aprsd/plugins/version.py
Normal file
22
aprsd/plugins/version.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import aprsd
|
||||||
|
from aprsd import plugin
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class VersionPlugin(plugin.APRSDPluginBase):
|
||||||
|
"""Version of APRSD Plugin."""
|
||||||
|
|
||||||
|
version = "1.0"
|
||||||
|
command_regex = "^[vV]"
|
||||||
|
command_name = "version"
|
||||||
|
|
||||||
|
# message_number:time combos so we don't resend the same email in
|
||||||
|
# five mins {int:int}
|
||||||
|
email_sent_dict = {}
|
||||||
|
|
||||||
|
def command(self, fromcall, message, ack):
|
||||||
|
LOG.info("Version COMMAND")
|
||||||
|
return "APRSD version '{}'".format(aprsd.__version__)
|
54
aprsd/plugins/weather.py
Normal file
54
aprsd/plugins/weather.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aprsd import plugin
|
||||||
|
import requests
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class WeatherPlugin(plugin.APRSDPluginBase):
|
||||||
|
"""Weather Command"""
|
||||||
|
|
||||||
|
version = "1.0"
|
||||||
|
command_regex = "^[wW]"
|
||||||
|
command_name = "weather"
|
||||||
|
|
||||||
|
def command(self, fromcall, message, ack):
|
||||||
|
LOG.info("Weather Plugin")
|
||||||
|
api_key = self.config["aprs.fi"]["apiKey"]
|
||||||
|
try:
|
||||||
|
url = (
|
||||||
|
"http://api.aprs.fi/api/get?"
|
||||||
|
"&what=loc&apikey={}&format=json"
|
||||||
|
"&name={}".format(api_key, fromcall)
|
||||||
|
)
|
||||||
|
response = requests.get(url)
|
||||||
|
# aprs_data = json.loads(response.read())
|
||||||
|
aprs_data = json.loads(response.text)
|
||||||
|
lat = aprs_data["entries"][0]["lat"]
|
||||||
|
lon = aprs_data["entries"][0]["lng"]
|
||||||
|
url2 = (
|
||||||
|
"https://forecast.weather.gov/MapClick.php?lat=%s"
|
||||||
|
"&lon=%s&FcstType=json" % (lat, lon)
|
||||||
|
)
|
||||||
|
response2 = requests.get(url2)
|
||||||
|
# wx_data = json.loads(response2.read())
|
||||||
|
wx_data = json.loads(response2.text)
|
||||||
|
reply = (
|
||||||
|
"%sF(%sF/%sF) %s. %s, %s."
|
||||||
|
% (
|
||||||
|
wx_data["currentobservation"]["Temp"],
|
||||||
|
wx_data["data"]["temperature"][0],
|
||||||
|
wx_data["data"]["temperature"][1],
|
||||||
|
wx_data["data"]["weather"][0],
|
||||||
|
wx_data["time"]["startPeriodName"][1],
|
||||||
|
wx_data["data"]["weather"][1],
|
||||||
|
)
|
||||||
|
).rstrip()
|
||||||
|
LOG.debug("reply: '{}' ".format(reply))
|
||||||
|
except Exception as e:
|
||||||
|
LOG.debug("Weather failed with: " + "%s" % str(e))
|
||||||
|
reply = "Unable to find you (send beacon?)"
|
||||||
|
|
||||||
|
return reply
|
@ -4,9 +4,8 @@ import queue
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import aprslib
|
|
||||||
|
|
||||||
from aprsd import client, messaging, plugin
|
from aprsd import client, messaging, plugin
|
||||||
|
import aprslib
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
@ -15,7 +14,7 @@ TX_THREAD = "TX"
|
|||||||
EMAIL_THREAD = "Email"
|
EMAIL_THREAD = "Email"
|
||||||
|
|
||||||
|
|
||||||
class APRSDThreadList(object):
|
class APRSDThreadList:
|
||||||
"""Singleton class that keeps track of application wide threads."""
|
"""Singleton class that keeps track of application wide threads."""
|
||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
@ -25,7 +24,7 @@ class APRSDThreadList(object):
|
|||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super(APRSDThreadList, cls).__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
cls.lock = threading.Lock()
|
cls.lock = threading.Lock()
|
||||||
cls.threads_list = []
|
cls.threads_list = []
|
||||||
return cls._instance
|
return cls._instance
|
||||||
@ -47,7 +46,7 @@ class APRSDThreadList(object):
|
|||||||
|
|
||||||
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
super(APRSDThread, self).__init__(name=name)
|
super().__init__(name=name)
|
||||||
self.thread_stop = False
|
self.thread_stop = False
|
||||||
APRSDThreadList().add(self)
|
APRSDThreadList().add(self)
|
||||||
|
|
||||||
@ -55,18 +54,18 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
|||||||
self.thread_stop = True
|
self.thread_stop = True
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
LOG.info("Starting")
|
LOG.debug("Starting")
|
||||||
while not self.thread_stop:
|
while not self.thread_stop:
|
||||||
can_loop = self.loop()
|
can_loop = self.loop()
|
||||||
if not can_loop:
|
if not can_loop:
|
||||||
self.stop()
|
self.stop()
|
||||||
APRSDThreadList().remove(self)
|
APRSDThreadList().remove(self)
|
||||||
LOG.info("Exiting")
|
LOG.debug("Exiting")
|
||||||
|
|
||||||
|
|
||||||
class APRSDRXThread(APRSDThread):
|
class APRSDRXThread(APRSDThread):
|
||||||
def __init__(self, msg_queues, config):
|
def __init__(self, msg_queues, config):
|
||||||
super(APRSDRXThread, self).__init__("RX_MSG")
|
super().__init__("RX_MSG")
|
||||||
self.msg_queues = msg_queues
|
self.msg_queues = msg_queues
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
@ -111,12 +110,14 @@ class APRSDRXThread(APRSDThread):
|
|||||||
ack_num = packet.get("msgNo")
|
ack_num = packet.get("msgNo")
|
||||||
LOG.info("Got ack for message {}".format(ack_num))
|
LOG.info("Got ack for message {}".format(ack_num))
|
||||||
messaging.log_message(
|
messaging.log_message(
|
||||||
"ACK", packet["raw"], None, ack=ack_num, fromcall=packet["from"]
|
"ACK",
|
||||||
|
packet["raw"],
|
||||||
|
None,
|
||||||
|
ack=ack_num,
|
||||||
|
fromcall=packet["from"],
|
||||||
)
|
)
|
||||||
tracker = messaging.MsgTrack()
|
tracker = messaging.MsgTrack()
|
||||||
tracker.remove(ack_num)
|
tracker.remove(ack_num)
|
||||||
LOG.debug("Length of MsgTrack is {}".format(len(tracker)))
|
|
||||||
# messaging.ack_dict.update({int(ack_num): 1})
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def process_mic_e_packet(self, packet):
|
def process_mic_e_packet(self, packet):
|
||||||
@ -125,7 +126,6 @@ class APRSDRXThread(APRSDThread):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def process_message_packet(self, packet):
|
def process_message_packet(self, packet):
|
||||||
LOG.info("Got a message packet")
|
|
||||||
fromcall = packet["from"]
|
fromcall = packet["from"]
|
||||||
message = packet.get("message_text", None)
|
message = packet.get("message_text", None)
|
||||||
|
|
||||||
@ -153,7 +153,9 @@ class APRSDRXThread(APRSDThread):
|
|||||||
LOG.debug("Sending '{}'".format(reply))
|
LOG.debug("Sending '{}'".format(reply))
|
||||||
|
|
||||||
msg = messaging.TextMessage(
|
msg = messaging.TextMessage(
|
||||||
self.config["aprs"]["login"], fromcall, reply
|
self.config["aprs"]["login"],
|
||||||
|
fromcall,
|
||||||
|
reply,
|
||||||
)
|
)
|
||||||
self.msg_queues["tx"].put(msg)
|
self.msg_queues["tx"].put(msg)
|
||||||
else:
|
else:
|
||||||
@ -164,9 +166,13 @@ class APRSDRXThread(APRSDThread):
|
|||||||
names = [x.command_name for x in plugins]
|
names = [x.command_name for x in plugins]
|
||||||
names.sort()
|
names.sort()
|
||||||
|
|
||||||
reply = "Usage: {}".format(", ".join(names))
|
# reply = "Usage: {}".format(", ".join(names))
|
||||||
|
reply = "Usage: weather, locate [call], time, fortune, ping"
|
||||||
|
|
||||||
msg = messaging.TextMessage(
|
msg = messaging.TextMessage(
|
||||||
self.config["aprs"]["login"], fromcall, reply
|
self.config["aprs"]["login"],
|
||||||
|
fromcall,
|
||||||
|
reply,
|
||||||
)
|
)
|
||||||
self.msg_queues["tx"].put(msg)
|
self.msg_queues["tx"].put(msg)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@ -178,7 +184,9 @@ class APRSDRXThread(APRSDThread):
|
|||||||
# let any threads do their thing, then ack
|
# let any threads do their thing, then ack
|
||||||
# send an ack last
|
# send an ack last
|
||||||
ack = messaging.AckMessage(
|
ack = messaging.AckMessage(
|
||||||
self.config["aprs"]["login"], fromcall, msg_id=msg_id
|
self.config["aprs"]["login"],
|
||||||
|
fromcall,
|
||||||
|
msg_id=msg_id,
|
||||||
)
|
)
|
||||||
self.msg_queues["tx"].put(ack)
|
self.msg_queues["tx"].put(ack)
|
||||||
LOG.debug("Packet processing complete")
|
LOG.debug("Packet processing complete")
|
||||||
@ -186,9 +194,8 @@ class APRSDRXThread(APRSDThread):
|
|||||||
def process_packet(self, packet):
|
def process_packet(self, packet):
|
||||||
"""Process a packet recieved from aprs-is server."""
|
"""Process a packet recieved from aprs-is server."""
|
||||||
|
|
||||||
LOG.debug("Process packet! {}".format(self.msg_queues))
|
|
||||||
try:
|
try:
|
||||||
LOG.debug("Got message: {}".format(packet))
|
LOG.info("Got message: {}".format(packet))
|
||||||
|
|
||||||
msg = packet.get("message_text", None)
|
msg = packet.get("message_text", None)
|
||||||
msg_format = packet.get("format", None)
|
msg_format = packet.get("format", None)
|
||||||
@ -213,14 +220,13 @@ class APRSDRXThread(APRSDThread):
|
|||||||
|
|
||||||
class APRSDTXThread(APRSDThread):
|
class APRSDTXThread(APRSDThread):
|
||||||
def __init__(self, msg_queues, config):
|
def __init__(self, msg_queues, config):
|
||||||
super(APRSDTXThread, self).__init__("TX_MSG")
|
super().__init__("TX_MSG")
|
||||||
self.msg_queues = msg_queues
|
self.msg_queues = msg_queues
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
try:
|
try:
|
||||||
msg = self.msg_queues["tx"].get(timeout=0.1)
|
msg = self.msg_queues["tx"].get(timeout=0.1)
|
||||||
LOG.info("TXQ: got message '{}'".format(msg))
|
|
||||||
msg.send()
|
msg.send()
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
pass
|
pass
|
||||||
|
@ -3,40 +3,40 @@
|
|||||||
import errno
|
import errno
|
||||||
import functools
|
import functools
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
from aprsd import plugin
|
||||||
import click
|
import click
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from aprsd import plugin
|
|
||||||
|
|
||||||
# an example of what should be in the ~/.aprsd/config.yml
|
# an example of what should be in the ~/.aprsd/config.yml
|
||||||
DEFAULT_CONFIG_DICT = {
|
DEFAULT_CONFIG_DICT = {
|
||||||
"ham": {"callsign": "KFART"},
|
"ham": {"callsign": "CALLSIGN"},
|
||||||
"aprs": {
|
"aprs": {
|
||||||
"login": "someusername",
|
"login": "CALLSIGN",
|
||||||
"password": "somepassword",
|
"password": "00000",
|
||||||
"host": "rotate.aprs.net",
|
"host": "rotate.aprs.net",
|
||||||
"port": 14580,
|
"port": 14580,
|
||||||
"logfile": "/tmp/arsd.log",
|
"logfile": "/tmp/aprsd.log",
|
||||||
},
|
},
|
||||||
|
"aprs.fi": {"apiKey": "set me"},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"aa": "5551239999@vtext.com",
|
"aa": "5551239999@vtext.com",
|
||||||
"cl": "craiglamparter@somedomain.org",
|
"cl": "craiglamparter@somedomain.org",
|
||||||
"wb": "555309@vtext.com",
|
"wb": "555309@vtext.com",
|
||||||
},
|
},
|
||||||
"smtp": {
|
"smtp": {
|
||||||
"login": "something",
|
"login": "SMTP_USERNAME",
|
||||||
"password": "some lame password",
|
"password": "SMTP_PASSWORD",
|
||||||
"host": "imap.gmail.com",
|
"host": "smtp.gmail.com",
|
||||||
"port": 465,
|
"port": 465,
|
||||||
"use_ssl": False,
|
"use_ssl": False,
|
||||||
},
|
},
|
||||||
"imap": {
|
"imap": {
|
||||||
"login": "imapuser",
|
"login": "IMAP_USERNAME",
|
||||||
"password": "something here too",
|
"password": "IMAP_PASSWORD",
|
||||||
"host": "imap.gmail.com",
|
"host": "imap.gmail.com",
|
||||||
"port": 993,
|
"port": 993,
|
||||||
"use_ssl": True,
|
"use_ssl": True,
|
||||||
@ -86,6 +86,34 @@ def mkdir_p(path):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def insert_str(string, str_to_insert, index):
|
||||||
|
return string[:index] + str_to_insert + string[index:]
|
||||||
|
|
||||||
|
|
||||||
|
def end_substr(original, substr):
|
||||||
|
"""Get the index of the end of the <substr>.
|
||||||
|
|
||||||
|
So you can insert a string after <substr>
|
||||||
|
"""
|
||||||
|
idx = original.find(substr)
|
||||||
|
if idx != -1:
|
||||||
|
idx += len(substr)
|
||||||
|
return idx
|
||||||
|
|
||||||
|
|
||||||
|
def add_config_comments(raw_yaml):
|
||||||
|
end_idx = end_substr(raw_yaml, "aprs.fi:")
|
||||||
|
if end_idx != -1:
|
||||||
|
# lets insert a comment
|
||||||
|
raw_yaml = insert_str(
|
||||||
|
raw_yaml,
|
||||||
|
"\n # Get the apiKey from your aprs.fi account here: http://aprs.fi/account",
|
||||||
|
end_idx,
|
||||||
|
)
|
||||||
|
|
||||||
|
return raw_yaml
|
||||||
|
|
||||||
|
|
||||||
def create_default_config():
|
def create_default_config():
|
||||||
"""Create a default config file."""
|
"""Create a default config file."""
|
||||||
# make sure the directory location exists
|
# make sure the directory location exists
|
||||||
@ -95,20 +123,21 @@ def create_default_config():
|
|||||||
click.echo("Config dir '{}' doesn't exist, creating.".format(config_dir))
|
click.echo("Config dir '{}' doesn't exist, creating.".format(config_dir))
|
||||||
mkdir_p(config_dir)
|
mkdir_p(config_dir)
|
||||||
with open(config_file_expanded, "w+") as cf:
|
with open(config_file_expanded, "w+") as cf:
|
||||||
yaml.dump(DEFAULT_CONFIG_DICT, cf)
|
raw_yaml = yaml.dump(DEFAULT_CONFIG_DICT)
|
||||||
|
cf.write(add_config_comments(raw_yaml))
|
||||||
|
|
||||||
|
|
||||||
def get_config(config_file):
|
def get_config(config_file):
|
||||||
"""This tries to read the yaml config from <config_file>."""
|
"""This tries to read the yaml config from <config_file>."""
|
||||||
config_file_expanded = os.path.expanduser(config_file)
|
config_file_expanded = os.path.expanduser(config_file)
|
||||||
if os.path.exists(config_file_expanded):
|
if os.path.exists(config_file_expanded):
|
||||||
with open(config_file_expanded, "r") as stream:
|
with open(config_file_expanded) as stream:
|
||||||
config = yaml.load(stream, Loader=yaml.FullLoader)
|
config = yaml.load(stream, Loader=yaml.FullLoader)
|
||||||
return config
|
return config
|
||||||
else:
|
else:
|
||||||
if config_file == DEFAULT_CONFIG_FILE:
|
if config_file == DEFAULT_CONFIG_FILE:
|
||||||
click.echo(
|
click.echo(
|
||||||
"{} is missing, creating config file".format(config_file_expanded)
|
"{} is missing, creating config file".format(config_file_expanded),
|
||||||
)
|
)
|
||||||
create_default_config()
|
create_default_config()
|
||||||
msg = (
|
msg = (
|
||||||
@ -143,7 +172,10 @@ def parse_config(config_file):
|
|||||||
if name and name not in config[section]:
|
if name and name not in config[section]:
|
||||||
if not default:
|
if not default:
|
||||||
fail(
|
fail(
|
||||||
"'%s' was not in '%s' section of config file" % (name, section)
|
"'{}' was not in '{}' section of config file".format(
|
||||||
|
name,
|
||||||
|
section,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
config[section][name] = default
|
config[section][name] = default
|
||||||
@ -165,7 +197,16 @@ def parse_config(config_file):
|
|||||||
# special check here to make sure user has edited the config file
|
# special check here to make sure user has edited the config file
|
||||||
# and changed the ham callsign
|
# and changed the ham callsign
|
||||||
check_option(
|
check_option(
|
||||||
config, "ham", "callsign", default_fail=DEFAULT_CONFIG_DICT["ham"]["callsign"]
|
config,
|
||||||
|
"ham",
|
||||||
|
"callsign",
|
||||||
|
default_fail=DEFAULT_CONFIG_DICT["ham"]["callsign"],
|
||||||
|
)
|
||||||
|
check_option(
|
||||||
|
config,
|
||||||
|
"aprs.fi",
|
||||||
|
"apiKey",
|
||||||
|
default_fail=DEFAULT_CONFIG_DICT["aprs.fi"]["apiKey"],
|
||||||
)
|
)
|
||||||
check_option(config, "aprs", "login")
|
check_option(config, "aprs", "login")
|
||||||
check_option(config, "aprs", "password")
|
check_option(config, "aprs", "password")
|
||||||
|
@ -5,11 +5,11 @@ export PATH=$PATH:$HOME/.local/bin
|
|||||||
export VIRTUAL_ENV=$HOME/.venv3
|
export VIRTUAL_ENV=$HOME/.venv3
|
||||||
source $VIRTUAL_ENV/bin/activate
|
source $VIRTUAL_ENV/bin/activate
|
||||||
|
|
||||||
if [ ! -z "${APRS_PLUGINS}" ]; then
|
if [ ! -z "${APRSD_PLUGINS}" ]; then
|
||||||
OLDIFS=$IFS
|
OLDIFS=$IFS
|
||||||
IFS=','
|
IFS=','
|
||||||
echo "Installing pypi plugins '$APRS_PLUGINS'";
|
echo "Installing pypi plugins '$APRSD_PLUGINS'";
|
||||||
for plugin in ${APRS_PLUGINS}; do
|
for plugin in ${APRSD_PLUGINS}; do
|
||||||
IFS=$OLDIFS
|
IFS=$OLDIFS
|
||||||
# call your procedure/other scripts here below
|
# call your procedure/other scripts here below
|
||||||
echo "Installing '$plugin'"
|
echo "Installing '$plugin'"
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
tox
|
black
|
||||||
|
flake8
|
||||||
|
isort
|
||||||
|
mypy
|
||||||
pytest
|
pytest
|
||||||
pytest-cov
|
pytest-cov
|
||||||
mypy
|
|
||||||
flake8
|
|
||||||
pep8-naming
|
pep8-naming
|
||||||
black
|
|
||||||
isort
|
|
||||||
Sphinx
|
Sphinx
|
||||||
|
tox
|
||||||
|
twine
|
||||||
|
@ -4,58 +4,166 @@
|
|||||||
#
|
#
|
||||||
# pip-compile dev-requirements.in
|
# pip-compile dev-requirements.in
|
||||||
#
|
#
|
||||||
alabaster==0.7.12 # via sphinx
|
alabaster==0.7.12
|
||||||
appdirs==1.4.4 # via black, virtualenv
|
# via sphinx
|
||||||
attrs==20.3.0 # via pytest
|
appdirs==1.4.4
|
||||||
babel==2.9.0 # via sphinx
|
# via
|
||||||
black==20.8b1 # via -r dev-requirements.in
|
# black
|
||||||
certifi==2020.12.5 # via requests
|
# virtualenv
|
||||||
chardet==4.0.0 # via requests
|
attrs==20.3.0
|
||||||
click==7.1.2 # via black
|
# via pytest
|
||||||
coverage==5.3 # via pytest-cov
|
babel==2.9.0
|
||||||
distlib==0.3.1 # via virtualenv
|
# via sphinx
|
||||||
docutils==0.16 # via sphinx
|
black==20.8b1
|
||||||
filelock==3.0.12 # via tox, virtualenv
|
# via -r dev-requirements.in
|
||||||
flake8-polyfill==1.0.2 # via pep8-naming
|
bleach==3.2.1
|
||||||
flake8==3.8.4 # via -r dev-requirements.in, flake8-polyfill
|
# via readme-renderer
|
||||||
idna==2.10 # via requests
|
certifi==2020.12.5
|
||||||
imagesize==1.2.0 # via sphinx
|
# via requests
|
||||||
iniconfig==1.1.1 # via pytest
|
chardet==4.0.0
|
||||||
isort==5.6.4 # via -r dev-requirements.in
|
# via requests
|
||||||
jinja2==2.11.2 # via sphinx
|
click==7.1.2
|
||||||
markupsafe==1.1.1 # via jinja2
|
# via black
|
||||||
mccabe==0.6.1 # via flake8
|
colorama==0.4.4
|
||||||
mypy-extensions==0.4.3 # via black, mypy
|
# via twine
|
||||||
mypy==0.790 # via -r dev-requirements.in
|
coverage==5.3.1
|
||||||
packaging==20.8 # via pytest, sphinx, tox
|
# via pytest-cov
|
||||||
pathspec==0.8.1 # via black
|
distlib==0.3.1
|
||||||
pep8-naming==0.11.1 # via -r dev-requirements.in
|
# via virtualenv
|
||||||
pluggy==0.13.1 # via pytest, tox
|
docutils==0.16
|
||||||
py==1.10.0 # via pytest, tox
|
# via
|
||||||
pycodestyle==2.6.0 # via flake8
|
# readme-renderer
|
||||||
pyflakes==2.2.0 # via flake8
|
# sphinx
|
||||||
pygments==2.7.3 # via sphinx
|
filelock==3.0.12
|
||||||
pyparsing==2.4.7 # via packaging
|
# via
|
||||||
pytest-cov==2.10.1 # via -r dev-requirements.in
|
# tox
|
||||||
pytest==6.2.1 # via -r dev-requirements.in, pytest-cov
|
# virtualenv
|
||||||
pytz==2020.4 # via babel
|
flake8-polyfill==1.0.2
|
||||||
regex==2020.11.13 # via black
|
# via pep8-naming
|
||||||
requests==2.25.1 # via sphinx
|
flake8==3.8.4
|
||||||
six==1.15.0 # via tox, virtualenv
|
# via
|
||||||
snowballstemmer==2.0.0 # via sphinx
|
# -r dev-requirements.in
|
||||||
sphinx==3.3.1 # via -r dev-requirements.in
|
# flake8-polyfill
|
||||||
sphinxcontrib-applehelp==1.0.2 # via sphinx
|
idna==2.10
|
||||||
sphinxcontrib-devhelp==1.0.2 # via sphinx
|
# via requests
|
||||||
sphinxcontrib-htmlhelp==1.0.3 # via sphinx
|
imagesize==1.2.0
|
||||||
sphinxcontrib-jsmath==1.0.1 # via sphinx
|
# via sphinx
|
||||||
sphinxcontrib-qthelp==1.0.3 # via sphinx
|
iniconfig==1.1.1
|
||||||
sphinxcontrib-serializinghtml==1.1.4 # via sphinx
|
# via pytest
|
||||||
toml==0.10.2 # via black, pytest, tox
|
isort==5.7.0
|
||||||
tox==3.20.1 # via -r dev-requirements.in
|
# via -r dev-requirements.in
|
||||||
typed-ast==1.4.1 # via black, mypy
|
jinja2==2.11.2
|
||||||
typing-extensions==3.7.4.3 # via black, mypy
|
# via sphinx
|
||||||
urllib3==1.26.2 # via requests
|
keyring==21.8.0
|
||||||
virtualenv==20.2.2 # via tox
|
# via twine
|
||||||
|
markupsafe==1.1.1
|
||||||
|
# via jinja2
|
||||||
|
mccabe==0.6.1
|
||||||
|
# via flake8
|
||||||
|
mypy-extensions==0.4.3
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# mypy
|
||||||
|
mypy==0.790
|
||||||
|
# via -r dev-requirements.in
|
||||||
|
packaging==20.8
|
||||||
|
# via
|
||||||
|
# bleach
|
||||||
|
# pytest
|
||||||
|
# sphinx
|
||||||
|
# tox
|
||||||
|
pathspec==0.8.1
|
||||||
|
# via black
|
||||||
|
pep8-naming==0.11.1
|
||||||
|
# via -r dev-requirements.in
|
||||||
|
pkginfo==1.6.1
|
||||||
|
# via twine
|
||||||
|
pluggy==0.13.1
|
||||||
|
# via
|
||||||
|
# pytest
|
||||||
|
# tox
|
||||||
|
py==1.10.0
|
||||||
|
# via
|
||||||
|
# pytest
|
||||||
|
# tox
|
||||||
|
pycodestyle==2.6.0
|
||||||
|
# via flake8
|
||||||
|
pyflakes==2.2.0
|
||||||
|
# via flake8
|
||||||
|
pygments==2.7.3
|
||||||
|
# via
|
||||||
|
# readme-renderer
|
||||||
|
# sphinx
|
||||||
|
pyparsing==2.4.7
|
||||||
|
# via packaging
|
||||||
|
pytest-cov==2.10.1
|
||||||
|
# via -r dev-requirements.in
|
||||||
|
pytest==6.2.1
|
||||||
|
# via
|
||||||
|
# -r dev-requirements.in
|
||||||
|
# pytest-cov
|
||||||
|
pytz==2020.5
|
||||||
|
# via babel
|
||||||
|
readme-renderer==28.0
|
||||||
|
# via twine
|
||||||
|
regex==2020.11.13
|
||||||
|
# via black
|
||||||
|
requests-toolbelt==0.9.1
|
||||||
|
# via twine
|
||||||
|
requests==2.25.1
|
||||||
|
# via
|
||||||
|
# requests-toolbelt
|
||||||
|
# sphinx
|
||||||
|
# twine
|
||||||
|
rfc3986==1.4.0
|
||||||
|
# via twine
|
||||||
|
six==1.15.0
|
||||||
|
# via
|
||||||
|
# bleach
|
||||||
|
# readme-renderer
|
||||||
|
# tox
|
||||||
|
# virtualenv
|
||||||
|
snowballstemmer==2.0.0
|
||||||
|
# via sphinx
|
||||||
|
sphinx==3.4.3
|
||||||
|
# via -r dev-requirements.in
|
||||||
|
sphinxcontrib-applehelp==1.0.2
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-devhelp==1.0.2
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-htmlhelp==1.0.3
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-jsmath==1.0.1
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-qthelp==1.0.3
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-serializinghtml==1.1.4
|
||||||
|
# via sphinx
|
||||||
|
toml==0.10.2
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# pytest
|
||||||
|
# tox
|
||||||
|
tox==3.21.0
|
||||||
|
# via -r dev-requirements.in
|
||||||
|
tqdm==4.55.1
|
||||||
|
# via twine
|
||||||
|
twine==3.3.0
|
||||||
|
# via -r dev-requirements.in
|
||||||
|
typed-ast==1.4.2
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# mypy
|
||||||
|
typing-extensions==3.7.4.3
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# mypy
|
||||||
|
urllib3==1.26.2
|
||||||
|
# via requests
|
||||||
|
virtualenv==20.2.2
|
||||||
|
# via tox
|
||||||
|
webencodings==0.5.1
|
||||||
|
# via bleach
|
||||||
|
|
||||||
# The following packages are considered to be unsafe in a requirements file:
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
# setuptools
|
# setuptools
|
||||||
|
0
docs/_static/.keep
vendored
Normal file
0
docs/_static/.keep
vendored
Normal file
BIN
docs/_static/aprsd_overview.png
vendored
Normal file
BIN
docs/_static/aprsd_overview.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
3
docs/_static/aprsd_overview.svg
vendored
Normal file
3
docs/_static/aprsd_overview.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 26 KiB |
0
docs/_templates/.keep
vendored
Normal file
0
docs/_templates/.keep
vendored
Normal file
77
docs/apidoc/aprsd.plugins.rst
Normal file
77
docs/apidoc/aprsd.plugins.rst
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
aprsd.plugins package
|
||||||
|
=====================
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
aprsd.plugins.email module
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.plugins.email
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.plugins.fortune module
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.plugins.fortune
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.plugins.location module
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.plugins.location
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.plugins.ping module
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.plugins.ping
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.plugins.query module
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.plugins.query
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.plugins.time module
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.plugins.time
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.plugins.version module
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.plugins.version
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.plugins.weather module
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.plugins.weather
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.plugins
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
93
docs/apidoc/aprsd.rst
Normal file
93
docs/apidoc/aprsd.rst
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
aprsd package
|
||||||
|
=============
|
||||||
|
|
||||||
|
Subpackages
|
||||||
|
-----------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 4
|
||||||
|
|
||||||
|
aprsd.plugins
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
aprsd.client module
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.client
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.email module
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.email
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.fake\_aprs module
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.fake_aprs
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.fuzzyclock module
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.fuzzyclock
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.main module
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.main
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.messaging module
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.messaging
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.plugin module
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.plugin
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.threads module
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.threads
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.utils module
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.utils
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
7
docs/apidoc/modules.rst
Normal file
7
docs/apidoc/modules.rst
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
aprsd
|
||||||
|
=====
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 4
|
||||||
|
|
||||||
|
aprsd
|
22
docs/clean_docs.py
Normal file
22
docs/clean_docs.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""Removes temporary Sphinx build artifacts to ensure a clean build.
|
||||||
|
|
||||||
|
This is needed if the Python source being documented changes significantly. Old sphinx-apidoc
|
||||||
|
RST files can be left behind.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
docs_dir = Path(__file__).resolve().parent
|
||||||
|
for folder in ("_build", "apidoc"):
|
||||||
|
delete_dir = docs_dir / folder
|
||||||
|
if delete_dir.exists():
|
||||||
|
shutil.rmtree(delete_dir)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
188
docs/conf.py
Normal file
188
docs/conf.py
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
#
|
||||||
|
# Configuration file for the Sphinx documentation builder.
|
||||||
|
#
|
||||||
|
# This file does only contain a selection of the most common options. For a
|
||||||
|
# full list see the documentation:
|
||||||
|
# http://www.sphinx-doc.org/en/master/config
|
||||||
|
|
||||||
|
# -- Path setup --------------------------------------------------------------
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath("../src"))
|
||||||
|
|
||||||
|
|
||||||
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
|
project = "APRSD"
|
||||||
|
copyright = ""
|
||||||
|
author = "Craig Lamparter"
|
||||||
|
|
||||||
|
# The short X.Y version
|
||||||
|
version = "v1.5.0"
|
||||||
|
# The full version, including alpha/beta/rc tags
|
||||||
|
release = ""
|
||||||
|
|
||||||
|
|
||||||
|
# -- General configuration ---------------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
#
|
||||||
|
# needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
|
# ones.
|
||||||
|
extensions = [
|
||||||
|
"sphinx.ext.autodoc",
|
||||||
|
"sphinx.ext.doctest",
|
||||||
|
"sphinx.ext.todo",
|
||||||
|
"sphinx.ext.viewcode",
|
||||||
|
"sphinx.ext.napoleon",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ["_templates"]
|
||||||
|
|
||||||
|
# The suffix(es) of source filenames.
|
||||||
|
# You can specify multiple suffix as a list of string:
|
||||||
|
#
|
||||||
|
# source_suffix = ['.rst', '.md']
|
||||||
|
source_suffix = ".rst"
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = "index"
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#
|
||||||
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
|
# Usually you set "language" from the command line for these cases.
|
||||||
|
language = None
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
# This pattern also affects html_static_path and html_extra_path.
|
||||||
|
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = None
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
#
|
||||||
|
html_theme = "alabaster"
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
|
# further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
#
|
||||||
|
html_theme_options = {
|
||||||
|
# Override the default alabaster line wrap, which wraps tightly at 940px.
|
||||||
|
"page_width": "auto",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ["_static"]
|
||||||
|
|
||||||
|
# Custom sidebar templates, must be a dictionary that maps document names
|
||||||
|
# to template names.
|
||||||
|
#
|
||||||
|
# The default sidebars (for documents that don't match any pattern) are
|
||||||
|
# defined by theme itself. Builtin themes are using these templates by
|
||||||
|
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
||||||
|
# 'searchbox.html']``.
|
||||||
|
#
|
||||||
|
# html_sidebars = {}
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTMLHelp output ---------------------------------------------
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = "adoc"
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for LaTeX output ------------------------------------------------
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
|
#
|
||||||
|
# 'papersize': 'letterpaper',
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#
|
||||||
|
# 'pointsize': '10pt',
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#
|
||||||
|
# 'preamble': '',
|
||||||
|
# Latex figure (float) alignment
|
||||||
|
#
|
||||||
|
# 'figure_align': 'htbp',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title,
|
||||||
|
# author, documentclass [howto, manual, or own class]).
|
||||||
|
latex_documents = [
|
||||||
|
(master_doc, "a.tex", "a Documentation", "a", "manual"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for manual page output ------------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [(master_doc, "a", "a Documentation", [author], 1)]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Texinfo output ----------------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree into Texinfo files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# dir menu entry, description, category)
|
||||||
|
texinfo_documents = [
|
||||||
|
(
|
||||||
|
master_doc,
|
||||||
|
"a",
|
||||||
|
"a Documentation",
|
||||||
|
author,
|
||||||
|
"a",
|
||||||
|
"One line description of project.",
|
||||||
|
"Miscellaneous",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Epub output -------------------------------------------------
|
||||||
|
|
||||||
|
# Bibliographic Dublin Core info.
|
||||||
|
epub_title = project
|
||||||
|
|
||||||
|
# The unique identifier of the text. This can be a ISBN number
|
||||||
|
# or the project homepage.
|
||||||
|
#
|
||||||
|
# epub_identifier = ''
|
||||||
|
|
||||||
|
# A unique identification for the text.
|
||||||
|
#
|
||||||
|
# epub_uid = ''
|
||||||
|
|
||||||
|
# A list of files that should not be packed into the epub file.
|
||||||
|
epub_exclude_files = ["search.html"]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Extension configuration -------------------------------------------------
|
||||||
|
|
||||||
|
# -- Options for todo extension ----------------------------------------------
|
||||||
|
|
||||||
|
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||||
|
todo_include_todos = True
|
71
docs/configure.rst
Normal file
71
docs/configure.rst
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
APRSD Configure
|
||||||
|
===============
|
||||||
|
|
||||||
|
Configure APRSD
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Once APRSD is :doc:`installed <install>` You will need to configure the config file
|
||||||
|
for running.
|
||||||
|
|
||||||
|
|
||||||
|
Generate config file
|
||||||
|
---------------------
|
||||||
|
If you have never run the server, running it the first time will generate
|
||||||
|
a sample config file in the default location of ~/.config/aprsd/aprsd.yml
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
└─[$] -> aprsd server
|
||||||
|
Load config
|
||||||
|
/home/aprsd/.config/aprsd/aprsd.yml is missing, creating config file
|
||||||
|
Default config file created at /home/aprsd/.config/aprsd/aprsd.yml. Please edit with your settings.
|
||||||
|
|
||||||
|
You can see the sample config file output
|
||||||
|
|
||||||
|
Sample config file
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
└─[$] -> cat ~/.config/aprsd/aprsd.yml
|
||||||
|
aprs:
|
||||||
|
host: rotate.aprs.net
|
||||||
|
logfile: /tmp/arsd.log
|
||||||
|
login: someusername
|
||||||
|
password: somepassword
|
||||||
|
port: 14580
|
||||||
|
aprsd:
|
||||||
|
enabled_plugins:
|
||||||
|
- aprsd.plugins.email.EmailPlugin
|
||||||
|
- aprsd.plugins.fortune.FortunePlugin
|
||||||
|
- aprsd.plugins.location.LocationPlugin
|
||||||
|
- aprsd.plugins.ping.PingPlugin
|
||||||
|
- aprsd.plugins.query.QueryPlugin
|
||||||
|
- aprsd.plugins.time.TimePlugin
|
||||||
|
- aprsd.plugins.weather.WeatherPlugin
|
||||||
|
- aprsd.plugins.version.VersionPlugin
|
||||||
|
plugin_dir: ~/.config/aprsd/plugins
|
||||||
|
ham:
|
||||||
|
callsign: KFART
|
||||||
|
imap:
|
||||||
|
host: imap.gmail.com
|
||||||
|
login: imapuser
|
||||||
|
password: something here too
|
||||||
|
port: 993
|
||||||
|
use_ssl: true
|
||||||
|
shortcuts:
|
||||||
|
aa: 5551239999@vtext.com
|
||||||
|
cl: craiglamparter@somedomain.org
|
||||||
|
wb: 555309@vtext.com
|
||||||
|
smtp:
|
||||||
|
host: imap.gmail.com
|
||||||
|
login: something
|
||||||
|
password: some lame password
|
||||||
|
port: 465
|
||||||
|
use_ssl: false
|
||||||
|
|
||||||
|
|
||||||
|
Note, You must edit the config file and change the ham callsign to your
|
||||||
|
legal FCC HAM callsign, or aprsd server will not start.
|
||||||
|
|
||||||
|
.. include:: links.rst
|
30
docs/index.rst
Normal file
30
docs/index.rst
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
.. a documentation master file, created by
|
||||||
|
sphinx-quickstart on Wed Dec 19 18:34:22 2018.
|
||||||
|
You can adapt this file completely to your liking, but it should at least
|
||||||
|
contain the root `toctree` directive.
|
||||||
|
|
||||||
|
``APRSD`` Documentation
|
||||||
|
=======================
|
||||||
|
|
||||||
|
.. include:: readme.rst
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
:caption: Contents:
|
||||||
|
|
||||||
|
readme
|
||||||
|
install
|
||||||
|
configure
|
||||||
|
server
|
||||||
|
plugin
|
||||||
|
|
||||||
|
apidoc/modules.rst
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
||||||
|
|
||||||
|
.. include:: links.rst
|
67
docs/install.rst
Normal file
67
docs/install.rst
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
APRSD installation
|
||||||
|
==================
|
||||||
|
|
||||||
|
Install info in a nutshell
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
**Pythons**: Python 3.6 or later
|
||||||
|
|
||||||
|
**Operating systems**: Linux, OSX, Unix
|
||||||
|
|
||||||
|
**Installer Requirements**: setuptools_
|
||||||
|
|
||||||
|
**License**: Apache license
|
||||||
|
|
||||||
|
**git repository**: https://github.com/craigerl/aprsd
|
||||||
|
|
||||||
|
Installation with pip
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
Use the following command:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
pip install aprsd
|
||||||
|
|
||||||
|
It is fine to install ``aprsd`` itself into a virtualenv_ environment.
|
||||||
|
|
||||||
|
Install from clone
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Consult the GitHub page how to clone the git repository:
|
||||||
|
|
||||||
|
https://github.com/craigerl/aprsd
|
||||||
|
|
||||||
|
and then install in your environment with something like:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
$ cd <path/to/clone>
|
||||||
|
$ pip install .
|
||||||
|
|
||||||
|
or install it `editable <https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs>`_ if you want code changes to propagate automatically:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
$ cd <path/to/clone>
|
||||||
|
$ pip install --editable .
|
||||||
|
|
||||||
|
so that you can do changes and submit patches.
|
||||||
|
|
||||||
|
|
||||||
|
Install for development
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
For developers you should clone the repo from github, then use the Makefile
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
$ cd <path/to/clone>
|
||||||
|
$ make
|
||||||
|
|
||||||
|
This creates a virtualenv_ directory, install all the requirements for
|
||||||
|
development as well as aprsd in `editable <https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs>`_ mode.
|
||||||
|
It will install all of the pre-commit git hooks required to test prior to committing code.
|
||||||
|
|
||||||
|
|
||||||
|
.. include:: links.rst
|
31
docs/links.rst
Normal file
31
docs/links.rst
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
.. _`Cookiecutter`: https://cookiecutter.readthedocs.io
|
||||||
|
.. _`pluggy`: https://pluggy.readthedocs.io
|
||||||
|
.. _`cookiecutter-tox-plugin`: https://github.com/tox-dev/cookiecutter-tox-plugin
|
||||||
|
.. _devpi: https://doc.devpi.net
|
||||||
|
.. _Python: https://www.python.org
|
||||||
|
.. _virtualenv: https://pypi.org/project/virtualenv
|
||||||
|
.. _`pytest`: https://pytest.org
|
||||||
|
.. _nosetests:
|
||||||
|
.. _`nose`: https://pypi.org/project/nose
|
||||||
|
.. _`Holger Krekel`: https://twitter.com/hpk42
|
||||||
|
.. _`pytest-xdist`: https://pypi.org/project/pytest-xdist
|
||||||
|
.. _ConfigParser: https://docs.python.org/3/library/configparser.html
|
||||||
|
|
||||||
|
.. _`easy_install`: http://peak.telecommunity.com/DevCenter/EasyInstall
|
||||||
|
.. _pip: https://pypi.org/project/pip
|
||||||
|
.. _setuptools: https://pypi.org/project/setuptools
|
||||||
|
.. _`jenkins`: https://jenkins.io/index.html
|
||||||
|
.. _sphinx: https://pypi.org/project/Sphinx
|
||||||
|
.. _discover: https://pypi.org/project/discover
|
||||||
|
.. _unittest2: https://pypi.org/project/unittest2
|
||||||
|
.. _mock: https://pypi.org/project/mock/
|
||||||
|
.. _flit: https://flit.readthedocs.io/en/latest/
|
||||||
|
.. _poetry: https://poetry.eustace.io/
|
||||||
|
.. _pypy: https://pypy.org
|
||||||
|
|
||||||
|
.. _`Python Packaging Guide`: https://packaging.python.org/tutorials/packaging-projects/
|
||||||
|
.. _`tox.ini`: :doc:configfile
|
||||||
|
|
||||||
|
.. _`PEP-508`: https://www.python.org/dev/peps/pep-0508/
|
||||||
|
.. _`PEP-517`: https://www.python.org/dev/peps/pep-0517/
|
||||||
|
.. _`PEP-518`: https://www.python.org/dev/peps/pep-0518/
|
55
docs/plugin.rst
Normal file
55
docs/plugin.rst
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
APRSD Command Plugin Development
|
||||||
|
================================
|
||||||
|
|
||||||
|
APRSDPluginBase
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Plugins are written as python objects that extend the APRSDPluginBase class.
|
||||||
|
This is an abstract class that has several properties and a method that must be implemented
|
||||||
|
by your subclass.
|
||||||
|
|
||||||
|
Properties
|
||||||
|
----------
|
||||||
|
|
||||||
|
* name - the Command name
|
||||||
|
* regex - The regular expression that if matched against the incoming APRS message,
|
||||||
|
will cause your plugin to be called.
|
||||||
|
|
||||||
|
Methods
|
||||||
|
-------
|
||||||
|
|
||||||
|
* command - This method is called when the regex matches the incoming message from APRS.
|
||||||
|
If you want to send a message back to the sending, just return a string
|
||||||
|
in your method implementation. If you get called and don't want to reply, then
|
||||||
|
you should return a messaging.NULL_MESSAGE to signal to the plugin processor
|
||||||
|
that you got called and processed the message correctly. Otherwise a usage
|
||||||
|
string may get returned to the sender.
|
||||||
|
|
||||||
|
|
||||||
|
Example Plugin
|
||||||
|
--------------
|
||||||
|
|
||||||
|
There is an example plugin in the aprsd source code here:
|
||||||
|
aprsd/examples/plugins/example_plugin.py
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aprsd import plugin
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class HelloPlugin(plugin.APRSDPluginBase):
|
||||||
|
"""Hello World."""
|
||||||
|
|
||||||
|
version = "1.0"
|
||||||
|
# matches any string starting with h or H
|
||||||
|
command_regex = "^[hH]"
|
||||||
|
command_name = "hello"
|
||||||
|
|
||||||
|
def command(self, fromcall, message, ack):
|
||||||
|
LOG.info("HelloPlugin")
|
||||||
|
reply = "Hello '{}'".format(fromcall)
|
||||||
|
return reply
|
410
docs/readme.rst
Normal file
410
docs/readme.rst
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
APRSD
|
||||||
|
-----
|
||||||
|
|
||||||
|
.. image:: https://badge.fury.io/py/aprsd.svg
|
||||||
|
:target: https://badge.fury.io/py/aprsd
|
||||||
|
|
||||||
|
.. image:: https://github.com/craigerl/aprsd/workflows/python/badge.svg
|
||||||
|
:target: https://github.com/craigerl/aprsd/actions
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/pyversions/aprsd.svg
|
||||||
|
:target: https://pypi.python.org/pypi/aprsd
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/:license-apache-blue.svg
|
||||||
|
:target: http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||||
|
:target: https://black.readthedocs.io/en/stable/
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336
|
||||||
|
:target: https://timothycrosley.github.io/isort/
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/github/issues/craigerl/aprsd
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/github/last-commit/craigerl/aprsd
|
||||||
|
|
||||||
|
.. image:: https://static.pepy.tech/personalized-badge/aprsd?period=month&units=international_system&left_color=black&right_color=orange&left_text=Downloads
|
||||||
|
:target: https://pepy.tech/project/aprsd
|
||||||
|
|
||||||
|
|
||||||
|
Summary
|
||||||
|
=======
|
||||||
|
|
||||||
|
`APRSD <http://github.com/craigerl/aprsd>`_ is a Ham radio `APRS <http://aprs.org>`_ message command gateway built on python.
|
||||||
|
|
||||||
|
APRSD listens on amateur radio aprs-is network for messages and respond to them.
|
||||||
|
It has a plugin architecture for extensibility. Users of APRSD can write their own
|
||||||
|
plugins that can respond to APRS-IS messages.
|
||||||
|
|
||||||
|
You must have an amateur radio callsign to use this software. APRSD gets
|
||||||
|
messages for the configured HAM callsign, and sends those messages to a
|
||||||
|
list of plugins for processing. There are a set of core plugins that
|
||||||
|
provide responding to messages to check email, get location, ping,
|
||||||
|
time of day, get weather, and fortune telling as well as version information
|
||||||
|
of aprsd itself.
|
||||||
|
|
||||||
|
APRSD overview diagram
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. figure:: _static/aprsd_overview.svg
|
||||||
|
:align: center
|
||||||
|
:width: 800px
|
||||||
|
|
||||||
|
|
||||||
|
Typical use case
|
||||||
|
================
|
||||||
|
|
||||||
|
Ham radio operator using an APRS enabled HAM radio sends a message to check
|
||||||
|
the weather. an APRS message is sent, and then picked up by APRSD. The
|
||||||
|
APRS packet is decoded, and the message is sent through the list of plugins
|
||||||
|
for processing. For example, the WeatherPlugin picks up the message, fetches the weather
|
||||||
|
for the area around the user who sent the request, and then responds with
|
||||||
|
the weather conditions in that area.
|
||||||
|
|
||||||
|
|
||||||
|
APRSD Capabilities
|
||||||
|
==================
|
||||||
|
|
||||||
|
* server - The main aprsd server processor. Send/Rx APRS messages to HAM callsign
|
||||||
|
* send-message - use aprsd to send a command/message to aprsd server. Used for development testing
|
||||||
|
* sample-config - generate a sample aprsd.yml config file for use/editing
|
||||||
|
* bash completion generation. Uses python click bash completion to generate completion code for your .bashrc/.zshrc
|
||||||
|
|
||||||
|
|
||||||
|
List of core server plugins
|
||||||
|
===========================
|
||||||
|
|
||||||
|
Plugins function by specifying a regex that is searched for in the APRS message.
|
||||||
|
If it matches, the plugin runs. IF the regex doesn't match, the plugin is skipped.
|
||||||
|
|
||||||
|
* EmailPlugin - Check email and reply with contents. Have to configure IMAP and SMTP settings in aprs.yml
|
||||||
|
* FortunePlugin - Replies with old unix fortune random fortune!
|
||||||
|
* LocationPlugin - Checks location of ham operator
|
||||||
|
* PingPlugin - Sends pong with timestamp
|
||||||
|
* QueryPlugin - Allows querying the list of delayed messages that were not ACK'd by radio
|
||||||
|
* TimePlugin - Current time of day
|
||||||
|
* WeatherPlugin - Get weather conditions for current location of HAM callsign
|
||||||
|
* VersionPlugin - Reports the version information for aprsd
|
||||||
|
|
||||||
|
|
||||||
|
Current messages this will respond to:
|
||||||
|
======================================
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
APRS messages:
|
||||||
|
l(ocation) [callsign] = descriptive current location of your radio
|
||||||
|
8 Miles E Auburn CA 1673' 39.92150,-120.93950 0.1h ago
|
||||||
|
w(eather) = weather forecast for your radio's current position
|
||||||
|
58F(58F/46F) Partly Cloudy. Tonight, Heavy Rain.
|
||||||
|
t(ime) = respond with the current time
|
||||||
|
f(ortune) = respond with a short fortune
|
||||||
|
-email_addr email text = send an email, say "mapme" to send a current position/map
|
||||||
|
-2 = resend the last 2 emails from your imap inbox to this radio
|
||||||
|
p(ing) = respond with Pong!/time
|
||||||
|
v(ersion) = Respond with current APRSD Version string
|
||||||
|
anything else = respond with usage
|
||||||
|
|
||||||
|
|
||||||
|
Meanwhile this code will monitor a single imap mailbox and forward email
|
||||||
|
to your BASECALLSIGN over the air. Only radios using the BASECALLSIGN are allowed
|
||||||
|
to send email, so consider this security risk before using this (or Amatuer radio in
|
||||||
|
general). Email is single user at this time.
|
||||||
|
|
||||||
|
There are additional parameters in the code (sorry), so be sure to set your
|
||||||
|
email server, and associated logins, passwords. search for "yourdomain",
|
||||||
|
"password". Search for "shortcuts" to setup email aliases as well.
|
||||||
|
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
==============
|
||||||
|
|
||||||
|
aprsd -h
|
||||||
|
|
||||||
|
Help
|
||||||
|
====
|
||||||
|
::
|
||||||
|
|
||||||
|
└─[$] > aprsd -h
|
||||||
|
Usage: aprsd [OPTIONS] COMMAND [ARGS]...
|
||||||
|
|
||||||
|
Shell completion for click-completion-command Available shell types:
|
||||||
|
bash Bourne again shell fish Friendly interactive shell
|
||||||
|
powershell Windows PowerShell zsh Z shell Default type: auto
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--version Show the version and exit.
|
||||||
|
-h, --help Show this message and exit.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
install Install the click-completion-command completion
|
||||||
|
sample-config This dumps the config to stdout.
|
||||||
|
send-message Send a message to a callsign via APRS_IS.
|
||||||
|
server Start the aprsd server process.
|
||||||
|
show Show the click-completion-command completion code
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
aprsd sample-config
|
||||||
|
|
||||||
|
Output
|
||||||
|
======
|
||||||
|
::
|
||||||
|
|
||||||
|
└─[$] > aprsd sample-config
|
||||||
|
|
||||||
|
aprs:
|
||||||
|
host: rotate.aprs.net
|
||||||
|
logfile: /tmp/arsd.log
|
||||||
|
login: someusername
|
||||||
|
password: somepassword
|
||||||
|
port: 14580
|
||||||
|
aprsd:
|
||||||
|
enabled_plugins:
|
||||||
|
- aprsd.plugin.EmailPlugin
|
||||||
|
- aprsd.plugin.FortunePlugin
|
||||||
|
- aprsd.plugin.LocationPlugin
|
||||||
|
- aprsd.plugin.PingPlugin
|
||||||
|
- aprsd.plugin.TimePlugin
|
||||||
|
- aprsd.plugin.WeatherPlugin
|
||||||
|
- aprsd.plugin.VersionPlugin
|
||||||
|
plugin_dir: ~/.config/aprsd/plugins
|
||||||
|
ham:
|
||||||
|
callsign: KFART
|
||||||
|
imap:
|
||||||
|
host: imap.gmail.com
|
||||||
|
login: imapuser
|
||||||
|
password: something here too
|
||||||
|
port: 993
|
||||||
|
use_ssl: true
|
||||||
|
shortcuts:
|
||||||
|
aa: 5551239999@vtext.com
|
||||||
|
cl: craiglamparter@somedomain.org
|
||||||
|
wb: 555309@vtext.com
|
||||||
|
smtp:
|
||||||
|
host: imap.gmail.com
|
||||||
|
login: something
|
||||||
|
password: some lame password
|
||||||
|
port: 465
|
||||||
|
use_ssl: false
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
└─[$] > aprsd server --help
|
||||||
|
Usage: aprsd server [OPTIONS]
|
||||||
|
|
||||||
|
Start the aprsd server process.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG]
|
||||||
|
The log level to use for aprsd.log
|
||||||
|
[default: DEBUG]
|
||||||
|
|
||||||
|
--quiet Don't log to stdout
|
||||||
|
--disable-validation Disable email shortcut validation. Bad
|
||||||
|
email addresses can result in broken email
|
||||||
|
responses!!
|
||||||
|
|
||||||
|
-c, --config TEXT The aprsd config file to use for options.
|
||||||
|
[default: ~/.config/aprsd/aprsd.yml]
|
||||||
|
|
||||||
|
-h, --help Show this message and exit.
|
||||||
|
(.venv3) ┌─[waboring@dl360-1] - [~/devel/aprsd] - [Sun Dec 20, 12:32] -
|
||||||
|
└─[$] <git:(master*)> aprsd server
|
||||||
|
Load config
|
||||||
|
[12/20/2020 12:33:03 PM] [MainThread ] [INFO ] APRSD Started version: 1.0.2
|
||||||
|
[12/20/2020 12:33:03 PM] [MainThread ] [INFO ] Checking IMAP configuration
|
||||||
|
[12/20/2020 12:33:04 PM] [MainThread ] [INFO ] Checking SMTP configuration
|
||||||
|
|
||||||
|
|
||||||
|
send-message
|
||||||
|
------------
|
||||||
|
|
||||||
|
This command is typically used for development to send another aprsd instance
|
||||||
|
test messages
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
└─[$] > aprsd send-message -h
|
||||||
|
Usage: aprsd send-message [OPTIONS] TOCALLSIGN [COMMAND]...
|
||||||
|
|
||||||
|
Send a message to a callsign via APRS_IS.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG]
|
||||||
|
The log level to use for aprsd.log
|
||||||
|
[default: DEBUG]
|
||||||
|
|
||||||
|
--quiet Don't log to stdout
|
||||||
|
-c, --config TEXT The aprsd config file to use for options.
|
||||||
|
[default: ~/.config/aprsd/aprsd.yml]
|
||||||
|
|
||||||
|
--aprs-login TEXT What callsign to send the message from.
|
||||||
|
[env var: APRS_LOGIN]
|
||||||
|
|
||||||
|
--aprs-password TEXT the APRS-IS password for APRS_LOGIN [env
|
||||||
|
var: APRS_PASSWORD]
|
||||||
|
|
||||||
|
-h, --help Show this message and exit.
|
||||||
|
|
||||||
|
|
||||||
|
Example Message output:
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
|
||||||
|
SEND EMAIL (radio to smtp server)
|
||||||
|
=================================
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
Received message______________
|
||||||
|
Raw : KM6XXX>APY400,WIDE1-1,qAO,KM6XXX-1::KM6XXX-9 :-user@host.com test new shortcuts global, radio to pc{29
|
||||||
|
From : KM6XXX
|
||||||
|
Message : -user@host.com test new shortcuts global, radio to pc
|
||||||
|
Msg number : 29
|
||||||
|
|
||||||
|
Sending Email_________________
|
||||||
|
To : user@host.com
|
||||||
|
Subject : KM6XXX
|
||||||
|
Body : test new shortcuts global, radio to pc
|
||||||
|
|
||||||
|
Sending ack __________________ Tx(3)
|
||||||
|
Raw : KM6XXX-9>APRS::KM6XXX :ack29
|
||||||
|
To : KM6XXX
|
||||||
|
Ack number : 29
|
||||||
|
|
||||||
|
|
||||||
|
RECEIVE EMAIL (imap server to radio)
|
||||||
|
====================================
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
Sending message_______________ 6(Tx3)
|
||||||
|
Raw : KM6XXX-9>APRS::KM6XXX :-somebody@gmail.com email from internet to radio{6
|
||||||
|
To : KM6XXX
|
||||||
|
Message : -somebody@gmail.com email from internet to radio
|
||||||
|
|
||||||
|
Received message______________
|
||||||
|
Raw : KM6XXX>APY400,WIDE1-1,qAO,KM6XXX-1::KM6XXX-9 :ack6
|
||||||
|
From : KM6XXX
|
||||||
|
Message : ack6
|
||||||
|
Msg number : 0
|
||||||
|
|
||||||
|
|
||||||
|
LOCATION
|
||||||
|
========
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
Received Message _______________
|
||||||
|
Raw : KM6XXX-6>APRS,TCPIP*,qAC,T2CAEAST::KM6XXX-14:location{2
|
||||||
|
From : KM6XXX-6
|
||||||
|
Message : location
|
||||||
|
Msg number : 2
|
||||||
|
Received Message _______________ Complete
|
||||||
|
|
||||||
|
Sending Message _______________
|
||||||
|
Raw : KM6XXX-14>APRS::KM6XXX-6 :KM6XXX-6: 8 Miles E Auburn CA 0' 0,-120.93584 1873.7h ago{2
|
||||||
|
To : KM6XXX-6
|
||||||
|
Message : KM6XXX-6: 8 Miles E Auburn CA 0' 0,-120.93584 1873.7h ago
|
||||||
|
Msg number : 2
|
||||||
|
Sending Message _______________ Complete
|
||||||
|
|
||||||
|
Sending ack _______________
|
||||||
|
Raw : KM6XXX-14>APRS::KM6XXX-6 :ack2
|
||||||
|
To : KM6XXX-6
|
||||||
|
Ack : 2
|
||||||
|
Sending ack _______________ Complete
|
||||||
|
|
||||||
|
AND... ping, fortune, time.....
|
||||||
|
|
||||||
|
|
||||||
|
Development
|
||||||
|
-----------
|
||||||
|
|
||||||
|
* git clone git@github.com:craigerl/aprsd.git
|
||||||
|
* cd aprsd
|
||||||
|
* make
|
||||||
|
|
||||||
|
Workflow
|
||||||
|
========
|
||||||
|
|
||||||
|
While working aprsd, The workflow is as follows
|
||||||
|
|
||||||
|
* Edit code, save file
|
||||||
|
* run tox -efmt
|
||||||
|
* run tox -p
|
||||||
|
* git commit ( This will run the pre-commit hooks which does checks too )
|
||||||
|
|
||||||
|
|
||||||
|
Release
|
||||||
|
=======
|
||||||
|
|
||||||
|
To do release to pypi:
|
||||||
|
|
||||||
|
* Tag release with
|
||||||
|
|
||||||
|
git tag -v1.XX -m "New release"
|
||||||
|
|
||||||
|
* push release tag up
|
||||||
|
|
||||||
|
git push origin master --tags
|
||||||
|
|
||||||
|
* Do a test build and verify build is valid
|
||||||
|
|
||||||
|
make build
|
||||||
|
|
||||||
|
* Once twine is happy, upload release to pypi
|
||||||
|
|
||||||
|
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
|
||||||
|
based off of the pip install version of aprsd and the Dockerfile-dev,
|
||||||
|
which is used for building a container based off of a git branch of
|
||||||
|
the repo.
|
||||||
|
|
||||||
|
Official Build
|
||||||
|
==============
|
||||||
|
|
||||||
|
docker build -t hemna6969/aprsd:latest .
|
||||||
|
|
||||||
|
Development Build
|
||||||
|
=================
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
53
docs/server.rst
Normal file
53
docs/server.rst
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
APRSD server
|
||||||
|
============
|
||||||
|
|
||||||
|
Running the APRSD server
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Once APRSD is :doc:`installed <install>` and :doc:`configured <configure>` the server can be started by
|
||||||
|
running.
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
aprsd server
|
||||||
|
|
||||||
|
The server will start several threads to deal handle incoming messages, outgoing
|
||||||
|
messages, checking and sending email.
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
[MainThread ] [INFO ] APRSD Started version: 1.5.1
|
||||||
|
[MainThread ] [INFO ] Checking IMAP configuration
|
||||||
|
[MainThread ] [INFO ] Checking SMTP configuration
|
||||||
|
[MainThread ] [DEBUG] Connect to SMTP host SSL smtp.gmail.com:465 with user 'test@hemna.com'
|
||||||
|
[MainThread ] [DEBUG] Connected to smtp host SSL smtp.gmail.com:465
|
||||||
|
[MainThread ] [DEBUG] Logged into SMTP server SSL smtp.gmail.com:465
|
||||||
|
[MainThread ] [INFO ] Validating 2 Email shortcuts. This can take up to 10 seconds per shortcut
|
||||||
|
[MainThread ] [ERROR] 'craiglamparter@somedomain.org' is an invalid email address. Removing shortcut
|
||||||
|
[MainThread ] [INFO ] Available shortcuts: {'wb': 'waboring@hemna.com'}
|
||||||
|
[MainThread ] [INFO ] Loading Core APRSD Command Plugins
|
||||||
|
[MainThread ] [INFO ] Registering Command plugin 'aprsd.plugins.email.EmailPlugin'(1.0) '^-.*'
|
||||||
|
[MainThread ] [INFO ] Registering Command plugin 'aprsd.plugins.fortune.FortunePlugin'(1.0) '^[fF]'
|
||||||
|
[MainThread ] [INFO ] Registering Command plugin 'aprsd.plugins.location.LocationPlugin'(1.0) '^[lL]'
|
||||||
|
[MainThread ] [INFO ] Registering Command plugin 'aprsd.plugins.ping.PingPlugin'(1.0) '^[pP]'
|
||||||
|
[MainThread ] [INFO ] Registering Command plugin 'aprsd.plugins.query.QueryPlugin'(1.0) '^\?.*'
|
||||||
|
[MainThread ] [INFO ] Registering Command plugin 'aprsd.plugins.time.TimePlugin'(1.0) '^[tT]'
|
||||||
|
[MainThread ] [INFO ] Registering Command plugin 'aprsd.plugins.weather.WeatherPlugin'(1.0) '^[wW]'
|
||||||
|
[MainThread ] [INFO ] Registering Command plugin 'aprsd.plugins.version.VersionPlugin'(1.0) '^[vV]'
|
||||||
|
[MainThread ] [INFO ] Skipping Custom Plugins directory.
|
||||||
|
[MainThread ] [INFO ] Completed Plugin Loading.
|
||||||
|
[MainThread ] [DEBUG] Loading saved MsgTrack object.
|
||||||
|
[RX_MSG ] [INFO ] Starting
|
||||||
|
[TX_MSG ] [INFO ] Starting
|
||||||
|
[MainThread ] [DEBUG] KeepAlive Tracker(0): {}
|
||||||
|
[RX_MSG ] [INFO ] Creating aprslib client
|
||||||
|
[RX_MSG ] [INFO ] Attempting connection to noam.aprs2.net:14580
|
||||||
|
[RX_MSG ] [INFO ] Connected to ('198.50.198.139', 14580)
|
||||||
|
[RX_MSG ] [DEBUG] Banner: # aprsc 2.1.8-gf8824e8
|
||||||
|
[RX_MSG ] [INFO ] Sending login information
|
||||||
|
[RX_MSG ] [DEBUG] Server: # logresp KM6XXX-14 verified, server T2VAN
|
||||||
|
[RX_MSG ] [INFO ] Login successful
|
||||||
|
[RX_MSG ] [DEBUG] Logging in to APRS-IS with user 'KM6XXX-14'
|
||||||
|
|
||||||
|
|
||||||
|
.. include:: links.rst
|
36
pyproject.toml
Normal file
36
pyproject.toml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=46.0", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
# Use the more relaxed max line length permitted in PEP8.
|
||||||
|
line-length = 88
|
||||||
|
target-version = ["py36", "py37", "py38"]
|
||||||
|
# black will automatically exclude all files listed in .gitignore
|
||||||
|
include = '\.pyi?$'
|
||||||
|
exclude = '''
|
||||||
|
/(
|
||||||
|
\.git
|
||||||
|
| \.hg
|
||||||
|
| \.mypy_cache
|
||||||
|
| \.tox
|
||||||
|
| \.venv
|
||||||
|
| _build
|
||||||
|
| buck-out
|
||||||
|
| build
|
||||||
|
| dist
|
||||||
|
)/
|
||||||
|
'''
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
profile = "black"
|
||||||
|
line_length = 88
|
||||||
|
force_sort_within_sections = true
|
||||||
|
# Inform isort of paths to import names that should be considered part of the "First Party" group.
|
||||||
|
src_paths = ["src/openstack_loadtest"]
|
||||||
|
skip_gitignore = true
|
||||||
|
# If you need to skip/exclude folders, consider using skip_glob as that will allow the
|
||||||
|
# isort defaults for skip to remain without the need to duplicate them.
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
branch = true
|
@ -2,7 +2,7 @@
|
|||||||
# This file is autogenerated by pip-compile
|
# This file is autogenerated by pip-compile
|
||||||
# To update, run:
|
# To update, run:
|
||||||
#
|
#
|
||||||
# pip-compile
|
# pip-compile requirements.in
|
||||||
#
|
#
|
||||||
appdirs==1.4.4
|
appdirs==1.4.4
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
@ -22,7 +22,7 @@ click==7.1.2
|
|||||||
# click-completion
|
# click-completion
|
||||||
distlib==0.3.1
|
distlib==0.3.1
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
dnspython==2.0.0
|
dnspython==2.1.0
|
||||||
# via py3-validate-email
|
# via py3-validate-email
|
||||||
filelock==3.0.12
|
filelock==3.0.12
|
||||||
# via
|
# via
|
||||||
|
34
setup.cfg
34
setup.cfg
@ -1,15 +1,28 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = aprsd
|
name = aprsd
|
||||||
summary = Amateur radio APRS daemon which listens for messages and responds
|
long_description = file: README.rst
|
||||||
description-file =
|
long_description_content_type = text/x-rst
|
||||||
README.rst
|
url = http://aprsd.readthedocs.org
|
||||||
long-description-content-type = text/x-rst; charset=UTF-8
|
|
||||||
author = Craig Lamparter
|
author = Craig Lamparter
|
||||||
author-email = something@somewhere.com
|
author_email = something@somewhere.com
|
||||||
|
license = Apache
|
||||||
|
license_file = LICENSE
|
||||||
classifier =
|
classifier =
|
||||||
|
License :: OSI Approved :: Apache Software License
|
||||||
Topic :: Communications :: Ham Radio
|
Topic :: Communications :: Ham Radio
|
||||||
Operating System :: POSIX :: Linux
|
Operating System :: POSIX :: Linux
|
||||||
Programming Language :: Python
|
Programming Language :: Python :: 3 :: Only
|
||||||
|
Programming Language :: Python :: 3
|
||||||
|
Programming Language :: Python :: 3.6
|
||||||
|
Programming Language :: Python :: 3.7
|
||||||
|
Programming Language :: Python :: 3.8
|
||||||
|
Programming Language :: Python :: 3.9
|
||||||
|
description_file =
|
||||||
|
README.rst
|
||||||
|
project_urls =
|
||||||
|
Source=https://github.com/craigerl/aprsd
|
||||||
|
Tracker=https://github.com/craigerl/aprsd/issues
|
||||||
|
summary = Amateur radio APRS daemon which listens for messages and responds
|
||||||
|
|
||||||
[global]
|
[global]
|
||||||
setup-hooks =
|
setup-hooks =
|
||||||
@ -25,9 +38,12 @@ console_scripts =
|
|||||||
fake_aprs = aprsd.fake_aprs:main
|
fake_aprs = aprsd.fake_aprs:main
|
||||||
|
|
||||||
[build_sphinx]
|
[build_sphinx]
|
||||||
source-dir = doc/source
|
source-dir = docs
|
||||||
build-dir = doc/build
|
build-dir = docs/_build
|
||||||
all_files = 1
|
all_files = 1
|
||||||
|
|
||||||
[upload_sphinx]
|
[upload_sphinx]
|
||||||
upload-dir = doc/build/html
|
upload-dir = docs/_build
|
||||||
|
|
||||||
|
[bdist_wheel]
|
||||||
|
universal = 1
|
||||||
|
25
tests/test_email.py
Normal file
25
tests/test_email.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from aprsd import email
|
||||||
|
|
||||||
|
|
||||||
|
class TestEmail(unittest.TestCase):
|
||||||
|
def test_get_email_from_shortcut(self):
|
||||||
|
email.CONFIG = {"shortcuts": {}}
|
||||||
|
email_address = "something@something.com"
|
||||||
|
addr = "-{}".format(email_address)
|
||||||
|
actual = email.get_email_from_shortcut(addr)
|
||||||
|
self.assertEqual(addr, actual)
|
||||||
|
|
||||||
|
email.CONFIG = {"nothing": "nothing"}
|
||||||
|
actual = email.get_email_from_shortcut(addr)
|
||||||
|
self.assertEqual(addr, actual)
|
||||||
|
|
||||||
|
email.CONFIG = {"shortcuts": {"not_used": "empty"}}
|
||||||
|
actual = email.get_email_from_shortcut(addr)
|
||||||
|
self.assertEqual(addr, actual)
|
||||||
|
|
||||||
|
email.CONFIG = {"shortcuts": {"-wb": email_address}}
|
||||||
|
short = "-wb"
|
||||||
|
actual = email.get_email_from_shortcut(short)
|
||||||
|
self.assertEqual(email_address, actual)
|
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
@ -7,7 +6,7 @@ from aprsd import email
|
|||||||
if sys.version_info >= (3, 2):
|
if sys.version_info >= (3, 2):
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
else:
|
else:
|
||||||
import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
|
||||||
class TestMain(unittest.TestCase):
|
class TestMain(unittest.TestCase):
|
||||||
|
150
tests/test_messaging.py
Normal file
150
tests/test_messaging.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import datetime
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from aprsd import messaging
|
||||||
|
|
||||||
|
|
||||||
|
class TestMessageTrack(unittest.TestCase):
|
||||||
|
def _clean_track(self):
|
||||||
|
track = messaging.MsgTrack()
|
||||||
|
track.track = {}
|
||||||
|
track.total_messages_tracked = 0
|
||||||
|
return track
|
||||||
|
|
||||||
|
def test_create(self):
|
||||||
|
track1 = messaging.MsgTrack()
|
||||||
|
track2 = messaging.MsgTrack()
|
||||||
|
|
||||||
|
self.assertEqual(track1, track2)
|
||||||
|
|
||||||
|
def test_add(self):
|
||||||
|
track = self._clean_track()
|
||||||
|
fromcall = "KFART"
|
||||||
|
tocall = "KHELP"
|
||||||
|
message = "somthing"
|
||||||
|
msg = messaging.TextMessage(fromcall, tocall, message)
|
||||||
|
|
||||||
|
track.add(msg)
|
||||||
|
self.assertEqual(msg, track.get(msg.id))
|
||||||
|
|
||||||
|
def test_remove(self):
|
||||||
|
track = self._clean_track()
|
||||||
|
fromcall = "KFART"
|
||||||
|
tocall = "KHELP"
|
||||||
|
message = "somthing"
|
||||||
|
msg = messaging.TextMessage(fromcall, tocall, message)
|
||||||
|
track.add(msg)
|
||||||
|
|
||||||
|
track.remove(msg.id)
|
||||||
|
self.assertEqual(None, track.get(msg.id))
|
||||||
|
|
||||||
|
def test_len(self):
|
||||||
|
"""Test getting length of tracked messages."""
|
||||||
|
track = self._clean_track()
|
||||||
|
fromcall = "KFART"
|
||||||
|
tocall = "KHELP"
|
||||||
|
message = "somthing"
|
||||||
|
msg = messaging.TextMessage(fromcall, tocall, message)
|
||||||
|
track.add(msg)
|
||||||
|
self.assertEqual(1, len(track))
|
||||||
|
msg2 = messaging.TextMessage(tocall, fromcall, message)
|
||||||
|
track.add(msg2)
|
||||||
|
self.assertEqual(2, len(track))
|
||||||
|
|
||||||
|
track.remove(msg.id)
|
||||||
|
self.assertEqual(1, len(track))
|
||||||
|
|
||||||
|
@mock.patch("aprsd.messaging.TextMessage.send")
|
||||||
|
def test__resend(self, mock_send):
|
||||||
|
"""Test the _resend method."""
|
||||||
|
track = self._clean_track()
|
||||||
|
fromcall = "KFART"
|
||||||
|
tocall = "KHELP"
|
||||||
|
message = "somthing"
|
||||||
|
msg = messaging.TextMessage(fromcall, tocall, message)
|
||||||
|
msg.last_send_attempt = 3
|
||||||
|
track.add(msg)
|
||||||
|
|
||||||
|
track._resend(msg)
|
||||||
|
msg.send.assert_called_with()
|
||||||
|
self.assertEqual(0, msg.last_send_attempt)
|
||||||
|
|
||||||
|
@mock.patch("aprsd.messaging.TextMessage.send")
|
||||||
|
def test_restart_delayed(self, mock_send):
|
||||||
|
"""Test the _resend method."""
|
||||||
|
track = self._clean_track()
|
||||||
|
fromcall = "KFART"
|
||||||
|
tocall = "KHELP"
|
||||||
|
message1 = "something"
|
||||||
|
message2 = "something another"
|
||||||
|
message3 = "something another again"
|
||||||
|
|
||||||
|
mock1_send = mock.MagicMock()
|
||||||
|
mock2_send = mock.MagicMock()
|
||||||
|
mock3_send = mock.MagicMock()
|
||||||
|
|
||||||
|
msg1 = messaging.TextMessage(fromcall, tocall, message1)
|
||||||
|
msg1.last_send_attempt = 3
|
||||||
|
msg1.last_send_time = datetime.datetime.now()
|
||||||
|
msg1.send = mock1_send
|
||||||
|
track.add(msg1)
|
||||||
|
|
||||||
|
msg2 = messaging.TextMessage(tocall, fromcall, message2)
|
||||||
|
msg2.last_send_attempt = 3
|
||||||
|
msg2.last_send_time = datetime.datetime.now()
|
||||||
|
msg2.send = mock2_send
|
||||||
|
track.add(msg2)
|
||||||
|
|
||||||
|
track.restart_delayed(count=None)
|
||||||
|
msg1.send.assert_called_once()
|
||||||
|
self.assertEqual(0, msg1.last_send_attempt)
|
||||||
|
msg2.send.assert_called_once()
|
||||||
|
self.assertEqual(0, msg2.last_send_attempt)
|
||||||
|
|
||||||
|
msg1.last_send_attempt = 3
|
||||||
|
msg1.send.reset_mock()
|
||||||
|
msg2.last_send_attempt = 3
|
||||||
|
msg2.send.reset_mock()
|
||||||
|
|
||||||
|
track.restart_delayed(count=1)
|
||||||
|
msg1.send.assert_not_called()
|
||||||
|
msg2.send.assert_called_once()
|
||||||
|
self.assertEqual(3, msg1.last_send_attempt)
|
||||||
|
self.assertEqual(0, msg2.last_send_attempt)
|
||||||
|
|
||||||
|
msg3 = messaging.TextMessage(tocall, fromcall, message3)
|
||||||
|
msg3.last_send_attempt = 3
|
||||||
|
msg3.last_send_time = datetime.datetime.now()
|
||||||
|
msg3.send = mock3_send
|
||||||
|
track.add(msg3)
|
||||||
|
|
||||||
|
msg1.last_send_attempt = 3
|
||||||
|
msg1.send.reset_mock()
|
||||||
|
msg2.last_send_attempt = 3
|
||||||
|
msg2.send.reset_mock()
|
||||||
|
msg3.last_send_attempt = 3
|
||||||
|
msg3.send.reset_mock()
|
||||||
|
|
||||||
|
track.restart_delayed(count=2)
|
||||||
|
msg1.send.assert_not_called()
|
||||||
|
msg2.send.assert_called_once()
|
||||||
|
msg3.send.assert_called_once()
|
||||||
|
self.assertEqual(3, msg1.last_send_attempt)
|
||||||
|
self.assertEqual(0, msg2.last_send_attempt)
|
||||||
|
self.assertEqual(0, msg3.last_send_attempt)
|
||||||
|
|
||||||
|
msg1.last_send_attempt = 3
|
||||||
|
msg1.send.reset_mock()
|
||||||
|
msg2.last_send_attempt = 3
|
||||||
|
msg2.send.reset_mock()
|
||||||
|
msg3.last_send_attempt = 3
|
||||||
|
msg3.send.reset_mock()
|
||||||
|
|
||||||
|
track.restart_delayed(count=2, most_recent=False)
|
||||||
|
msg1.send.assert_called_once()
|
||||||
|
msg2.send.assert_called_once()
|
||||||
|
msg3.send.assert_not_called()
|
||||||
|
self.assertEqual(0, msg1.last_send_attempt)
|
||||||
|
self.assertEqual(0, msg2.last_send_attempt)
|
||||||
|
self.assertEqual(3, msg3.last_send_attempt)
|
@ -1,42 +1,73 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import plugin
|
from aprsd import messaging
|
||||||
from aprsd.fuzzyclock import fuzzy
|
from aprsd.fuzzyclock import fuzzy
|
||||||
|
from aprsd.plugins import fortune as fortune_plugin
|
||||||
|
from aprsd.plugins import ping as ping_plugin
|
||||||
|
from aprsd.plugins import query as query_plugin
|
||||||
|
from aprsd.plugins import time as time_plugin
|
||||||
|
from aprsd.plugins import version as version_plugin
|
||||||
|
|
||||||
|
|
||||||
class TestPlugin(unittest.TestCase):
|
class TestPlugin(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.fromcall = "KFART"
|
self.fromcall = "KFART"
|
||||||
self.ack = 1
|
self.ack = 1
|
||||||
self.config = mock.MagicMock()
|
self.config = {"ham": {"callsign": self.fromcall}}
|
||||||
|
|
||||||
@mock.patch("shutil.which")
|
@mock.patch("shutil.which")
|
||||||
def test_fortune_fail(self, mock_which):
|
def test_fortune_fail(self, mock_which):
|
||||||
fortune_plugin = plugin.FortunePlugin(self.config)
|
fortune = fortune_plugin.FortunePlugin(self.config)
|
||||||
mock_which.return_value = None
|
mock_which.return_value = None
|
||||||
message = "fortune"
|
message = "fortune"
|
||||||
expected = "Fortune command not installed"
|
expected = "Fortune command not installed"
|
||||||
actual = fortune_plugin.run(self.fromcall, message, self.ack)
|
actual = fortune.run(self.fromcall, message, self.ack)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
@mock.patch("subprocess.Popen")
|
@mock.patch("subprocess.check_output")
|
||||||
@mock.patch("shutil.which")
|
@mock.patch("shutil.which")
|
||||||
def test_fortune_success(self, mock_which, mock_popen):
|
def test_fortune_success(self, mock_which, mock_output):
|
||||||
fortune_plugin = plugin.FortunePlugin(self.config)
|
fortune = fortune_plugin.FortunePlugin(self.config)
|
||||||
mock_which.return_value = "/usr/bin/games"
|
mock_which.return_value = "/usr/bin/games"
|
||||||
|
|
||||||
mock_process = mock.MagicMock()
|
mock_output.return_value = "Funny fortune"
|
||||||
mock_process.communicate.return_value = [b"Funny fortune"]
|
|
||||||
mock_popen.return_value = mock_process
|
|
||||||
|
|
||||||
message = "fortune"
|
message = "fortune"
|
||||||
expected = "Funny fortune"
|
expected = "Funny fortune"
|
||||||
actual = fortune_plugin.run(self.fromcall, message, self.ack)
|
actual = fortune.run(self.fromcall, message, self.ack)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
@mock.patch("aprsd.messaging.MsgTrack.flush")
|
||||||
|
def test_query_flush(self, mock_flush):
|
||||||
|
message = "?delete"
|
||||||
|
query = query_plugin.QueryPlugin(self.config)
|
||||||
|
|
||||||
|
expected = "Deleted ALL pending msgs."
|
||||||
|
actual = query.run(self.fromcall, message, self.ack)
|
||||||
|
mock_flush.assert_called_once()
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
@mock.patch("aprsd.messaging.MsgTrack.restart_delayed")
|
||||||
|
def test_query_restart_delayed(self, mock_restart):
|
||||||
|
track = messaging.MsgTrack()
|
||||||
|
track.track = {}
|
||||||
|
message = "?4"
|
||||||
|
query = query_plugin.QueryPlugin(self.config)
|
||||||
|
|
||||||
|
expected = "No pending msgs to resend"
|
||||||
|
actual = query.run(self.fromcall, message, self.ack)
|
||||||
|
mock_restart.assert_not_called()
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
mock_restart.reset_mock()
|
||||||
|
|
||||||
|
# add a message
|
||||||
|
msg = messaging.TextMessage(self.fromcall, "testing", self.ack)
|
||||||
|
track.add(msg)
|
||||||
|
actual = query.run(self.fromcall, message, self.ack)
|
||||||
|
mock_restart.assert_called_once()
|
||||||
|
|
||||||
@mock.patch("time.localtime")
|
@mock.patch("time.localtime")
|
||||||
def test_time(self, mock_time):
|
def test_time(self, mock_time):
|
||||||
fake_time = mock.MagicMock()
|
fake_time = mock.MagicMock()
|
||||||
@ -44,22 +75,25 @@ class TestPlugin(unittest.TestCase):
|
|||||||
m = fake_time.tm_min = 12
|
m = fake_time.tm_min = 12
|
||||||
fake_time.tm_sec = 55
|
fake_time.tm_sec = 55
|
||||||
mock_time.return_value = fake_time
|
mock_time.return_value = fake_time
|
||||||
time_plugin = plugin.TimePlugin(self.config)
|
time = time_plugin.TimePlugin(self.config)
|
||||||
|
|
||||||
fromcall = "KFART"
|
fromcall = "KFART"
|
||||||
message = "location"
|
message = "location"
|
||||||
ack = 1
|
ack = 1
|
||||||
|
|
||||||
actual = time_plugin.run(fromcall, message, ack)
|
actual = time.run(fromcall, message, ack)
|
||||||
self.assertEqual(None, actual)
|
self.assertEqual(None, actual)
|
||||||
|
|
||||||
cur_time = fuzzy(h, m, 1)
|
cur_time = fuzzy(h, m, 1)
|
||||||
|
|
||||||
message = "time"
|
message = "time"
|
||||||
expected = "{} ({}:{} PDT) ({})".format(
|
expected = "{} ({}:{} PDT) ({})".format(
|
||||||
cur_time, str(h), str(m).rjust(2, "0"), message.rstrip()
|
cur_time,
|
||||||
|
str(h),
|
||||||
|
str(m).rjust(2, "0"),
|
||||||
|
message.rstrip(),
|
||||||
)
|
)
|
||||||
actual = time_plugin.run(fromcall, message, ack)
|
actual = time.run(fromcall, message, ack)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
@mock.patch("time.localtime")
|
@mock.patch("time.localtime")
|
||||||
@ -70,7 +104,7 @@ class TestPlugin(unittest.TestCase):
|
|||||||
s = fake_time.tm_sec = 55
|
s = fake_time.tm_sec = 55
|
||||||
mock_time.return_value = fake_time
|
mock_time.return_value = fake_time
|
||||||
|
|
||||||
ping = plugin.PingPlugin(self.config)
|
ping = ping_plugin.PingPlugin(self.config)
|
||||||
|
|
||||||
fromcall = "KFART"
|
fromcall = "KFART"
|
||||||
message = "location"
|
message = "location"
|
||||||
@ -100,19 +134,19 @@ class TestPlugin(unittest.TestCase):
|
|||||||
|
|
||||||
def test_version(self):
|
def test_version(self):
|
||||||
expected = "APRSD version '{}'".format(aprsd.__version__)
|
expected = "APRSD version '{}'".format(aprsd.__version__)
|
||||||
version_plugin = plugin.VersionPlugin(self.config)
|
version = version_plugin.VersionPlugin(self.config)
|
||||||
|
|
||||||
fromcall = "KFART"
|
fromcall = "KFART"
|
||||||
message = "No"
|
message = "No"
|
||||||
ack = 1
|
ack = 1
|
||||||
|
|
||||||
actual = version_plugin.run(fromcall, message, ack)
|
actual = version.run(fromcall, message, ack)
|
||||||
self.assertEqual(None, actual)
|
self.assertEqual(None, actual)
|
||||||
|
|
||||||
message = "version"
|
message = "version"
|
||||||
actual = version_plugin.run(fromcall, message, ack)
|
actual = version.run(fromcall, message, ack)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
message = "Version"
|
message = "Version"
|
||||||
actual = version_plugin.run(fromcall, message, ack)
|
actual = version.run(fromcall, message, ack)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
19
tox.ini
19
tox.ini
@ -2,7 +2,7 @@
|
|||||||
minversion = 2.9.0
|
minversion = 2.9.0
|
||||||
skipdist = True
|
skipdist = True
|
||||||
skip_missing_interpreters = true
|
skip_missing_interpreters = true
|
||||||
envlist = pep8,py{36,37,38},fmt-check
|
envlist = pre-commit,pep8,fmt-check,py{36,37,38}
|
||||||
|
|
||||||
# Activate isolated build environment. tox will use a virtual environment
|
# Activate isolated build environment. tox will use a virtual environment
|
||||||
# to build a source distribution from the source tree. For build tools and
|
# to build a source distribution from the source tree. For build tools and
|
||||||
@ -23,8 +23,16 @@ commands =
|
|||||||
{envpython} -bb -m pytest {posargs}
|
{envpython} -bb -m pytest {posargs}
|
||||||
|
|
||||||
[testenv:docs]
|
[testenv:docs]
|
||||||
deps = -r{toxinidir}/test-requirements.txt
|
skip_install = true
|
||||||
commands = sphinx-build -b html docs/source docs/html
|
deps =
|
||||||
|
-r{toxinidir}/requirements.txt
|
||||||
|
-r{toxinidir}/dev-requirements.txt
|
||||||
|
{toxinidir}/.
|
||||||
|
changedir = {toxinidir}/docs
|
||||||
|
commands =
|
||||||
|
{envpython} clean_docs.py
|
||||||
|
sphinx-apidoc --force --output-dir apidoc {toxinidir}/aprsd
|
||||||
|
sphinx-build -a -W . _build
|
||||||
|
|
||||||
[testenv:pep8]
|
[testenv:pep8]
|
||||||
commands =
|
commands =
|
||||||
@ -91,3 +99,8 @@ deps =
|
|||||||
-r{toxinidir}/dev-requirements.txt
|
-r{toxinidir}/dev-requirements.txt
|
||||||
commands =
|
commands =
|
||||||
mypy aprsd
|
mypy aprsd
|
||||||
|
|
||||||
|
[testenv:pre-commit]
|
||||||
|
skip_install = true
|
||||||
|
deps = pre-commit
|
||||||
|
commands = pre-commit run --all-files --show-diff-on-failure
|
||||||
|
Loading…
x
Reference in New Issue
Block a user