mirror of
https://github.com/craigerl/aprsd.git
synced 2026-08-17 17:13:49 -04:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d898ea20f | |||
| 8161719697 | |||
| b2609548f2 | |||
| e68d3f92e1 | |||
| 18b269a39d | |||
| c39fddef67 | |||
| 6d2160c7b7 | |||
| d3ee1b78bc | |||
| 0992ab8176 | |||
| 6a61765eba | |||
| 3261710bf8 | |||
| a566990a2f | |||
| 923e1a7c3d | |||
| fa51f8fdf2 | |||
| 08c73a17d1 | |||
| 79d2139fe1 | |||
| b5fb248b46 |
@@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [2.7, 3.6, 3.7, 3.8, 3.9]
|
||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
|
||||
+3
-1
@@ -18,6 +18,7 @@ develop-eggs
|
||||
.installed.cfg
|
||||
lib
|
||||
lib64
|
||||
venv
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
@@ -46,10 +47,11 @@ doc/build
|
||||
|
||||
# pbr generates these
|
||||
AUTHORS
|
||||
ChangeLog
|
||||
# ChangeLog
|
||||
|
||||
# Editors
|
||||
*~
|
||||
.*.swp
|
||||
.*sw?
|
||||
.ropeproject
|
||||
.idea
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
CHANGES
|
||||
=======
|
||||
|
||||
v1.1.0
|
||||
------
|
||||
|
||||
* Refactored the main process\_packet method
|
||||
* Update README with version 1.1.0 related info
|
||||
* Added fix for an unknown packet type
|
||||
* Ensure fortune is installed
|
||||
* Updated docker-compose
|
||||
* Added Changelog
|
||||
* Fixed issue when RX ack
|
||||
* Updated the aprsd-slack-plugin required version
|
||||
* Updated README.rst
|
||||
* Fixed send-message with email command and others
|
||||
* Update .gitignore
|
||||
* Big patch
|
||||
* Major refactor
|
||||
* Updated the Dockerfile to use alpine
|
||||
|
||||
v1.0.1
|
||||
------
|
||||
|
||||
* Fix unknown characterset emails
|
||||
* Updated loggin timestamp to include []
|
||||
* Updated README with a TOC
|
||||
* Updates for building containers
|
||||
* Don't use the dirname for the plugin path search
|
||||
* Reworked Plugin loading
|
||||
* Updated README with development information
|
||||
* Fixed an issue with weather plugin
|
||||
|
||||
v1.0.0
|
||||
------
|
||||
|
||||
* Rewrote the README.md to README.rst
|
||||
* Fixed the usage string after plugins introduced
|
||||
* Created plugin.py for Command Plugins
|
||||
* Refactor networking and commands
|
||||
* get rid of some debug statements
|
||||
* yet another unicode problem, in resend\_email fixed
|
||||
* reset default email check delay to 60, fix a few comments
|
||||
* Update tox environment to fix formatting python errors
|
||||
* fixed fortune. yet another unicode issue, tested in py3 and py2
|
||||
* lose some logging statements
|
||||
* completely off urllib now, tested locate/weather in py2 and py3
|
||||
* add urllib import back until i replace all calls with requests
|
||||
* cleaned up weather code after switch to requests ... from urllib. works on py2 and py3
|
||||
* switch from urlib to requests for weather, tested in py3 and py2. still need to update locate, and all other http calls
|
||||
* imap tags are unicode in py3. .decode tags
|
||||
* Update INSTALL.txt
|
||||
* Initial conversion to click
|
||||
* Reconnect on socket timeout
|
||||
* clean up code around closed\_socket and reconnect
|
||||
* Update INSTALL.txt
|
||||
* Fixed all pep8 errors and some py3 errors
|
||||
* fix check\_email\_thread to do proper threading, take delay as arg
|
||||
* found another .decode that didn't include errors='ignore'
|
||||
* some failed attempts at getting the first txt or html from a multipart message, currently sends the last
|
||||
* fix parse\_email unicode probs by using body.decode(errors='ignore').. again
|
||||
* fix parse\_email unicode probs by using body.decode(errors='ignore')
|
||||
* clean up code around closed\_socket and reconnect
|
||||
* socket timeout 5 minutes
|
||||
* Detect closed socket, reconnect, with a bit more grace
|
||||
* can detect closed socket and reconnect now
|
||||
* Update INSTALL.txt
|
||||
* more debugging messages trying to find rare tight loop in main
|
||||
* Update INSTALL.txt
|
||||
* main loop went into tight loop, more debug prints
|
||||
* main loop went into tight loop, added debug print before every continue
|
||||
* Update INSTALL.txt
|
||||
* Update INSTALL.txt
|
||||
* George Carlin profanity filter
|
||||
* added decaying email check timer which resets with activity
|
||||
* Fixed all pep8 errors and some py3 errors
|
||||
* Fixed all pep8 errors and some py3 errors
|
||||
* Reconnect on socket timeout
|
||||
* socket reconnect on timeout testing
|
||||
* socket timeout of 300 instead of 60
|
||||
* Reconnect on socket timeout
|
||||
* socket reconnect on timeout testing
|
||||
* Fixed all pep8 errors and some py3 errors
|
||||
* fix check\_email\_thread to do proper threading, take delay as arg
|
||||
* INSTALL.txt for the average person
|
||||
* fix bugs after beautification and yaml config additions. Convert to sockets. case insensitive commands
|
||||
* fix INBOX
|
||||
* Update README.md
|
||||
* Added tox support
|
||||
* Fixed SMTP settings
|
||||
* Created fake\_aprs.py
|
||||
* select inbox if gmail server
|
||||
* removed ASS
|
||||
* Added a try block around imap login
|
||||
* Added port and fixed telnet user
|
||||
* Require ~/.aprsd/config.yml
|
||||
* updated README for install and usage instructions
|
||||
* added test to ensure shortcuts in config.yml
|
||||
* added exit if missing config file
|
||||
* Added reading of a config file
|
||||
* update readme
|
||||
* update readme
|
||||
* sanitize readme
|
||||
* readme again again
|
||||
* readme again again
|
||||
* readme again
|
||||
* readme
|
||||
* readme update
|
||||
* First stab at migrating this to a pytpi repo
|
||||
* First stab at migrating this to a pytpi repo
|
||||
* Added password, callsign and host
|
||||
* Added argparse for cli options
|
||||
* comments
|
||||
* Cleaned up trailing whitespace
|
||||
* add tweaked fuzzyclock
|
||||
* make tn a global
|
||||
* Added standard python main()
|
||||
* tweaks to readme
|
||||
* drop virtenv on first line
|
||||
* sanitize readme a bit more
|
||||
* sanitize readme a bit more
|
||||
* sanitize readme
|
||||
* added weather and location 3
|
||||
* added weather and location 2
|
||||
* added weather and location
|
||||
* mapme
|
||||
* de-localize
|
||||
* Update README.md
|
||||
* Update README.md
|
||||
* Update README.md
|
||||
* Update README.md
|
||||
* de-localize
|
||||
* Update README.md
|
||||
* Update README.md
|
||||
* Update aprsd.py
|
||||
* Add files via upload
|
||||
* Update README.md
|
||||
* Update aprsd.py
|
||||
* Update README.md
|
||||
* Update README.md
|
||||
* Update README.md
|
||||
* Update README.md
|
||||
* Update README.md
|
||||
* Update README.md
|
||||
* Update README.md
|
||||
* Update README.md
|
||||
* Update README.md
|
||||
* Update README.md
|
||||
* Update README.md
|
||||
* Update README.md
|
||||
* Add files via upload
|
||||
* Initial commit
|
||||
+3
-7
@@ -1,4 +1,4 @@
|
||||
FROM ubuntu:20.04 as aprsd
|
||||
FROM alpine:latest as aprsd
|
||||
|
||||
ENV VERSION=1.0.0
|
||||
ENV APRS_USER=aprs
|
||||
@@ -6,11 +6,7 @@ ENV HOME=/home/aprs
|
||||
ENV VIRTUAL_ENV=$HOME/.venv3
|
||||
|
||||
ENV INSTALL=$HOME/install
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get -y update
|
||||
RUN apt-get install -y wget gnupg git-core
|
||||
RUN apt-get install -y apt-utils pkg-config sudo vim
|
||||
RUN apt-get install -y python3 python3-pip python3-virtualenv python3-venv
|
||||
RUN apk add --update git wget py3-pip py3-virtualenv bash fortune
|
||||
|
||||
# Setup Timezone
|
||||
ENV TZ=US/Eastern
|
||||
@@ -20,7 +16,7 @@ RUN dpkg-reconfigure --frontend noninteractive tzdata
|
||||
|
||||
|
||||
RUN addgroup --gid 1000 $APRS_USER
|
||||
RUN useradd -m -u 1000 -g 1000 -p $APRS_USER $APRS_USER
|
||||
RUN adduser -h $HOME -D -u 1001 -G $APRS_USER $APRS_USER
|
||||
|
||||
ENV LC_ALL=C.UTF-8
|
||||
ENV LANG=C.UTF-8
|
||||
|
||||
+8
-13
@@ -1,4 +1,4 @@
|
||||
FROM ubuntu:20.04 as aprsd
|
||||
FROM alpine:latest as aprsd
|
||||
|
||||
# Dockerfile for building a container during aprsd development.
|
||||
|
||||
@@ -6,25 +6,20 @@ ENV VERSION=1.0.0
|
||||
ENV APRS_USER=aprs
|
||||
ENV HOME=/home/aprs
|
||||
ENV APRSD=http://github.com/craigerl/aprsd.git
|
||||
ENV APRSD_BRANCH="master"
|
||||
ENV APRSD_BRANCH="v1.1.0"
|
||||
ENV VIRTUAL_ENV=$HOME/.venv3
|
||||
|
||||
ENV INSTALL=$HOME/install
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get -y update
|
||||
RUN apt-get install -y wget gnupg git-core
|
||||
RUN apt-get install -y apt-utils pkg-config sudo vim
|
||||
RUN apt-get install -y python3 python3-pip python3-virtualenv python3-venv
|
||||
RUN apk add --update git vim wget py3-pip py3-virtualenv bash fortune
|
||||
|
||||
# Setup Timezone
|
||||
ENV TZ=US/Eastern
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
RUN apt-get install -y tzdata
|
||||
RUN dpkg-reconfigure --frontend noninteractive tzdata
|
||||
#RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
#RUN apt-get install -y tzdata
|
||||
#RUN dpkg-reconfigure --frontend noninteractive tzdata
|
||||
|
||||
|
||||
RUN addgroup --gid 1000 $APRS_USER
|
||||
RUN useradd -m -u 1000 -g 1000 -p $APRS_USER $APRS_USER
|
||||
RUN addgroup --gid 1001 $APRS_USER
|
||||
RUN adduser -h $HOME -D -u 1001 -G $APRS_USER $APRS_USER
|
||||
|
||||
ENV LC_ALL=C.UTF-8
|
||||
ENV LANG=C.UTF-8
|
||||
|
||||
+184
-4
@@ -14,12 +14,51 @@ APRSD
|
||||
.. contents:: :local:
|
||||
|
||||
Listen on amateur radio aprs-is network for messages and respond to them.
|
||||
You must have an amateur radio callsign to use this software. Put your
|
||||
callsign in the "USER" variable and update your aprs-is password in "PASS".
|
||||
You must also have an imap email account available for polling.
|
||||
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.
|
||||
|
||||
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. 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
|
||||
* 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:
|
||||
--------------------------------------
|
||||
======================================
|
||||
|
||||
::
|
||||
|
||||
@@ -56,9 +95,150 @@ 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
|
||||
|
||||
|
||||
Commands
|
||||
--------
|
||||
|
||||
sample-config
|
||||
=============
|
||||
This command outputs a sample config yml formatted block that you can edit
|
||||
and use to pass in to aprsd with -c.
|
||||
|
||||
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 output:
|
||||
---------------
|
||||
|
||||
|
||||
SEND EMAIL (radio to smtp server)
|
||||
=================================
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
import aprslib
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""Singleton client class that constructs the aprslib connection."""
|
||||
|
||||
_instance = None
|
||||
aprs_client = None
|
||||
config = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""This magic turns this into a singleton."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super(Client, cls).__new__(cls)
|
||||
# Put any initialization here.
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, config=None):
|
||||
"""Initialize the object instance."""
|
||||
if config:
|
||||
self.config = config
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
if not self.aprs_client:
|
||||
self.aprs_client = self.setup_connection()
|
||||
return self.aprs_client
|
||||
|
||||
def reset(self):
|
||||
"""Call this to force a rebuild/reconnect."""
|
||||
del self.aprs_client
|
||||
|
||||
def setup_connection(self):
|
||||
user = self.config["aprs"]["login"]
|
||||
password = self.config["aprs"]["password"]
|
||||
host = self.config["aprs"].get("host", "rotate.aprs.net")
|
||||
port = self.config["aprs"].get("port", 14580)
|
||||
connected = False
|
||||
backoff = 1
|
||||
while not connected:
|
||||
try:
|
||||
LOG.info("Creating aprslib client")
|
||||
aprs_client = aprslib.IS(user, passwd=password, host=host, port=port)
|
||||
# Force the logging to be the same
|
||||
aprs_client.logger = LOG
|
||||
aprs_client.connect()
|
||||
connected = True
|
||||
backoff = 1
|
||||
except Exception as e:
|
||||
LOG.error("Unable to connect to APRS-IS server. '{}' ".format(e))
|
||||
time.sleep(backoff)
|
||||
backoff = backoff * 2
|
||||
continue
|
||||
LOG.debug("Logging in to APRS-IS with user '%s'" % user)
|
||||
return aprs_client
|
||||
|
||||
|
||||
def get_client():
|
||||
cl = Client()
|
||||
return cl.client
|
||||
+399
@@ -0,0 +1,399 @@
|
||||
import datetime
|
||||
import email
|
||||
import imaplib
|
||||
import logging
|
||||
import re
|
||||
import smtplib
|
||||
import threading
|
||||
import time
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
import imapclient
|
||||
import six
|
||||
from validate_email import validate_email
|
||||
|
||||
from aprsd import messaging
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
# This gets forced set from main.py prior to being used internally
|
||||
CONFIG = None
|
||||
|
||||
|
||||
def start_thread():
|
||||
checkemailthread = threading.Thread(
|
||||
target=check_email_thread, name="check_email", args=()
|
||||
) # args must be tuple
|
||||
checkemailthread.start()
|
||||
|
||||
|
||||
def _imap_connect():
|
||||
imap_port = CONFIG["imap"].get("port", 143)
|
||||
use_ssl = CONFIG["imap"].get("use_ssl", False)
|
||||
host = CONFIG["imap"]["host"]
|
||||
msg = "{}{}:{}".format("TLS " if use_ssl else "", host, imap_port)
|
||||
# LOG.debug("Connect to IMAP host {} with user '{}'".
|
||||
# format(msg, CONFIG['imap']['login']))
|
||||
|
||||
try:
|
||||
server = imapclient.IMAPClient(
|
||||
CONFIG["imap"]["host"], port=imap_port, use_uid=True, ssl=use_ssl
|
||||
)
|
||||
except Exception:
|
||||
LOG.error("Failed to connect IMAP server")
|
||||
return
|
||||
|
||||
try:
|
||||
server.login(CONFIG["imap"]["login"], CONFIG["imap"]["password"])
|
||||
except (imaplib.IMAP4.error, Exception) as e:
|
||||
msg = getattr(e, "message", repr(e))
|
||||
LOG.error("Failed to login {}".format(msg))
|
||||
return
|
||||
|
||||
server.select_folder("INBOX")
|
||||
return server
|
||||
|
||||
|
||||
def _smtp_connect():
|
||||
host = CONFIG["smtp"]["host"]
|
||||
smtp_port = CONFIG["smtp"]["port"]
|
||||
use_ssl = CONFIG["smtp"].get("use_ssl", False)
|
||||
msg = "{}{}:{}".format("SSL " if use_ssl else "", host, smtp_port)
|
||||
LOG.debug(
|
||||
"Connect to SMTP host {} with user '{}'".format(msg, CONFIG["imap"]["login"])
|
||||
)
|
||||
|
||||
try:
|
||||
if use_ssl:
|
||||
server = smtplib.SMTP_SSL(host=host, port=smtp_port)
|
||||
else:
|
||||
server = smtplib.SMTP(host=host, port=smtp_port)
|
||||
except Exception:
|
||||
LOG.error("Couldn't connect to SMTP Server")
|
||||
return
|
||||
|
||||
LOG.debug("Connected to smtp host {}".format(msg))
|
||||
|
||||
try:
|
||||
server.login(CONFIG["smtp"]["login"], CONFIG["smtp"]["password"])
|
||||
except Exception:
|
||||
LOG.error("Couldn't connect to SMTP Server")
|
||||
return
|
||||
|
||||
LOG.debug("Logged into SMTP server {}".format(msg))
|
||||
return server
|
||||
|
||||
|
||||
def validate_shortcuts(config):
|
||||
shortcuts = config.get("shortcuts", None)
|
||||
if not shortcuts:
|
||||
return
|
||||
|
||||
LOG.info(
|
||||
"Validating {} Email shortcuts. This can take up to 10 seconds"
|
||||
" per shortcut".format(len(shortcuts))
|
||||
)
|
||||
delete_keys = []
|
||||
for key in shortcuts:
|
||||
is_valid = validate_email(
|
||||
email_address=shortcuts[key],
|
||||
check_regex=True,
|
||||
check_mx=True,
|
||||
from_address=config["smtp"]["login"],
|
||||
helo_host=config["smtp"]["host"],
|
||||
smtp_timeout=10,
|
||||
dns_timeout=10,
|
||||
use_blacklist=False,
|
||||
debug=False,
|
||||
)
|
||||
if not is_valid:
|
||||
LOG.error(
|
||||
"'{}' is an invalid email address. Removing shortcut".format(
|
||||
shortcuts[key]
|
||||
)
|
||||
)
|
||||
delete_keys.append(key)
|
||||
|
||||
for key in delete_keys:
|
||||
del config["shortcuts"][key]
|
||||
|
||||
LOG.info("Available shortcuts: {}".format(config["shortcuts"]))
|
||||
|
||||
|
||||
def validate_email_config(config, disable_validation=False):
|
||||
"""function to simply ensure we can connect to email services.
|
||||
|
||||
This helps with failing early during startup.
|
||||
"""
|
||||
LOG.info("Checking IMAP configuration")
|
||||
imap_server = _imap_connect()
|
||||
LOG.info("Checking SMTP configuration")
|
||||
smtp_server = _smtp_connect()
|
||||
|
||||
# Now validate and flag any shortcuts as invalid
|
||||
if not disable_validation:
|
||||
validate_shortcuts(config)
|
||||
else:
|
||||
LOG.info("Shortcuts email validation is Disabled!!, you were warned.")
|
||||
|
||||
if imap_server and smtp_server:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def parse_email(msgid, data, server):
|
||||
envelope = data[b"ENVELOPE"]
|
||||
# email address match
|
||||
# use raw string to avoid invalid escape secquence errors r"string here"
|
||||
f = re.search(r"([\.\w_-]+@[\.\w_-]+)", str(envelope.from_[0]))
|
||||
if f is not None:
|
||||
from_addr = f.group(1)
|
||||
else:
|
||||
from_addr = "noaddr"
|
||||
LOG.debug("Got a message from '{}'".format(from_addr))
|
||||
m = server.fetch([msgid], ["RFC822"])
|
||||
msg = email.message_from_string(m[msgid][b"RFC822"].decode(errors="ignore"))
|
||||
if msg.is_multipart():
|
||||
text = ""
|
||||
html = None
|
||||
# default in case body somehow isn't set below - happened once
|
||||
body = b"* unreadable msg received"
|
||||
# this uses the last text or html part in the email, phone companies often put content in an attachment
|
||||
for part in msg.get_payload():
|
||||
if part.get_content_charset() is None:
|
||||
# or BREAK when we hit a text or html?
|
||||
# We cannot know the character set,
|
||||
# so return decoded "something"
|
||||
LOG.debug("Email got unknown content type")
|
||||
text = part.get_payload(decode=True)
|
||||
continue
|
||||
|
||||
charset = part.get_content_charset()
|
||||
|
||||
if part.get_content_type() == "text/plain":
|
||||
LOG.debug("Email got text/plain")
|
||||
text = six.text_type(
|
||||
part.get_payload(decode=True), str(charset), "ignore"
|
||||
).encode("utf8", "replace")
|
||||
|
||||
if part.get_content_type() == "text/html":
|
||||
LOG.debug("Email got text/html")
|
||||
html = six.text_type(
|
||||
part.get_payload(decode=True), str(charset), "ignore"
|
||||
).encode("utf8", "replace")
|
||||
|
||||
if text is not None:
|
||||
# strip removes white space fore and aft of string
|
||||
body = text.strip()
|
||||
else:
|
||||
body = html.strip()
|
||||
else: # message is not multipart
|
||||
# email.uscc.net sends no charset, blows up unicode function below
|
||||
LOG.debug("Email is not multipart")
|
||||
if msg.get_content_charset() is None:
|
||||
text = six.text_type(
|
||||
msg.get_payload(decode=True), "US-ASCII", "ignore"
|
||||
).encode("utf8", "replace")
|
||||
else:
|
||||
text = six.text_type(
|
||||
msg.get_payload(decode=True), msg.get_content_charset(), "ignore"
|
||||
).encode("utf8", "replace")
|
||||
body = text.strip()
|
||||
|
||||
# FIXED: UnicodeDecodeError: 'ascii' codec can't decode byte 0xf0 in position 6: ordinal not in range(128)
|
||||
# decode with errors='ignore'. be sure to encode it before we return it below, also with errors='ignore'
|
||||
try:
|
||||
body = body.decode(errors="ignore")
|
||||
except Exception as e:
|
||||
LOG.error("Unicode decode failure: " + str(e))
|
||||
LOG.error("Unidoce decode failed: " + str(body))
|
||||
body = "Unreadable unicode msg"
|
||||
# strip all html tags
|
||||
body = re.sub("<[^<]+?>", "", body)
|
||||
# strip CR/LF, make it one line, .rstrip fails at this
|
||||
body = body.replace("\n", " ").replace("\r", " ")
|
||||
# ascii might be out of range, so encode it, removing any error characters
|
||||
body = body.encode(errors="ignore")
|
||||
return (body, from_addr)
|
||||
|
||||
|
||||
# end parse_email
|
||||
|
||||
|
||||
def resend_email(count, fromcall):
|
||||
global check_email_delay
|
||||
date = datetime.datetime.now()
|
||||
month = date.strftime("%B")[:3] # Nov, Mar, Apr
|
||||
day = date.day
|
||||
year = date.year
|
||||
today = "%s-%s-%s" % (day, month, year)
|
||||
|
||||
shortcuts = CONFIG["shortcuts"]
|
||||
# swap key/value
|
||||
shortcuts_inverted = dict([[v, k] for k, v in shortcuts.items()])
|
||||
|
||||
try:
|
||||
server = _imap_connect()
|
||||
except Exception as e:
|
||||
LOG.exception("Failed to Connect to IMAP. Cannot resend email ", e)
|
||||
return
|
||||
|
||||
messages = server.search(["SINCE", today])
|
||||
# LOG.debug("%d messages received today" % len(messages))
|
||||
|
||||
msgexists = False
|
||||
|
||||
messages.sort(reverse=True)
|
||||
del messages[int(count) :] # only the latest "count" messages
|
||||
for message in messages:
|
||||
for msgid, data in list(server.fetch(message, ["ENVELOPE"]).items()):
|
||||
# one at a time, otherwise order is random
|
||||
(body, from_addr) = parse_email(msgid, data, server)
|
||||
# unset seen flag, will stay bold in email client
|
||||
server.remove_flags(msgid, [imapclient.SEEN])
|
||||
if from_addr in shortcuts_inverted:
|
||||
# reverse lookup of a shortcut
|
||||
from_addr = shortcuts_inverted[from_addr]
|
||||
# asterisk indicates a resend
|
||||
reply = "-" + from_addr + " * " + body.decode(errors="ignore")
|
||||
messaging.send_message(fromcall, reply)
|
||||
msgexists = True
|
||||
|
||||
if msgexists is not True:
|
||||
stm = time.localtime()
|
||||
h = stm.tm_hour
|
||||
m = stm.tm_min
|
||||
s = stm.tm_sec
|
||||
# append time as a kind of serial number to prevent FT1XDR from
|
||||
# thinking this is a duplicate message.
|
||||
# The FT1XDR pretty much ignores the aprs message number in this
|
||||
# regard. The FTM400 gets it right.
|
||||
reply = "No new msg %s:%s:%s" % (
|
||||
str(h).zfill(2),
|
||||
str(m).zfill(2),
|
||||
str(s).zfill(2),
|
||||
)
|
||||
messaging.send_message(fromcall, reply)
|
||||
|
||||
# check email more often since we're resending one now
|
||||
check_email_delay = 60
|
||||
|
||||
server.logout()
|
||||
# end resend_email()
|
||||
|
||||
|
||||
def check_email_thread():
|
||||
global check_email_delay
|
||||
|
||||
# LOG.debug("FIXME initial email delay is 10 seconds")
|
||||
check_email_delay = 60
|
||||
while True:
|
||||
# LOG.debug("Top of check_email_thread.")
|
||||
|
||||
time.sleep(check_email_delay)
|
||||
|
||||
# slowly increase delay every iteration, max out at 300 seconds
|
||||
# any send/receive/resend activity will reset this to 60 seconds
|
||||
if check_email_delay < 300:
|
||||
check_email_delay += 1
|
||||
LOG.debug("check_email_delay is " + str(check_email_delay) + " seconds")
|
||||
|
||||
shortcuts = CONFIG["shortcuts"]
|
||||
# swap key/value
|
||||
shortcuts_inverted = dict([[v, k] for k, v in shortcuts.items()])
|
||||
|
||||
date = datetime.datetime.now()
|
||||
month = date.strftime("%B")[:3] # Nov, Mar, Apr
|
||||
day = date.day
|
||||
year = date.year
|
||||
today = "%s-%s-%s" % (day, month, year)
|
||||
|
||||
server = None
|
||||
try:
|
||||
server = _imap_connect()
|
||||
except Exception as e:
|
||||
LOG.exception("Failed to get IMAP server Can't check email.", e)
|
||||
|
||||
if not server:
|
||||
continue
|
||||
|
||||
messages = server.search(["SINCE", today])
|
||||
# LOG.debug("{} messages received today".format(len(messages)))
|
||||
|
||||
for msgid, data in server.fetch(messages, ["ENVELOPE"]).items():
|
||||
envelope = data[b"ENVELOPE"]
|
||||
# LOG.debug('ID:%d "%s" (%s)' % (msgid, envelope.subject.decode(), envelope.date))
|
||||
f = re.search(
|
||||
r"'([[A-a][0-9]_-]+@[[A-a][0-9]_-\.]+)", str(envelope.from_[0])
|
||||
)
|
||||
if f is not None:
|
||||
from_addr = f.group(1)
|
||||
else:
|
||||
from_addr = "noaddr"
|
||||
|
||||
# LOG.debug("Message flags/tags: " + str(server.get_flags(msgid)[msgid]))
|
||||
# if "APRS" not in server.get_flags(msgid)[msgid]:
|
||||
# in python3, imap tags are unicode. in py2 they're strings. so .decode them to handle both
|
||||
taglist = [
|
||||
x.decode(errors="ignore") for x in server.get_flags(msgid)[msgid]
|
||||
]
|
||||
if "APRS" not in taglist:
|
||||
# if msg not flagged as sent via aprs
|
||||
server.fetch([msgid], ["RFC822"])
|
||||
(body, from_addr) = parse_email(msgid, data, server)
|
||||
# unset seen flag, will stay bold in email client
|
||||
server.remove_flags(msgid, [imapclient.SEEN])
|
||||
|
||||
if from_addr in shortcuts_inverted:
|
||||
# reverse lookup of a shortcut
|
||||
from_addr = shortcuts_inverted[from_addr]
|
||||
|
||||
reply = "-" + from_addr + " " + body.decode(errors="ignore")
|
||||
messaging.send_message(CONFIG["ham"]["callsign"], reply)
|
||||
# flag message as sent via aprs
|
||||
server.add_flags(msgid, ["APRS"])
|
||||
# unset seen flag, will stay bold in email client
|
||||
server.remove_flags(msgid, [imapclient.SEEN])
|
||||
# check email more often since we just received an email
|
||||
check_email_delay = 60
|
||||
|
||||
server.logout()
|
||||
|
||||
|
||||
# end check_email()
|
||||
|
||||
|
||||
def send_email(to_addr, content):
|
||||
global check_email_delay
|
||||
|
||||
LOG.info("Sending Email_________________")
|
||||
shortcuts = CONFIG["shortcuts"]
|
||||
if to_addr in shortcuts:
|
||||
LOG.info("To : " + to_addr)
|
||||
to_addr = shortcuts[to_addr]
|
||||
LOG.info(" (" + to_addr + ")")
|
||||
subject = CONFIG["ham"]["callsign"]
|
||||
# content = content + "\n\n(NOTE: reply with one line)"
|
||||
LOG.info("Subject : " + subject)
|
||||
LOG.info("Body : " + content)
|
||||
|
||||
# check email more often since there's activity right now
|
||||
check_email_delay = 60
|
||||
|
||||
msg = MIMEText(content)
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = CONFIG["smtp"]["login"]
|
||||
msg["To"] = to_addr
|
||||
server = _smtp_connect()
|
||||
if server:
|
||||
try:
|
||||
server.sendmail(CONFIG["smtp"]["login"], [to_addr], msg.as_string())
|
||||
except Exception as e:
|
||||
msg = getattr(e, "message", repr(e))
|
||||
LOG.error("Sendmail Error!!!! '{}'", msg)
|
||||
server.quit()
|
||||
return -1
|
||||
server.quit()
|
||||
return 0
|
||||
# end send_email
|
||||
+279
-735
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,267 @@
|
||||
import logging
|
||||
import pprint
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
|
||||
from aprsd import client
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
CONFIG = None
|
||||
|
||||
# current aprs radio message number, increments for each message we
|
||||
# send over rf {int}
|
||||
message_number = 0
|
||||
|
||||
# message_nubmer:ack combos so we stop sending a message after an
|
||||
# ack from radio {int:int}
|
||||
ack_dict = {}
|
||||
|
||||
# What to return from a plugin if we have processed the message
|
||||
# and it's ok, but don't send a usage string back
|
||||
NULL_MESSAGE = -1
|
||||
|
||||
|
||||
def send_ack_thread(tocall, ack, retry_count):
|
||||
cl = client.get_client()
|
||||
tocall = tocall.ljust(9) # pad to nine chars
|
||||
line = "{}>APRS::{}:ack{}\n".format(CONFIG["aprs"]["login"], tocall, ack)
|
||||
for i in range(retry_count, 0, -1):
|
||||
log_message(
|
||||
"Sending ack",
|
||||
line.rstrip("\n"),
|
||||
None,
|
||||
ack=ack,
|
||||
tocall=tocall,
|
||||
retry_number=i,
|
||||
)
|
||||
cl.sendall(line)
|
||||
# aprs duplicate detection is 30 secs?
|
||||
# (21 only sends first, 28 skips middle)
|
||||
time.sleep(31)
|
||||
# end_send_ack_thread
|
||||
|
||||
|
||||
def send_ack(tocall, ack):
|
||||
LOG.debug("Send ACK({}:{}) to radio.".format(tocall, ack))
|
||||
retry_count = 3
|
||||
thread = threading.Thread(
|
||||
target=send_ack_thread, name="send_ack", args=(tocall, ack, retry_count)
|
||||
)
|
||||
thread.start()
|
||||
# end send_ack()
|
||||
|
||||
|
||||
def send_ack_direct(tocall, ack):
|
||||
"""Send an ack message without a separate thread."""
|
||||
LOG.debug("Send ACK({}:{}) to radio.".format(tocall, ack))
|
||||
cl = client.get_client()
|
||||
fromcall = CONFIG["aprs"]["login"]
|
||||
line = "{}>APRS::{}:ack{}\n".format(fromcall, tocall, ack)
|
||||
log_message(
|
||||
"Sending ack",
|
||||
line.rstrip("\n"),
|
||||
None,
|
||||
ack=ack,
|
||||
tocall=tocall,
|
||||
fromcall=fromcall,
|
||||
)
|
||||
cl.sendall(line)
|
||||
|
||||
|
||||
def send_message_thread(tocall, message, this_message_number, retry_count):
|
||||
cl = client.get_client()
|
||||
line = "{}>APRS::{}:{}{{{}\n".format(
|
||||
CONFIG["aprs"]["login"],
|
||||
tocall,
|
||||
message,
|
||||
str(this_message_number),
|
||||
)
|
||||
for i in range(retry_count, 0, -1):
|
||||
LOG.debug("DEBUG: send_message_thread msg:ack combos are: ")
|
||||
LOG.debug(pprint.pformat(ack_dict))
|
||||
if ack_dict[this_message_number] != 1:
|
||||
log_message(
|
||||
"Sending Message",
|
||||
line.rstrip("\n"),
|
||||
message,
|
||||
tocall=tocall,
|
||||
retry_number=i,
|
||||
)
|
||||
# tn.write(line)
|
||||
cl.sendall(line)
|
||||
# decaying repeats, 31 to 93 second intervals
|
||||
sleeptime = (retry_count - i + 1) * 31
|
||||
time.sleep(sleeptime)
|
||||
else:
|
||||
break
|
||||
return
|
||||
# end send_message_thread
|
||||
|
||||
|
||||
def send_message(tocall, message):
|
||||
global message_number
|
||||
global ack_dict
|
||||
|
||||
retry_count = 3
|
||||
if message_number > 98: # global
|
||||
message_number = 0
|
||||
message_number += 1
|
||||
if len(ack_dict) > 90:
|
||||
# empty ack dict if it's really big, could result in key error later
|
||||
LOG.debug(
|
||||
"DEBUG: Length of ack dictionary is big at %s clearing." % len(ack_dict)
|
||||
)
|
||||
ack_dict.clear()
|
||||
LOG.debug(pprint.pformat(ack_dict))
|
||||
LOG.debug(
|
||||
"DEBUG: Cleared ack dictionary, ack_dict length is now %s." % len(ack_dict)
|
||||
)
|
||||
ack_dict[message_number] = 0 # clear ack for this message number
|
||||
tocall = tocall.ljust(9) # pad to nine chars
|
||||
|
||||
# max? ftm400 displays 64, raw msg shows 74
|
||||
# and ftm400-send is max 64. setting this to
|
||||
# 67 displays 64 on the ftm400. (+3 {01 suffix)
|
||||
# feature req: break long ones into two msgs
|
||||
message = message[:67]
|
||||
# We all miss George Carlin
|
||||
message = re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message)
|
||||
thread = threading.Thread(
|
||||
target=send_message_thread,
|
||||
name="send_message",
|
||||
args=(tocall, message, message_number, retry_count),
|
||||
)
|
||||
thread.start()
|
||||
return ()
|
||||
# end send_message()
|
||||
|
||||
|
||||
def send_message_direct(tocall, message, message_number=None):
|
||||
"""Send a message without a separate thread."""
|
||||
cl = client.get_client()
|
||||
if not message_number:
|
||||
this_message_number = 1
|
||||
else:
|
||||
this_message_number = message_number
|
||||
fromcall = CONFIG["aprs"]["login"]
|
||||
line = "{}>APRS::{}:{}{{{}\n".format(
|
||||
fromcall,
|
||||
tocall,
|
||||
message,
|
||||
str(this_message_number),
|
||||
)
|
||||
LOG.debug("DEBUG: send_message_thread msg:ack combos are: ")
|
||||
log_message(
|
||||
"Sending Message", line.rstrip("\n"), message, tocall=tocall, fromcall=fromcall
|
||||
)
|
||||
cl.sendall(line)
|
||||
|
||||
|
||||
def log_packet(packet):
|
||||
fromcall = packet.get("from", None)
|
||||
tocall = packet.get("to", None)
|
||||
|
||||
response_type = packet.get("response", None)
|
||||
msg = packet.get("message_text", None)
|
||||
msg_num = packet.get("msgNo", None)
|
||||
ack = packet.get("ack", None)
|
||||
|
||||
log_message(
|
||||
"Packet",
|
||||
packet["raw"],
|
||||
msg,
|
||||
fromcall=fromcall,
|
||||
tocall=tocall,
|
||||
ack=ack,
|
||||
packet_type=response_type,
|
||||
msg_num=msg_num,
|
||||
)
|
||||
|
||||
|
||||
def log_message(
|
||||
header,
|
||||
raw,
|
||||
message,
|
||||
tocall=None,
|
||||
fromcall=None,
|
||||
msg_num=None,
|
||||
retry_number=None,
|
||||
ack=None,
|
||||
packet_type=None,
|
||||
):
|
||||
"""
|
||||
|
||||
Log a message entry.
|
||||
|
||||
This builds a long string with newlines for the log entry, so that
|
||||
it's thread safe. If we log each item as a separate log.debug() call
|
||||
Then the message information could get multiplexed with other log
|
||||
messages. Each python log call is automatically synchronized.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
log_list = [""]
|
||||
if retry_number:
|
||||
# LOG.info(" {} _______________(TX:{})".format(header, retry_number))
|
||||
log_list.append(" {} _______________(TX:{})".format(header, retry_number))
|
||||
else:
|
||||
# LOG.info(" {} _______________".format(header))
|
||||
log_list.append(" {} _______________".format(header))
|
||||
|
||||
# LOG.info(" Raw : {}".format(raw))
|
||||
log_list.append(" Raw : {}".format(raw))
|
||||
|
||||
if packet_type:
|
||||
# LOG.info(" Packet : {}".format(packet_type))
|
||||
log_list.append(" Packet : {}".format(packet_type))
|
||||
if tocall:
|
||||
# LOG.info(" To : {}".format(tocall))
|
||||
log_list.append(" To : {}".format(tocall))
|
||||
if fromcall:
|
||||
# LOG.info(" From : {}".format(fromcall))
|
||||
log_list.append(" From : {}".format(fromcall))
|
||||
|
||||
if ack:
|
||||
# LOG.info(" Ack : {}".format(ack))
|
||||
log_list.append(" Ack : {}".format(ack))
|
||||
else:
|
||||
# LOG.info(" Message : {}".format(message))
|
||||
log_list.append(" Message : {}".format(message))
|
||||
if msg_num:
|
||||
# LOG.info(" Msg number : {}".format(msg_num))
|
||||
log_list.append(" Msg number : {}".format(msg_num))
|
||||
# LOG.info(" {} _______________ Complete".format(header))
|
||||
log_list.append(" {} _______________ Complete".format(header))
|
||||
|
||||
LOG.info("\n".join(log_list))
|
||||
|
||||
|
||||
def process_message(line):
|
||||
f = re.search("^(.*)>", line)
|
||||
fromcall = f.group(1)
|
||||
searchstring = "::%s[ ]*:(.*)" % CONFIG["aprs"]["login"]
|
||||
# verify this, callsign is padded out with spaces to colon
|
||||
m = re.search(searchstring, line)
|
||||
fullmessage = m.group(1)
|
||||
|
||||
ack_attached = re.search("(.*){([0-9A-Z]+)", fullmessage)
|
||||
# ack formats include: {1, {AB}, {12
|
||||
if ack_attached:
|
||||
# "{##" suffix means radio wants an ack back
|
||||
# message content
|
||||
message = ack_attached.group(1)
|
||||
# suffix number to use in ack
|
||||
ack_num = ack_attached.group(2)
|
||||
else:
|
||||
message = fullmessage
|
||||
# ack not requested, but lets send one as 0
|
||||
ack_num = "0"
|
||||
|
||||
log_message(
|
||||
"Received message", line, message, fromcall=fromcall, msg_num=str(ack_num)
|
||||
)
|
||||
|
||||
return (fromcall, message, ack_num)
|
||||
# end process_message()
|
||||
+244
-81
@@ -1,11 +1,13 @@
|
||||
# The base plugin class
|
||||
import abc
|
||||
import fnmatch
|
||||
import importlib
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
@@ -14,6 +16,8 @@ import requests
|
||||
import six
|
||||
from thesmuggler import smuggle
|
||||
|
||||
import aprsd
|
||||
from aprsd import email, messaging
|
||||
from aprsd.fuzzyclock import fuzzy
|
||||
|
||||
# setup the global logger
|
||||
@@ -23,81 +27,43 @@ hookspec = pluggy.HookspecMarker("aprsd")
|
||||
hookimpl = pluggy.HookimplMarker("aprsd")
|
||||
|
||||
CORE_PLUGINS = [
|
||||
"FortunePlugin",
|
||||
"LocationPlugin",
|
||||
"PingPlugin",
|
||||
"TimePlugin",
|
||||
"WeatherPlugin",
|
||||
"aprsd.plugin.EmailPlugin",
|
||||
"aprsd.plugin.FortunePlugin",
|
||||
"aprsd.plugin.LocationPlugin",
|
||||
"aprsd.plugin.PingPlugin",
|
||||
"aprsd.plugin.TimePlugin",
|
||||
"aprsd.plugin.WeatherPlugin",
|
||||
"aprsd.plugin.VersionPlugin",
|
||||
]
|
||||
|
||||
|
||||
def setup_plugins(config):
|
||||
"""Create the plugin manager and register plugins."""
|
||||
|
||||
LOG.info("Loading Core APRSD Command Plugins")
|
||||
enabled_plugins = config["aprsd"].get("enabled_plugins", None)
|
||||
pm = pluggy.PluginManager("aprsd")
|
||||
pm.add_hookspecs(APRSDCommandSpec)
|
||||
for p_name in CORE_PLUGINS:
|
||||
plugin_obj = None
|
||||
if enabled_plugins:
|
||||
if p_name in enabled_plugins:
|
||||
plugin_obj = globals()[p_name](config)
|
||||
else:
|
||||
# Enabled plugins isn't set, so we default to loading all of
|
||||
# the core plugins.
|
||||
plugin_obj = globals()[p_name](config)
|
||||
|
||||
if plugin_obj:
|
||||
LOG.info(
|
||||
"Registering Command plugin '{}'({}) '{}'".format(
|
||||
p_name, plugin_obj.version, plugin_obj.command_regex
|
||||
)
|
||||
)
|
||||
pm.register(plugin_obj)
|
||||
|
||||
plugin_dir = config["aprsd"].get("plugin_dir", None)
|
||||
if plugin_dir:
|
||||
LOG.info("Trying to load custom plugins from '{}'".format(plugin_dir))
|
||||
cpm = PluginManager(config)
|
||||
plugins_list = cpm.load_plugins(plugin_dir)
|
||||
LOG.info("Discovered {} modules to load".format(len(plugins_list)))
|
||||
for o in plugins_list:
|
||||
plugin_obj = None
|
||||
if enabled_plugins:
|
||||
if o["name"] in enabled_plugins:
|
||||
plugin_obj = o["obj"]
|
||||
else:
|
||||
LOG.info(
|
||||
"'{}' plugin not listed in config aprsd:enabled_plugins".format(
|
||||
o["name"]
|
||||
)
|
||||
)
|
||||
else:
|
||||
# not setting enabled plugins means load all?
|
||||
plugin_obj = o["obj"]
|
||||
|
||||
if plugin_obj:
|
||||
LOG.info(
|
||||
"Registering Command plugin '{}'({}) '{}'".format(
|
||||
o["name"], o["obj"].version, o["obj"].command_regex
|
||||
)
|
||||
)
|
||||
pm.register(o["obj"])
|
||||
|
||||
else:
|
||||
LOG.info("Skipping Custom Plugins.")
|
||||
|
||||
LOG.info("Completed Plugin Loading.")
|
||||
return pm
|
||||
|
||||
|
||||
class PluginManager(object):
|
||||
def __init__(self, config):
|
||||
self.obj_list = []
|
||||
self.config = config
|
||||
# The singleton instance object for this class
|
||||
_instance = None
|
||||
|
||||
# the pluggy PluginManager
|
||||
_pluggy_pm = None
|
||||
|
||||
# aprsd config dict
|
||||
config = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""This magic turns this into a singleton."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super(PluginManager, cls).__new__(cls)
|
||||
# Put any initialization here.
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, config=None):
|
||||
self.obj_list = []
|
||||
if config:
|
||||
self.config = config
|
||||
|
||||
def load_plugins_from_path(self, module_path):
|
||||
if not os.path.exists(module_path):
|
||||
LOG.error("plugin path '{}' doesn't exist.".format(module_path))
|
||||
return None
|
||||
|
||||
def load_plugins(self, module_path):
|
||||
dir_path = os.path.realpath(module_path)
|
||||
pattern = "*.py"
|
||||
|
||||
@@ -123,6 +89,108 @@ class PluginManager(object):
|
||||
|
||||
return False
|
||||
|
||||
def _create_class(self, module_class_string, super_cls: type = None, **kwargs):
|
||||
"""
|
||||
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 super_cls: expected super class for validity, None if bypass
|
||||
:param kwargs: parameters to pass
|
||||
:return:
|
||||
"""
|
||||
module_name, class_name = module_class_string.rsplit(".", 1)
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
except Exception as ex:
|
||||
LOG.error("Failed to load Plugin '{}' : '{}'".format(module_name, ex))
|
||||
return
|
||||
|
||||
assert hasattr(module, class_name), "class {} is not in {}".format(
|
||||
class_name, module_name
|
||||
)
|
||||
# click.echo('reading class {} from module {}'.format(
|
||||
# class_name, module_name))
|
||||
cls = getattr(module, class_name)
|
||||
if super_cls is not None:
|
||||
assert issubclass(cls, super_cls), "class {} should inherit from {}".format(
|
||||
class_name, super_cls.__name__
|
||||
)
|
||||
# click.echo('initialising {} with params {}'.format(class_name, kwargs))
|
||||
obj = cls(**kwargs)
|
||||
return obj
|
||||
|
||||
def _load_plugin(self, plugin_name):
|
||||
"""
|
||||
|
||||
Given a python fully qualified class path.name,
|
||||
Try importing the path, then creating the object,
|
||||
then registering it as a aprsd Command Plugin
|
||||
"""
|
||||
plugin_obj = None
|
||||
try:
|
||||
plugin_obj = self._create_class(
|
||||
plugin_name, APRSDPluginBase, config=self.config
|
||||
)
|
||||
if plugin_obj:
|
||||
LOG.info(
|
||||
"Registering Command plugin '{}'({}) '{}'".format(
|
||||
plugin_name, plugin_obj.version, plugin_obj.command_regex
|
||||
)
|
||||
)
|
||||
self._pluggy_pm.register(plugin_obj)
|
||||
except Exception as ex:
|
||||
LOG.exception("Couldn't load plugin '{}'".format(plugin_name), ex)
|
||||
|
||||
def setup_plugins(self):
|
||||
"""Create the plugin manager and register plugins."""
|
||||
|
||||
LOG.info("Loading Core APRSD Command Plugins")
|
||||
enabled_plugins = self.config["aprsd"].get("enabled_plugins", None)
|
||||
self._pluggy_pm = pluggy.PluginManager("aprsd")
|
||||
self._pluggy_pm.add_hookspecs(APRSDCommandSpec)
|
||||
if enabled_plugins:
|
||||
for p_name in enabled_plugins:
|
||||
self._load_plugin(p_name)
|
||||
else:
|
||||
# Enabled plugins isn't set, so we default to loading all of
|
||||
# the core plugins.
|
||||
for p_name in CORE_PLUGINS:
|
||||
self._load_plugin(p_name)
|
||||
|
||||
plugin_dir = self.config["aprsd"].get("plugin_dir", None)
|
||||
if plugin_dir:
|
||||
LOG.info("Trying to load custom plugins from '{}'".format(plugin_dir))
|
||||
plugins_list = self.load_plugins_from_path(plugin_dir)
|
||||
if plugins_list:
|
||||
LOG.info("Discovered {} modules to load".format(len(plugins_list)))
|
||||
for o in plugins_list:
|
||||
plugin_obj = None
|
||||
# not setting enabled plugins means load all?
|
||||
plugin_obj = o["obj"]
|
||||
|
||||
if plugin_obj:
|
||||
LOG.info(
|
||||
"Registering Command plugin '{}'({}) '{}'".format(
|
||||
o["name"], o["obj"].version, o["obj"].command_regex
|
||||
)
|
||||
)
|
||||
self._pluggy_pm.register(o["obj"])
|
||||
|
||||
else:
|
||||
LOG.info("Skipping Custom Plugins directory.")
|
||||
LOG.info("Completed Plugin Loading.")
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
"""Execute all the pluguns run method."""
|
||||
return self._pluggy_pm.hook.run(*args, **kwargs)
|
||||
|
||||
def register(self, obj):
|
||||
"""Register the plugin."""
|
||||
self._pluggy_pm.register(obj)
|
||||
|
||||
def get_plugins(self):
|
||||
return self._pluggy_pm.get_plugins()
|
||||
|
||||
|
||||
class APRSDCommandSpec:
|
||||
"""A hook specification namespace."""
|
||||
@@ -179,12 +247,17 @@ class FortunePlugin(APRSDPluginBase):
|
||||
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(
|
||||
["/usr/games/fortune", "-s", "-n 60"], stdout=subprocess.PIPE
|
||||
[fortune_path, "-s", "-n 60"], stdout=subprocess.PIPE
|
||||
)
|
||||
reply = process.communicate()[0]
|
||||
# send_message(fromcall, reply.rstrip())
|
||||
reply = reply.decode(errors="ignore").rstrip()
|
||||
except Exception as ex:
|
||||
reply = "Fortune command failed '{}'".format(ex)
|
||||
@@ -200,20 +273,20 @@ class LocationPlugin(APRSDPluginBase):
|
||||
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:
|
||||
a = re.search(
|
||||
r"^.*\s+(.*)", message
|
||||
) # optional second argument is a callsign to search
|
||||
# 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:
|
||||
searchcall = (
|
||||
fromcall # if no second argument, search for calling station
|
||||
)
|
||||
# if no second argument, search for calling station
|
||||
searchcall = fromcall
|
||||
url = (
|
||||
"http://api.aprs.fi/api/get?name="
|
||||
+ searchcall
|
||||
@@ -222,6 +295,7 @@ class LocationPlugin(APRSDPluginBase):
|
||||
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
|
||||
@@ -230,9 +304,9 @@ class LocationPlugin(APRSDPluginBase):
|
||||
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
|
||||
# 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 = (
|
||||
@@ -342,3 +416,92 @@ class WeatherPlugin(APRSDPluginBase):
|
||||
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)
|
||||
# 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:
|
||||
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
|
||||
reply = "mapme email sent"
|
||||
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__)
|
||||
|
||||
+16
-3
@@ -1,8 +1,10 @@
|
||||
"""Utilities and helper functions."""
|
||||
|
||||
import errno
|
||||
import functools
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import click
|
||||
import yaml
|
||||
@@ -15,7 +17,7 @@ DEFAULT_CONFIG_DICT = {
|
||||
"aprs": {
|
||||
"login": "someusername",
|
||||
"password": "somepassword",
|
||||
"host": "noam.aprs2.net",
|
||||
"host": "rotate.aprs.net",
|
||||
"port": 14580,
|
||||
"logfile": "/tmp/arsd.log",
|
||||
},
|
||||
@@ -47,6 +49,17 @@ DEFAULT_CONFIG_DICT = {
|
||||
DEFAULT_CONFIG_FILE = "~/.config/aprsd/aprsd.yml"
|
||||
|
||||
|
||||
def synchronized(wrapped):
|
||||
lock = threading.Lock()
|
||||
|
||||
@functools.wraps(wrapped)
|
||||
def _wrap(*args, **kwargs):
|
||||
with lock:
|
||||
return wrapped(*args, **kwargs)
|
||||
|
||||
return _wrap
|
||||
|
||||
|
||||
def env(*vars, **kwargs):
|
||||
"""This returns the first environment variable set.
|
||||
if none are non-empty, defaults to '' or keyword arg default
|
||||
@@ -152,8 +165,8 @@ def parse_config(config_file):
|
||||
)
|
||||
check_option(config, "aprs", "login")
|
||||
check_option(config, "aprs", "password")
|
||||
check_option(config, "aprs", "host")
|
||||
check_option(config, "aprs", "port")
|
||||
# check_option(config, "aprs", "host")
|
||||
# check_option(config, "aprs", "port")
|
||||
check_option(config, "aprs", "logfile", "./aprsd.log")
|
||||
check_option(config, "imap", "host")
|
||||
check_option(config, "imap", "login")
|
||||
|
||||
@@ -7,4 +7,3 @@ pep8-naming
|
||||
black
|
||||
isort
|
||||
Sphinx
|
||||
thesmuggler
|
||||
|
||||
@@ -10,7 +10,7 @@ attrs==20.3.0 # via pytest
|
||||
babel==2.9.0 # via sphinx
|
||||
black==20.8b1 # via -r dev-requirements.in
|
||||
certifi==2020.12.5 # via requests
|
||||
chardet==3.0.4 # via requests
|
||||
chardet==4.0.0 # via requests
|
||||
click==7.1.2 # via black
|
||||
coverage==5.3 # via pytest-cov
|
||||
distlib==0.3.1 # via virtualenv
|
||||
@@ -37,10 +37,10 @@ pyflakes==2.2.0 # via flake8
|
||||
pygments==2.7.3 # via sphinx
|
||||
pyparsing==2.4.7 # via packaging
|
||||
pytest-cov==2.10.1 # via -r dev-requirements.in
|
||||
pytest==6.2.0 # via -r dev-requirements.in, pytest-cov
|
||||
pytest==6.2.1 # via -r dev-requirements.in, pytest-cov
|
||||
pytz==2020.4 # via babel
|
||||
regex==2020.11.13 # via black
|
||||
requests==2.25.0 # via sphinx
|
||||
requests==2.25.1 # via sphinx
|
||||
six==1.15.0 # via tox, virtualenv
|
||||
snowballstemmer==2.0.0 # via sphinx
|
||||
sphinx==3.3.1 # via -r dev-requirements.in
|
||||
@@ -50,7 +50,6 @@ 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
|
||||
thesmuggler==1.0.1 # via -r dev-requirements.in
|
||||
toml==0.10.2 # via black, pytest, tox
|
||||
tox==3.20.1 # via -r dev-requirements.in
|
||||
typed-ast==1.4.1 # via black, mypy
|
||||
|
||||
+2
-3
@@ -4,9 +4,8 @@ services:
|
||||
image: hemna6969/aprsd:latest
|
||||
container_name: aprsd
|
||||
volumes:
|
||||
- /opt/docker/aprsd/config:/config
|
||||
- /opt/docker/aprsd/plugins:/plugins
|
||||
- $HOME/.config/aprsd:/config
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- APRS_PLUGINS=aprsd-slack-plugin
|
||||
- APRS_PLUGINS=aprsd-slack-plugin>=1.0.2
|
||||
|
||||
@@ -7,3 +7,5 @@ pyyaml
|
||||
six
|
||||
requests
|
||||
thesmuggler
|
||||
aprslib
|
||||
py3-validate-email
|
||||
|
||||
+7
-3
@@ -4,18 +4,22 @@
|
||||
#
|
||||
# pip-compile
|
||||
#
|
||||
aprslib==0.6.47 # via -r requirements.in
|
||||
certifi==2020.12.5 # via requests
|
||||
chardet==3.0.4 # via requests
|
||||
chardet==4.0.0 # via requests
|
||||
click-completion==0.5.2 # via -r requirements.in
|
||||
click==7.1.2 # via -r requirements.in, click-completion
|
||||
idna==2.10 # via requests
|
||||
dnspython==2.0.0 # via py3-validate-email
|
||||
filelock==3.0.12 # via py3-validate-email
|
||||
idna==2.10 # via py3-validate-email, requests
|
||||
imapclient==2.1.0 # via -r requirements.in
|
||||
jinja2==2.11.2 # via click-completion
|
||||
markupsafe==1.1.1 # via jinja2
|
||||
pbr==5.5.1 # via -r requirements.in
|
||||
pluggy==0.13.1 # via -r requirements.in
|
||||
py3-validate-email==0.2.12 # via -r requirements.in
|
||||
pyyaml==5.3.1 # via -r requirements.in
|
||||
requests==2.25.0 # via -r requirements.in
|
||||
requests==2.25.1 # via -r requirements.in
|
||||
shellingham==1.3.2 # via click-completion
|
||||
six==1.15.0 # via -r requirements.in, click-completion, imapclient
|
||||
thesmuggler==1.0.1 # via -r requirements.in
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
flake8
|
||||
pytest
|
||||
mock
|
||||
@@ -1,2 +0,0 @@
|
||||
flake8
|
||||
pytest
|
||||
+5
-4
@@ -4,7 +4,7 @@ import unittest
|
||||
|
||||
import pytest
|
||||
|
||||
from aprsd import main
|
||||
from aprsd import email
|
||||
|
||||
if sys.version_info >= (3, 2):
|
||||
from unittest import mock
|
||||
@@ -13,11 +13,12 @@ else:
|
||||
|
||||
|
||||
class testMain(unittest.TestCase):
|
||||
@mock.patch("aprsd.main._imap_connect")
|
||||
@mock.patch("aprsd.main._smtp_connect")
|
||||
@mock.patch("aprsd.email._imap_connect")
|
||||
@mock.patch("aprsd.email._smtp_connect")
|
||||
def test_validate_email(self, imap_mock, smtp_mock):
|
||||
"""Test to make sure we fail."""
|
||||
imap_mock.return_value = None
|
||||
smtp_mock.return_value = {"smaiof": "fire"}
|
||||
config = mock.MagicMock()
|
||||
|
||||
main.validate_email()
|
||||
email.validate_email_config(config, True)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
minversion = 2.9.0
|
||||
skipdist = True
|
||||
skip_missing_interpreters = true
|
||||
envlist = py{27,36,37,38},pep8,fmt-check
|
||||
envlist = pep8,py{36,37,38},fmt-check
|
||||
|
||||
# Activate isolated build environment. tox will use a virtual environment
|
||||
# to build a source distribution from the source tree. For build tools and
|
||||
@@ -14,7 +14,6 @@ setenv = VIRTUAL_ENV={envdir}
|
||||
usedevelop = True
|
||||
install_command = pip install {opts} {packages}
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
-r{toxinidir}/dev-requirements.txt
|
||||
commands =
|
||||
# Use -bb to enable BytesWarnings as error to catch str/bytes misuse.
|
||||
@@ -23,29 +22,10 @@ commands =
|
||||
# --cov="{envsitepackagesdir}/aprsd" --cov-report=html --cov-report=term {posargs}
|
||||
{envpython} -bb -m pytest {posargs}
|
||||
|
||||
[testenv:py27]
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
usedevelop = True
|
||||
install_command = pip install {opts} {packages}
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements-py2.txt
|
||||
commands =
|
||||
# Use -bb to enable BytesWarnings as error to catch str/bytes misuse.
|
||||
# Use -Werror to treat warnings as errors.
|
||||
# {envpython} -bb -Werror -m pytest \
|
||||
# --cov="{envsitepackagesdir}/aprsd" --cov-report=html --cov-report=term {posargs}
|
||||
{envpython} -bb -Werror -m pytest
|
||||
|
||||
[testenv:docs]
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
commands = sphinx-build -b html docs/source docs/html
|
||||
|
||||
[testenv:pep8-27]
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements-py2.txt
|
||||
commands =
|
||||
flake8 {posargs} aprsd
|
||||
|
||||
[testenv:pep8]
|
||||
commands =
|
||||
flake8 {posargs} aprsd
|
||||
@@ -78,7 +58,6 @@ exclude = .venv,.git,.tox,dist,doc,.ropeproject
|
||||
# This section is not needed if not using GitHub Actions for CI.
|
||||
[gh-actions]
|
||||
python =
|
||||
2.7: py27, pep8-27
|
||||
3.6: py36, pep8, fmt-check
|
||||
3.7: py38, pep8, fmt-check
|
||||
3.8: py38, pep8, fmt-check, type-check, docs
|
||||
|
||||
Reference in New Issue
Block a user