mirror of
https://github.com/craigerl/aprsd.git
synced 2026-08-19 10:13:44 -04:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29a60b7ed0 | |||
| e8100d8777 | |||
| 764730c123 | |||
| 610e40aecd | |||
| 2f6e7e17e8 | |||
| a7bbde4a43 | |||
| 7530bcf55c | |||
| ab37a5e7a7 | |||
| 3b9970c0e7 | |||
| e57a2e2ffc | |||
| 6a1cea63e4 | |||
| 592b328956 | |||
| 450bacfe99 | |||
| cd62db95c1 | |||
| 28b54c330d | |||
| 7c653cc100 |
@@ -1,9 +1,30 @@
|
|||||||
CHANGES
|
CHANGES
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
v2.5.8
|
||||||
|
------
|
||||||
|
|
||||||
|
* Removed debug code
|
||||||
|
* Updated list-plugins
|
||||||
|
* Renamed virtualenv dir to .aprsd-venv
|
||||||
|
* Added unit tests for dev test-plugin
|
||||||
|
* Send Message command defaults to config
|
||||||
|
|
||||||
|
v2.5.7
|
||||||
|
------
|
||||||
|
|
||||||
|
* Updated Changelog
|
||||||
|
* Fixed an KISS config disabled issue
|
||||||
|
* Fixed a bug with multiple notify plugins enabled
|
||||||
|
* Unify the logging to file and stdout
|
||||||
|
* Added new feature to list-plugins command
|
||||||
|
* more README.rst cleanup
|
||||||
|
* Updated README examples
|
||||||
|
|
||||||
v2.5.6
|
v2.5.6
|
||||||
------
|
------
|
||||||
|
|
||||||
|
* Changelog
|
||||||
* Tightened up the packet logging
|
* Tightened up the packet logging
|
||||||
* Added unit tests for USWeatherPlugin, USMetarPlugin
|
* Added unit tests for USWeatherPlugin, USMetarPlugin
|
||||||
* Added test\_location to test LocationPlugin
|
* Added test\_location to test LocationPlugin
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
REQUIREMENTS_TXT ?= requirements.txt dev-requirements.txt
|
REQUIREMENTS_TXT ?= requirements.txt dev-requirements.txt
|
||||||
|
WORKDIR?=.
|
||||||
|
VENVDIR ?= $(WORKDIR)/.aprsd-venv
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
.PHONY: dev docs server test
|
.PHONY: dev docs server test
|
||||||
|
|
||||||
include Makefile.venv
|
include Makefile.venv
|
||||||
Makefile.venv:
|
Makefile.venv:
|
||||||
curl \
|
curl \
|
||||||
|
|||||||
+103
-85
@@ -46,56 +46,47 @@ callsigns to look out for. The watch list can notify you when a HAM callsign
|
|||||||
in the list is seen and now available to message on the APRS network.
|
in the list is seen and now available to message on the APRS network.
|
||||||
|
|
||||||
|
|
||||||
List of core server plugins
|
Current List of built-in 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
|
|
||||||
* NotifySeenPlugin - Send a message when a message is seen from a callsign in
|
|
||||||
the watch list. This is helpful when you want to know
|
|
||||||
when a friend is online in the ARPS network, but haven't
|
|
||||||
been seen in a while.
|
|
||||||
|
|
||||||
|
|
||||||
Current messages this will respond to:
|
|
||||||
======================================
|
======================================
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
APRS messages:
|
└─> aprsd list-plugins
|
||||||
l(ocation) [callsign] = descriptive current location of your radio
|
🐍 APRSD Built-in Plugins 🐍
|
||||||
8 Miles E Auburn CA 1673' 39.92150,-120.93950 0.1h ago
|
┏━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
w(eather) = weather forecast for your radio's current position
|
┃ Plugin Name ┃ Info ┃ Type ┃ Plugin Path ┃
|
||||||
58F(58F/46F) Partly Cloudy. Tonight, Heavy Rain.
|
┡━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
t(ime) = respond with the current time
|
│ AVWXWeatherPlugin │ AVWX weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.AVWXWeatherPlugin │
|
||||||
f(ortune) = respond with a short fortune
|
│ EmailPlugin │ Send and Receive email │ RegexCommand │ aprsd.plugins.email.EmailPlugin │
|
||||||
-email_addr email text = send an email, say "mapme" to send a current position/map
|
│ FortunePlugin │ Give me a fortune │ RegexCommand │ aprsd.plugins.fortune.FortunePlugin │
|
||||||
-2 = resend the last 2 emails from your imap inbox to this radio
|
│ LocationPlugin │ Where in the world is a CALLSIGN's last GPS beacon? │ RegexCommand │ aprsd.plugins.location.LocationPlugin │
|
||||||
p(ing) = respond with Pong!/time
|
│ NotifySeenPlugin │ Notify me when a CALLSIGN is recently seen on APRS-IS │ WatchList │ aprsd.plugins.notify.NotifySeenPlugin │
|
||||||
v(ersion) = Respond with current APRSD Version string
|
│ OWMWeatherPlugin │ OpenWeatherMap weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.OWMWeatherPlugin │
|
||||||
anything else = respond with usage
|
│ PingPlugin │ reply with a Pong! │ RegexCommand │ aprsd.plugins.ping.PingPlugin │
|
||||||
|
│ QueryPlugin │ APRSD Owner command to query messages in the MsgTrack │ RegexCommand │ aprsd.plugins.query.QueryPlugin │
|
||||||
|
│ TimeOWMPlugin │ Current time of GPS beacon's timezone. Uses OpenWeatherMap │ RegexCommand │ aprsd.plugins.time.TimeOWMPlugin │
|
||||||
|
│ TimeOpenCageDataPlugin │ Current time of GPS beacon timezone. Uses OpenCage │ RegexCommand │ aprsd.plugins.time.TimeOpenCageDataPlugin │
|
||||||
|
│ TimePlugin │ What is the current local time. │ RegexCommand │ aprsd.plugins.time.TimePlugin │
|
||||||
|
│ USMetarPlugin │ USA only METAR of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USMetarPlugin │
|
||||||
|
│ USWeatherPlugin │ Provide USA only weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USWeatherPlugin │
|
||||||
|
│ VersionPlugin │ What is the APRSD Version │ RegexCommand │ aprsd.plugins.version.VersionPlugin │
|
||||||
|
└────────────────────────┴────────────────────────────────────────────────────────────┴──────────────┴───────────────────────────────────────────┘
|
||||||
|
|
||||||
|
|
||||||
Meanwhile this code will monitor a single imap mailbox and forward email
|
Pypi.org APRSD Installable Plugin Packages
|
||||||
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
|
Install any of the following plugins with pip install <Plugin Package Name>
|
||||||
email server, and associated logins, passwords. search for "yourdomain",
|
┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
|
||||||
"password". Search for "shortcuts" to setup email aliases as well.
|
┃ Plugin Package Name ┃ Description ┃ Version ┃ Released ┃ Installed? ┃
|
||||||
|
┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
|
||||||
|
│ 📂 aprsd-stock-plugin │ Ham Radio APRSD Plugin for fetching stock quotes │ 0.1.2 │ Nov 9, 2021 │ No │
|
||||||
|
│ 📂 aprsd-weewx-plugin │ HAM Radio APRSD that reports weather from a weewx weather station. │ 0.1.4 │ Dec 7, 2021 │ No │
|
||||||
|
│ 📂 aprsd-telegram-plugin │ Ham Radio APRS APRSD plugin for Telegram IM service │ 0.1.2 │ Nov 9, 2021 │ No │
|
||||||
|
│ 📂 aprsd-twitter-plugin │ Python APRSD plugin to send tweets │ 0.3.0 │ Dec 7, 2021 │ No │
|
||||||
|
│ 📂 aprsd-slack-plugin │ Amateur radio APRS daemon which listens for messages and responds │ 1.0.4 │ Jan 15, 2021 │ No │
|
||||||
|
└──────────────────────────┴────────────────────────────────────────────────────────────────────┴─────────┴──────────────┴────────────┘
|
||||||
|
|
||||||
|
installation:
|
||||||
Installation:
|
|
||||||
=============
|
=============
|
||||||
|
|
||||||
pip install aprsd
|
pip install aprsd
|
||||||
@@ -112,20 +103,21 @@ Help
|
|||||||
└─[$] > aprsd -h
|
└─[$] > aprsd -h
|
||||||
Usage: aprsd [OPTIONS] COMMAND [ARGS]...
|
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:
|
Options:
|
||||||
--version Show the version and exit.
|
--version Show the version and exit.
|
||||||
-h, --help Show this message and exit.
|
-h, --help Show this message and exit.
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
install Install the click-completion-command completion
|
check-version Check this version against the latest in pypi.org.
|
||||||
|
completion Click Completion subcommands
|
||||||
|
dev Development type subcommands
|
||||||
|
healthcheck Check the health of the running aprsd server.
|
||||||
|
list-plugins List the built in plugins available to APRSD.
|
||||||
|
listen Listen to packets on the APRS-IS Network based on FILTER.
|
||||||
sample-config This dumps the config to stdout.
|
sample-config This dumps the config to stdout.
|
||||||
send-message Send a message to a callsign via APRS_IS.
|
send-message Send a message to a callsign via APRS_IS.
|
||||||
server Start the aprsd server process.
|
server Start the aprsd server gateway process.
|
||||||
show Show the click-completion-command completion code
|
version Show the APRSD version.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -145,8 +137,12 @@ Output
|
|||||||
|
|
||||||
└─> aprsd sample-config
|
└─> aprsd sample-config
|
||||||
aprs:
|
aprs:
|
||||||
|
# Set enabled to False if there is no internet connectivity.
|
||||||
|
# This is useful for a direwolf KISS aprs connection only.
|
||||||
|
|
||||||
# Get the passcode for your callsign here:
|
# Get the passcode for your callsign here:
|
||||||
# https://apps.magicbug.co.uk/passcode
|
# https://apps.magicbug.co.uk/passcode
|
||||||
|
enabled: true
|
||||||
host: rotate.aprs2.net
|
host: rotate.aprs2.net
|
||||||
login: CALLSIGN
|
login: CALLSIGN
|
||||||
password: '00000'
|
password: '00000'
|
||||||
@@ -184,18 +180,38 @@ Output
|
|||||||
- aprsd.plugins.weather.USWeatherPlugin
|
- aprsd.plugins.weather.USWeatherPlugin
|
||||||
- aprsd.plugins.version.VersionPlugin
|
- aprsd.plugins.version.VersionPlugin
|
||||||
logfile: /tmp/aprsd.log
|
logfile: /tmp/aprsd.log
|
||||||
logformat: '[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s] %(message)s - [%(pathname)s:%(lineno)d]'
|
logformat: '[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s] %(message)s
|
||||||
|
- [%(pathname)s:%(lineno)d]'
|
||||||
|
rich_logging: false
|
||||||
|
save_location: /Users/i530566/.config/aprsd/
|
||||||
trace: false
|
trace: false
|
||||||
units: imperial
|
units: imperial
|
||||||
|
watch_list:
|
||||||
|
alert_callsign: NOCALL
|
||||||
|
alert_time_seconds: 43200
|
||||||
|
callsigns: []
|
||||||
|
enabled: false
|
||||||
|
enabled_plugins:
|
||||||
|
- aprsd.plugins.notify.NotifySeenPlugin
|
||||||
|
packet_keep_count: 10
|
||||||
web:
|
web:
|
||||||
enabled: true
|
enabled: true
|
||||||
host: 0.0.0.0
|
host: 0.0.0.0
|
||||||
logging_enabled: true
|
logging_enabled: true
|
||||||
port: 8001
|
port: 8001
|
||||||
users:
|
users:
|
||||||
admin: aprsd
|
admin: password-here
|
||||||
ham:
|
ham:
|
||||||
callsign: CALLSIGN
|
callsign: NOCALL
|
||||||
|
kiss:
|
||||||
|
serial:
|
||||||
|
baudrate: 9600
|
||||||
|
device: /dev/ttyS0
|
||||||
|
enabled: false
|
||||||
|
tcp:
|
||||||
|
enabled: false
|
||||||
|
host: direwolf.ip.address
|
||||||
|
port: '8001'
|
||||||
services:
|
services:
|
||||||
aprs.fi:
|
aprs.fi:
|
||||||
# Get the apiKey from your aprs.fi account here:
|
# Get the apiKey from your aprs.fi account here:
|
||||||
@@ -229,35 +245,35 @@ look for incomming commands to the callsign configured in the config file
|
|||||||
::
|
::
|
||||||
|
|
||||||
└─[$] > aprsd server --help
|
└─[$] > aprsd server --help
|
||||||
Usage: aprsd server [OPTIONS]
|
Usage: aprsd server [OPTIONS]
|
||||||
|
|
||||||
Start the aprsd server process.
|
Start the aprsd server gateway process.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG]
|
--loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG]
|
||||||
The log level to use for aprsd.log
|
The log level to use for aprsd.log
|
||||||
[default: INFO]
|
[default: INFO]
|
||||||
|
-c, --config TEXT The aprsd config file to use for options.
|
||||||
|
[default:
|
||||||
|
/Users/i530566/.config/aprsd/aprsd.yml]
|
||||||
|
--quiet Don't log to stdout
|
||||||
|
-f, --flush Flush out all old aged messages on disk.
|
||||||
|
[default: False]
|
||||||
|
-h, --help Show this message and exit.
|
||||||
|
|
||||||
--quiet Don't log to stdout
|
└─> aprsd server
|
||||||
--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:
|
|
||||||
/home/waboring/.config/aprsd/aprsd.yml]
|
|
||||||
|
|
||||||
-f, --flush Flush out all old aged messages on disk.
|
|
||||||
[default: False]
|
|
||||||
|
|
||||||
-h, --help Show this message and exit.
|
|
||||||
|
|
||||||
$ aprsd server
|
|
||||||
Load config
|
Load config
|
||||||
[02/13/2021 09:22:09 AM] [MainThread ] [INFO ] APRSD Started version: 1.6.0
|
12/07/2021 03:16:17 PM MainThread INFO APRSD is up to date server.py:51
|
||||||
[02/13/2021 09:22:09 AM] [MainThread ] [INFO ] Checking IMAP configuration
|
12/07/2021 03:16:17 PM MainThread INFO APRSD Started version: 2.5.6 server.py:52
|
||||||
[02/13/2021 09:22:09 AM] [MainThread ] [INFO ] Checking SMTP configuration
|
12/07/2021 03:16:17 PM MainThread INFO Using CONFIG values: server.py:55
|
||||||
[02/13/2021 09:22:10 AM] [MainThread ] [INFO ] Validating 2 Email shortcuts. This can take up to 10 seconds per shortcut
|
12/07/2021 03:16:17 PM MainThread INFO ham.callsign = WB4BOR server.py:60
|
||||||
|
12/07/2021 03:16:17 PM MainThread INFO aprs.login = WB4BOR-12 server.py:60
|
||||||
|
12/07/2021 03:16:17 PM MainThread INFO aprs.password = XXXXXXXXXXXXXXXXXXX server.py:58
|
||||||
|
12/07/2021 03:16:17 PM MainThread INFO aprs.host = noam.aprs2.net server.py:60
|
||||||
|
12/07/2021 03:16:17 PM MainThread INFO aprs.port = 14580 server.py:60
|
||||||
|
12/07/2021 03:16:17 PM MainThread INFO aprs.logfile = /tmp/aprsd.log server.py:60
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
send-message
|
send-message
|
||||||
@@ -269,25 +285,27 @@ test messages
|
|||||||
::
|
::
|
||||||
|
|
||||||
└─[$] > aprsd send-message -h
|
└─[$] > aprsd send-message -h
|
||||||
Usage: aprsd send-message [OPTIONS] TOCALLSIGN [COMMAND]...
|
Usage: aprsd send-message [OPTIONS] TOCALLSIGN COMMAND...
|
||||||
|
|
||||||
Send a message to a callsign via APRS_IS.
|
Send a message to a callsign via APRS_IS.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG]
|
--loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG]
|
||||||
The log level to use for aprsd.log
|
The log level to use for aprsd.log
|
||||||
[default: DEBUG]
|
[default: INFO]
|
||||||
|
|
||||||
--quiet Don't log to stdout
|
|
||||||
-c, --config TEXT The aprsd config file to use for options.
|
-c, --config TEXT The aprsd config file to use for options.
|
||||||
[default: ~/.config/aprsd/aprsd.yml]
|
[default:
|
||||||
|
/Users/i530566/.config/aprsd/aprsd.yml]
|
||||||
|
--quiet Don't log to stdout
|
||||||
--aprs-login TEXT What callsign to send the message from.
|
--aprs-login TEXT What callsign to send the message from.
|
||||||
[env var: APRS_LOGIN]
|
[env var: APRS_LOGIN]
|
||||||
|
|
||||||
--aprs-password TEXT the APRS-IS password for APRS_LOGIN [env
|
--aprs-password TEXT the APRS-IS password for APRS_LOGIN [env
|
||||||
var: APRS_PASSWORD]
|
var: APRS_PASSWORD]
|
||||||
|
-n, --no-ack Don't wait for an ack, just sent it to APRS-
|
||||||
|
IS and bail. [default: False]
|
||||||
|
-w, --wait-response Wait for a response to the message?
|
||||||
|
[default: False]
|
||||||
|
--raw TEXT Send a raw message. Implies --no-ack
|
||||||
-h, --help Show this message and exit.
|
-h, --help Show this message and exit.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ class KISSClient(Client):
|
|||||||
if config.get("kiss.tcp.enabled", default=False):
|
if config.get("kiss.tcp.enabled", default=False):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def transport(config):
|
def transport(config):
|
||||||
if config.get("kiss.serial.enabled", default=False):
|
if config.get("kiss.serial.enabled", default=False):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import click
|
import click
|
||||||
import click_completion
|
import click_completion
|
||||||
|
|
||||||
from ..aprsd import cli
|
from aprsd.aprsd import cli
|
||||||
|
|
||||||
|
|
||||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||||
|
|||||||
+12
-4
@@ -9,8 +9,7 @@ import click
|
|||||||
|
|
||||||
# local imports here
|
# local imports here
|
||||||
from aprsd import cli_helper, client, messaging, packets, plugin, stats, trace
|
from aprsd import cli_helper, client, messaging, packets, plugin, stats, trace
|
||||||
|
from aprsd.aprsd import cli
|
||||||
from ..aprsd import cli
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@@ -69,13 +68,22 @@ def test_plugin(
|
|||||||
):
|
):
|
||||||
"""Test an individual APRSD plugin given a python path."""
|
"""Test an individual APRSD plugin given a python path."""
|
||||||
config = ctx.obj["config"]
|
config = ctx.obj["config"]
|
||||||
fromcall = aprs_login
|
|
||||||
|
if not aprs_login:
|
||||||
|
if not config.exists("aprs.login"):
|
||||||
|
click.echo("Must set --aprs_login or APRS_LOGIN")
|
||||||
|
ctx.exit(-1)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
fromcall = config.get("aprs.login")
|
||||||
|
else:
|
||||||
|
fromcall = aprs_login
|
||||||
|
|
||||||
if not plugin_path:
|
if not plugin_path:
|
||||||
click.echo(ctx.get_help())
|
click.echo(ctx.get_help())
|
||||||
click.echo("")
|
click.echo("")
|
||||||
ctx.fail("Failed to provide -p option to test a plugin")
|
ctx.fail("Failed to provide -p option to test a plugin")
|
||||||
ctx.exit()
|
return
|
||||||
|
|
||||||
if type(message) is tuple:
|
if type(message) is tuple:
|
||||||
message = " ".join(message)
|
message = " ".join(message)
|
||||||
|
|||||||
+34
-32
@@ -11,17 +11,18 @@ import sys
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
import requests
|
import requests
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper, utils
|
from aprsd import cli_helper, utils
|
||||||
|
|
||||||
# local imports here
|
# local imports here
|
||||||
from ..aprsd import cli
|
from aprsd.aprsd import cli
|
||||||
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@@ -43,36 +44,37 @@ LOG = logging.getLogger("APRSD")
|
|||||||
@cli_helper.process_standard_options_no_config
|
@cli_helper.process_standard_options_no_config
|
||||||
def healthcheck(ctx, health_url, timeout):
|
def healthcheck(ctx, health_url, timeout):
|
||||||
"""Check the health of the running aprsd server."""
|
"""Check the health of the running aprsd server."""
|
||||||
LOG.debug(f"APRSD HealthCheck version: {aprsd.__version__}")
|
console.log(f"APRSD HealthCheck version: {aprsd.__version__}")
|
||||||
|
with console.status(f"APRSD HealthCheck version: {aprsd.__version__}") as status:
|
||||||
try:
|
try:
|
||||||
url = health_url
|
status.update(f"Contacting APRSD on {health_url}")
|
||||||
response = requests.get(url, timeout=timeout)
|
url = health_url
|
||||||
response.raise_for_status()
|
response = requests.get(url, timeout=timeout)
|
||||||
except Exception as ex:
|
response.raise_for_status()
|
||||||
LOG.error(f"Failed to fetch healthcheck url '{url}' : '{ex}'")
|
except Exception as ex:
|
||||||
sys.exit(-1)
|
console.log(f"Failed to fetch healthcheck url '{url}' : '{ex}'")
|
||||||
else:
|
|
||||||
stats = json.loads(response.text)
|
|
||||||
LOG.debug(stats)
|
|
||||||
|
|
||||||
email_thread_last_update = stats["stats"]["email"]["thread_last_update"]
|
|
||||||
|
|
||||||
delta = utils.parse_delta_str(email_thread_last_update)
|
|
||||||
d = datetime.timedelta(**delta)
|
|
||||||
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
|
||||||
max_delta = datetime.timedelta(**max_timeout)
|
|
||||||
if d > max_delta:
|
|
||||||
LOG.error(f"Email thread is very old! {d}")
|
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
else:
|
||||||
|
status.update("Contacted APRSD. Parsing results.")
|
||||||
|
stats = json.loads(response.text)
|
||||||
|
email_thread_last_update = stats["stats"]["email"]["thread_last_update"]
|
||||||
|
|
||||||
aprsis_last_update = stats["stats"]["aprs-is"]["last_update"]
|
if email_thread_last_update != "never":
|
||||||
delta = utils.parse_delta_str(aprsis_last_update)
|
delta = utils.parse_delta_str(email_thread_last_update)
|
||||||
d = datetime.timedelta(**delta)
|
d = datetime.timedelta(**delta)
|
||||||
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
||||||
max_delta = datetime.timedelta(**max_timeout)
|
max_delta = datetime.timedelta(**max_timeout)
|
||||||
if d > max_delta:
|
if d > max_delta:
|
||||||
LOG.error(f"APRS-IS last update is very old! {d}")
|
console.log(f"Email thread is very old! {d}")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
sys.exit(0)
|
aprsis_last_update = stats["stats"]["aprs-is"]["last_update"]
|
||||||
|
delta = utils.parse_delta_str(aprsis_last_update)
|
||||||
|
d = datetime.timedelta(**delta)
|
||||||
|
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
||||||
|
max_delta = datetime.timedelta(**max_timeout)
|
||||||
|
if d > max_delta:
|
||||||
|
LOG.error(f"APRS-IS last update is very old! {d}")
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
|
|||||||
+202
-19
@@ -1,28 +1,109 @@
|
|||||||
|
import fnmatch
|
||||||
|
import importlib
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
from textwrap import indent
|
import os
|
||||||
|
import pkgutil
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from traceback import print_tb
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
import click
|
import click
|
||||||
from tabulate import tabulate
|
import requests
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.table import Table
|
||||||
|
from rich.text import Text
|
||||||
|
from thesmuggler import smuggle
|
||||||
|
|
||||||
from aprsd import cli_helper, plugin
|
from aprsd import cli_helper
|
||||||
|
from aprsd import plugin as aprsd_plugin
|
||||||
|
from aprsd.aprsd import cli
|
||||||
from aprsd.plugins import (
|
from aprsd.plugins import (
|
||||||
email, fortune, location, notify, ping, query, time, version, weather,
|
email, fortune, location, notify, ping, query, time, version, weather,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..aprsd import cli
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
def onerror(name):
|
||||||
@cli_helper.add_options(cli_helper.common_options)
|
print(f"Error importing module {name}")
|
||||||
@click.pass_context
|
type, value, traceback = sys.exc_info()
|
||||||
@cli_helper.process_standard_options_no_config
|
print_tb(traceback)
|
||||||
def list_plugins(ctx):
|
|
||||||
"""List the built in plugins available to APRSD."""
|
|
||||||
|
|
||||||
|
|
||||||
|
def is_plugin(obj):
|
||||||
|
for c in inspect.getmro(obj):
|
||||||
|
if issubclass(c, aprsd_plugin.APRSDPluginBase):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def plugin_type(obj):
|
||||||
|
for c in inspect.getmro(obj):
|
||||||
|
if issubclass(c, aprsd_plugin.APRSDRegexCommandPluginBase):
|
||||||
|
return "RegexCommand"
|
||||||
|
if issubclass(c, aprsd_plugin.APRSDWatchListPluginBase):
|
||||||
|
return "WatchList"
|
||||||
|
if issubclass(c, aprsd_plugin.APRSDPluginBase):
|
||||||
|
return "APRSDPluginBase"
|
||||||
|
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def walk_package(package):
|
||||||
|
return pkgutil.walk_packages(
|
||||||
|
package.__path__,
|
||||||
|
package.__name__ + ".",
|
||||||
|
onerror=onerror,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_module_info(package_name, module_name, module_path):
|
||||||
|
if not os.path.exists(module_path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
dir_path = os.path.realpath(module_path)
|
||||||
|
pattern = "*.py"
|
||||||
|
|
||||||
|
obj_list = []
|
||||||
|
|
||||||
|
for path, _subdirs, files in os.walk(dir_path):
|
||||||
|
for name in files:
|
||||||
|
if fnmatch.fnmatch(name, pattern):
|
||||||
|
module = smuggle(f"{path}/{name}")
|
||||||
|
for mem_name, obj in inspect.getmembers(module):
|
||||||
|
if inspect.isclass(obj) and is_plugin(obj):
|
||||||
|
obj_list.append(
|
||||||
|
{
|
||||||
|
"package": package_name,
|
||||||
|
"name": mem_name, "obj": obj,
|
||||||
|
"version": obj.version,
|
||||||
|
"path": f"{'.'.join([module_name, obj.__name__])}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return obj_list
|
||||||
|
|
||||||
|
|
||||||
|
def get_installed_plugins():
|
||||||
|
# installed plugins
|
||||||
|
ip = {}
|
||||||
|
for finder, name, ispkg in pkgutil.iter_modules():
|
||||||
|
if name.startswith("aprsd_"):
|
||||||
|
if ispkg:
|
||||||
|
module = importlib.import_module(name)
|
||||||
|
pkgs = walk_package(module)
|
||||||
|
for pkg in pkgs:
|
||||||
|
pkg_info = get_module_info(module.__name__, pkg.name, module.__path__[0])
|
||||||
|
ip[name] = pkg_info
|
||||||
|
return ip
|
||||||
|
|
||||||
|
|
||||||
|
def show_built_in_plugins(console):
|
||||||
modules = [email, fortune, location, notify, ping, query, time, version, weather]
|
modules = [email, fortune, location, notify, ping, query, time, version, weather]
|
||||||
plugins = []
|
plugins = []
|
||||||
|
|
||||||
@@ -30,7 +111,7 @@ def list_plugins(ctx):
|
|||||||
entries = inspect.getmembers(module, inspect.isclass)
|
entries = inspect.getmembers(module, inspect.isclass)
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
cls = entry[1]
|
cls = entry[1]
|
||||||
if issubclass(cls, plugin.APRSDPluginBase):
|
if issubclass(cls, aprsd_plugin.APRSDPluginBase):
|
||||||
info = {
|
info = {
|
||||||
"name": cls.__qualname__,
|
"name": cls.__qualname__,
|
||||||
"path": f"{cls.__module__}.{cls.__qualname__}",
|
"path": f"{cls.__module__}.{cls.__qualname__}",
|
||||||
@@ -39,21 +120,123 @@ def list_plugins(ctx):
|
|||||||
"short_desc": cls.short_description,
|
"short_desc": cls.short_description,
|
||||||
}
|
}
|
||||||
|
|
||||||
if issubclass(cls, plugin.APRSDRegexCommandPluginBase):
|
if issubclass(cls, aprsd_plugin.APRSDRegexCommandPluginBase):
|
||||||
info["command_regex"] = cls.command_regex
|
info["command_regex"] = cls.command_regex
|
||||||
info["type"] = "RegexCommand"
|
info["type"] = "RegexCommand"
|
||||||
|
|
||||||
if issubclass(cls, plugin.APRSDWatchListPluginBase):
|
if issubclass(cls, aprsd_plugin.APRSDWatchListPluginBase):
|
||||||
info["type"] = "WatchList"
|
info["type"] = "WatchList"
|
||||||
|
|
||||||
plugins.append(info)
|
plugins.append(info)
|
||||||
|
|
||||||
lines = []
|
plugins = sorted(plugins, key=lambda i: i["name"])
|
||||||
headers = ("Plugin Name", "Plugin Path", "Type", "Info")
|
|
||||||
|
|
||||||
|
table = Table(
|
||||||
|
title="[not italic]:snake:[/] [bold][magenta]APRSD Built-in Plugins [not italic]:snake:[/]",
|
||||||
|
)
|
||||||
|
table.add_column("Plugin Name", style="cyan", no_wrap=True)
|
||||||
|
table.add_column("Info", style="bold yellow")
|
||||||
|
table.add_column("Type", style="bold green")
|
||||||
|
table.add_column("Plugin Path", style="bold blue")
|
||||||
for entry in plugins:
|
for entry in plugins:
|
||||||
lines.append(
|
table.add_row(entry["name"], entry["short_desc"], entry["type"], entry["path"])
|
||||||
(entry["name"], entry["path"], entry["type"], entry["short_desc"]),
|
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
|
||||||
|
def show_pypi_plugins(installed_plugins, console):
|
||||||
|
query = "aprsd"
|
||||||
|
api_url = "https://pypi.org/search/"
|
||||||
|
snippets = []
|
||||||
|
s = requests.Session()
|
||||||
|
for page in range(1, 3):
|
||||||
|
params = {"q": query, "page": page}
|
||||||
|
r = s.get(api_url, params=params)
|
||||||
|
soup = BeautifulSoup(r.text, "html.parser")
|
||||||
|
snippets += soup.select('a[class*="snippet"]')
|
||||||
|
if not hasattr(s, "start_url"):
|
||||||
|
s.start_url = r.url.rsplit("&page", maxsplit=1).pop(0)
|
||||||
|
|
||||||
|
title = Text.assemble(
|
||||||
|
("Pypi.org APRSD Installable Plugin Packages\n\n", "bold magenta"),
|
||||||
|
("Install any of the following plugins with ", "bold yellow"),
|
||||||
|
("'pip install ", "bold white"),
|
||||||
|
("<Plugin Package Name>'", "cyan"),
|
||||||
|
)
|
||||||
|
|
||||||
|
table = Table(title=title)
|
||||||
|
table.add_column("Plugin Package Name", style="cyan", no_wrap=True)
|
||||||
|
table.add_column("Description", style="yellow")
|
||||||
|
table.add_column("Version", style="yellow", justify="center")
|
||||||
|
table.add_column("Released", style="bold green", justify="center")
|
||||||
|
table.add_column("Installed?", style="red", justify="center")
|
||||||
|
for snippet in snippets:
|
||||||
|
link = urljoin(api_url, snippet.get("href"))
|
||||||
|
package = re.sub(r"\s+", " ", snippet.select_one('span[class*="name"]').text.strip())
|
||||||
|
version = re.sub(r"\s+", " ", snippet.select_one('span[class*="version"]').text.strip())
|
||||||
|
released = re.sub(r"\s+", " ", snippet.select_one('span[class*="released"]').text.strip())
|
||||||
|
description = re.sub(r"\s+", " ", snippet.select_one('p[class*="description"]').text.strip())
|
||||||
|
emoji = ":open_file_folder:"
|
||||||
|
|
||||||
|
if "aprsd-" not in package or "-plugin" not in package:
|
||||||
|
continue
|
||||||
|
|
||||||
|
under = package.replace("-", "_")
|
||||||
|
if under in installed_plugins:
|
||||||
|
installed = "Yes"
|
||||||
|
else:
|
||||||
|
installed = "No"
|
||||||
|
|
||||||
|
table.add_row(
|
||||||
|
f"[link={link}]{emoji}[/link] {package}",
|
||||||
|
description, version, released, installed,
|
||||||
)
|
)
|
||||||
|
|
||||||
click.echo(indent(tabulate(lines, headers, disable_numparse=True), " "))
|
console.print("\n")
|
||||||
|
console.print(table)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def show_installed_plugins(installed_plugins, console):
|
||||||
|
if not installed_plugins:
|
||||||
|
return
|
||||||
|
|
||||||
|
table = Table(
|
||||||
|
title="[not italic]:snake:[/] [bold][magenta]APRSD Installed 3rd party Plugins [not italic]:snake:[/]",
|
||||||
|
)
|
||||||
|
table.add_column("Package Name", style=" bold white", no_wrap=True)
|
||||||
|
table.add_column("Plugin Name", style="cyan", no_wrap=True)
|
||||||
|
table.add_column("Version", style="yellow", justify="center")
|
||||||
|
table.add_column("Type", style="bold green")
|
||||||
|
table.add_column("Plugin Path", style="bold blue")
|
||||||
|
for name in installed_plugins:
|
||||||
|
for plugin in installed_plugins[name]:
|
||||||
|
table.add_row(
|
||||||
|
name.replace("_", "-"),
|
||||||
|
plugin["name"],
|
||||||
|
plugin["version"],
|
||||||
|
plugin_type(plugin["obj"]),
|
||||||
|
plugin["path"],
|
||||||
|
)
|
||||||
|
|
||||||
|
console.print("\n")
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@cli_helper.add_options(cli_helper.common_options)
|
||||||
|
@click.pass_context
|
||||||
|
@cli_helper.process_standard_options_no_config
|
||||||
|
def list_plugins(ctx):
|
||||||
|
"""List the built in plugins available to APRSD."""
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
with console.status("Show Built-in Plugins") as status:
|
||||||
|
show_built_in_plugins(console)
|
||||||
|
|
||||||
|
status.update("Fetching pypi.org plugins")
|
||||||
|
installed_plugins = get_installed_plugins()
|
||||||
|
show_pypi_plugins(installed_plugins, console)
|
||||||
|
|
||||||
|
status.update("Looking for installed APRSD plugins")
|
||||||
|
show_installed_plugins(installed_plugins, console)
|
||||||
|
|||||||
@@ -10,19 +10,20 @@ import time
|
|||||||
|
|
||||||
import aprslib
|
import aprslib
|
||||||
import click
|
import click
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
# local imports here
|
# local imports here
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import (
|
from aprsd import (
|
||||||
cli_helper, client, messaging, packets, stats, threads, trace, utils,
|
cli_helper, client, messaging, packets, stats, threads, trace, utils,
|
||||||
)
|
)
|
||||||
|
from aprsd.aprsd import cli
|
||||||
from ..aprsd import cli
|
|
||||||
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
def signal_handler(sig, frame):
|
def signal_handler(sig, frame):
|
||||||
@@ -114,7 +115,7 @@ def listen(
|
|||||||
resp = packet.get("response", None)
|
resp = packet.get("response", None)
|
||||||
if resp == "ack":
|
if resp == "ack":
|
||||||
ack_num = packet.get("msgNo")
|
ack_num = packet.get("msgNo")
|
||||||
LOG.info(f"We saw an ACK {ack_num} Ignoring")
|
console.log(f"We saw an ACK {ack_num} Ignoring")
|
||||||
messaging.log_packet(packet)
|
messaging.log_packet(packet)
|
||||||
else:
|
else:
|
||||||
message = packet.get("message_text", None)
|
message = packet.get("message_text", None)
|
||||||
@@ -126,6 +127,7 @@ def listen(
|
|||||||
message,
|
message,
|
||||||
fromcall=fromcall,
|
fromcall=fromcall,
|
||||||
ack=msg_number,
|
ack=msg_number,
|
||||||
|
console=console,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize the client factory and create
|
# Initialize the client factory and create
|
||||||
@@ -149,7 +151,8 @@ def listen(
|
|||||||
# 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
|
||||||
# the packet
|
# the packet
|
||||||
aprs_client.consumer(rx_packet, raw=False)
|
with console.status("Listening for packets"):
|
||||||
|
aprs_client.consumer(rx_packet, raw=False)
|
||||||
except aprslib.exceptions.ConnectionDrop:
|
except aprslib.exceptions.ConnectionDrop:
|
||||||
LOG.error("Connection dropped, reconnecting")
|
LOG.error("Connection dropped, reconnecting")
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|||||||
+15
-11
@@ -8,8 +8,7 @@ import click
|
|||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper, client, messaging, packets
|
from aprsd import cli_helper, client, messaging, packets
|
||||||
|
from aprsd.aprsd import cli
|
||||||
from ..aprsd import cli
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@@ -21,13 +20,13 @@ LOG = logging.getLogger("APRSD")
|
|||||||
"--aprs-login",
|
"--aprs-login",
|
||||||
envvar="APRS_LOGIN",
|
envvar="APRS_LOGIN",
|
||||||
show_envvar=True,
|
show_envvar=True,
|
||||||
help="What callsign to send the message from.",
|
help="What callsign to send the message from. Defaults to config entry.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--aprs-password",
|
"--aprs-password",
|
||||||
envvar="APRS_PASSWORD",
|
envvar="APRS_PASSWORD",
|
||||||
show_envvar=True,
|
show_envvar=True,
|
||||||
help="the APRS-IS password for APRS_LOGIN",
|
help="the APRS-IS password for APRS_LOGIN. Defaults to config entry.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--no-ack",
|
"--no-ack",
|
||||||
@@ -66,15 +65,20 @@ def send_message(
|
|||||||
quiet = ctx.obj["quiet"]
|
quiet = ctx.obj["quiet"]
|
||||||
|
|
||||||
if not aprs_login:
|
if not aprs_login:
|
||||||
click.echo("Must set --aprs_login or APRS_LOGIN")
|
if not config.exists("aprs.login"):
|
||||||
return
|
click.echo("Must set --aprs_login or APRS_LOGIN")
|
||||||
|
ctx.exit(-1)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
config["aprs"]["login"] = aprs_login
|
||||||
|
|
||||||
if not aprs_password:
|
if not aprs_password:
|
||||||
click.echo("Must set --aprs-password or APRS_PASSWORD")
|
if not config.exists("aprs.password"):
|
||||||
return
|
click.echo("Must set --aprs-password or APRS_PASSWORD")
|
||||||
|
ctx.exit(-1)
|
||||||
config["aprs"]["login"] = aprs_login
|
return
|
||||||
config["aprs"]["password"] = aprs_password
|
else:
|
||||||
|
config["aprs"]["password"] = aprs_password
|
||||||
|
|
||||||
LOG.info(f"APRSD LISTEN Started version: {aprsd.__version__}")
|
LOG.info(f"APRSD LISTEN Started version: {aprsd.__version__}")
|
||||||
if type(command) is tuple:
|
if type(command) is tuple:
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ from aprsd import (
|
|||||||
trace, utils,
|
trace, utils,
|
||||||
)
|
)
|
||||||
from aprsd import aprsd as aprsd_main
|
from aprsd import aprsd as aprsd_main
|
||||||
|
from aprsd.aprsd import cli
|
||||||
from ..aprsd import cli
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|||||||
+33
-22
@@ -1,9 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from logging import NullHandler
|
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
import sys
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -11,6 +9,7 @@ import aprslib
|
|||||||
from aprslib.exceptions import LoginError
|
from aprslib.exceptions import LoginError
|
||||||
import flask
|
import flask
|
||||||
from flask import request
|
from flask import request
|
||||||
|
from flask.logging import default_handler
|
||||||
import flask_classful
|
import flask_classful
|
||||||
from flask_httpauth import HTTPBasicAuth
|
from flask_httpauth import HTTPBasicAuth
|
||||||
from flask_socketio import Namespace, SocketIO
|
from flask_socketio import Namespace, SocketIO
|
||||||
@@ -21,6 +20,7 @@ from aprsd import client
|
|||||||
from aprsd import config as aprsd_config
|
from aprsd import config as aprsd_config
|
||||||
from aprsd import log, messaging, packets, plugin, stats, threads, utils
|
from aprsd import log, messaging, packets, plugin, stats, threads, utils
|
||||||
from aprsd.clients import aprsis
|
from aprsd.clients import aprsis
|
||||||
|
from aprsd.logging import logging as aprsd_logging
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@@ -552,6 +552,15 @@ class LoggingNamespace(Namespace):
|
|||||||
|
|
||||||
def setup_logging(config, flask_app, loglevel, quiet):
|
def setup_logging(config, flask_app, loglevel, quiet):
|
||||||
flask_log = logging.getLogger("werkzeug")
|
flask_log = logging.getLogger("werkzeug")
|
||||||
|
flask_app.logger.removeHandler(default_handler)
|
||||||
|
flask_log.removeHandler(default_handler)
|
||||||
|
|
||||||
|
log_level = aprsd_config.LOG_LEVELS[loglevel]
|
||||||
|
flask_log.setLevel(log_level)
|
||||||
|
date_format = config["aprsd"].get(
|
||||||
|
"dateformat",
|
||||||
|
aprsd_config.DEFAULT_DATE_FORMAT,
|
||||||
|
)
|
||||||
|
|
||||||
if not config["aprsd"]["web"].get("logging_enabled", False):
|
if not config["aprsd"]["web"].get("logging_enabled", False):
|
||||||
# disable web logging
|
# disable web logging
|
||||||
@@ -559,28 +568,30 @@ def setup_logging(config, flask_app, loglevel, quiet):
|
|||||||
flask_app.logger.disabled = True
|
flask_app.logger.disabled = True
|
||||||
return
|
return
|
||||||
|
|
||||||
log_level = aprsd_config.LOG_LEVELS[loglevel]
|
if config["aprsd"].get("rich_logging", False) and not quiet:
|
||||||
LOG.setLevel(log_level)
|
log_format = "%(message)s"
|
||||||
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
|
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||||
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
|
rh = aprsd_logging.APRSDRichHandler(
|
||||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
show_thread=True, thread_width=15,
|
||||||
|
rich_tracebacks=True, omit_repeated_times=False,
|
||||||
|
)
|
||||||
|
rh.setFormatter(log_formatter)
|
||||||
|
flask_log.addHandler(rh)
|
||||||
|
|
||||||
log_file = config["aprsd"].get("logfile", None)
|
log_file = config["aprsd"].get("logfile", None)
|
||||||
|
|
||||||
if log_file:
|
if log_file:
|
||||||
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
log_format = config["aprsd"].get(
|
||||||
else:
|
"logformat",
|
||||||
fh = NullHandler()
|
aprsd_config.DEFAULT_LOG_FORMAT,
|
||||||
|
)
|
||||||
fh.setFormatter(log_formatter)
|
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||||
for handler in flask_app.logger.handlers:
|
fh = RotatingFileHandler(
|
||||||
handler.setFormatter(log_formatter)
|
log_file, maxBytes=(10248576 * 5),
|
||||||
print(handler)
|
backupCount=4,
|
||||||
|
)
|
||||||
flask_log.addHandler(fh)
|
fh.setFormatter(log_formatter)
|
||||||
|
flask_log.addHandler(fh)
|
||||||
if not quiet:
|
|
||||||
sh = logging.StreamHandler(sys.stdout)
|
|
||||||
sh.setFormatter(log_formatter)
|
|
||||||
flask_log.addHandler(sh)
|
|
||||||
|
|
||||||
|
|
||||||
def init_flask(config, loglevel, quiet):
|
def init_flask(config, loglevel, quiet):
|
||||||
|
|||||||
+11
-15
@@ -18,35 +18,31 @@ logging_queue = queue.Queue()
|
|||||||
def setup_logging(config, loglevel, quiet):
|
def setup_logging(config, loglevel, quiet):
|
||||||
log_level = aprsd_config.LOG_LEVELS[loglevel]
|
log_level = aprsd_config.LOG_LEVELS[loglevel]
|
||||||
LOG.setLevel(log_level)
|
LOG.setLevel(log_level)
|
||||||
if config["aprsd"].get("rich_logging", False):
|
|
||||||
log_format = "%(message)s"
|
|
||||||
else:
|
|
||||||
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
|
|
||||||
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
|
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
|
||||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
|
||||||
log_file = config["aprsd"].get("logfile", None)
|
|
||||||
|
|
||||||
rich_logging = False
|
rich_logging = False
|
||||||
if config["aprsd"].get("rich_logging", False):
|
if config["aprsd"].get("rich_logging", False) and not quiet:
|
||||||
|
log_format = "%(message)s"
|
||||||
|
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||||
rh = aprsd_logging.APRSDRichHandler(
|
rh = aprsd_logging.APRSDRichHandler(
|
||||||
show_thread=True, thread_width=15,
|
show_thread=True, thread_width=20,
|
||||||
rich_tracebacks=True, omit_repeated_times=False,
|
rich_tracebacks=True, omit_repeated_times=False,
|
||||||
)
|
)
|
||||||
rh.setFormatter(log_formatter)
|
rh.setFormatter(log_formatter)
|
||||||
LOG.addHandler(rh)
|
LOG.addHandler(rh)
|
||||||
rich_logging = True
|
rich_logging = True
|
||||||
|
|
||||||
if log_file:
|
log_file = config["aprsd"].get("logfile", None)
|
||||||
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
|
||||||
else:
|
|
||||||
fh = NullHandler()
|
|
||||||
|
|
||||||
fh.setFormatter(log_formatter)
|
if log_file:
|
||||||
LOG.addHandler(fh)
|
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
|
||||||
|
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||||
|
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
||||||
|
fh.setFormatter(log_formatter)
|
||||||
|
LOG.addHandler(fh)
|
||||||
|
|
||||||
imap_logger = None
|
imap_logger = None
|
||||||
if config.get("aprsd.email.enabled", default=False) and config.get("aprsd.email.imap.debug", default=False):
|
if config.get("aprsd.email.enabled", default=False) and config.get("aprsd.email.imap.debug", default=False):
|
||||||
|
|
||||||
imap_logger = logging.getLogger("imapclient.imaplib")
|
imap_logger = logging.getLogger("imapclient.imaplib")
|
||||||
imap_logger.setLevel(log_level)
|
imap_logger.setLevel(log_level)
|
||||||
imap_logger.addHandler(fh)
|
imap_logger.addHandler(fh)
|
||||||
|
|||||||
+18
-7
@@ -190,6 +190,7 @@ class Message(metaclass=abc.ABCMeta):
|
|||||||
fromcall,
|
fromcall,
|
||||||
tocall,
|
tocall,
|
||||||
msg_id=None,
|
msg_id=None,
|
||||||
|
allow_delay=True,
|
||||||
):
|
):
|
||||||
self.fromcall = fromcall
|
self.fromcall = fromcall
|
||||||
self.tocall = tocall
|
self.tocall = tocall
|
||||||
@@ -199,6 +200,10 @@ class Message(metaclass=abc.ABCMeta):
|
|||||||
msg_id = c.value
|
msg_id = c.value
|
||||||
self.id = msg_id
|
self.id = msg_id
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
self.allow_delay = allow_delay
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def send(self):
|
def send(self):
|
||||||
"""Child class must declare."""
|
"""Child class must declare."""
|
||||||
@@ -214,8 +219,8 @@ class RawMessage(Message):
|
|||||||
|
|
||||||
message = None
|
message = None
|
||||||
|
|
||||||
def __init__(self, message):
|
def __init__(self, message, allow_delay=True):
|
||||||
super().__init__(None, None, msg_id=None)
|
super().__init__(fromcall=None, tocall=None, msg_id=None, allow_delay=allow_delay)
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
def dict(self):
|
def dict(self):
|
||||||
@@ -269,11 +274,11 @@ class TextMessage(Message):
|
|||||||
msg_id=None,
|
msg_id=None,
|
||||||
allow_delay=True,
|
allow_delay=True,
|
||||||
):
|
):
|
||||||
super().__init__(fromcall, tocall, msg_id)
|
super().__init__(
|
||||||
|
fromcall=fromcall, tocall=tocall,
|
||||||
|
msg_id=msg_id, allow_delay=allow_delay,
|
||||||
|
)
|
||||||
self.message = message
|
self.message = message
|
||||||
# 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.
|
|
||||||
self.allow_delay = allow_delay
|
|
||||||
|
|
||||||
def dict(self):
|
def dict(self):
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
@@ -369,6 +374,8 @@ class SendMessageThread(threads.APRSDThread):
|
|||||||
# we reached the send limit, don't send again
|
# we reached the send limit, don't send again
|
||||||
# TODO(hemna) - Need to put this in a delayed queue?
|
# TODO(hemna) - Need to put this in a delayed queue?
|
||||||
LOG.info("Message Send Complete. Max attempts reached.")
|
LOG.info("Message Send Complete. Max attempts reached.")
|
||||||
|
if not msg.allow_delay:
|
||||||
|
tracker.remove(msg.id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Message is still outstanding and needs to be acked.
|
# Message is still outstanding and needs to be acked.
|
||||||
@@ -527,6 +534,7 @@ def log_packet(packet):
|
|||||||
def log_message(
|
def log_message(
|
||||||
header, raw, message, tocall=None, fromcall=None, msg_num=None,
|
header, raw, message, tocall=None, fromcall=None, msg_num=None,
|
||||||
retry_number=None, ack=None, packet_type=None, uuid=None,
|
retry_number=None, ack=None, packet_type=None, uuid=None,
|
||||||
|
console=None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -565,4 +573,7 @@ def log_message(
|
|||||||
log_list.append(f" UUID : {uuid}")
|
log_list.append(f" UUID : {uuid}")
|
||||||
log_list.append(f"{header} _______________ Complete")
|
log_list.append(f"{header} _______________ Complete")
|
||||||
|
|
||||||
LOG.info("\n".join(log_list))
|
if console:
|
||||||
|
console.log("\n".join(log_list))
|
||||||
|
else:
|
||||||
|
LOG.info("\n".join(log_list))
|
||||||
|
|||||||
+1
-1
@@ -86,7 +86,7 @@ class WatchList(objectstore.ObjectStoreMixin):
|
|||||||
if config:
|
if config:
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
ring_size = config["aprsd"]["watch_list"]["packet_keep_count"]
|
ring_size = config["aprsd"]["watch_list"].get("packet_keep_count", 10)
|
||||||
|
|
||||||
for callsign in config["aprsd"]["watch_list"].get("callsigns", []):
|
for callsign in config["aprsd"]["watch_list"].get("callsigns", []):
|
||||||
call = callsign.replace("*", "")
|
call = callsign.replace("*", "")
|
||||||
|
|||||||
@@ -177,7 +177,6 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
|||||||
)
|
)
|
||||||
if result:
|
if result:
|
||||||
self.tx_inc()
|
self.tx_inc()
|
||||||
wl.update_seen(packet)
|
|
||||||
else:
|
else:
|
||||||
LOG.warning(f"{self.__class__} plugin is not enabled")
|
LOG.warning(f"{self.__class__} plugin is not enabled")
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aprsd import messaging, packets, plugin
|
from aprsd import messaging, packets, plugin, trace
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@@ -17,6 +17,7 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
|
|||||||
|
|
||||||
short_description = "Notify me when a CALLSIGN is recently seen on APRS-IS"
|
short_description = "Notify me when a CALLSIGN is recently seen on APRS-IS"
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def process(self, packet):
|
def process(self, packet):
|
||||||
LOG.info("NotifySeenPlugin")
|
LOG.info("NotifySeenPlugin")
|
||||||
|
|
||||||
@@ -46,10 +47,11 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
|
|||||||
)
|
)
|
||||||
return msg
|
return msg
|
||||||
else:
|
else:
|
||||||
|
LOG.debug("fromcall and notify_callsign are the same, not notifying")
|
||||||
return messaging.NULL_MESSAGE
|
return messaging.NULL_MESSAGE
|
||||||
else:
|
else:
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
"Not old enough to notify callsign '{}' : {} < {}".format(
|
"Not old enough to notify on callsign '{}' : {} < {}".format(
|
||||||
fromcall,
|
fromcall,
|
||||||
age,
|
age,
|
||||||
wl.max_delta(),
|
wl.max_delta(),
|
||||||
|
|||||||
+3
-1
@@ -202,7 +202,7 @@ class APRSDProcessPacketThread(APRSDThread):
|
|||||||
self.packet = packet
|
self.packet = packet
|
||||||
self.config = config
|
self.config = config
|
||||||
name = self.packet["raw"][:10]
|
name = self.packet["raw"][:10]
|
||||||
super().__init__(f"RX_PACKET-{name}")
|
super().__init__(f"RXPKT-{name}")
|
||||||
|
|
||||||
def process_ack_packet(self, packet):
|
def process_ack_packet(self, packet):
|
||||||
ack_num = packet.get("msgNo")
|
ack_num = packet.get("msgNo")
|
||||||
@@ -261,6 +261,8 @@ class APRSDProcessPacketThread(APRSDThread):
|
|||||||
pm = plugin.PluginManager()
|
pm = plugin.PluginManager()
|
||||||
try:
|
try:
|
||||||
results = pm.run(packet)
|
results = pm.run(packet)
|
||||||
|
wl = packets.WatchList()
|
||||||
|
wl.update_seen(packet)
|
||||||
replied = False
|
replied = False
|
||||||
for reply in results:
|
for reply in results:
|
||||||
if isinstance(reply, list):
|
if isinstance(reply, list):
|
||||||
|
|||||||
@@ -22,3 +22,5 @@ flask-socketio
|
|||||||
eventlet
|
eventlet
|
||||||
tabulate
|
tabulate
|
||||||
rich
|
rich
|
||||||
|
# For the list-plugins pypi.org search scraping
|
||||||
|
beautifulsoup4
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ aprslib==0.7.0
|
|||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
backoff==1.11.1
|
backoff==1.11.1
|
||||||
# via opencage
|
# via opencage
|
||||||
|
beautifulsoup4==4.10.0
|
||||||
|
# via -r requirements.in
|
||||||
bidict==0.21.2
|
bidict==0.21.2
|
||||||
# via python-socketio
|
# via python-socketio
|
||||||
certifi==2021.5.30
|
certifi==2021.5.30
|
||||||
@@ -112,6 +114,8 @@ six==1.16.0
|
|||||||
# imapclient
|
# imapclient
|
||||||
# pyopenssl
|
# pyopenssl
|
||||||
# signalslot
|
# signalslot
|
||||||
|
soupsieve==2.3.1
|
||||||
|
# via beautifulsoup4
|
||||||
tabulate==0.8.9
|
tabulate==0.8.9
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
thesmuggler==1.0.1
|
thesmuggler==1.0.1
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import typing as t
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from aprsd import config as aprsd_config
|
||||||
|
from aprsd.aprsd import cli
|
||||||
|
from aprsd.cmds import dev # noqa
|
||||||
|
|
||||||
|
|
||||||
|
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||||
|
|
||||||
|
|
||||||
|
class TestDevTestPluginCommand(unittest.TestCase):
|
||||||
|
|
||||||
|
def _build_config(self, login=None, password=None):
|
||||||
|
config = {"aprs": {}}
|
||||||
|
if login:
|
||||||
|
config["aprs"]["login"] = login
|
||||||
|
|
||||||
|
if password:
|
||||||
|
config["aprs"]["password"] = password
|
||||||
|
|
||||||
|
return aprsd_config.Config(config)
|
||||||
|
|
||||||
|
@mock.patch("aprsd.config.parse_config")
|
||||||
|
@mock.patch("aprsd.log.setup_logging")
|
||||||
|
def test_no_login(self, mock_logging, mock_parse_config):
|
||||||
|
"""Make sure we get an error if there is no login and config."""
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
mock_parse_config.return_value = self._build_config()
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
cli, ["dev", "test-plugin", "bogus command"],
|
||||||
|
catch_exceptions=False,
|
||||||
|
)
|
||||||
|
# rich.print(f"EXIT CODE {result.exit_code}")
|
||||||
|
# rich.print(f"Exception {result.exception}")
|
||||||
|
# rich.print(f"OUTPUT {result.output}")
|
||||||
|
assert result.exit_code == -1
|
||||||
|
assert "Must set --aprs_login or APRS_LOGIN" in result.output
|
||||||
|
|
||||||
|
@mock.patch("aprsd.config.parse_config")
|
||||||
|
@mock.patch("aprsd.log.setup_logging")
|
||||||
|
def test_no_plugin_arg(self, mock_logging, mock_parse_config):
|
||||||
|
"""Make sure we get an error if there is no login and config."""
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
mock_parse_config.return_value = self._build_config(login="something")
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
cli, ["dev", "test-plugin", "bogus command"],
|
||||||
|
catch_exceptions=False,
|
||||||
|
)
|
||||||
|
assert result.exit_code == 2
|
||||||
|
assert "Failed to provide -p option to test a plugin" in result.output
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import typing as t
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from aprsd import config as aprsd_config
|
||||||
|
from aprsd.aprsd import cli
|
||||||
|
from aprsd.cmds import send_message # noqa
|
||||||
|
|
||||||
|
|
||||||
|
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||||
|
|
||||||
|
|
||||||
|
class TestSendMessageCommand(unittest.TestCase):
|
||||||
|
|
||||||
|
def _build_config(self, login=None, password=None):
|
||||||
|
config = {"aprs": {}}
|
||||||
|
if login:
|
||||||
|
config["aprs"]["login"] = login
|
||||||
|
|
||||||
|
if password:
|
||||||
|
config["aprs"]["password"] = password
|
||||||
|
|
||||||
|
return aprsd_config.Config(config)
|
||||||
|
|
||||||
|
@mock.patch("aprsd.config.parse_config")
|
||||||
|
@mock.patch("aprsd.log.setup_logging")
|
||||||
|
def test_no_login(self, mock_logging, mock_parse_config):
|
||||||
|
"""Make sure we get an error if there is no login and config."""
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
mock_parse_config.return_value = self._build_config()
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
cli, ["send-message", "WB4BOR", "wx"],
|
||||||
|
catch_exceptions=False,
|
||||||
|
)
|
||||||
|
# rich.print(f"EXIT CODE {result.exit_code}")
|
||||||
|
# rich.print(f"Exception {result.exception}")
|
||||||
|
# rich.print(f"OUTPUT {result.output}")
|
||||||
|
assert result.exit_code == -1
|
||||||
|
assert "Must set --aprs_login or APRS_LOGIN" in result.output
|
||||||
|
|
||||||
|
@mock.patch("aprsd.config.parse_config")
|
||||||
|
@mock.patch("aprsd.log.setup_logging")
|
||||||
|
def test_no_password(self, mock_logging, mock_parse_config):
|
||||||
|
"""Make sure we get an error if there is no password and config."""
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
mock_parse_config.return_value = self._build_config(login="something")
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
cli, ["send-message", "WB4BOR", "wx"],
|
||||||
|
catch_exceptions=False,
|
||||||
|
)
|
||||||
|
assert result.exit_code == -1
|
||||||
|
assert "Must set --aprs-password or APRS_PASSWORD" in result.output
|
||||||
|
|
||||||
|
@mock.patch("aprsd.config.parse_config")
|
||||||
|
@mock.patch("aprsd.log.setup_logging")
|
||||||
|
def test_no_tocallsign(self, mock_logging, mock_parse_config):
|
||||||
|
"""Make sure we get an error if there is no tocallsign."""
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
mock_parse_config.return_value = self._build_config(
|
||||||
|
login="something",
|
||||||
|
password="another",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
cli, ["send-message"],
|
||||||
|
catch_exceptions=False,
|
||||||
|
)
|
||||||
|
assert result.exit_code == 2
|
||||||
|
assert "Error: Missing argument 'TOCALLSIGN'" in result.output
|
||||||
|
|
||||||
|
@mock.patch("aprsd.config.parse_config")
|
||||||
|
@mock.patch("aprsd.log.setup_logging")
|
||||||
|
def test_no_command(self, mock_logging, mock_parse_config):
|
||||||
|
"""Make sure we get an error if there is no command."""
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
mock_parse_config.return_value = self._build_config(
|
||||||
|
login="something",
|
||||||
|
password="another",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
cli, ["send-message", "WB4BOR"],
|
||||||
|
catch_exceptions=False,
|
||||||
|
)
|
||||||
|
assert result.exit_code == 2
|
||||||
|
assert "Error: Missing argument 'COMMAND...'" in result.output
|
||||||
Reference in New Issue
Block a user