mirror of
https://github.com/craigerl/aprsd.git
synced 2026-08-16 00:23:34 -04:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a266c987fd | |||
| e2d8cc60e5 | |||
| 3a6316fa8a | |||
| 7df6462d91 | |||
| 24edcad60a | |||
| 9ba44a076c | |||
| afc53624e1 | |||
| 131919bdfb | |||
| d0d0aef077 | |||
| a5cc274ff5 | |||
| d71937b176 | |||
| 47135c6086 | |||
| db2b537317 | |||
| 0b44fc08eb | |||
| af48c43eb2 | |||
| 94bad95e26 | |||
| 57d768e010 | |||
| 030b02551f | |||
| cfb172481d | |||
| 3ca0eeff56 | |||
| c1e6792721 | |||
| aa290692ab | |||
| 0d18e54969 | |||
| 51894bbab8 | |||
| 8bfdefd5ad | |||
| f5ae161ab8 | |||
| c870207a96 | |||
| f932c203d7 | |||
| cae8746690 | |||
| 5c949343ec | |||
| 9630279d14 | |||
| c686543323 | |||
| 982f24c5f5 | |||
| a656508ba6 | |||
| ce5b09233c | |||
| 2f7c1bfcc1 | |||
| a35cb04ca7 | |||
| fefb626c97 | |||
| 2349024539 | |||
| f8c001dc49 | |||
| fc3a747aa4 | |||
| fdd5a6ba41 | |||
| 9f38fd179e | |||
| ca05676c98 | |||
| 83f42dd7b7 | |||
| 5fb363c9e7 | |||
| 7de2820caa | |||
| 55360ba5d0 | |||
| b9f6fcfa0c | |||
| cc8fd178ce | |||
| 658e6b6202 | |||
| 98e3c246ad | |||
| 47ccce53ea |
@@ -44,6 +44,7 @@ output/*/index.html
|
||||
|
||||
# Sphinx
|
||||
doc/build
|
||||
docs/_build
|
||||
|
||||
# pbr generates these
|
||||
AUTHORS
|
||||
|
||||
@@ -1,9 +1,58 @@
|
||||
CHANGES
|
||||
=======
|
||||
|
||||
v1.6.0
|
||||
------
|
||||
|
||||
* Updated path of run.sh for docker build
|
||||
* Moved docker related stuffs to docker dir
|
||||
* Removed some noisy debug log
|
||||
* Bump cryptography from 3.3.1 to 3.3.2
|
||||
* Wrap another server call with try except
|
||||
* Wrap all imap calls with try except blocks
|
||||
* Bump bleach from 3.2.1 to 3.3.0
|
||||
* EmailThread was exiting because of IMAP timeout, added exceptions for this
|
||||
* Added memory tracing in keeplive
|
||||
* Fixed tox pep8 failure for trace
|
||||
* Added tracing facility
|
||||
* Fixed email login issue
|
||||
* duplicate email messages from RF would generate usage response
|
||||
* Enable debug logging for smtp and imap
|
||||
* more debug around email thread
|
||||
* debug around EmailThread hanging or vanishing
|
||||
* Fixed resend email after config rework
|
||||
* Added flask messages web UI and basic auth
|
||||
* Fixed an issue with LocationPlugin
|
||||
* Cleaned up the KeepAlive output
|
||||
* updated .gitignore
|
||||
* Added healthcheck app
|
||||
* Add flask and flask\_classful reqs
|
||||
* Added Flask web thread and stats collection
|
||||
* First hack at flask
|
||||
* Allow email to be disabled
|
||||
* Reworked the config file and options
|
||||
* Updated documentation and config output
|
||||
* Fixed extracting lat/lon
|
||||
* Added openweathermap weather plugin
|
||||
* Added new time plugins
|
||||
* Fixed TimePlugin timezone issue
|
||||
* remove fortune white space
|
||||
* fix git with install.txt
|
||||
* change query char from ? to !
|
||||
* Updated readme to include readthedocs link
|
||||
* Added aprsd-dev plugin test cli and WxPlugin
|
||||
|
||||
v1.5.1
|
||||
------
|
||||
|
||||
* Updated Changelog for v1.5.1
|
||||
* Updated README to fix pypi page
|
||||
* Update INSTALL.txt
|
||||
|
||||
v1.5.0
|
||||
------
|
||||
|
||||
* Updated Changelog for v1.5.0 release
|
||||
* Fix tox tests
|
||||
* fix usage statement
|
||||
* Enabled some emailthread messages and added timestamp
|
||||
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
FROM alpine:latest as aprsd
|
||||
|
||||
ENV VERSION=1.0.0
|
||||
ENV APRS_USER=aprs
|
||||
ENV HOME=/home/aprs
|
||||
ENV VIRTUAL_ENV=$HOME/.venv3
|
||||
|
||||
ENV INSTALL=$HOME/install
|
||||
RUN apk add --update git 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 addgroup --gid 1000 $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
|
||||
|
||||
USER $APRS_USER
|
||||
RUN pip3 install wheel
|
||||
RUN python3 -m venv $VIRTUAL_ENV
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
RUN echo "export PATH=\$PATH:\$HOME/.local/bin" >> $HOME/.bashrc
|
||||
VOLUME ["/config", "/plugins"]
|
||||
|
||||
WORKDIR $HOME
|
||||
RUN pip install aprsd
|
||||
USER root
|
||||
RUN aprsd sample-config > /config/aprsd.yml
|
||||
RUN chown -R $APRS_USER:$APRS_USER /config
|
||||
|
||||
# override this to run another configuration
|
||||
ENV CONF default
|
||||
USER $APRS_USER
|
||||
|
||||
ADD build/bin/run.sh $HOME/
|
||||
ENTRYPOINT ["/home/aprs/run.sh"]
|
||||
+4
-4
@@ -13,12 +13,12 @@ cd ~/aprsd2
|
||||
git clone https://github.com/craigerl/aprsd.git
|
||||
cd aprsd
|
||||
pip install -e .
|
||||
cd ~/.venv_aprsd
|
||||
cd ~/.venv_aprsd/bin
|
||||
|
||||
./bin/aprsd # generates a config.yml template on first run
|
||||
./aprsd sample-config # generates a config.yml template
|
||||
|
||||
vi ~/.aprsd/config.yml
|
||||
vi ~/.config/aprsd/config.yml # copy/edit config here
|
||||
|
||||
./bin/aprsd
|
||||
./aprsd server
|
||||
|
||||
# profit! #
|
||||
|
||||
@@ -33,7 +33,7 @@ clean:
|
||||
|
||||
test: dev
|
||||
.venv/bin/pre-commit run --all-files
|
||||
tox -p
|
||||
tox -p all
|
||||
|
||||
build: test
|
||||
rm -rf dist/*
|
||||
@@ -43,6 +43,9 @@ build: test
|
||||
upload: build
|
||||
.venv/bin/twine upload dist/*
|
||||
|
||||
docker: test
|
||||
docker build -t hemna6969/aprsd:latest -f docker/Dockerfile docker
|
||||
|
||||
update-requirements: dev pip-tools
|
||||
.venv/bin/pip-compile -q -U requirements.in
|
||||
.venv/bin/pip-compile -q -U dev-requirements.in
|
||||
|
||||
+3
-1
@@ -36,11 +36,13 @@ 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.
|
||||
|
||||
Documentation: https://aprsd.readthedocs.io
|
||||
|
||||
|
||||
APRSD Overview Diagram
|
||||
----------------------
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/craigerl/aprsd/crager-stable/docs/_static/aprsd_overview.svg?sanitize=true
|
||||
.. image:: https://raw.githubusercontent.com/craigerl/aprsd/master/docs/_static/aprsd_overview.svg?sanitize=true
|
||||
|
||||
|
||||
Typical use case
|
||||
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
#
|
||||
# Dev.py is used to help develop plugins
|
||||
#
|
||||
#
|
||||
# python included libs
|
||||
import logging
|
||||
from logging import NullHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os
|
||||
import sys
|
||||
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import client, email, plugin, utils
|
||||
import click
|
||||
import click_completion
|
||||
|
||||
# setup the global logger
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
LOG_LEVELS = {
|
||||
"CRITICAL": logging.CRITICAL,
|
||||
"ERROR": logging.ERROR,
|
||||
"WARNING": logging.WARNING,
|
||||
"INFO": logging.INFO,
|
||||
"DEBUG": logging.DEBUG,
|
||||
}
|
||||
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
|
||||
def custom_startswith(string, incomplete):
|
||||
"""A custom completion match that supports case insensitive matching."""
|
||||
if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"):
|
||||
string = string.lower()
|
||||
incomplete = incomplete.lower()
|
||||
return string.startswith(incomplete)
|
||||
|
||||
|
||||
click_completion.core.startswith = custom_startswith
|
||||
click_completion.init()
|
||||
|
||||
|
||||
cmd_help = """Shell completion for click-completion-command
|
||||
Available shell types:
|
||||
\b
|
||||
%s
|
||||
Default type: auto
|
||||
""" % "\n ".join(
|
||||
"{:<12} {}".format(k, click_completion.core.shells[k])
|
||||
for k in sorted(click_completion.core.shells.keys())
|
||||
)
|
||||
|
||||
|
||||
@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS)
|
||||
@click.version_option()
|
||||
def main():
|
||||
pass
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
def show(shell, case_insensitive):
|
||||
"""Show the click-completion-command completion code"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
click.echo(click_completion.core.get_code(shell, extra_env=extra_env))
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--append/--overwrite",
|
||||
help="Append the completion code to the file",
|
||||
default=None,
|
||||
)
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
@click.argument("path", required=False)
|
||||
def install(append, case_insensitive, shell, path):
|
||||
"""Install the click-completion-command completion"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
shell, path = click_completion.core.install(
|
||||
shell=shell,
|
||||
path=path,
|
||||
append=append,
|
||||
extra_env=extra_env,
|
||||
)
|
||||
click.echo("{} completion installed in {}".format(shell, path))
|
||||
|
||||
|
||||
# Setup the logging faciility
|
||||
# to disable logging to stdout, but still log to file
|
||||
# use the --quiet option on the cmdln
|
||||
def setup_logging(config, loglevel, quiet):
|
||||
log_level = LOG_LEVELS[loglevel]
|
||||
LOG.setLevel(log_level)
|
||||
log_format = "[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s]" " %(message)s"
|
||||
date_format = "%m/%d/%Y %I:%M:%S %p"
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
log_file = config["aprs"].get("logfile", None)
|
||||
if log_file:
|
||||
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
||||
else:
|
||||
fh = NullHandler()
|
||||
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
if not quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
LOG.addHandler(sh)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--loglevel",
|
||||
default="DEBUG",
|
||||
show_default=True,
|
||||
type=click.Choice(
|
||||
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
case_sensitive=False,
|
||||
),
|
||||
show_choices=True,
|
||||
help="The log level to use for aprsd.log",
|
||||
)
|
||||
@click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_file",
|
||||
show_default=True,
|
||||
default=utils.DEFAULT_CONFIG_FILE,
|
||||
help="The aprsd config file to use for options.",
|
||||
)
|
||||
@click.option(
|
||||
"-p",
|
||||
"--plugin",
|
||||
"plugin_path",
|
||||
show_default=True,
|
||||
default="aprsd.plugins.wx.WxPlugin",
|
||||
help="The plugin to run",
|
||||
)
|
||||
@click.argument("fromcall")
|
||||
@click.argument("message", nargs=-1, required=True)
|
||||
def test_plugin(
|
||||
loglevel,
|
||||
config_file,
|
||||
plugin_path,
|
||||
fromcall,
|
||||
message,
|
||||
):
|
||||
"""APRSD Plugin test app."""
|
||||
|
||||
config = utils.parse_config(config_file)
|
||||
email.CONFIG = config
|
||||
|
||||
setup_logging(config, loglevel, False)
|
||||
LOG.info("Test APRSD PLugin version: {}".format(aprsd.__version__))
|
||||
if type(message) is tuple:
|
||||
message = " ".join(message)
|
||||
LOG.info("P'{}' F'{}' C'{}'".format(plugin_path, fromcall, message))
|
||||
client.Client(config)
|
||||
|
||||
pm = plugin.PluginManager(config)
|
||||
obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config)
|
||||
|
||||
reply = obj.run(fromcall, message, 1)
|
||||
LOG.info("Result = '{}'".format(reply))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+152
-49
@@ -7,7 +7,7 @@ import re
|
||||
import smtplib
|
||||
import time
|
||||
|
||||
from aprsd import messaging, threads
|
||||
from aprsd import messaging, stats, threads, trace
|
||||
import imapclient
|
||||
from validate_email import validate_email
|
||||
|
||||
@@ -17,58 +17,88 @@ LOG = logging.getLogger("APRSD")
|
||||
CONFIG = None
|
||||
|
||||
|
||||
@trace.trace
|
||||
def _imap_connect():
|
||||
imap_port = CONFIG["imap"].get("port", 143)
|
||||
use_ssl = CONFIG["imap"].get("use_ssl", False)
|
||||
host = CONFIG["imap"]["host"]
|
||||
imap_port = CONFIG["aprsd"]["email"]["imap"].get("port", 143)
|
||||
use_ssl = CONFIG["aprsd"]["email"]["imap"].get("use_ssl", False)
|
||||
host = CONFIG["aprsd"]["email"]["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"],
|
||||
CONFIG["aprsd"]["email"]["imap"]["host"],
|
||||
port=imap_port,
|
||||
use_uid=True,
|
||||
ssl=use_ssl,
|
||||
timeout=30,
|
||||
)
|
||||
except Exception:
|
||||
LOG.error("Failed to connect IMAP server")
|
||||
except Exception as e:
|
||||
LOG.error("Failed to connect IMAP server", e)
|
||||
return
|
||||
|
||||
try:
|
||||
server.login(CONFIG["imap"]["login"], CONFIG["imap"]["password"])
|
||||
server.login(
|
||||
CONFIG["aprsd"]["email"]["imap"]["login"],
|
||||
CONFIG["aprsd"]["email"]["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")
|
||||
|
||||
server.fetch = trace.trace(server.fetch)
|
||||
server.search = trace.trace(server.search)
|
||||
server.remove_flags = trace.trace(server.remove_flags)
|
||||
server.add_flags = trace.trace(server.add_flags)
|
||||
return server
|
||||
|
||||
|
||||
@trace.trace
|
||||
def _smtp_connect():
|
||||
host = CONFIG["smtp"]["host"]
|
||||
smtp_port = CONFIG["smtp"]["port"]
|
||||
use_ssl = CONFIG["smtp"].get("use_ssl", False)
|
||||
host = CONFIG["aprsd"]["email"]["smtp"]["host"]
|
||||
smtp_port = CONFIG["aprsd"]["email"]["smtp"]["port"]
|
||||
use_ssl = CONFIG["aprsd"]["email"]["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"]),
|
||||
"Connect to SMTP host {} with user '{}'".format(
|
||||
msg,
|
||||
CONFIG["aprsd"]["email"]["imap"]["login"],
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
if use_ssl:
|
||||
server = smtplib.SMTP_SSL(host=host, port=smtp_port)
|
||||
server = smtplib.SMTP_SSL(
|
||||
host=host,
|
||||
port=smtp_port,
|
||||
timeout=30,
|
||||
)
|
||||
else:
|
||||
server = smtplib.SMTP(host=host, port=smtp_port)
|
||||
server = smtplib.SMTP(
|
||||
host=host,
|
||||
port=smtp_port,
|
||||
timeout=30,
|
||||
)
|
||||
except Exception:
|
||||
LOG.error("Couldn't connect to SMTP Server")
|
||||
return
|
||||
|
||||
LOG.debug("Connected to smtp host {}".format(msg))
|
||||
|
||||
debug = CONFIG["aprsd"]["email"]["smtp"].get("debug", False)
|
||||
if debug:
|
||||
server.set_debuglevel(5)
|
||||
server.sendmail = trace.trace(server.sendmail)
|
||||
|
||||
try:
|
||||
server.login(CONFIG["smtp"]["login"], CONFIG["smtp"]["password"])
|
||||
server.login(
|
||||
CONFIG["aprsd"]["email"]["smtp"]["login"],
|
||||
CONFIG["aprsd"]["email"]["smtp"]["password"],
|
||||
)
|
||||
except Exception:
|
||||
LOG.error("Couldn't connect to SMTP Server")
|
||||
return
|
||||
@@ -78,7 +108,7 @@ def _smtp_connect():
|
||||
|
||||
|
||||
def validate_shortcuts(config):
|
||||
shortcuts = config.get("shortcuts", None)
|
||||
shortcuts = config["aprsd"]["email"].get("shortcuts", None)
|
||||
if not shortcuts:
|
||||
return
|
||||
|
||||
@@ -93,8 +123,8 @@ def validate_shortcuts(config):
|
||||
email_address=shortcuts[key],
|
||||
check_regex=True,
|
||||
check_mx=False,
|
||||
from_address=config["smtp"]["login"],
|
||||
helo_host=config["smtp"]["host"],
|
||||
from_address=config["aprsd"]["email"]["smtp"]["login"],
|
||||
helo_host=config["aprsd"]["email"]["smtp"]["host"],
|
||||
smtp_timeout=10,
|
||||
dns_timeout=10,
|
||||
use_blacklist=True,
|
||||
@@ -109,14 +139,14 @@ def validate_shortcuts(config):
|
||||
delete_keys.append(key)
|
||||
|
||||
for key in delete_keys:
|
||||
del config["shortcuts"][key]
|
||||
del config["aprsd"]["email"]["shortcuts"][key]
|
||||
|
||||
LOG.info("Available shortcuts: {}".format(config["shortcuts"]))
|
||||
LOG.info("Available shortcuts: {}".format(config["aprsd"]["email"]["shortcuts"]))
|
||||
|
||||
|
||||
def get_email_from_shortcut(addr):
|
||||
if CONFIG.get("shortcuts", False):
|
||||
return CONFIG["shortcuts"].get(addr, addr)
|
||||
if CONFIG["aprsd"]["email"].get("shortcuts", False):
|
||||
return CONFIG["aprsd"]["email"]["shortcuts"].get(addr, addr)
|
||||
else:
|
||||
return addr
|
||||
|
||||
@@ -143,6 +173,7 @@ def validate_email_config(config, disable_validation=False):
|
||||
return False
|
||||
|
||||
|
||||
@trace.trace
|
||||
def parse_email(msgid, data, server):
|
||||
envelope = data[b"ENVELOPE"]
|
||||
# email address match
|
||||
@@ -153,7 +184,12 @@ def parse_email(msgid, data, server):
|
||||
else:
|
||||
from_addr = "noaddr"
|
||||
LOG.debug("Got a message from '{}'".format(from_addr))
|
||||
m = server.fetch([msgid], ["RFC822"])
|
||||
try:
|
||||
m = server.fetch([msgid], ["RFC822"])
|
||||
except Exception as e:
|
||||
LOG.exception("Couldn't fetch email from server in parse_email", e)
|
||||
return
|
||||
|
||||
msg = email.message_from_string(m[msgid][b"RFC822"].decode(errors="ignore"))
|
||||
if msg.is_multipart():
|
||||
text = ""
|
||||
@@ -229,10 +265,11 @@ def parse_email(msgid, data, server):
|
||||
# end parse_email
|
||||
|
||||
|
||||
@trace.trace
|
||||
def send_email(to_addr, content):
|
||||
global check_email_delay
|
||||
|
||||
shortcuts = CONFIG["shortcuts"]
|
||||
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
|
||||
email_address = get_email_from_shortcut(to_addr)
|
||||
LOG.info("Sending Email_________________")
|
||||
|
||||
@@ -250,12 +287,17 @@ def send_email(to_addr, content):
|
||||
|
||||
msg = MIMEText(content)
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = CONFIG["smtp"]["login"]
|
||||
msg["From"] = CONFIG["aprsd"]["email"]["smtp"]["login"]
|
||||
msg["To"] = to_addr
|
||||
server = _smtp_connect()
|
||||
if server:
|
||||
try:
|
||||
server.sendmail(CONFIG["smtp"]["login"], [to_addr], msg.as_string())
|
||||
server.sendmail(
|
||||
CONFIG["aprsd"]["email"]["smtp"]["login"],
|
||||
[to_addr],
|
||||
msg.as_string(),
|
||||
)
|
||||
stats.APRSDStats().email_tx_inc()
|
||||
except Exception as e:
|
||||
msg = getattr(e, "message", repr(e))
|
||||
LOG.error("Sendmail Error!!!! '{}'", msg)
|
||||
@@ -268,6 +310,7 @@ def send_email(to_addr, content):
|
||||
# end send_email
|
||||
|
||||
|
||||
@trace.trace
|
||||
def resend_email(count, fromcall):
|
||||
global check_email_delay
|
||||
date = datetime.datetime.now()
|
||||
@@ -276,7 +319,7 @@ def resend_email(count, fromcall):
|
||||
year = date.year
|
||||
today = "{}-{}-{}".format(day, month, year)
|
||||
|
||||
shortcuts = CONFIG["shortcuts"]
|
||||
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
|
||||
# swap key/value
|
||||
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
|
||||
|
||||
@@ -286,7 +329,12 @@ def resend_email(count, fromcall):
|
||||
LOG.exception("Failed to Connect to IMAP. Cannot resend email ", e)
|
||||
return
|
||||
|
||||
messages = server.search(["SINCE", today])
|
||||
try:
|
||||
messages = server.search(["SINCE", today])
|
||||
except Exception as e:
|
||||
LOG.exception("Couldn't search for emails in resend_email ", e)
|
||||
return
|
||||
|
||||
# LOG.debug("%d messages received today" % len(messages))
|
||||
|
||||
msgexists = False
|
||||
@@ -294,18 +342,32 @@ def resend_email(count, fromcall):
|
||||
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()):
|
||||
try:
|
||||
parts = server.fetch(message, ["ENVELOPE"]).items()
|
||||
except Exception as e:
|
||||
LOG.exception("Couldn't fetch email parts in resend_email", e)
|
||||
continue
|
||||
|
||||
for msgid, data in list(parts):
|
||||
# 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])
|
||||
try:
|
||||
server.remove_flags(msgid, [imapclient.SEEN])
|
||||
except Exception as e:
|
||||
LOG.exception("Failed to remove SEEN flag in resend_email", e)
|
||||
|
||||
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)
|
||||
msg = messaging.TextMessage(CONFIG["aprs"]["login"], fromcall, reply)
|
||||
msg = messaging.TextMessage(
|
||||
CONFIG["aprs"]["login"],
|
||||
fromcall,
|
||||
reply,
|
||||
)
|
||||
msg.send()
|
||||
msgexists = True
|
||||
|
||||
@@ -340,6 +402,7 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||
self.msg_queues = msg_queues
|
||||
self.config = config
|
||||
|
||||
@trace.trace
|
||||
def run(self):
|
||||
global check_email_delay
|
||||
|
||||
@@ -349,6 +412,7 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||
past = datetime.datetime.now()
|
||||
while not self.thread_stop:
|
||||
time.sleep(5)
|
||||
stats.APRSDStats().email_thread_update()
|
||||
# always sleep for 5 seconds and see if we need to check email
|
||||
# This allows CTRL-C to stop the execution of this loop sooner
|
||||
# than check_email_delay time
|
||||
@@ -362,7 +426,7 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||
check_email_delay += 1
|
||||
LOG.debug("check_email_delay is " + str(check_email_delay) + " seconds")
|
||||
|
||||
shortcuts = CONFIG["shortcuts"]
|
||||
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
|
||||
# swap key/value
|
||||
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
|
||||
|
||||
@@ -376,17 +440,30 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||
try:
|
||||
server = _imap_connect()
|
||||
except Exception as e:
|
||||
LOG.exception("Failed to get IMAP server Can't check email.", e)
|
||||
LOG.exception("IMAP failed to connect.", e)
|
||||
|
||||
if not server:
|
||||
continue
|
||||
|
||||
messages = server.search(["SINCE", today])
|
||||
try:
|
||||
messages = server.search(["SINCE", today])
|
||||
except Exception as e:
|
||||
LOG.exception("IMAP failed to search for messages since today.", e)
|
||||
continue
|
||||
LOG.debug("{} messages received today".format(len(messages)))
|
||||
|
||||
for msgid, data in server.fetch(messages, ["ENVELOPE"]).items():
|
||||
try:
|
||||
_msgs = server.fetch(messages, ["ENVELOPE"])
|
||||
except Exception as e:
|
||||
LOG.exception("IMAP failed to fetch/flag messages: ", e)
|
||||
continue
|
||||
|
||||
for msgid, data in _msgs.items():
|
||||
envelope = data[b"ENVELOPE"]
|
||||
# LOG.debug('ID:%d "%s" (%s)' % (msgid, envelope.subject.decode(), envelope.date))
|
||||
LOG.debug(
|
||||
'ID:%d "%s" (%s)'
|
||||
% (msgid, envelope.subject.decode(), envelope.date),
|
||||
)
|
||||
f = re.search(
|
||||
r"'([[A-a][0-9]_-]+@[[A-a][0-9]_-\.]+)",
|
||||
str(envelope.from_[0]),
|
||||
@@ -399,16 +476,31 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||
# 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]
|
||||
]
|
||||
try:
|
||||
taglist = [
|
||||
x.decode(errors="ignore")
|
||||
for x in server.get_flags(msgid)[msgid]
|
||||
]
|
||||
except Exception as e:
|
||||
LOG.exception("Failed to get flags.", e)
|
||||
break
|
||||
|
||||
if "APRS" not in taglist:
|
||||
# if msg not flagged as sent via aprs
|
||||
server.fetch([msgid], ["RFC822"])
|
||||
try:
|
||||
server.fetch([msgid], ["RFC822"])
|
||||
except Exception as e:
|
||||
LOG.exception("Failed single server fetch for RFC822", e)
|
||||
break
|
||||
|
||||
(body, from_addr) = parse_email(msgid, data, server)
|
||||
# unset seen flag, will stay bold in email client
|
||||
server.remove_flags(msgid, [imapclient.SEEN])
|
||||
try:
|
||||
server.remove_flags(msgid, [imapclient.SEEN])
|
||||
except Exception as e:
|
||||
LOG.exception("Failed to remove flags SEEN", e)
|
||||
# Not much we can do here, so lets try and
|
||||
# send the aprs message anyway
|
||||
|
||||
if from_addr in shortcuts_inverted:
|
||||
# reverse lookup of a shortcut
|
||||
@@ -422,14 +514,28 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||
)
|
||||
self.msg_queues["tx"].put(msg)
|
||||
# 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])
|
||||
try:
|
||||
server.add_flags(msgid, ["APRS"])
|
||||
# unset seen flag, will stay bold in email client
|
||||
except Exception as e:
|
||||
LOG.exception("Couldn't add APRS flag to email", e)
|
||||
|
||||
try:
|
||||
server.remove_flags(msgid, [imapclient.SEEN])
|
||||
except Exception as e:
|
||||
LOG.exception("Couldn't remove seen flag from email", e)
|
||||
|
||||
# check email more often since we just received an email
|
||||
check_email_delay = 60
|
||||
|
||||
# reset clock
|
||||
LOG.debug("Done looping over Server.fetch, logging out.")
|
||||
past = datetime.datetime.now()
|
||||
server.logout()
|
||||
try:
|
||||
server.logout()
|
||||
except Exception as e:
|
||||
LOG.exception("IMAP failed to logout: ", e)
|
||||
continue
|
||||
else:
|
||||
# We haven't hit the email delay yet.
|
||||
# LOG.debug("Delta({}) < {}".format(now - past, check_email_delay))
|
||||
@@ -438,6 +544,3 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||
# Remove ourselves from the global threads list
|
||||
threads.APRSDThreadList().remove(self)
|
||||
LOG.info("Exiting")
|
||||
|
||||
|
||||
# end check_email()
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
import aprsd
|
||||
from aprsd import messaging, stats
|
||||
import flask
|
||||
import flask_classful
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
auth = HTTPBasicAuth()
|
||||
users = None
|
||||
|
||||
|
||||
# HTTPBasicAuth doesn't work on a class method.
|
||||
# This has to be out here. Rely on the APRSDFlask
|
||||
# class to initialize the users from the config
|
||||
@auth.verify_password
|
||||
def verify_password(username, password):
|
||||
global users
|
||||
|
||||
if username in users and check_password_hash(users.get(username), password):
|
||||
return username
|
||||
|
||||
|
||||
class APRSDFlask(flask_classful.FlaskView):
|
||||
config = None
|
||||
|
||||
def set_config(self, config):
|
||||
global users
|
||||
self.config = config
|
||||
self.users = {}
|
||||
for user in self.config["aprsd"]["web"]["users"]:
|
||||
self.users[user] = generate_password_hash(
|
||||
self.config["aprsd"]["web"]["users"][user],
|
||||
)
|
||||
|
||||
users = self.users
|
||||
|
||||
def index(self):
|
||||
return "Hello"
|
||||
# return flask.render_template("index.html", message=msg)
|
||||
|
||||
@auth.login_required
|
||||
def messages(self):
|
||||
track = messaging.MsgTrack()
|
||||
msgs = []
|
||||
for id in track:
|
||||
LOG.info(track[id].dict())
|
||||
msgs.append(track[id].dict())
|
||||
|
||||
return flask.render_template("messages.html", messages=json.dumps(msgs))
|
||||
|
||||
@auth.login_required
|
||||
def save(self):
|
||||
"""Save the existing queue to disk."""
|
||||
track = messaging.MsgTrack()
|
||||
track.save()
|
||||
return json.dumps({"messages": "saved"})
|
||||
|
||||
def stats(self):
|
||||
stats_obj = stats.APRSDStats()
|
||||
track = messaging.MsgTrack()
|
||||
|
||||
result = {
|
||||
"version": aprsd.__version__,
|
||||
"uptime": stats_obj.uptime,
|
||||
"size_tracker": len(track),
|
||||
"stats": stats_obj.stats(),
|
||||
}
|
||||
return json.dumps(result)
|
||||
|
||||
|
||||
def init_flask(config):
|
||||
flask_app = flask.Flask(
|
||||
"aprsd",
|
||||
static_url_path="",
|
||||
static_folder="web/static",
|
||||
template_folder="web/templates",
|
||||
)
|
||||
server = APRSDFlask()
|
||||
server.set_config(config)
|
||||
# flask_app.route('/', methods=['GET'])(server.index)
|
||||
flask_app.route("/stats", methods=["GET"])(server.stats)
|
||||
flask_app.route("/messages", methods=["GET"])(server.messages)
|
||||
flask_app.route("/save", methods=["GET"])(server.save)
|
||||
return flask_app
|
||||
@@ -0,0 +1,222 @@
|
||||
#
|
||||
# Used to fetch the stats url and determine if
|
||||
# aprsd server is 'healthy'
|
||||
#
|
||||
#
|
||||
# python included libs
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
from logging import NullHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import utils
|
||||
import click
|
||||
import click_completion
|
||||
import requests
|
||||
|
||||
# setup the global logger
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
LOG_LEVELS = {
|
||||
"CRITICAL": logging.CRITICAL,
|
||||
"ERROR": logging.ERROR,
|
||||
"WARNING": logging.WARNING,
|
||||
"INFO": logging.INFO,
|
||||
"DEBUG": logging.DEBUG,
|
||||
}
|
||||
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
|
||||
def custom_startswith(string, incomplete):
|
||||
"""A custom completion match that supports case insensitive matching."""
|
||||
if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"):
|
||||
string = string.lower()
|
||||
incomplete = incomplete.lower()
|
||||
return string.startswith(incomplete)
|
||||
|
||||
|
||||
click_completion.core.startswith = custom_startswith
|
||||
click_completion.init()
|
||||
|
||||
|
||||
cmd_help = """Shell completion for click-completion-command
|
||||
Available shell types:
|
||||
\b
|
||||
%s
|
||||
Default type: auto
|
||||
""" % "\n ".join(
|
||||
"{:<12} {}".format(k, click_completion.core.shells[k])
|
||||
for k in sorted(click_completion.core.shells.keys())
|
||||
)
|
||||
|
||||
|
||||
@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS)
|
||||
@click.version_option()
|
||||
def main():
|
||||
pass
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
def show(shell, case_insensitive):
|
||||
"""Show the click-completion-command completion code"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
click.echo(click_completion.core.get_code(shell, extra_env=extra_env))
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--append/--overwrite",
|
||||
help="Append the completion code to the file",
|
||||
default=None,
|
||||
)
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
@click.argument("path", required=False)
|
||||
def install(append, case_insensitive, shell, path):
|
||||
"""Install the click-completion-command completion"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
shell, path = click_completion.core.install(
|
||||
shell=shell,
|
||||
path=path,
|
||||
append=append,
|
||||
extra_env=extra_env,
|
||||
)
|
||||
click.echo("{} completion installed in {}".format(shell, path))
|
||||
|
||||
|
||||
# Setup the logging faciility
|
||||
# to disable logging to stdout, but still log to file
|
||||
# use the --quiet option on the cmdln
|
||||
def setup_logging(config, loglevel, quiet):
|
||||
log_level = LOG_LEVELS[loglevel]
|
||||
LOG.setLevel(log_level)
|
||||
log_format = "[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s]" " %(message)s"
|
||||
date_format = "%m/%d/%Y %I:%M:%S %p"
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
log_file = config["aprs"].get("logfile", None)
|
||||
if log_file:
|
||||
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
||||
else:
|
||||
fh = NullHandler()
|
||||
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
if not quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
LOG.addHandler(sh)
|
||||
|
||||
|
||||
def parse_delta_str(s):
|
||||
if "day" in s:
|
||||
m = re.match(
|
||||
r"(?P<days>[-\d]+) day[s]*, (?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d[\.\d+]*)",
|
||||
s,
|
||||
)
|
||||
else:
|
||||
m = re.match(r"(?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d[\.\d+]*)", s)
|
||||
return {key: float(val) for key, val in m.groupdict().items()}
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--loglevel",
|
||||
default="INFO",
|
||||
show_default=True,
|
||||
type=click.Choice(
|
||||
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
case_sensitive=False,
|
||||
),
|
||||
show_choices=True,
|
||||
help="The log level to use for aprsd.log",
|
||||
)
|
||||
@click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_file",
|
||||
show_default=True,
|
||||
default=utils.DEFAULT_CONFIG_FILE,
|
||||
help="The aprsd config file to use for options.",
|
||||
)
|
||||
@click.option(
|
||||
"--url",
|
||||
"health_url",
|
||||
show_default=True,
|
||||
default="http://localhost:8001/stats",
|
||||
help="The aprsd url to call for checking health/stats",
|
||||
)
|
||||
@click.option(
|
||||
"--timeout",
|
||||
show_default=True,
|
||||
default=3,
|
||||
help="How long to wait for healtcheck url to come back",
|
||||
)
|
||||
def check(loglevel, config_file, health_url, timeout):
|
||||
"""APRSD Plugin test app."""
|
||||
|
||||
config = utils.parse_config(config_file)
|
||||
|
||||
setup_logging(config, loglevel, False)
|
||||
LOG.debug("APRSD HealthCheck version: {}".format(aprsd.__version__))
|
||||
|
||||
try:
|
||||
url = health_url
|
||||
response = requests.get(url, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
except Exception as ex:
|
||||
LOG.error("Failed to fetch healthcheck url '{}' : '{}'".format(url, ex))
|
||||
sys.exit(-1)
|
||||
else:
|
||||
stats = json.loads(response.text)
|
||||
LOG.debug(stats)
|
||||
|
||||
email_thread_last_update = stats["stats"]["email"]["thread_last_update"]
|
||||
|
||||
delta = parse_delta_str(email_thread_last_update)
|
||||
d = datetime.timedelta(**delta)
|
||||
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
||||
max_delta = datetime.timedelta(**max_timeout)
|
||||
if d > max_delta:
|
||||
LOG.error("Email thread is very old! {}".format(d))
|
||||
sys.exit(-1)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+69
-44
@@ -20,6 +20,7 @@
|
||||
#
|
||||
|
||||
# python included libs
|
||||
import datetime
|
||||
import logging
|
||||
from logging import NullHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
@@ -27,17 +28,15 @@ import os
|
||||
import queue
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import client, email, messaging, plugin, threads, utils
|
||||
from aprsd import client, email, flask, messaging, plugin, stats, threads, trace, utils
|
||||
import aprslib
|
||||
from aprslib.exceptions import LoginError
|
||||
import click
|
||||
import click_completion
|
||||
import yaml
|
||||
|
||||
# setup the global logger
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
@@ -53,7 +52,9 @@ LOG_LEVELS = {
|
||||
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
server_event = threading.Event()
|
||||
flask_enabled = False
|
||||
|
||||
# server_event = threading.Event()
|
||||
|
||||
# localization, please edit:
|
||||
# HOST = "noam.aprs2.net" # north america tier2 servers round robin
|
||||
@@ -151,18 +152,23 @@ def install(append, case_insensitive, shell, path):
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
global server_vent
|
||||
global flask_enabled
|
||||
|
||||
LOG.info(
|
||||
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds to exit all threads",
|
||||
)
|
||||
threads.APRSDThreadList().stop_all()
|
||||
server_event.set()
|
||||
time.sleep(1)
|
||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
|
||||
|
||||
# end signal_handler
|
||||
if "subprocess" not in str(frame):
|
||||
LOG.info(
|
||||
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format(
|
||||
datetime.datetime.now(),
|
||||
),
|
||||
)
|
||||
time.sleep(5)
|
||||
tracker = messaging.MsgTrack()
|
||||
tracker.save()
|
||||
LOG.info(stats.APRSDStats())
|
||||
# signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
# sys.exit(0)
|
||||
if flask_enabled:
|
||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
|
||||
|
||||
# Setup the logging faciility
|
||||
@@ -174,7 +180,7 @@ def setup_logging(config, loglevel, quiet):
|
||||
log_format = "[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s]" " %(message)s"
|
||||
date_format = "%m/%d/%Y %I:%M:%S %p"
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
log_file = config["aprs"].get("logfile", None)
|
||||
log_file = config["aprsd"].get("logfile", None)
|
||||
if log_file:
|
||||
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
||||
else:
|
||||
@@ -183,16 +189,27 @@ def setup_logging(config, loglevel, quiet):
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
imap_logger = None
|
||||
if config["aprsd"]["email"].get("enabled", False) and config["aprsd"]["email"][
|
||||
"imap"
|
||||
].get("debug", False):
|
||||
|
||||
imap_logger = logging.getLogger("imapclient.imaplib")
|
||||
imap_logger.setLevel(log_level)
|
||||
imap_logger.addHandler(fh)
|
||||
|
||||
if not quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
LOG.addHandler(sh)
|
||||
if imap_logger:
|
||||
imap_logger.addHandler(sh)
|
||||
|
||||
|
||||
@main.command()
|
||||
def sample_config():
|
||||
"""This dumps the config to stdout."""
|
||||
click.echo(utils.add_config_comments(yaml.dump(utils.DEFAULT_CONFIG_DICT)))
|
||||
click.echo(utils.dump_default_cfg())
|
||||
|
||||
|
||||
@main.command()
|
||||
@@ -385,24 +402,15 @@ def send_message(
|
||||
default=False,
|
||||
help="Flush out all old aged messages on disk.",
|
||||
)
|
||||
@click.option(
|
||||
"--stats-server",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Run a stats web server on port 5001?",
|
||||
)
|
||||
def server(
|
||||
loglevel,
|
||||
quiet,
|
||||
disable_validation,
|
||||
config_file,
|
||||
flush,
|
||||
stats_server,
|
||||
):
|
||||
"""Start the aprsd server process."""
|
||||
global event
|
||||
|
||||
event = threading.Event()
|
||||
global flask_enabled
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
if not quiet:
|
||||
@@ -416,14 +424,22 @@ def server(
|
||||
email.CONFIG = config
|
||||
|
||||
setup_logging(config, loglevel, quiet)
|
||||
if config["aprsd"].get("trace", False):
|
||||
trace.setup_tracing(["method", "api"])
|
||||
LOG.info("APRSD Started version: {}".format(aprsd.__version__))
|
||||
stats.APRSDStats(config)
|
||||
|
||||
# TODO(walt): Make email processing/checking optional?
|
||||
# Maybe someone only wants this to process messages with plugins only.
|
||||
valid = email.validate_email_config(config, disable_validation)
|
||||
if not valid:
|
||||
LOG.error("Failed to validate email config options")
|
||||
sys.exit(-1)
|
||||
email_enabled = config["aprsd"]["email"].get("enabled", False)
|
||||
|
||||
if email_enabled:
|
||||
# TODO(walt): Make email processing/checking optional?
|
||||
# Maybe someone only wants this to process messages with plugins only.
|
||||
valid = email.validate_email_config(config, disable_validation)
|
||||
if not valid:
|
||||
LOG.error("Failed to validate email config options")
|
||||
sys.exit(-1)
|
||||
else:
|
||||
LOG.info("Email services not enabled.")
|
||||
|
||||
# Create the initial PM singleton and Register plugins
|
||||
plugin_manager = plugin.PluginManager(config)
|
||||
@@ -449,26 +465,35 @@ def server(
|
||||
|
||||
rx_thread = threads.APRSDRXThread(msg_queues=msg_queues, config=config)
|
||||
tx_thread = threads.APRSDTXThread(msg_queues=msg_queues, config=config)
|
||||
email_thread = email.APRSDEmailThread(msg_queues=msg_queues, config=config)
|
||||
email_thread.start()
|
||||
|
||||
if email_enabled:
|
||||
email_thread = email.APRSDEmailThread(msg_queues=msg_queues, config=config)
|
||||
email_thread.start()
|
||||
|
||||
rx_thread.start()
|
||||
tx_thread.start()
|
||||
|
||||
messaging.MsgTrack().restart()
|
||||
|
||||
cntr = 0
|
||||
while not server_event.is_set():
|
||||
# to keep the log noise down
|
||||
if cntr % 12 == 0:
|
||||
tracker = messaging.MsgTrack()
|
||||
LOG.debug("KeepAlive Tracker({}): {}".format(len(tracker), str(tracker)))
|
||||
cntr += 1
|
||||
time.sleep(10)
|
||||
keepalive = threads.KeepAliveThread()
|
||||
keepalive.start()
|
||||
|
||||
try:
|
||||
web_enabled = utils.check_config_option(config, ["aprsd", "web", "enabled"])
|
||||
except Exception:
|
||||
web_enabled = False
|
||||
|
||||
if web_enabled:
|
||||
flask_enabled = True
|
||||
app = flask.init_flask(config)
|
||||
app.run(
|
||||
host=config["aprsd"]["web"]["host"],
|
||||
port=config["aprsd"]["web"]["port"],
|
||||
)
|
||||
|
||||
# If there are items in the msgTracker, then save them
|
||||
tracker = messaging.MsgTrack()
|
||||
tracker.save()
|
||||
LOG.info("APRSD Exiting.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+102
-48
@@ -9,7 +9,7 @@ import re
|
||||
import threading
|
||||
import time
|
||||
|
||||
from aprsd import client, threads, utils
|
||||
from aprsd import client, stats, threads, utils
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
@@ -49,26 +49,29 @@ class MsgTrack:
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance.track = {}
|
||||
cls._start_time = datetime.datetime.now()
|
||||
cls._instance._start_time = datetime.datetime.now()
|
||||
cls._instance.lock = threading.Lock()
|
||||
return cls._instance
|
||||
|
||||
def add(self, msg):
|
||||
def __getitem__(self, name):
|
||||
with self.lock:
|
||||
key = int(msg.id)
|
||||
self.track[key] = msg
|
||||
self.total_messages_tracked += 1
|
||||
return self.track[name]
|
||||
|
||||
def get(self, id):
|
||||
def __iter__(self):
|
||||
with self.lock:
|
||||
if id in self.track:
|
||||
return self.track[id]
|
||||
return iter(self.track)
|
||||
|
||||
def remove(self, id):
|
||||
def keys(self):
|
||||
with self.lock:
|
||||
key = int(id)
|
||||
if key in self.track.keys():
|
||||
del self.track[key]
|
||||
return self.track.keys()
|
||||
|
||||
def items(self):
|
||||
with self.lock:
|
||||
return self.track.items()
|
||||
|
||||
def values(self):
|
||||
with self.lock:
|
||||
return self.track.values()
|
||||
|
||||
def __len__(self):
|
||||
with self.lock:
|
||||
@@ -82,12 +85,36 @@ class MsgTrack:
|
||||
result += "}"
|
||||
return result
|
||||
|
||||
def add(self, msg):
|
||||
with self.lock:
|
||||
key = int(msg.id)
|
||||
self.track[key] = msg
|
||||
stats.APRSDStats().msgs_tracked_inc()
|
||||
self.total_messages_tracked += 1
|
||||
|
||||
def get(self, id):
|
||||
with self.lock:
|
||||
if id in self.track:
|
||||
return self.track[id]
|
||||
|
||||
def remove(self, id):
|
||||
with self.lock:
|
||||
key = int(id)
|
||||
if key in self.track.keys():
|
||||
del self.track[key]
|
||||
|
||||
def save(self):
|
||||
"""Save this shit to disk?"""
|
||||
"""Save any queued to disk?"""
|
||||
LOG.debug("Save tracker to disk? {}".format(len(self)))
|
||||
if len(self) > 0:
|
||||
LOG.info("Saving {} tracking messages to disk".format(len(self)))
|
||||
pickle.dump(self.dump(), open(utils.DEFAULT_SAVE_FILE, "wb+"))
|
||||
else:
|
||||
LOG.debug(
|
||||
"Nothing to save, flushing old save file '{}'".format(
|
||||
utils.DEFAULT_SAVE_FILE,
|
||||
),
|
||||
)
|
||||
self.flush()
|
||||
|
||||
def dump(self):
|
||||
@@ -228,8 +255,17 @@ class RawMessage(Message):
|
||||
super().__init__(None, None, msg_id=None)
|
||||
self.message = message
|
||||
|
||||
def __repr__(self):
|
||||
return self.message
|
||||
def dict(self):
|
||||
now = datetime.datetime.now()
|
||||
return {
|
||||
"type": "raw",
|
||||
"message": self.message.rstrip("\n"),
|
||||
"raw": self.message.rstrip("\n"),
|
||||
"retry_count": self.retry_count,
|
||||
"last_send_attempt": self.last_send_attempt,
|
||||
"last_send_time": str(self.last_send_time),
|
||||
"last_send_age": str(now - self.last_send_time),
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
@@ -245,12 +281,13 @@ class RawMessage(Message):
|
||||
cl = client.get_client()
|
||||
log_message(
|
||||
"Sending Message Direct",
|
||||
repr(self).rstrip("\n"),
|
||||
str(self).rstrip("\n"),
|
||||
self.message,
|
||||
tocall=self.tocall,
|
||||
fromcall=self.fromcall,
|
||||
)
|
||||
cl.sendall(repr(self))
|
||||
cl.sendall(str(self))
|
||||
stats.APRSDStats().msgs_sent_inc()
|
||||
|
||||
|
||||
class TextMessage(Message):
|
||||
@@ -265,28 +302,30 @@ class TextMessage(Message):
|
||||
# an ack? Some messages we don't want to do this ever.
|
||||
self.allow_delay = allow_delay
|
||||
|
||||
def __repr__(self):
|
||||
def dict(self):
|
||||
now = datetime.datetime.now()
|
||||
return {
|
||||
"id": self.id,
|
||||
"type": "text-message",
|
||||
"fromcall": self.fromcall,
|
||||
"tocall": self.tocall,
|
||||
"message": self.message.rstrip("\n"),
|
||||
"raw": str(self).rstrip("\n"),
|
||||
"retry_count": self.retry_count,
|
||||
"last_send_attempt": self.last_send_attempt,
|
||||
"last_send_time": str(self.last_send_time),
|
||||
"last_send_age": str(now - self.last_send_time),
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
"""Build raw string to send over the air."""
|
||||
return "{}>APRS::{}:{}{{{}\n".format(
|
||||
return "{}>APZ100::{}:{}{{{}\n".format(
|
||||
self.fromcall,
|
||||
self.tocall.ljust(9),
|
||||
self._filter_for_send(),
|
||||
str(self.id),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
delta = "Never"
|
||||
if self.last_send_time:
|
||||
now = datetime.datetime.now()
|
||||
delta = now - self.last_send_time
|
||||
return "{}>{} Msg({})({}): '{}'".format(
|
||||
self.fromcall,
|
||||
self.tocall,
|
||||
self.id,
|
||||
delta,
|
||||
self.message,
|
||||
)
|
||||
|
||||
def _filter_for_send(self):
|
||||
"""Filter and format message string for FCC."""
|
||||
# max? ftm400 displays 64, raw msg shows 74
|
||||
@@ -309,12 +348,13 @@ class TextMessage(Message):
|
||||
cl = client.get_client()
|
||||
log_message(
|
||||
"Sending Message Direct",
|
||||
repr(self).rstrip("\n"),
|
||||
str(self).rstrip("\n"),
|
||||
self.message,
|
||||
tocall=self.tocall,
|
||||
fromcall=self.fromcall,
|
||||
)
|
||||
cl.sendall(repr(self))
|
||||
cl.sendall(str(self))
|
||||
stats.APRSDStats().msgs_tx_inc()
|
||||
|
||||
|
||||
class SendMessageThread(threads.APRSDThread):
|
||||
@@ -367,13 +407,14 @@ class SendMessageThread(threads.APRSDThread):
|
||||
# tracking the time.
|
||||
log_message(
|
||||
"Sending Message",
|
||||
repr(msg).rstrip("\n"),
|
||||
str(msg).rstrip("\n"),
|
||||
msg.message,
|
||||
tocall=self.msg.tocall,
|
||||
retry_number=msg.last_send_attempt,
|
||||
msg_num=msg.id,
|
||||
)
|
||||
cl.sendall(repr(msg))
|
||||
cl.sendall(str(msg))
|
||||
stats.APRSDStats().msgs_tx_inc()
|
||||
msg.last_send_time = datetime.datetime.now()
|
||||
msg.last_send_attempt += 1
|
||||
|
||||
@@ -388,29 +429,41 @@ class AckMessage(Message):
|
||||
def __init__(self, fromcall, tocall, msg_id):
|
||||
super().__init__(fromcall, tocall, msg_id=msg_id)
|
||||
|
||||
def __repr__(self):
|
||||
return "{}>APRS::{}:ack{}\n".format(
|
||||
def dict(self):
|
||||
now = datetime.datetime.now()
|
||||
return {
|
||||
"id": self.id,
|
||||
"type": "ack",
|
||||
"fromcall": self.fromcall,
|
||||
"tocall": self.tocall,
|
||||
"raw": str(self).rstrip("\n"),
|
||||
"retry_count": self.retry_count,
|
||||
"last_send_attempt": self.last_send_attempt,
|
||||
"last_send_time": str(self.last_send_time),
|
||||
"last_send_age": str(now - self.last_send_time),
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return "{}>APZ100::{}:ack{}\n".format(
|
||||
self.fromcall,
|
||||
self.tocall.ljust(9),
|
||||
self.id,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "From({}) TO({}) Ack ({})".format(self.fromcall, self.tocall, self.id)
|
||||
|
||||
def send_thread(self):
|
||||
"""Separate thread to send acks with retries."""
|
||||
cl = client.get_client()
|
||||
for i in range(self.retry_count, 0, -1):
|
||||
log_message(
|
||||
"Sending ack",
|
||||
repr(self).rstrip("\n"),
|
||||
str(self).rstrip("\n"),
|
||||
None,
|
||||
ack=self.id,
|
||||
tocall=self.tocall,
|
||||
retry_number=i,
|
||||
)
|
||||
cl.sendall(repr(self))
|
||||
cl.sendall(str(self))
|
||||
stats.APRSDStats().ack_tx_inc()
|
||||
# aprs duplicate detection is 30 secs?
|
||||
# (21 only sends first, 28 skips middle)
|
||||
time.sleep(31)
|
||||
@@ -428,13 +481,13 @@ class AckMessage(Message):
|
||||
cl = client.get_client()
|
||||
log_message(
|
||||
"Sending ack",
|
||||
repr(self).rstrip("\n"),
|
||||
str(self).rstrip("\n"),
|
||||
None,
|
||||
ack=self.id,
|
||||
tocall=self.tocall,
|
||||
fromcall=self.fromcall,
|
||||
)
|
||||
cl.sendall(repr(self))
|
||||
cl.sendall(str(self))
|
||||
|
||||
|
||||
class SendAckThread(threads.APRSDThread):
|
||||
@@ -471,13 +524,14 @@ class SendAckThread(threads.APRSDThread):
|
||||
cl = client.get_client()
|
||||
log_message(
|
||||
"Sending ack",
|
||||
repr(self.ack).rstrip("\n"),
|
||||
str(self.ack).rstrip("\n"),
|
||||
None,
|
||||
ack=self.ack.id,
|
||||
tocall=self.ack.tocall,
|
||||
retry_number=self.ack.last_send_attempt,
|
||||
)
|
||||
cl.sendall(repr(self.ack))
|
||||
cl.sendall(str(self.ack))
|
||||
stats.APRSDStats().ack_tx_inc()
|
||||
self.ack.last_send_attempt += 1
|
||||
self.ack.last_send_time = datetime.datetime.now()
|
||||
time.sleep(5)
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ CORE_PLUGINS = [
|
||||
"aprsd.plugins.ping.PingPlugin",
|
||||
"aprsd.plugins.query.QueryPlugin",
|
||||
"aprsd.plugins.time.TimePlugin",
|
||||
"aprsd.plugins.weather.WeatherPlugin",
|
||||
"aprsd.plugins.weather.USWeatherPlugin",
|
||||
"aprsd.plugins.version.VersionPlugin",
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# Utilities for plugins to use
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
def get_aprs_fi(api_key, callsign):
|
||||
LOG.debug("Fetch aprs.fi location for '{}'".format(callsign))
|
||||
try:
|
||||
url = (
|
||||
"http://api.aprs.fi/api/get?"
|
||||
"&what=loc&apikey={}&format=json"
|
||||
"&name={}".format(api_key, callsign)
|
||||
)
|
||||
response = requests.get(url)
|
||||
except Exception:
|
||||
raise Exception("Failed to get aprs.fi location")
|
||||
else:
|
||||
response.raise_for_status()
|
||||
return json.loads(response.text)
|
||||
|
||||
|
||||
def get_weather_gov_for_gps(lat, lon):
|
||||
LOG.debug("Fetch station at {}, {}".format(lat, lon))
|
||||
try:
|
||||
url2 = (
|
||||
"https://forecast.weather.gov/MapClick.php?lat=%s"
|
||||
"&lon=%s&FcstType=json" % (lat, lon)
|
||||
)
|
||||
LOG.debug("Fetching weather '{}'".format(url2))
|
||||
response = requests.get(url2)
|
||||
except Exception as e:
|
||||
LOG.error(e)
|
||||
raise Exception("Failed to get weather")
|
||||
else:
|
||||
response.raise_for_status()
|
||||
return json.loads(response.text)
|
||||
|
||||
|
||||
def get_weather_gov_metar(station):
|
||||
LOG.debug("Fetch metar for station '{}'".format(station))
|
||||
try:
|
||||
url = "https://api.weather.gov/stations/{}/observations/latest".format(
|
||||
station,
|
||||
)
|
||||
response = requests.get(url)
|
||||
except Exception:
|
||||
raise Exception("Failed to fetch metar")
|
||||
else:
|
||||
response.raise_for_status()
|
||||
return json.loads(response)
|
||||
|
||||
|
||||
def fetch_openweathermap(api_key, lat, lon, units="metric", exclude=None):
|
||||
LOG.debug("Fetch openweathermap for {}, {}".format(lat, lon))
|
||||
if not exclude:
|
||||
exclude = "minutely,hourly,daily,alerts"
|
||||
try:
|
||||
url = (
|
||||
"https://api.openweathermap.org/data/2.5/onecall?"
|
||||
"lat={}&lon={}&appid={}&units={}&exclude={}".format(
|
||||
lat,
|
||||
lon,
|
||||
api_key,
|
||||
units,
|
||||
exclude,
|
||||
)
|
||||
)
|
||||
response = requests.get(url)
|
||||
except Exception as e:
|
||||
LOG.error(e)
|
||||
raise Exception("Failed to get weather")
|
||||
else:
|
||||
response.raise_for_status()
|
||||
return json.loads(response.text)
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
from aprsd import email, messaging, plugin
|
||||
from aprsd import email, messaging, plugin, trace
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
@@ -18,9 +18,13 @@ class EmailPlugin(plugin.APRSDPluginBase):
|
||||
# five mins {int:int}
|
||||
email_sent_dict = {}
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("Email COMMAND")
|
||||
reply = None
|
||||
if not self.config["aprsd"]["email"].get("enabled", False):
|
||||
LOG.debug("Email is not enabled in config file ignoring.")
|
||||
return "Email not enabled."
|
||||
|
||||
searchstring = "^" + self.config["ham"]["callsign"] + ".*"
|
||||
# only I can do email
|
||||
@@ -76,6 +80,7 @@ class EmailPlugin(plugin.APRSDPluginBase):
|
||||
self.email_sent_dict.clear()
|
||||
self.email_sent_dict[ack] = now
|
||||
else:
|
||||
reply = messaging.NULL_MESSAGE
|
||||
LOG.info(
|
||||
"Email for message number "
|
||||
+ ack
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from aprsd import plugin
|
||||
from aprsd import plugin, trace
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
@@ -14,6 +14,7 @@ class FortunePlugin(plugin.APRSDPluginBase):
|
||||
command_regex = "^[fF]"
|
||||
command_name = "fortune"
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("FortunePlugin")
|
||||
reply = None
|
||||
@@ -32,6 +33,12 @@ class FortunePlugin(plugin.APRSDPluginBase):
|
||||
timeout=3,
|
||||
universal_newlines=True,
|
||||
)
|
||||
output = (
|
||||
output.replace("\r", "")
|
||||
.replace("\n", "")
|
||||
.replace(" ", "")
|
||||
.replace("\t", " ")
|
||||
)
|
||||
except subprocess.CalledProcessError as ex:
|
||||
reply = "Fortune command failed '{}'".format(ex.output)
|
||||
else:
|
||||
|
||||
+60
-56
@@ -1,10 +1,8 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
from aprsd import plugin
|
||||
import requests
|
||||
from aprsd import plugin, plugin_utils, trace, utils
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
@@ -16,63 +14,69 @@ class LocationPlugin(plugin.APRSDPluginBase):
|
||||
command_regex = "^[lL]"
|
||||
command_name = "location"
|
||||
|
||||
config_items = {"apikey": "aprs.fi api key here"}
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("Location Plugin")
|
||||
# get last location of a callsign, get descriptive name from weather service
|
||||
api_key = self.config["aprs.fi"]["apiKey"]
|
||||
try:
|
||||
# optional second argument is a callsign to search
|
||||
a = re.search(r"^.*\s+(.*)", message)
|
||||
if a is not None:
|
||||
searchcall = a.group(1)
|
||||
searchcall = searchcall.upper()
|
||||
else:
|
||||
# if no second argument, search for calling station
|
||||
searchcall = fromcall
|
||||
url = (
|
||||
"http://api.aprs.fi/api/get?name="
|
||||
+ searchcall
|
||||
+ "&what=loc&apikey={}&format=json".format(api_key)
|
||||
)
|
||||
response = requests.get(url)
|
||||
# aprs_data = json.loads(response.read())
|
||||
aprs_data = json.loads(response.text)
|
||||
LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
|
||||
lat = aprs_data["entries"][0]["lat"]
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
try: # altitude not always provided
|
||||
alt = aprs_data["entries"][0]["altitude"]
|
||||
except Exception:
|
||||
alt = 0
|
||||
altfeet = int(alt * 3.28084)
|
||||
aprs_lasttime_seconds = aprs_data["entries"][0]["lasttime"]
|
||||
# aprs_lasttime_seconds = aprs_lasttime_seconds.encode(
|
||||
# "ascii", errors="ignore"
|
||||
# ) # unicode to ascii
|
||||
delta_seconds = time.time() - int(aprs_lasttime_seconds)
|
||||
delta_hours = delta_seconds / 60 / 60
|
||||
url2 = (
|
||||
"https://forecast.weather.gov/MapClick.php?lat="
|
||||
+ str(lat)
|
||||
+ "&lon="
|
||||
+ str(lon)
|
||||
+ "&FcstType=json"
|
||||
)
|
||||
response2 = requests.get(url2)
|
||||
wx_data = json.loads(response2.text)
|
||||
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
||||
except Exception as ex:
|
||||
LOG.error("Failed to find config aprs.fi:apikey {}".format(ex))
|
||||
return "No aprs.fi apikey found"
|
||||
|
||||
reply = "{}: {} {}' {},{} {}h ago".format(
|
||||
searchcall,
|
||||
wx_data["location"]["areaDescription"],
|
||||
str(altfeet),
|
||||
str(lat),
|
||||
str(lon),
|
||||
str("%.1f" % round(delta_hours, 1)),
|
||||
).rstrip()
|
||||
except Exception as e:
|
||||
LOG.debug("Locate failed with: " + "%s" % str(e))
|
||||
reply = "Unable to find station " + searchcall + ". Sending beacons?"
|
||||
api_key = self.config["services"]["aprs.fi"]["apiKey"]
|
||||
|
||||
# optional second argument is a callsign to search
|
||||
a = re.search(r"^.*\s+(.*)", message)
|
||||
if a is not None:
|
||||
searchcall = a.group(1)
|
||||
searchcall = searchcall.upper()
|
||||
else:
|
||||
# if no second argument, search for calling station
|
||||
searchcall = fromcall
|
||||
|
||||
try:
|
||||
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
|
||||
except Exception as ex:
|
||||
LOG.error("Failed to fetch aprs.fi '{}'".format(ex))
|
||||
return "Failed to fetch aprs.fi location"
|
||||
|
||||
LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
|
||||
if not len(aprs_data["entries"]):
|
||||
LOG.error("Didn't get any entries from aprs.fi")
|
||||
return "Failed to fetch aprs.fi location"
|
||||
|
||||
lat = aprs_data["entries"][0]["lat"]
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
try: # altitude not always provided
|
||||
alt = float(aprs_data["entries"][0]["altitude"])
|
||||
except Exception:
|
||||
alt = 0
|
||||
altfeet = int(alt * 3.28084)
|
||||
aprs_lasttime_seconds = aprs_data["entries"][0]["lasttime"]
|
||||
# aprs_lasttime_seconds = aprs_lasttime_seconds.encode(
|
||||
# "ascii", errors="ignore"
|
||||
# ) # unicode to ascii
|
||||
delta_seconds = time.time() - int(aprs_lasttime_seconds)
|
||||
delta_hours = delta_seconds / 60 / 60
|
||||
|
||||
try:
|
||||
wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon)
|
||||
except Exception as ex:
|
||||
LOG.error("Couldn't fetch forecast.weather.gov '{}'".format(ex))
|
||||
wx_data = {"location": {"areaDescription": "Unknown Location"}}
|
||||
|
||||
if "location" not in wx_data:
|
||||
LOG.error("Couldn't fetch forecast.weather.gov '{}'".format(wx_data))
|
||||
wx_data = {"location": {"areaDescription": "Unknown Location"}}
|
||||
|
||||
reply = "{}: {} {}' {},{} {}h ago".format(
|
||||
searchcall,
|
||||
wx_data["location"]["areaDescription"],
|
||||
str(altfeet),
|
||||
str(lat),
|
||||
str(lon),
|
||||
str("%.1f" % round(delta_hours, 1)),
|
||||
).rstrip()
|
||||
|
||||
return reply
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
from aprsd import plugin
|
||||
from aprsd import plugin, trace
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
@@ -13,6 +13,7 @@ class PingPlugin(plugin.APRSDPluginBase):
|
||||
command_regex = "^[pP]"
|
||||
command_name = "ping"
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("PINGPlugin")
|
||||
stm = time.localtime()
|
||||
|
||||
@@ -2,7 +2,7 @@ import datetime
|
||||
import logging
|
||||
import re
|
||||
|
||||
from aprsd import messaging, plugin
|
||||
from aprsd import messaging, plugin, trace
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
@@ -11,9 +11,10 @@ class QueryPlugin(plugin.APRSDPluginBase):
|
||||
"""Query command."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = r"^\?.*"
|
||||
command_regex = r"^\!.*"
|
||||
command_name = "query"
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("Query COMMAND")
|
||||
|
||||
@@ -28,8 +29,8 @@ class QueryPlugin(plugin.APRSDPluginBase):
|
||||
# only I can do admin commands
|
||||
if re.search(searchstring, fromcall):
|
||||
|
||||
# resend last N most recent: "?3"
|
||||
r = re.search(r"^\?([0-9]).*", message)
|
||||
# resend last N most recent: "!3"
|
||||
r = re.search(r"^\!([0-9]).*", message)
|
||||
if r is not None:
|
||||
if len(tracker) > 0:
|
||||
last_n = r.group(1)
|
||||
@@ -41,8 +42,8 @@ class QueryPlugin(plugin.APRSDPluginBase):
|
||||
LOG.debug(reply)
|
||||
return reply
|
||||
|
||||
# resend all: "?a"
|
||||
r = re.search(r"^\?[aA].*", message)
|
||||
# resend all: "!a"
|
||||
r = re.search(r"^\![aA].*", message)
|
||||
if r is not None:
|
||||
if len(tracker) > 0:
|
||||
reply = messaging.NULL_MESSAGE
|
||||
@@ -53,8 +54,8 @@ class QueryPlugin(plugin.APRSDPluginBase):
|
||||
LOG.debug(reply)
|
||||
return reply
|
||||
|
||||
# delete all: "?d"
|
||||
r = re.search(r"^\?[dD].*", message)
|
||||
# delete all: "!d"
|
||||
r = re.search(r"^\![dD].*", message)
|
||||
if r is not None:
|
||||
reply = "Deleted ALL pending msgs."
|
||||
LOG.debug(reply)
|
||||
|
||||
+114
-12
@@ -1,7 +1,9 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
from aprsd import fuzzyclock, plugin
|
||||
from aprsd import fuzzyclock, plugin, plugin_utils, trace, utils
|
||||
from opencage.geocoder import OpenCageGeocode
|
||||
import pytz
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
@@ -13,16 +15,116 @@ class TimePlugin(plugin.APRSDPluginBase):
|
||||
command_regex = "^[tT]"
|
||||
command_name = "time"
|
||||
|
||||
def _get_local_tz(self):
|
||||
return pytz.timezone(time.strftime("%Z"))
|
||||
|
||||
def _get_utcnow(self):
|
||||
return pytz.datetime.datetime.utcnow()
|
||||
|
||||
def build_date_str(self, localzone):
|
||||
utcnow = self._get_utcnow()
|
||||
gmt_t = pytz.utc.localize(utcnow)
|
||||
local_t = gmt_t.astimezone(localzone)
|
||||
|
||||
local_short_str = local_t.strftime("%H:%M %Z")
|
||||
local_hour = local_t.strftime("%H")
|
||||
local_min = local_t.strftime("%M")
|
||||
cur_time = fuzzyclock.fuzzy(int(local_hour), int(local_min), 1)
|
||||
|
||||
reply = "{} ({})".format(
|
||||
cur_time,
|
||||
local_short_str,
|
||||
)
|
||||
|
||||
return reply
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("TIME COMMAND")
|
||||
stm = time.localtime()
|
||||
h = stm.tm_hour
|
||||
m = stm.tm_min
|
||||
cur_time = fuzzyclock.fuzzy(h, m, 1)
|
||||
reply = "{} ({}:{} PDT) ({})".format(
|
||||
cur_time,
|
||||
str(h),
|
||||
str(m).rjust(2, "0"),
|
||||
message.rstrip(),
|
||||
)
|
||||
return reply
|
||||
# So we can mock this in unit tests
|
||||
localzone = self._get_local_tz()
|
||||
return self.build_date_str(localzone)
|
||||
|
||||
|
||||
class TimeOpenCageDataPlugin(TimePlugin):
|
||||
"""geocage based timezone fetching."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[tT]"
|
||||
command_name = "Time"
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
api_key = self.config["services"]["aprs.fi"]["apiKey"]
|
||||
try:
|
||||
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
|
||||
except Exception as ex:
|
||||
LOG.error("Failed to fetch aprs.fi data {}".format(ex))
|
||||
return "Failed to fetch location"
|
||||
|
||||
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
|
||||
lat = aprs_data["entries"][0]["lat"]
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
|
||||
try:
|
||||
utils.check_config_option(self.config, "opencagedata", "apiKey")
|
||||
except Exception as ex:
|
||||
LOG.error("Failed to find config opencage:apiKey {}".format(ex))
|
||||
return "No opencage apiKey found"
|
||||
|
||||
try:
|
||||
opencage_key = self.config["opencagedata"]["apiKey"]
|
||||
geocoder = OpenCageGeocode(opencage_key)
|
||||
results = geocoder.reverse_geocode(lat, lon)
|
||||
except Exception as ex:
|
||||
LOG.error("Couldn't fetch opencagedata api '{}'".format(ex))
|
||||
# Default to UTC instead
|
||||
localzone = pytz.timezone("UTC")
|
||||
else:
|
||||
tzone = results[0]["annotations"]["timezone"]["name"]
|
||||
localzone = pytz.timezone(tzone)
|
||||
|
||||
return self.build_date_str(localzone)
|
||||
|
||||
|
||||
class TimeOWMPlugin(TimePlugin):
|
||||
"""OpenWeatherMap based timezone fetching."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[tT]"
|
||||
command_name = "Time"
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
api_key = self.config["services"]["aprs.fi"]["apiKey"]
|
||||
try:
|
||||
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
|
||||
except Exception as ex:
|
||||
LOG.error("Failed to fetch aprs.fi data {}".format(ex))
|
||||
return "Failed to fetch location"
|
||||
|
||||
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
|
||||
lat = aprs_data["entries"][0]["lat"]
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
|
||||
try:
|
||||
utils.check_config_option(
|
||||
self.config,
|
||||
["services", "openweathermap", "apiKey"],
|
||||
)
|
||||
except Exception as ex:
|
||||
LOG.error("Failed to find config openweathermap:apiKey {}".format(ex))
|
||||
return "No openweathermap apiKey found"
|
||||
|
||||
api_key = self.config["services"]["openweathermap"]["apiKey"]
|
||||
try:
|
||||
results = plugin_utils.fetch_openweathermap(api_key, lat, lon)
|
||||
except Exception as ex:
|
||||
LOG.error("Couldn't fetch openweathermap api '{}'".format(ex))
|
||||
# default to UTC
|
||||
localzone = pytz.timezone("UTC")
|
||||
else:
|
||||
tzone = results["timezone"]
|
||||
localzone = pytz.timezone(tzone)
|
||||
|
||||
return self.build_date_str(localzone)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
|
||||
import aprsd
|
||||
from aprsd import plugin
|
||||
from aprsd import plugin, trace
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
@@ -17,6 +17,7 @@ class VersionPlugin(plugin.APRSDPluginBase):
|
||||
# five mins {int:int}
|
||||
email_sent_dict = {}
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("Version COMMAND")
|
||||
return "APRSD version '{}'".format(aprsd.__version__)
|
||||
|
||||
+364
-33
@@ -1,54 +1,385 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
from aprsd import plugin
|
||||
from aprsd import plugin, plugin_utils, trace, utils
|
||||
import requests
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class WeatherPlugin(plugin.APRSDPluginBase):
|
||||
"""Weather Command"""
|
||||
class USWeatherPlugin(plugin.APRSDPluginBase):
|
||||
"""USWeather Command
|
||||
|
||||
Returns a weather report for the calling weather station
|
||||
inside the United States only. This uses the
|
||||
forecast.weather.gov API to fetch the weather.
|
||||
|
||||
This service does not require an apiKey.
|
||||
|
||||
How to Call: Send a message to aprsd
|
||||
"weather" - returns weather near the calling callsign
|
||||
"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[wW]"
|
||||
command_name = "weather"
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("Weather Plugin")
|
||||
api_key = self.config["aprs.fi"]["apiKey"]
|
||||
try:
|
||||
url = (
|
||||
"http://api.aprs.fi/api/get?"
|
||||
"&what=loc&apikey={}&format=json"
|
||||
"&name={}".format(api_key, fromcall)
|
||||
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
||||
except Exception as ex:
|
||||
LOG.error("Failed to find config aprs.fi:apikey {}".format(ex))
|
||||
return "No aprs.fi apikey found"
|
||||
|
||||
api_key = self.config["services"]["aprs.fi"]["apiKey"]
|
||||
try:
|
||||
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
|
||||
except Exception as ex:
|
||||
LOG.error("Failed to fetch aprs.fi data {}".format(ex))
|
||||
return "Failed to fetch location"
|
||||
|
||||
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
|
||||
lat = aprs_data["entries"][0]["lat"]
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
|
||||
try:
|
||||
wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon)
|
||||
except Exception as ex:
|
||||
LOG.error("Couldn't fetch forecast.weather.gov '{}'".format(ex))
|
||||
return "Unable to get weather"
|
||||
|
||||
reply = (
|
||||
"%sF(%sF/%sF) %s. %s, %s."
|
||||
% (
|
||||
wx_data["currentobservation"]["Temp"],
|
||||
wx_data["data"]["temperature"][0],
|
||||
wx_data["data"]["temperature"][1],
|
||||
wx_data["data"]["weather"][0],
|
||||
wx_data["time"]["startPeriodName"][1],
|
||||
wx_data["data"]["weather"][1],
|
||||
)
|
||||
response = requests.get(url)
|
||||
# aprs_data = json.loads(response.read())
|
||||
aprs_data = json.loads(response.text)
|
||||
).rstrip()
|
||||
LOG.debug("reply: '{}' ".format(reply))
|
||||
return reply
|
||||
|
||||
|
||||
class USMetarPlugin(plugin.APRSDPluginBase):
|
||||
"""METAR Command
|
||||
|
||||
This provides a METAR weather report from a station near the caller
|
||||
or callsign using the forecast.weather.gov api. This only works
|
||||
for stations inside the United States.
|
||||
|
||||
This service does not require an apiKey.
|
||||
|
||||
How to Call: Send a message to aprsd
|
||||
"metar" - returns metar report near the calling callsign
|
||||
"metar CALLSIGN" - returns metar report near CALLSIGN
|
||||
|
||||
"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[metar]"
|
||||
command_name = "Metar"
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("WX Plugin '{}'".format(message))
|
||||
a = re.search(r"^.*\s+(.*)", message)
|
||||
if a is not None:
|
||||
searchcall = a.group(1)
|
||||
station = searchcall.upper()
|
||||
try:
|
||||
resp = plugin_utils.get_weather_gov_metar(station)
|
||||
except Exception as e:
|
||||
LOG.debug("Weather failed with: {}".format(str(e)))
|
||||
reply = "Unable to find station METAR"
|
||||
else:
|
||||
station_data = json.loads(resp.text)
|
||||
reply = station_data["properties"]["rawMessage"]
|
||||
|
||||
return reply
|
||||
else:
|
||||
# if no second argument, search for calling station
|
||||
fromcall = fromcall
|
||||
|
||||
try:
|
||||
utils.check_config_option(
|
||||
self.config,
|
||||
["services", "aprs.fi", "apiKey"],
|
||||
)
|
||||
except Exception as ex:
|
||||
LOG.error("Failed to find config aprs.fi:apikey {}".format(ex))
|
||||
return "No aprs.fi apikey found"
|
||||
|
||||
api_key = self.config["services"]["aprs.fi"]["apiKey"]
|
||||
|
||||
try:
|
||||
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
|
||||
except Exception as ex:
|
||||
LOG.error("Failed to fetch aprs.fi data {}".format(ex))
|
||||
return "Failed to fetch location"
|
||||
|
||||
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
|
||||
if not len(aprs_data["entries"]):
|
||||
LOG.error("Found no entries from aprs.fi!")
|
||||
return "Failed to fetch location"
|
||||
|
||||
lat = aprs_data["entries"][0]["lat"]
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
url2 = (
|
||||
"https://forecast.weather.gov/MapClick.php?lat=%s"
|
||||
"&lon=%s&FcstType=json" % (lat, lon)
|
||||
)
|
||||
response2 = requests.get(url2)
|
||||
# wx_data = json.loads(response2.read())
|
||||
wx_data = json.loads(response2.text)
|
||||
reply = (
|
||||
"%sF(%sF/%sF) %s. %s, %s."
|
||||
% (
|
||||
wx_data["currentobservation"]["Temp"],
|
||||
wx_data["data"]["temperature"][0],
|
||||
wx_data["data"]["temperature"][1],
|
||||
wx_data["data"]["weather"][0],
|
||||
wx_data["time"]["startPeriodName"][1],
|
||||
wx_data["data"]["weather"][1],
|
||||
)
|
||||
).rstrip()
|
||||
LOG.debug("reply: '{}' ".format(reply))
|
||||
except Exception as e:
|
||||
LOG.debug("Weather failed with: " + "%s" % str(e))
|
||||
reply = "Unable to find you (send beacon?)"
|
||||
|
||||
try:
|
||||
wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon)
|
||||
except Exception as ex:
|
||||
LOG.error("Couldn't fetch forecast.weather.gov '{}'".format(ex))
|
||||
return "Unable to metar find station."
|
||||
|
||||
if wx_data["location"]["metar"]:
|
||||
station = wx_data["location"]["metar"]
|
||||
try:
|
||||
resp = plugin_utils.get_weather_gov_metar(station)
|
||||
except Exception as e:
|
||||
LOG.debug("Weather failed with: {}".format(str(e)))
|
||||
reply = "Failed to get Metar"
|
||||
else:
|
||||
station_data = json.loads(resp.text)
|
||||
reply = station_data["properties"]["rawMessage"]
|
||||
else:
|
||||
# Couldn't find a station
|
||||
reply = "No Metar station found"
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
class OWMWeatherPlugin(plugin.APRSDPluginBase):
|
||||
"""OpenWeatherMap Weather Command
|
||||
|
||||
This provides weather near the caller or callsign.
|
||||
|
||||
How to Call: Send a message to aprsd
|
||||
"weather" - returns the weather near the calling callsign
|
||||
"weather CALLSIGN" - returns the weather near CALLSIGN
|
||||
|
||||
This plugin uses the openweathermap API to fetch
|
||||
location and weather information.
|
||||
|
||||
To use this plugin you need to get an openweathermap
|
||||
account and apikey.
|
||||
|
||||
https://home.openweathermap.org/api_keys
|
||||
|
||||
"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[wW]"
|
||||
command_name = "Weather"
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("OWMWeather Plugin '{}'".format(message))
|
||||
a = re.search(r"^.*\s+(.*)", message)
|
||||
if a is not None:
|
||||
searchcall = a.group(1)
|
||||
searchcall = searchcall.upper()
|
||||
else:
|
||||
searchcall = fromcall
|
||||
|
||||
try:
|
||||
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
||||
except Exception as ex:
|
||||
LOG.error("Failed to find config aprs.fi:apikey {}".format(ex))
|
||||
return "No aprs.fi apikey found"
|
||||
|
||||
api_key = self.config["services"]["aprs.fi"]["apiKey"]
|
||||
try:
|
||||
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
|
||||
except Exception as ex:
|
||||
LOG.error("Failed to fetch aprs.fi data {}".format(ex))
|
||||
return "Failed to fetch location"
|
||||
|
||||
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
|
||||
if not len(aprs_data["entries"]):
|
||||
LOG.error("Found no entries from aprs.fi!")
|
||||
return "Failed to fetch location"
|
||||
|
||||
lat = aprs_data["entries"][0]["lat"]
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
|
||||
try:
|
||||
utils.check_config_option(
|
||||
self.config,
|
||||
["services", "openweathermap", "apiKey"],
|
||||
)
|
||||
except Exception as ex:
|
||||
LOG.error("Failed to find config openweathermap:apiKey {}".format(ex))
|
||||
return "No openweathermap apiKey found"
|
||||
|
||||
try:
|
||||
utils.check_config_option(self.config, ["aprsd", "units"])
|
||||
except Exception:
|
||||
LOG.debug("Couldn't find untis in aprsd:services:units")
|
||||
units = "metric"
|
||||
else:
|
||||
units = self.config["aprsd"]["units"]
|
||||
|
||||
api_key = self.config["services"]["openweathermap"]["apiKey"]
|
||||
try:
|
||||
wx_data = plugin_utils.fetch_openweathermap(
|
||||
api_key,
|
||||
lat,
|
||||
lon,
|
||||
units=units,
|
||||
exclude="minutely,hourly",
|
||||
)
|
||||
except Exception as ex:
|
||||
LOG.error("Couldn't fetch openweathermap api '{}'".format(ex))
|
||||
# default to UTC
|
||||
return "Unable to get weather"
|
||||
|
||||
if units == "metric":
|
||||
degree = "C"
|
||||
else:
|
||||
degree = "F"
|
||||
|
||||
if "wind_gust" in wx_data["current"]:
|
||||
wind = "{:.0f}@{}G{:.0f}".format(
|
||||
wx_data["current"]["wind_speed"],
|
||||
wx_data["current"]["wind_deg"],
|
||||
wx_data["current"]["wind_gust"],
|
||||
)
|
||||
else:
|
||||
wind = "{:.0f}@{}".format(
|
||||
wx_data["current"]["wind_speed"],
|
||||
wx_data["current"]["wind_deg"],
|
||||
)
|
||||
|
||||
# LOG.debug(wx_data["current"])
|
||||
# LOG.debug(wx_data["daily"])
|
||||
reply = "{} {:.1f}{}/{:.1f}{} Wind {} {}%".format(
|
||||
wx_data["current"]["weather"][0]["description"],
|
||||
wx_data["current"]["temp"],
|
||||
degree,
|
||||
wx_data["current"]["dew_point"],
|
||||
degree,
|
||||
wind,
|
||||
wx_data["current"]["humidity"],
|
||||
)
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
class AVWXWeatherPlugin(plugin.APRSDPluginBase):
|
||||
"""AVWXWeatherMap Weather Command
|
||||
|
||||
Fetches a METAR weather report for the nearest
|
||||
weather station from the callsign
|
||||
Can be called with:
|
||||
metar - fetches metar for caller
|
||||
metar <CALLSIGN> - fetches metar for <CALLSIGN>
|
||||
|
||||
This plugin requires the avwx-api service
|
||||
to provide the metar for a station near
|
||||
the callsign.
|
||||
|
||||
avwx-api is an opensource project that has
|
||||
a hosted service here: https://avwx.rest/
|
||||
|
||||
You can launch your own avwx-api in a container
|
||||
by cloning the githug repo here: https://github.com/avwx-rest/AVWX-API
|
||||
|
||||
Then build the docker container with:
|
||||
docker build -f Dockerfile -t avwx-api:master .
|
||||
"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[metar]"
|
||||
command_name = "Weather"
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("OWMWeather Plugin '{}'".format(message))
|
||||
a = re.search(r"^.*\s+(.*)", message)
|
||||
if a is not None:
|
||||
searchcall = a.group(1)
|
||||
searchcall = searchcall.upper()
|
||||
else:
|
||||
searchcall = fromcall
|
||||
|
||||
try:
|
||||
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
||||
except Exception as ex:
|
||||
LOG.error("Failed to find config aprs.fi:apikey {}".format(ex))
|
||||
return "No aprs.fi apikey found"
|
||||
|
||||
api_key = self.config["services"]["aprs.fi"]["apiKey"]
|
||||
try:
|
||||
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
|
||||
except Exception as ex:
|
||||
LOG.error("Failed to fetch aprs.fi data {}".format(ex))
|
||||
return "Failed to fetch location"
|
||||
|
||||
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
|
||||
if not len(aprs_data["entries"]):
|
||||
LOG.error("Found no entries from aprs.fi!")
|
||||
return "Failed to fetch location"
|
||||
|
||||
lat = aprs_data["entries"][0]["lat"]
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
|
||||
try:
|
||||
utils.check_config_option(self.config, ["services", "avwx", "apiKey"])
|
||||
except Exception as ex:
|
||||
LOG.error("Failed to find config avwx:apiKey {}".format(ex))
|
||||
return "No avwx apiKey found"
|
||||
|
||||
try:
|
||||
utils.check_config_option(self.config, ["services", "avwx", "base_url"])
|
||||
except Exception as ex:
|
||||
LOG.debut("Didn't find avwx:base_url {}".format(ex))
|
||||
base_url = "https://avwx.rest"
|
||||
else:
|
||||
base_url = self.config["services"]["avwx"]["base_url"]
|
||||
|
||||
api_key = self.config["services"]["avwx"]["apiKey"]
|
||||
token = "TOKEN {}".format(api_key)
|
||||
headers = {"Authorization": token}
|
||||
try:
|
||||
coord = "{},{}".format(lat, lon)
|
||||
url = (
|
||||
"{}/api/station/near/{}?"
|
||||
"n=1&airport=false&reporting=true&format=json".format(base_url, coord)
|
||||
)
|
||||
|
||||
LOG.debug("Get stations near me '{}'".format(url))
|
||||
response = requests.get(url, headers=headers)
|
||||
except Exception as ex:
|
||||
LOG.error(ex)
|
||||
raise Exception("Failed to get the weather '{}'".format(ex))
|
||||
else:
|
||||
wx_data = json.loads(response.text)
|
||||
|
||||
# LOG.debug(wx_data)
|
||||
station = wx_data[0]["station"]["icao"]
|
||||
|
||||
try:
|
||||
url = (
|
||||
"{}/api/metar/{}?options=info,translate,summary"
|
||||
"&airport=true&reporting=true&format=json&onfail=cache".format(
|
||||
base_url,
|
||||
station,
|
||||
)
|
||||
)
|
||||
|
||||
LOG.debug("Get METAR '{}'".format(url))
|
||||
response = requests.get(url, headers=headers)
|
||||
except Exception as ex:
|
||||
LOG.error(ex)
|
||||
raise Exception("Failed to get metar {}".format(ex))
|
||||
else:
|
||||
metar_data = json.loads(response.text)
|
||||
|
||||
# LOG.debug(metar_data)
|
||||
return metar_data["raw"]
|
||||
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import datetime
|
||||
import logging
|
||||
import threading
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class APRSDStats:
|
||||
|
||||
_instance = None
|
||||
lock = None
|
||||
config = None
|
||||
|
||||
start_time = None
|
||||
|
||||
_msgs_tracked = 0
|
||||
_msgs_tx = 0
|
||||
_msgs_rx = 0
|
||||
|
||||
_msgs_mice_rx = 0
|
||||
|
||||
_ack_tx = 0
|
||||
_ack_rx = 0
|
||||
|
||||
_email_thread_last_time = None
|
||||
_email_tx = 0
|
||||
_email_rx = 0
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
# any initializetion here
|
||||
cls._instance.lock = threading.Lock()
|
||||
cls._instance.start_time = datetime.datetime.now()
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, config=None):
|
||||
if config:
|
||||
self.config = config
|
||||
|
||||
@property
|
||||
def uptime(self):
|
||||
with self.lock:
|
||||
return str(datetime.datetime.now() - self.start_time)
|
||||
|
||||
@property
|
||||
def msgs_tx(self):
|
||||
with self.lock:
|
||||
return self._msgs_tx
|
||||
|
||||
def msgs_tx_inc(self):
|
||||
with self.lock:
|
||||
self._msgs_tx += 1
|
||||
|
||||
@property
|
||||
def msgs_rx(self):
|
||||
with self.lock:
|
||||
return self._msgs_rx
|
||||
|
||||
def msgs_rx_inc(self):
|
||||
with self.lock:
|
||||
self._msgs_rx += 1
|
||||
|
||||
@property
|
||||
def msgs_mice_rx(self):
|
||||
with self.lock:
|
||||
return self._msgs_mice_rx
|
||||
|
||||
def msgs_mice_inc(self):
|
||||
with self.lock:
|
||||
self._msgs_mice_rx += 1
|
||||
|
||||
@property
|
||||
def ack_tx(self):
|
||||
with self.lock:
|
||||
return self._ack_tx
|
||||
|
||||
def ack_tx_inc(self):
|
||||
with self.lock:
|
||||
self._ack_tx += 1
|
||||
|
||||
@property
|
||||
def ack_rx(self):
|
||||
with self.lock:
|
||||
return self._ack_rx
|
||||
|
||||
def ack_rx_inc(self):
|
||||
with self.lock:
|
||||
self._ack_rx += 1
|
||||
|
||||
@property
|
||||
def msgs_tracked(self):
|
||||
with self.lock:
|
||||
return self._msgs_tracked
|
||||
|
||||
def msgs_tracked_inc(self):
|
||||
with self.lock:
|
||||
self._msgs_tracked += 1
|
||||
|
||||
@property
|
||||
def email_tx(self):
|
||||
with self.lock:
|
||||
return self._email_tx
|
||||
|
||||
def email_tx_inc(self):
|
||||
with self.lock:
|
||||
self._email_tx += 1
|
||||
|
||||
@property
|
||||
def email_rx(self):
|
||||
with self.lock:
|
||||
return self._email_rx
|
||||
|
||||
def email_rx_inc(self):
|
||||
with self.lock:
|
||||
self._email_rx += 1
|
||||
|
||||
@property
|
||||
def email_thread_time(self):
|
||||
with self.lock:
|
||||
return self._email_thread_last_time
|
||||
|
||||
def email_thread_update(self):
|
||||
with self.lock:
|
||||
self._email_thread_last_time = datetime.datetime.now()
|
||||
|
||||
def stats(self):
|
||||
now = datetime.datetime.now()
|
||||
stats = {
|
||||
"messages": {
|
||||
"tracked": self.msgs_tracked,
|
||||
"sent": self.msgs_tx,
|
||||
"recieved": self.msgs_rx,
|
||||
"ack_sent": self.ack_tx,
|
||||
"ack_recieved": self.ack_rx,
|
||||
"mic-e recieved": self.msgs_mice_rx,
|
||||
},
|
||||
"email": {
|
||||
"sent": self._email_tx,
|
||||
"recieved": self._email_rx,
|
||||
"thread_last_update": str(now - self._email_thread_last_time),
|
||||
},
|
||||
}
|
||||
return stats
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
"Uptime:{} Msgs TX:{} RX:{} "
|
||||
"ACK: TX:{} RX:{} "
|
||||
"Email TX:{} RX:{} LastLoop:{} ".format(
|
||||
self.uptime,
|
||||
self._msgs_tx,
|
||||
self._msgs_rx,
|
||||
self._ack_tx,
|
||||
self._ack_rx,
|
||||
self._email_tx,
|
||||
self._email_rx,
|
||||
self._email_thread_last_time,
|
||||
)
|
||||
)
|
||||
+43
-9
@@ -1,10 +1,12 @@
|
||||
import abc
|
||||
import datetime
|
||||
import logging
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
import tracemalloc
|
||||
|
||||
from aprsd import client, messaging, plugin
|
||||
from aprsd import client, messaging, plugin, stats, trace
|
||||
import aprslib
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
@@ -63,6 +65,42 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
||||
LOG.debug("Exiting")
|
||||
|
||||
|
||||
class KeepAliveThread(APRSDThread):
|
||||
cntr = 0
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("KeepAlive")
|
||||
tracemalloc.start()
|
||||
|
||||
def loop(self):
|
||||
if self.cntr % 6 == 0:
|
||||
tracker = messaging.MsgTrack()
|
||||
stats_obj = stats.APRSDStats()
|
||||
now = datetime.datetime.now()
|
||||
last_email = stats.APRSDStats().email_thread_time
|
||||
if last_email:
|
||||
email_thread_time = str(now - last_email)
|
||||
else:
|
||||
email_thread_time = "N/A"
|
||||
|
||||
current, peak = tracemalloc.get_traced_memory()
|
||||
LOG.debug(
|
||||
"Uptime ({}) Tracker({}) "
|
||||
"Msgs: TX:{} RX:{} EmailThread: {} RAM: Current:{} Peak:{}".format(
|
||||
stats_obj.uptime,
|
||||
len(tracker),
|
||||
stats_obj.msgs_tx,
|
||||
stats_obj.msgs_rx,
|
||||
email_thread_time,
|
||||
current,
|
||||
peak,
|
||||
),
|
||||
)
|
||||
self.cntr += 1
|
||||
time.sleep(10)
|
||||
return True
|
||||
|
||||
|
||||
class APRSDRXThread(APRSDThread):
|
||||
def __init__(self, msg_queues, config):
|
||||
super().__init__("RX_MSG")
|
||||
@@ -73,13 +111,6 @@ class APRSDRXThread(APRSDThread):
|
||||
self.thread_stop = True
|
||||
client.get_client().stop()
|
||||
|
||||
def callback(self, packet):
|
||||
try:
|
||||
packet = aprslib.parse(packet)
|
||||
print(packet)
|
||||
except (aprslib.ParseError, aprslib.UnknownFormat):
|
||||
pass
|
||||
|
||||
def loop(self):
|
||||
aprs_client = client.get_client()
|
||||
|
||||
@@ -118,11 +149,13 @@ class APRSDRXThread(APRSDThread):
|
||||
)
|
||||
tracker = messaging.MsgTrack()
|
||||
tracker.remove(ack_num)
|
||||
stats.APRSDStats().ack_rx_inc()
|
||||
return
|
||||
|
||||
def process_mic_e_packet(self, packet):
|
||||
LOG.info("Mic-E Packet detected. Currenlty unsupported.")
|
||||
messaging.log_packet(packet)
|
||||
stats.APRSDStats().msgs_mice_inc()
|
||||
return
|
||||
|
||||
def process_message_packet(self, packet):
|
||||
@@ -191,11 +224,12 @@ class APRSDRXThread(APRSDThread):
|
||||
self.msg_queues["tx"].put(ack)
|
||||
LOG.debug("Packet processing complete")
|
||||
|
||||
@trace.trace
|
||||
def process_packet(self, packet):
|
||||
"""Process a packet recieved from aprs-is server."""
|
||||
|
||||
try:
|
||||
LOG.info("Got message: {}".format(packet))
|
||||
stats.APRSDStats().msgs_rx_inc()
|
||||
|
||||
msg = packet.get("message_text", None)
|
||||
msg_format = packet.get("format", None)
|
||||
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
import abc
|
||||
import functools
|
||||
import inspect
|
||||
import logging
|
||||
import time
|
||||
import types
|
||||
|
||||
VALID_TRACE_FLAGS = {"method", "api"}
|
||||
TRACE_API = False
|
||||
TRACE_METHOD = False
|
||||
TRACE_ENABLED = False
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
def trace(*dec_args, **dec_kwargs):
|
||||
"""Trace calls to the decorated function.
|
||||
|
||||
This decorator should always be defined as the outermost decorator so it
|
||||
is defined last. This is important so it does not interfere
|
||||
with other decorators.
|
||||
|
||||
Using this decorator on a function will cause its execution to be logged at
|
||||
`DEBUG` level with arguments, return values, and exceptions.
|
||||
|
||||
:returns: a function decorator
|
||||
"""
|
||||
|
||||
def _decorator(f):
|
||||
|
||||
func_name = f.__name__
|
||||
|
||||
@functools.wraps(f)
|
||||
def trace_logging_wrapper(*args, **kwargs):
|
||||
filter_function = dec_kwargs.get("filter_function")
|
||||
logger = LOG
|
||||
|
||||
# NOTE(ameade): Don't bother going any further if DEBUG log level
|
||||
# is not enabled for the logger.
|
||||
if not logger.isEnabledFor(logging.DEBUG) or not TRACE_ENABLED:
|
||||
return f(*args, **kwargs)
|
||||
|
||||
all_args = inspect.getcallargs(f, *args, **kwargs)
|
||||
|
||||
pass_filter = filter_function is None or filter_function(all_args)
|
||||
|
||||
if pass_filter:
|
||||
logger.debug(
|
||||
"==> %(func)s: call %(all_args)r",
|
||||
{
|
||||
"func": func_name,
|
||||
"all_args": str(all_args),
|
||||
},
|
||||
)
|
||||
|
||||
start_time = time.time() * 1000
|
||||
try:
|
||||
result = f(*args, **kwargs)
|
||||
except Exception as exc:
|
||||
total_time = int(round(time.time() * 1000)) - start_time
|
||||
logger.debug(
|
||||
"<== %(func)s: exception (%(time)dms) %(exc)r",
|
||||
{
|
||||
"func": func_name,
|
||||
"time": total_time,
|
||||
"exc": exc,
|
||||
},
|
||||
)
|
||||
raise
|
||||
total_time = int(round(time.time() * 1000)) - start_time
|
||||
|
||||
if isinstance(result, dict):
|
||||
mask_result = result
|
||||
elif isinstance(result, str):
|
||||
mask_result = result
|
||||
else:
|
||||
mask_result = result
|
||||
|
||||
if pass_filter:
|
||||
logger.debug(
|
||||
"<== %(func)s: return (%(time)dms) %(result)r",
|
||||
{
|
||||
"func": func_name,
|
||||
"time": total_time,
|
||||
"result": mask_result,
|
||||
},
|
||||
)
|
||||
return result
|
||||
|
||||
return trace_logging_wrapper
|
||||
|
||||
if len(dec_args) == 0:
|
||||
# filter_function is passed and args does not contain f
|
||||
return _decorator
|
||||
else:
|
||||
# filter_function is not passed
|
||||
return _decorator(dec_args[0])
|
||||
|
||||
|
||||
def trace_api(*dec_args, **dec_kwargs):
|
||||
"""Decorates a function if TRACE_API is true."""
|
||||
|
||||
def _decorator(f):
|
||||
@functools.wraps(f)
|
||||
def trace_api_logging_wrapper(*args, **kwargs):
|
||||
if TRACE_API:
|
||||
return trace(f, *dec_args, **dec_kwargs)(*args, **kwargs)
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return trace_api_logging_wrapper
|
||||
|
||||
if len(dec_args) == 0:
|
||||
# filter_function is passed and args does not contain f
|
||||
return _decorator
|
||||
else:
|
||||
# filter_function is not passed
|
||||
return _decorator(dec_args[0])
|
||||
|
||||
|
||||
def trace_method(f):
|
||||
"""Decorates a function if TRACE_METHOD is true."""
|
||||
|
||||
@functools.wraps(f)
|
||||
def trace_method_logging_wrapper(*args, **kwargs):
|
||||
if TRACE_METHOD:
|
||||
return trace(f)(*args, **kwargs)
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return trace_method_logging_wrapper
|
||||
|
||||
|
||||
class TraceWrapperMetaclass(type):
|
||||
"""Metaclass that wraps all methods of a class with trace_method.
|
||||
|
||||
This metaclass will cause every function inside of the class to be
|
||||
decorated with the trace_method decorator.
|
||||
|
||||
To use the metaclass you define a class like so:
|
||||
class MyClass(object, metaclass=utils.TraceWrapperMetaclass):
|
||||
"""
|
||||
|
||||
def __new__(cls, classname, bases, class_dict):
|
||||
new_class_dict = {}
|
||||
for attribute_name, attribute in class_dict.items():
|
||||
if isinstance(attribute, types.FunctionType):
|
||||
# replace it with a wrapped version
|
||||
attribute = functools.update_wrapper(
|
||||
trace_method(attribute),
|
||||
attribute,
|
||||
)
|
||||
new_class_dict[attribute_name] = attribute
|
||||
|
||||
return type.__new__(cls, classname, bases, new_class_dict)
|
||||
|
||||
|
||||
class TraceWrapperWithABCMetaclass(abc.ABCMeta, TraceWrapperMetaclass):
|
||||
"""Metaclass that wraps all methods of a class with trace."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def setup_tracing(trace_flags):
|
||||
"""Set global variables for each trace flag.
|
||||
|
||||
Sets variables TRACE_METHOD and TRACE_API, which represent
|
||||
whether to log methods or api traces.
|
||||
|
||||
:param trace_flags: a list of strings
|
||||
"""
|
||||
global TRACE_METHOD
|
||||
global TRACE_API
|
||||
global TRACE_ENABLED
|
||||
|
||||
try:
|
||||
trace_flags = [flag.strip() for flag in trace_flags]
|
||||
except TypeError: # Handle when trace_flags is None or a test mock
|
||||
trace_flags = []
|
||||
for invalid_flag in set(trace_flags) - VALID_TRACE_FLAGS:
|
||||
LOG.warning("Invalid trace flag: %s", invalid_flag)
|
||||
TRACE_METHOD = "method" in trace_flags
|
||||
TRACE_API = "api" in trace_flags
|
||||
TRACE_ENABLED = True
|
||||
+187
-67
@@ -17,33 +17,53 @@ DEFAULT_CONFIG_DICT = {
|
||||
"aprs": {
|
||||
"login": "CALLSIGN",
|
||||
"password": "00000",
|
||||
"host": "rotate.aprs.net",
|
||||
"host": "rotate.aprs2.net",
|
||||
"port": 14580,
|
||||
"logfile": "/tmp/aprsd.log",
|
||||
},
|
||||
"aprs.fi": {"apiKey": "set me"},
|
||||
"shortcuts": {
|
||||
"aa": "5551239999@vtext.com",
|
||||
"cl": "craiglamparter@somedomain.org",
|
||||
"wb": "555309@vtext.com",
|
||||
},
|
||||
"smtp": {
|
||||
"login": "SMTP_USERNAME",
|
||||
"password": "SMTP_PASSWORD",
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
"use_ssl": False,
|
||||
},
|
||||
"imap": {
|
||||
"login": "IMAP_USERNAME",
|
||||
"password": "IMAP_PASSWORD",
|
||||
"host": "imap.gmail.com",
|
||||
"port": 993,
|
||||
"use_ssl": True,
|
||||
},
|
||||
"aprsd": {
|
||||
"logfile": "/tmp/aprsd.log",
|
||||
"trace": False,
|
||||
"plugin_dir": "~/.config/aprsd/plugins",
|
||||
"enabled_plugins": plugin.CORE_PLUGINS,
|
||||
"units": "imperial",
|
||||
"web": {
|
||||
"enabled": True,
|
||||
"host": "0.0.0.0",
|
||||
"port": 8001,
|
||||
"users": {
|
||||
"admin": "aprsd",
|
||||
},
|
||||
},
|
||||
"email": {
|
||||
"enabled": True,
|
||||
"shortcuts": {
|
||||
"aa": "5551239999@vtext.com",
|
||||
"cl": "craiglamparter@somedomain.org",
|
||||
"wb": "555309@vtext.com",
|
||||
},
|
||||
"smtp": {
|
||||
"login": "SMTP_USERNAME",
|
||||
"password": "SMTP_PASSWORD",
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
"use_ssl": False,
|
||||
"debug": False,
|
||||
},
|
||||
"imap": {
|
||||
"login": "IMAP_USERNAME",
|
||||
"password": "IMAP_PASSWORD",
|
||||
"host": "imap.gmail.com",
|
||||
"port": 993,
|
||||
"use_ssl": True,
|
||||
"debug": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
"services": {
|
||||
"aprs.fi": {"apiKey": "APIKEYVALUE"},
|
||||
"openweathermap": {"apiKey": "APIKEYVALUE"},
|
||||
"opencagedata": {"apiKey": "APIKEYVALUE"},
|
||||
"avwx": {"base_url": "http://host:port", "apiKey": "APIKEYVALUE"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -101,13 +121,68 @@ def end_substr(original, substr):
|
||||
return idx
|
||||
|
||||
|
||||
def dump_default_cfg():
|
||||
return add_config_comments(
|
||||
yaml.dump(
|
||||
DEFAULT_CONFIG_DICT,
|
||||
indent=4,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def add_config_comments(raw_yaml):
|
||||
end_idx = end_substr(raw_yaml, "aprs:")
|
||||
if end_idx != -1:
|
||||
# lets insert a comment
|
||||
raw_yaml = insert_str(
|
||||
raw_yaml,
|
||||
"\n # Get the passcode for your callsign here: "
|
||||
"\n # https://apps.magicbug.co.uk/passcode",
|
||||
end_idx,
|
||||
)
|
||||
|
||||
end_idx = end_substr(raw_yaml, "aprs.fi:")
|
||||
if end_idx != -1:
|
||||
# lets insert a comment
|
||||
raw_yaml = insert_str(
|
||||
raw_yaml,
|
||||
"\n # Get the apiKey from your aprs.fi account here: http://aprs.fi/account",
|
||||
"\n # Get the apiKey from your aprs.fi account here: "
|
||||
"\n # http://aprs.fi/account",
|
||||
end_idx,
|
||||
)
|
||||
|
||||
end_idx = end_substr(raw_yaml, "opencagedata:")
|
||||
if end_idx != -1:
|
||||
# lets insert a comment
|
||||
raw_yaml = insert_str(
|
||||
raw_yaml,
|
||||
"\n # (Optional for TimeOpenCageDataPlugin) "
|
||||
"\n # Get the apiKey from your opencagedata account here: "
|
||||
"\n # https://opencagedata.com/dashboard#api-keys",
|
||||
end_idx,
|
||||
)
|
||||
|
||||
end_idx = end_substr(raw_yaml, "openweathermap:")
|
||||
if end_idx != -1:
|
||||
# lets insert a comment
|
||||
raw_yaml = insert_str(
|
||||
raw_yaml,
|
||||
"\n # (Optional for OWMWeatherPlugin) "
|
||||
"\n # Get the apiKey from your "
|
||||
"\n # openweathermap account here: "
|
||||
"\n # https://home.openweathermap.org/api_keys",
|
||||
end_idx,
|
||||
)
|
||||
|
||||
end_idx = end_substr(raw_yaml, "avwx:")
|
||||
if end_idx != -1:
|
||||
# lets insert a comment
|
||||
raw_yaml = insert_str(
|
||||
raw_yaml,
|
||||
"\n # (Optional for AVWXWeatherPlugin) "
|
||||
"\n # Use hosted avwx-api here: https://avwx.rest "
|
||||
"\n # or deploy your own from here: "
|
||||
"\n # https://github.com/avwx-rest/avwx-api",
|
||||
end_idx,
|
||||
)
|
||||
|
||||
@@ -123,8 +198,7 @@ def create_default_config():
|
||||
click.echo("Config dir '{}' doesn't exist, creating.".format(config_dir))
|
||||
mkdir_p(config_dir)
|
||||
with open(config_file_expanded, "w+") as cf:
|
||||
raw_yaml = yaml.dump(DEFAULT_CONFIG_DICT)
|
||||
cf.write(add_config_comments(raw_yaml))
|
||||
cf.write(dump_default_cfg())
|
||||
|
||||
|
||||
def get_config(config_file):
|
||||
@@ -154,6 +228,34 @@ def get_config(config_file):
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
def conf_option_exists(conf, chain):
|
||||
_key = chain.pop(0)
|
||||
if _key in conf:
|
||||
return conf_option_exists(conf[_key], chain) if chain else conf[_key]
|
||||
|
||||
|
||||
def check_config_option(config, chain, default_fail=None):
|
||||
result = conf_option_exists(config, chain.copy())
|
||||
if not result:
|
||||
raise Exception(
|
||||
"'{}' was not in config file".format(
|
||||
chain,
|
||||
),
|
||||
)
|
||||
else:
|
||||
if default_fail:
|
||||
if result == default_fail:
|
||||
# We have to fail and bail if the user hasn't edited
|
||||
# this config option.
|
||||
raise Exception(
|
||||
"Config file needs to be edited from provided defaults for {}.".format(
|
||||
chain,
|
||||
),
|
||||
)
|
||||
else:
|
||||
return config
|
||||
|
||||
|
||||
# This method tries to parse the config yaml file
|
||||
# and consume the settings.
|
||||
# If the required params don't exist,
|
||||
@@ -166,59 +268,77 @@ def parse_config(config_file):
|
||||
click.echo(msg)
|
||||
sys.exit(-1)
|
||||
|
||||
def check_option(config, section, name=None, default=None, default_fail=None):
|
||||
if section in config:
|
||||
|
||||
if name and name not in config[section]:
|
||||
if not default:
|
||||
fail(
|
||||
"'{}' was not in '{}' section of config file".format(
|
||||
name,
|
||||
section,
|
||||
),
|
||||
)
|
||||
else:
|
||||
config[section][name] = default
|
||||
else:
|
||||
if (
|
||||
default_fail
|
||||
and name in config[section]
|
||||
and config[section][name] == default_fail
|
||||
):
|
||||
# We have to fail and bail if the user hasn't edited
|
||||
# this config option.
|
||||
fail("Config file needs to be edited from provided defaults.")
|
||||
def check_option(config, chain, default_fail=None):
|
||||
try:
|
||||
config = check_config_option(config, chain, default_fail=default_fail)
|
||||
except Exception as ex:
|
||||
fail(repr(ex))
|
||||
else:
|
||||
fail("'%s' section wasn't in config file" % section)
|
||||
return config
|
||||
return config
|
||||
|
||||
config = get_config(config_file)
|
||||
check_option(config, "shortcuts")
|
||||
|
||||
# special check here to make sure user has edited the config file
|
||||
# and changed the ham callsign
|
||||
check_option(
|
||||
config,
|
||||
"ham",
|
||||
"callsign",
|
||||
[
|
||||
"ham",
|
||||
"callsign",
|
||||
],
|
||||
default_fail=DEFAULT_CONFIG_DICT["ham"]["callsign"],
|
||||
)
|
||||
check_option(
|
||||
config,
|
||||
"aprs.fi",
|
||||
"apiKey",
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprs.fi"]["apiKey"],
|
||||
["services", "aprs.fi", "apiKey"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["services"]["aprs.fi"]["apiKey"],
|
||||
)
|
||||
check_option(config, "aprs", "login")
|
||||
check_option(config, "aprs", "password")
|
||||
# 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")
|
||||
check_option(config, "imap", "password")
|
||||
check_option(config, "smtp", "host")
|
||||
check_option(config, "smtp", "port")
|
||||
check_option(config, "smtp", "login")
|
||||
check_option(config, "smtp", "password")
|
||||
check_option(
|
||||
config,
|
||||
["aprs", "login"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprs"]["login"],
|
||||
)
|
||||
check_option(
|
||||
config,
|
||||
["aprs", "password"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprs"]["password"],
|
||||
)
|
||||
|
||||
# Ensure they change the admin password
|
||||
if config["aprsd"]["web"]["enabled"] is True:
|
||||
check_option(
|
||||
config,
|
||||
["aprsd", "web", "users", "admin"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["web"]["users"]["admin"],
|
||||
)
|
||||
|
||||
if config["aprsd"]["email"]["enabled"] is True:
|
||||
# Check IMAP server settings
|
||||
check_option(config, ["aprsd", "email", "imap", "host"])
|
||||
check_option(config, ["aprsd", "email", "imap", "port"])
|
||||
check_option(
|
||||
config,
|
||||
["aprsd", "email", "imap", "login"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["login"],
|
||||
)
|
||||
check_option(
|
||||
config,
|
||||
["aprsd", "email", "imap", "password"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["password"],
|
||||
)
|
||||
|
||||
# Check SMTP server settings
|
||||
check_option(config, ["aprsd", "email", "smtp", "host"])
|
||||
check_option(config, ["aprsd", "email", "smtp", "port"])
|
||||
check_option(
|
||||
config,
|
||||
["aprsd", "email", "smtp", "login"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["login"],
|
||||
)
|
||||
check_option(
|
||||
config,
|
||||
["aprsd", "email", "smtp", "password"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["password"],
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/* Root element */
|
||||
.json-document {
|
||||
padding: 1em 2em;
|
||||
}
|
||||
|
||||
/* Syntax highlighting for JSON objects */
|
||||
ul.json-dict, ol.json-array {
|
||||
list-style-type: none;
|
||||
margin: 0 0 0 1px;
|
||||
border-left: 1px dotted #ccc;
|
||||
padding-left: 2em;
|
||||
}
|
||||
.json-string {
|
||||
color: #0B7500;
|
||||
}
|
||||
.json-literal {
|
||||
color: #1A01CC;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Toggle button */
|
||||
a.json-toggle {
|
||||
position: relative;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.json-toggle:focus {
|
||||
outline: none;
|
||||
}
|
||||
a.json-toggle:before {
|
||||
font-size: 1.1em;
|
||||
color: #c0c0c0;
|
||||
content: "\25BC"; /* down arrow */
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
text-align: center;
|
||||
line-height: 1em;
|
||||
left: -1.2em;
|
||||
}
|
||||
a.json-toggle:hover:before {
|
||||
color: #aaa;
|
||||
}
|
||||
a.json-toggle.collapsed:before {
|
||||
/* Use rotated down arrow, prevents right arrow appearing smaller than down arrow in some browsers */
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
/* Collapsable placeholder links */
|
||||
a.json-placeholder {
|
||||
color: #aaa;
|
||||
padding: 0 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.json-placeholder:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* jQuery json-viewer
|
||||
* @author: Alexandre Bodelot <alexandre.bodelot@gmail.com>
|
||||
* @link: https://github.com/abodelot/jquery.json-viewer
|
||||
*/
|
||||
(function($) {
|
||||
|
||||
/**
|
||||
* Check if arg is either an array with at least 1 element, or a dict with at least 1 key
|
||||
* @return boolean
|
||||
*/
|
||||
function isCollapsable(arg) {
|
||||
return arg instanceof Object && Object.keys(arg).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string represents a valid url
|
||||
* @return boolean
|
||||
*/
|
||||
function isUrl(string) {
|
||||
var urlRegexp = /^(https?:\/\/|ftps?:\/\/)?([a-z0-9%-]+\.){1,}([a-z0-9-]+)?(:(\d{1,5}))?(\/([a-z0-9\-._~:/?#[\]@!$&'()*+,;=%]+)?)?$/i;
|
||||
return urlRegexp.test(string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a json object into html representation
|
||||
* @return string
|
||||
*/
|
||||
function json2html(json, options) {
|
||||
var html = '';
|
||||
if (typeof json === 'string') {
|
||||
// Escape tags and quotes
|
||||
json = json
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/"/g, '"');
|
||||
|
||||
if (options.withLinks && isUrl(json)) {
|
||||
html += '<a href="' + json + '" class="json-string" target="_blank">' + json + '</a>';
|
||||
} else {
|
||||
// Escape double quotes in the rendered non-URL string.
|
||||
json = json.replace(/"/g, '\\"');
|
||||
html += '<span class="json-string">"' + json + '"</span>';
|
||||
}
|
||||
} else if (typeof json === 'number') {
|
||||
html += '<span class="json-literal">' + json + '</span>';
|
||||
} else if (typeof json === 'boolean') {
|
||||
html += '<span class="json-literal">' + json + '</span>';
|
||||
} else if (json === null) {
|
||||
html += '<span class="json-literal">null</span>';
|
||||
} else if (json instanceof Array) {
|
||||
if (json.length > 0) {
|
||||
html += '[<ol class="json-array">';
|
||||
for (var i = 0; i < json.length; ++i) {
|
||||
html += '<li>';
|
||||
// Add toggle button if item is collapsable
|
||||
if (isCollapsable(json[i])) {
|
||||
html += '<a href class="json-toggle"></a>';
|
||||
}
|
||||
html += json2html(json[i], options);
|
||||
// Add comma if item is not last
|
||||
if (i < json.length - 1) {
|
||||
html += ',';
|
||||
}
|
||||
html += '</li>';
|
||||
}
|
||||
html += '</ol>]';
|
||||
} else {
|
||||
html += '[]';
|
||||
}
|
||||
} else if (typeof json === 'object') {
|
||||
var keyCount = Object.keys(json).length;
|
||||
if (keyCount > 0) {
|
||||
html += '{<ul class="json-dict">';
|
||||
for (var key in json) {
|
||||
if (Object.prototype.hasOwnProperty.call(json, key)) {
|
||||
html += '<li>';
|
||||
var keyRepr = options.withQuotes ?
|
||||
'<span class="json-string">"' + key + '"</span>' : key;
|
||||
// Add toggle button if item is collapsable
|
||||
if (isCollapsable(json[key])) {
|
||||
html += '<a href class="json-toggle">' + keyRepr + '</a>';
|
||||
} else {
|
||||
html += keyRepr;
|
||||
}
|
||||
html += ': ' + json2html(json[key], options);
|
||||
// Add comma if item is not last
|
||||
if (--keyCount > 0) {
|
||||
html += ',';
|
||||
}
|
||||
html += '</li>';
|
||||
}
|
||||
}
|
||||
html += '</ul>}';
|
||||
} else {
|
||||
html += '{}';
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* jQuery plugin method
|
||||
* @param json: a javascript object
|
||||
* @param options: an optional options hash
|
||||
*/
|
||||
$.fn.jsonViewer = function(json, options) {
|
||||
// Merge user options with default options
|
||||
options = Object.assign({}, {
|
||||
collapsed: false,
|
||||
rootCollapsable: true,
|
||||
withQuotes: false,
|
||||
withLinks: true
|
||||
}, options);
|
||||
|
||||
// jQuery chaining
|
||||
return this.each(function() {
|
||||
|
||||
// Transform to HTML
|
||||
var html = json2html(json, options);
|
||||
if (options.rootCollapsable && isCollapsable(json)) {
|
||||
html = '<a href class="json-toggle"></a>' + html;
|
||||
}
|
||||
|
||||
// Insert HTML in target DOM element
|
||||
$(this).html(html);
|
||||
$(this).addClass('json-document');
|
||||
|
||||
// Bind click on toggle buttons
|
||||
$(this).off('click');
|
||||
$(this).on('click', 'a.json-toggle', function() {
|
||||
var target = $(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array');
|
||||
target.toggle();
|
||||
if (target.is(':visible')) {
|
||||
target.siblings('.json-placeholder').remove();
|
||||
} else {
|
||||
var count = target.children('li').length;
|
||||
var placeholder = count + (count > 1 ? ' items' : ' item');
|
||||
target.after('<a href class="json-placeholder">' + placeholder + '</a>');
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Simulate click on toggle button when placeholder is clicked
|
||||
$(this).on('click', 'a.json-placeholder', function() {
|
||||
$(this).siblings('a.json-toggle').click();
|
||||
return false;
|
||||
});
|
||||
|
||||
if (options.collapsed == true) {
|
||||
// Trigger click to collapse all nodes
|
||||
$(this).find('a.json-toggle').click();
|
||||
}
|
||||
});
|
||||
};
|
||||
})(jQuery);
|
||||
@@ -0,0 +1,4 @@
|
||||
<html>
|
||||
|
||||
<body><h1>{{ message }}</h1></body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
|
||||
<script src="json-viewer/jquery.json-viewer.js"></script>
|
||||
<link href="json-viewer/jquery.json-viewer.css" type="text/css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<pre id="json-viewer"></pre>
|
||||
|
||||
<script>
|
||||
var data = {{ messages | safe }}
|
||||
$('#json-viewer').jsonViewer(data)
|
||||
</script>
|
||||
|
||||
</html>
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Use this script to locally build the docker image
|
||||
docker build --no-cache -t hemna6969/aprsd:latest ..
|
||||
+15
-2
@@ -16,10 +16,12 @@ babel==2.9.0
|
||||
# via sphinx
|
||||
black==20.8b1
|
||||
# via -r dev-requirements.in
|
||||
bleach==3.2.1
|
||||
bleach==3.3.0
|
||||
# via readme-renderer
|
||||
certifi==2020.12.5
|
||||
# via requests
|
||||
cffi==1.14.4
|
||||
# via cryptography
|
||||
chardet==4.0.0
|
||||
# via requests
|
||||
click==7.1.2
|
||||
@@ -28,6 +30,8 @@ colorama==0.4.4
|
||||
# via twine
|
||||
coverage==5.3.1
|
||||
# via pytest-cov
|
||||
cryptography==3.3.2
|
||||
# via secretstorage
|
||||
distlib==0.3.1
|
||||
# via virtualenv
|
||||
docutils==0.16
|
||||
@@ -52,6 +56,10 @@ iniconfig==1.1.1
|
||||
# via pytest
|
||||
isort==5.7.0
|
||||
# via -r dev-requirements.in
|
||||
jeepney==0.6.0
|
||||
# via
|
||||
# keyring
|
||||
# secretstorage
|
||||
jinja2==2.11.2
|
||||
# via sphinx
|
||||
keyring==21.8.0
|
||||
@@ -88,6 +96,8 @@ py==1.10.0
|
||||
# tox
|
||||
pycodestyle==2.6.0
|
||||
# via flake8
|
||||
pycparser==2.20
|
||||
# via cffi
|
||||
pyflakes==2.2.0
|
||||
# via flake8
|
||||
pygments==2.7.3
|
||||
@@ -117,9 +127,12 @@ requests==2.25.1
|
||||
# twine
|
||||
rfc3986==1.4.0
|
||||
# via twine
|
||||
secretstorage==3.3.0
|
||||
# via keyring
|
||||
six==1.15.0
|
||||
# via
|
||||
# bleach
|
||||
# cryptography
|
||||
# readme-renderer
|
||||
# tox
|
||||
# virtualenv
|
||||
@@ -160,7 +173,7 @@ typing-extensions==3.7.4.3
|
||||
# mypy
|
||||
urllib3==1.26.2
|
||||
# via requests
|
||||
virtualenv==20.2.2
|
||||
virtualenv==20.4.0
|
||||
# via tox
|
||||
webencodings==0.5.1
|
||||
# via bleach
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
FROM alpine:latest as aprsd
|
||||
|
||||
# Dockerfile for building a container during aprsd development.
|
||||
ARG BRANCH
|
||||
|
||||
ENV VERSION=1.0.0
|
||||
ENV APRS_USER=aprs
|
||||
ENV HOME=/home/aprs
|
||||
ENV APRSD=http://github.com/craigerl/aprsd.git
|
||||
ENV APRSD_BRANCH="v1.1.0"
|
||||
ENV APRSD_BRANCH=${BRANCH:-master}
|
||||
ENV VIRTUAL_ENV=$HOME/.venv3
|
||||
|
||||
ENV INSTALL=$HOME/install
|
||||
RUN apk add --update git vim wget py3-pip py3-virtualenv bash fortune
|
||||
RUN apk add --update gcc python3-dev linux-headers musl-dev libffi-dev libc-dev
|
||||
RUN apk add --update openssl-dev
|
||||
|
||||
# Setup Timezone
|
||||
ENV TZ=US/Eastern
|
||||
@@ -44,5 +46,5 @@ RUN chown -R $APRS_USER:$APRS_USER /config
|
||||
ENV CONF default
|
||||
USER $APRS_USER
|
||||
|
||||
ADD build/bin/run.sh $HOME/
|
||||
ADD bin/run.sh $HOME/
|
||||
ENTRYPOINT ["/home/aprs/run.sh"]
|
||||
@@ -23,4 +23,4 @@ if [ ! -e "$APRSD_CONFIG" ]; then
|
||||
echo "'$APRSD_CONFIG' File does not exist. Creating."
|
||||
aprsd sample-config > $APRSD_CONFIG
|
||||
fi
|
||||
$VIRTUAL_ENV/bin/aprsd server -c $APRSD_CONFIG
|
||||
$VIRTUAL_ENV/bin/aprsd server -c $APRSD_CONFIG --loglevel DEBUG
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Use this script to locally build the docker image
|
||||
docker build --no-cache -t hemna6969/aprsd:latest -f ./Dockerfile .
|
||||
@@ -20,6 +20,22 @@ aprsd.client module
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
aprsd.dev module
|
||||
----------------
|
||||
|
||||
.. automodule:: aprsd.dev
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
aprsd.dev module
|
||||
----------------
|
||||
|
||||
.. automodule:: aprsd.dev
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
aprsd.email module
|
||||
------------------
|
||||
|
||||
@@ -36,6 +52,14 @@ aprsd.fake\_aprs module
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
aprsd.flask module
|
||||
------------------
|
||||
|
||||
.. automodule:: aprsd.flask
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
aprsd.fuzzyclock module
|
||||
-----------------------
|
||||
|
||||
@@ -68,6 +92,22 @@ aprsd.plugin module
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
aprsd.plugin\_utils module
|
||||
--------------------------
|
||||
|
||||
.. automodule:: aprsd.plugin_utils
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
aprsd.plugin\_utils module
|
||||
--------------------------
|
||||
|
||||
.. automodule:: aprsd.plugin_utils
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
aprsd.threads module
|
||||
--------------------
|
||||
|
||||
|
||||
@@ -10,3 +10,8 @@ thesmuggler
|
||||
aprslib
|
||||
py3-validate-email
|
||||
pre-commit
|
||||
pytz
|
||||
opencage
|
||||
flask
|
||||
flask-classful
|
||||
flask-httpauth
|
||||
|
||||
+41
-6
@@ -8,8 +8,12 @@ appdirs==1.4.4
|
||||
# via virtualenv
|
||||
aprslib==0.6.47
|
||||
# via -r requirements.in
|
||||
backoff==1.10.0
|
||||
# via opencage
|
||||
certifi==2020.12.5
|
||||
# via requests
|
||||
cffi==1.14.4
|
||||
# via cryptography
|
||||
cfgv==3.2.0
|
||||
# via pre-commit
|
||||
chardet==4.0.0
|
||||
@@ -20,6 +24,9 @@ click==7.1.2
|
||||
# via
|
||||
# -r requirements.in
|
||||
# click-completion
|
||||
# flask
|
||||
cryptography==3.3.2
|
||||
# via pyopenssl
|
||||
distlib==0.3.1
|
||||
# via virtualenv
|
||||
dnspython==2.1.0
|
||||
@@ -28,20 +35,35 @@ filelock==3.0.12
|
||||
# via
|
||||
# py3-validate-email
|
||||
# virtualenv
|
||||
identify==1.5.11
|
||||
flask-classful==0.14.2
|
||||
# via -r requirements.in
|
||||
flask-httpauth==4.2.0
|
||||
# via -r requirements.in
|
||||
flask==1.1.2
|
||||
# via
|
||||
# -r requirements.in
|
||||
# flask-classful
|
||||
# flask-httpauth
|
||||
identify==1.5.13
|
||||
# via pre-commit
|
||||
idna==2.10
|
||||
# via
|
||||
# py3-validate-email
|
||||
# requests
|
||||
imapclient==2.1.0
|
||||
imapclient==2.2.0
|
||||
# via -r requirements.in
|
||||
itsdangerous==1.1.0
|
||||
# via flask
|
||||
jinja2==2.11.2
|
||||
# via click-completion
|
||||
# via
|
||||
# click-completion
|
||||
# flask
|
||||
markupsafe==1.1.1
|
||||
# via jinja2
|
||||
nodeenv==1.5.0
|
||||
# via pre-commit
|
||||
opencage==1.2.2
|
||||
# via -r requirements.in
|
||||
pbr==5.5.1
|
||||
# via -r requirements.in
|
||||
pluggy==0.13.1
|
||||
@@ -50,19 +72,30 @@ pre-commit==2.9.3
|
||||
# via -r requirements.in
|
||||
py3-validate-email==0.2.12
|
||||
# via -r requirements.in
|
||||
pyyaml==5.3.1
|
||||
pycparser==2.20
|
||||
# via cffi
|
||||
pyopenssl==20.0.1
|
||||
# via opencage
|
||||
pytz==2020.5
|
||||
# via -r requirements.in
|
||||
pyyaml==5.4.1
|
||||
# via
|
||||
# -r requirements.in
|
||||
# pre-commit
|
||||
requests==2.25.1
|
||||
# via -r requirements.in
|
||||
# via
|
||||
# -r requirements.in
|
||||
# opencage
|
||||
shellingham==1.3.2
|
||||
# via click-completion
|
||||
six==1.15.0
|
||||
# via
|
||||
# -r requirements.in
|
||||
# click-completion
|
||||
# cryptography
|
||||
# imapclient
|
||||
# opencage
|
||||
# pyopenssl
|
||||
# virtualenv
|
||||
thesmuggler==1.0.1
|
||||
# via -r requirements.in
|
||||
@@ -70,5 +103,7 @@ toml==0.10.2
|
||||
# via pre-commit
|
||||
urllib3==1.26.2
|
||||
# via requests
|
||||
virtualenv==20.2.2
|
||||
virtualenv==20.4.0
|
||||
# via pre-commit
|
||||
werkzeug==1.0.1
|
||||
# via flask
|
||||
|
||||
@@ -35,6 +35,8 @@ packages =
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
aprsd = aprsd.main:main
|
||||
aprsd-dev = aprsd.dev:main
|
||||
aprsd-healthcheck = aprsd.healthcheck:main
|
||||
fake_aprs = aprsd.fake_aprs:main
|
||||
|
||||
[build_sphinx]
|
||||
|
||||
+4
-4
@@ -5,21 +5,21 @@ from aprsd import email
|
||||
|
||||
class TestEmail(unittest.TestCase):
|
||||
def test_get_email_from_shortcut(self):
|
||||
email.CONFIG = {"shortcuts": {}}
|
||||
email.CONFIG = {"aprsd": {"email": {"shortcuts": {}}}}
|
||||
email_address = "something@something.com"
|
||||
addr = "-{}".format(email_address)
|
||||
actual = email.get_email_from_shortcut(addr)
|
||||
self.assertEqual(addr, actual)
|
||||
|
||||
email.CONFIG = {"nothing": "nothing"}
|
||||
email.CONFIG = {"aprsd": {"email": {"nothing": "nothing"}}}
|
||||
actual = email.get_email_from_shortcut(addr)
|
||||
self.assertEqual(addr, actual)
|
||||
|
||||
email.CONFIG = {"shortcuts": {"not_used": "empty"}}
|
||||
email.CONFIG = {"aprsd": {"email": {"shortcuts": {"not_used": "empty"}}}}
|
||||
actual = email.get_email_from_shortcut(addr)
|
||||
self.assertEqual(addr, actual)
|
||||
|
||||
email.CONFIG = {"shortcuts": {"-wb": email_address}}
|
||||
email.CONFIG = {"aprsd": {"email": {"shortcuts": {"-wb": email_address}}}}
|
||||
short = "-wb"
|
||||
actual = email.get_email_from_shortcut(short)
|
||||
self.assertEqual(email_address, actual)
|
||||
|
||||
+20
-12
@@ -9,6 +9,7 @@ from aprsd.plugins import ping as ping_plugin
|
||||
from aprsd.plugins import query as query_plugin
|
||||
from aprsd.plugins import time as time_plugin
|
||||
from aprsd.plugins import version as version_plugin
|
||||
import pytz
|
||||
|
||||
|
||||
class TestPlugin(unittest.TestCase):
|
||||
@@ -41,7 +42,7 @@ class TestPlugin(unittest.TestCase):
|
||||
|
||||
@mock.patch("aprsd.messaging.MsgTrack.flush")
|
||||
def test_query_flush(self, mock_flush):
|
||||
message = "?delete"
|
||||
message = "!delete"
|
||||
query = query_plugin.QueryPlugin(self.config)
|
||||
|
||||
expected = "Deleted ALL pending msgs."
|
||||
@@ -53,7 +54,7 @@ class TestPlugin(unittest.TestCase):
|
||||
def test_query_restart_delayed(self, mock_restart):
|
||||
track = messaging.MsgTrack()
|
||||
track.track = {}
|
||||
message = "?4"
|
||||
message = "!4"
|
||||
query = query_plugin.QueryPlugin(self.config)
|
||||
|
||||
expected = "No pending msgs to resend"
|
||||
@@ -68,13 +69,21 @@ class TestPlugin(unittest.TestCase):
|
||||
actual = query.run(self.fromcall, message, self.ack)
|
||||
mock_restart.assert_called_once()
|
||||
|
||||
@mock.patch("time.localtime")
|
||||
def test_time(self, mock_time):
|
||||
@mock.patch("aprsd.plugins.time.TimePlugin._get_local_tz")
|
||||
@mock.patch("aprsd.plugins.time.TimePlugin._get_utcnow")
|
||||
def test_time(self, mock_utcnow, mock_localtz):
|
||||
utcnow = pytz.datetime.datetime.utcnow()
|
||||
mock_utcnow.return_value = utcnow
|
||||
tz = pytz.timezone("US/Pacific")
|
||||
mock_localtz.return_value = tz
|
||||
|
||||
gmt_t = pytz.utc.localize(utcnow)
|
||||
local_t = gmt_t.astimezone(tz)
|
||||
|
||||
fake_time = mock.MagicMock()
|
||||
h = fake_time.tm_hour = 16
|
||||
m = fake_time.tm_min = 12
|
||||
fake_time.tm_sec = 55
|
||||
mock_time.return_value = fake_time
|
||||
h = int(local_t.strftime("%H"))
|
||||
m = int(local_t.strftime("%M"))
|
||||
fake_time.tm_sec = 13
|
||||
time = time_plugin.TimePlugin(self.config)
|
||||
|
||||
fromcall = "KFART"
|
||||
@@ -87,11 +96,10 @@ class TestPlugin(unittest.TestCase):
|
||||
cur_time = fuzzy(h, m, 1)
|
||||
|
||||
message = "time"
|
||||
expected = "{} ({}:{} PDT) ({})".format(
|
||||
local_short_str = local_t.strftime("%H:%M %Z")
|
||||
expected = "{} ({})".format(
|
||||
cur_time,
|
||||
str(h),
|
||||
str(m).rjust(2, "0"),
|
||||
message.rstrip(),
|
||||
local_short_str,
|
||||
)
|
||||
actual = time.run(fromcall, message, ack)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
Reference in New Issue
Block a user