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

Compare commits

..

17 Commits

Author SHA1 Message Date
hemna 1d898ea20f Refactored the main process_packet method
This patch refactored the process_packet method
and adjusted the logic for determining if we got
a message to filter on.  We now look at the format
to make a determination.  Also isolated the processing
of message packets, ack packets and mic-e packets into
their own functions.
2020-12-21 11:59:58 -05:00
hemna 8161719697 Update README with version 1.1.0 related info
This patch adds more detailed information about the aprsd app, and
plugins.
2020-12-20 21:19:43 -05:00
hemna b2609548f2 Added fix for an unknown packet type
If we get a packet that doesn't have a message in it, nor an ack,
we don't know what to do with it, so log it and then stop processing it.
2020-12-20 20:28:56 -05:00
hemna e68d3f92e1 Merge pull request #25 from johng42/patch-1
Update .gitignore
2020-12-20 19:17:06 -05:00
hemna 18b269a39d Ensure fortune is installed
This patch uses python3's shutil to find the path
to fortune app, which can be very different depending on
the host OS that aprsd is running on.
2020-12-20 19:04:33 -05:00
hemna c39fddef67 Updated docker-compose
This patch update the docker-compose file to use the
host system's $HOME/.config/aprsd directory as the container's
config directory by default.
2020-12-20 16:46:19 -05:00
hemna 6d2160c7b7 Added Changelog
The changelog is generated by pbr automatically when you run

python setup.py sdist bdist_wheel

So when you are ready for a new release, run
python setup.py sdist bdist_wheel

git add Changelog && git commit -m "Updated ChangeLog for version XXXX"

git tag -a vXXXX -m "Release version XXX"

git push origin --tags

Then release to pypi

twine check dist/*
twine upload dist/*
2020-12-20 16:41:24 -05:00
hemna d3ee1b78bc Fixed issue when RX ack
This patch ensures that after we get an ACK, then proessing of the ack
message is complete and we don't try and send the ack through plugin
filtering.

Created send_ack_direct for the send-message command.

Also added logic to the send-message command to ensure we wait for an
ack from the command sent to APRSD and we also wait for a response
message and send an ack to that response before we exit.
2020-12-20 16:35:23 -05:00
hemna 0992ab8176 Updated the aprsd-slack-plugin required version
this patch updates the docker-compose file to use
the 1.0.2 or > version of aprsd-slack-plugin
2020-12-20 15:29:16 -05:00
hemna 6a61765eba Updated README.rst
This patch updates the README.rst with sample help and output from
various commands
2020-12-20 12:36:47 -05:00
hemna 3261710bf8 Fixed send-message with email command and others
This patch fixes a minor issue with the new send-message command
You now should use nargs to send the email command because it includes
a - as the start.  click assumed that any -<foo>  looks ike an argument.
So call aprsd with

aprsd send-command <callsign> -- -wb sendmap

This patch also adds -h as a help option for aprsd to make it simpler to
type.

This patch adds the VersionPlugin so you can remotely request the
version of aprsd that's running.
2020-12-20 12:21:30 -05:00
John a566990a2f Update .gitignore
I want to add some tests but before that I want git to ignore the venv folder and all the Idea (Pycharm) settings that get created in the home folder by default.
2020-12-19 17:25:27 -08:00
Craig Lamparter 923e1a7c3d Merge pull request #23 from craigerl/aprslib
Major refactor
2020-12-19 14:00:01 -08:00
hemna fa51f8fdf2 Big patch
This commit adds the new send-message command for sending messages.
This also redoes the logging of sent/rx'd packets to a single method
which is syncrhonized, so we don't get intermixed log messages for
packets.

Also adds email address validation during startup, and
optionally disables the validation via a command line switch.  without
email validation for production running aprsd, emails sent can turn up
garbage and cause issues when those emails are received by aprsd
message processing as invalid content.
2020-12-19 16:35:53 -05:00
hemna 08c73a17d1 Major refactor
This branch refactors the majority of main.py out into individual
modules to compartmentalize the code.  Migrated all email related
features unti email.py, sending of messages into messaging.py

Also refactored all of the socket code to use aprslib for all APRS-IS
communication as well as message/packet processing.

Moved the email command into it's own Plugin.
2020-12-18 16:21:35 -05:00
hemna 79d2139fe1 Merge pull request #24 from craigerl/docker_alpine
Updated the Dockerfile to use alpine
2020-12-17 10:22:22 -05:00
hemna b5fb248b46 Updated the Dockerfile to use alpine
The ubuntu images were running about 533MB in size, so
I refactored the Dockerfiles to use alpine linux, and they run
about 130MB.
2020-12-17 10:16:58 -05:00
21 changed files with 1641 additions and 887 deletions
+1 -1
View File
@@ -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
View File
@@ -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
+152
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
=================================
+65
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+267
View File
@@ -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
View File
@@ -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
View File
@@ -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")
-1
View File
@@ -7,4 +7,3 @@ pep8-naming
black
isort
Sphinx
thesmuggler
+3 -4
View File
@@ -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
View File
@@ -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
+2
View File
@@ -7,3 +7,5 @@ pyyaml
six
requests
thesmuggler
aprslib
py3-validate-email
+7 -3
View File
@@ -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
-3
View File
@@ -1,3 +0,0 @@
flake8
pytest
mock
-2
View File
@@ -1,2 +0,0 @@
flake8
pytest
+5 -4
View File
@@ -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)
+1 -22
View File
@@ -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