mirror of
https://github.com/craigerl/aprsd.git
synced 2025-08-12 10:12:25 -04:00
Merge pull request #47 from craigerl/stabilize_1_6_0
Branch to stabilize for the 1.6.0 release.
This commit is contained in:
commit
3a6316fa8a
42
Dockerfile
42
Dockerfile
@ -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"]
|
|
144
aprsd/email.py
144
aprsd/email.py
@ -7,7 +7,7 @@ import re
|
|||||||
import smtplib
|
import smtplib
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from aprsd import messaging, stats, threads
|
from aprsd import messaging, stats, threads, trace
|
||||||
import imapclient
|
import imapclient
|
||||||
from validate_email import validate_email
|
from validate_email import validate_email
|
||||||
|
|
||||||
@ -17,6 +17,7 @@ LOG = logging.getLogger("APRSD")
|
|||||||
CONFIG = None
|
CONFIG = None
|
||||||
|
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def _imap_connect():
|
def _imap_connect():
|
||||||
imap_port = CONFIG["aprsd"]["email"]["imap"].get("port", 143)
|
imap_port = CONFIG["aprsd"]["email"]["imap"].get("port", 143)
|
||||||
use_ssl = CONFIG["aprsd"]["email"]["imap"].get("use_ssl", False)
|
use_ssl = CONFIG["aprsd"]["email"]["imap"].get("use_ssl", False)
|
||||||
@ -31,9 +32,10 @@ def _imap_connect():
|
|||||||
port=imap_port,
|
port=imap_port,
|
||||||
use_uid=True,
|
use_uid=True,
|
||||||
ssl=use_ssl,
|
ssl=use_ssl,
|
||||||
|
timeout=30,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
LOG.error("Failed to connect IMAP server")
|
LOG.error("Failed to connect IMAP server", e)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -47,9 +49,15 @@ def _imap_connect():
|
|||||||
return
|
return
|
||||||
|
|
||||||
server.select_folder("INBOX")
|
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
|
return server
|
||||||
|
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def _smtp_connect():
|
def _smtp_connect():
|
||||||
host = CONFIG["aprsd"]["email"]["smtp"]["host"]
|
host = CONFIG["aprsd"]["email"]["smtp"]["host"]
|
||||||
smtp_port = CONFIG["aprsd"]["email"]["smtp"]["port"]
|
smtp_port = CONFIG["aprsd"]["email"]["smtp"]["port"]
|
||||||
@ -64,15 +72,28 @@ def _smtp_connect():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if use_ssl:
|
if use_ssl:
|
||||||
server = smtplib.SMTP_SSL(host=host, port=smtp_port)
|
server = smtplib.SMTP_SSL(
|
||||||
|
host=host,
|
||||||
|
port=smtp_port,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
server = smtplib.SMTP(host=host, port=smtp_port)
|
server = smtplib.SMTP(
|
||||||
|
host=host,
|
||||||
|
port=smtp_port,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
LOG.error("Couldn't connect to SMTP Server")
|
LOG.error("Couldn't connect to SMTP Server")
|
||||||
return
|
return
|
||||||
|
|
||||||
LOG.debug("Connected to smtp host {}".format(msg))
|
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:
|
try:
|
||||||
server.login(
|
server.login(
|
||||||
CONFIG["aprsd"]["email"]["smtp"]["login"],
|
CONFIG["aprsd"]["email"]["smtp"]["login"],
|
||||||
@ -87,7 +108,7 @@ def _smtp_connect():
|
|||||||
|
|
||||||
|
|
||||||
def validate_shortcuts(config):
|
def validate_shortcuts(config):
|
||||||
shortcuts = config.get("shortcuts", None)
|
shortcuts = config["aprsd"]["email"].get("shortcuts", None)
|
||||||
if not shortcuts:
|
if not shortcuts:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -120,7 +141,7 @@ def validate_shortcuts(config):
|
|||||||
for key in delete_keys:
|
for key in delete_keys:
|
||||||
del config["aprsd"]["email"]["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):
|
def get_email_from_shortcut(addr):
|
||||||
@ -152,6 +173,7 @@ def validate_email_config(config, disable_validation=False):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def parse_email(msgid, data, server):
|
def parse_email(msgid, data, server):
|
||||||
envelope = data[b"ENVELOPE"]
|
envelope = data[b"ENVELOPE"]
|
||||||
# email address match
|
# email address match
|
||||||
@ -162,7 +184,12 @@ def parse_email(msgid, data, server):
|
|||||||
else:
|
else:
|
||||||
from_addr = "noaddr"
|
from_addr = "noaddr"
|
||||||
LOG.debug("Got a message from '{}'".format(from_addr))
|
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"))
|
msg = email.message_from_string(m[msgid][b"RFC822"].decode(errors="ignore"))
|
||||||
if msg.is_multipart():
|
if msg.is_multipart():
|
||||||
text = ""
|
text = ""
|
||||||
@ -238,6 +265,7 @@ def parse_email(msgid, data, server):
|
|||||||
# end parse_email
|
# end parse_email
|
||||||
|
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def send_email(to_addr, content):
|
def send_email(to_addr, content):
|
||||||
global check_email_delay
|
global check_email_delay
|
||||||
|
|
||||||
@ -282,6 +310,7 @@ def send_email(to_addr, content):
|
|||||||
# end send_email
|
# end send_email
|
||||||
|
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def resend_email(count, fromcall):
|
def resend_email(count, fromcall):
|
||||||
global check_email_delay
|
global check_email_delay
|
||||||
date = datetime.datetime.now()
|
date = datetime.datetime.now()
|
||||||
@ -290,7 +319,7 @@ def resend_email(count, fromcall):
|
|||||||
year = date.year
|
year = date.year
|
||||||
today = "{}-{}-{}".format(day, month, year)
|
today = "{}-{}-{}".format(day, month, year)
|
||||||
|
|
||||||
shortcuts = CONFIG["shortcuts"]
|
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
|
||||||
# swap key/value
|
# swap key/value
|
||||||
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
|
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
|
||||||
|
|
||||||
@ -300,7 +329,12 @@ def resend_email(count, fromcall):
|
|||||||
LOG.exception("Failed to Connect to IMAP. Cannot resend email ", e)
|
LOG.exception("Failed to Connect to IMAP. Cannot resend email ", e)
|
||||||
return
|
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))
|
# LOG.debug("%d messages received today" % len(messages))
|
||||||
|
|
||||||
msgexists = False
|
msgexists = False
|
||||||
@ -308,11 +342,21 @@ def resend_email(count, fromcall):
|
|||||||
messages.sort(reverse=True)
|
messages.sort(reverse=True)
|
||||||
del messages[int(count) :] # only the latest "count" messages
|
del messages[int(count) :] # only the latest "count" messages
|
||||||
for message in 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
|
# one at a time, otherwise order is random
|
||||||
(body, from_addr) = parse_email(msgid, data, server)
|
(body, from_addr) = parse_email(msgid, data, server)
|
||||||
# unset seen flag, will stay bold in email client
|
# 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:
|
if from_addr in shortcuts_inverted:
|
||||||
# reverse lookup of a shortcut
|
# reverse lookup of a shortcut
|
||||||
from_addr = shortcuts_inverted[from_addr]
|
from_addr = shortcuts_inverted[from_addr]
|
||||||
@ -320,7 +364,7 @@ def resend_email(count, fromcall):
|
|||||||
reply = "-" + from_addr + " * " + body.decode(errors="ignore")
|
reply = "-" + from_addr + " * " + body.decode(errors="ignore")
|
||||||
# messaging.send_message(fromcall, reply)
|
# messaging.send_message(fromcall, reply)
|
||||||
msg = messaging.TextMessage(
|
msg = messaging.TextMessage(
|
||||||
CONFIG["aprsd"]["email"]["aprs"]["login"],
|
CONFIG["aprs"]["login"],
|
||||||
fromcall,
|
fromcall,
|
||||||
reply,
|
reply,
|
||||||
)
|
)
|
||||||
@ -358,6 +402,7 @@ class APRSDEmailThread(threads.APRSDThread):
|
|||||||
self.msg_queues = msg_queues
|
self.msg_queues = msg_queues
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def run(self):
|
def run(self):
|
||||||
global check_email_delay
|
global check_email_delay
|
||||||
|
|
||||||
@ -395,17 +440,30 @@ class APRSDEmailThread(threads.APRSDThread):
|
|||||||
try:
|
try:
|
||||||
server = _imap_connect()
|
server = _imap_connect()
|
||||||
except Exception as e:
|
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:
|
if not server:
|
||||||
continue
|
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)))
|
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"]
|
envelope = data[b"ENVELOPE"]
|
||||||
# LOG.debug('ID:%d "%s" (%s)' % (msgid, envelope.subject.decode(), envelope.date))
|
LOG.debug(
|
||||||
|
'ID:%d "%s" (%s)'
|
||||||
|
% (msgid, envelope.subject.decode(), envelope.date),
|
||||||
|
)
|
||||||
f = re.search(
|
f = re.search(
|
||||||
r"'([[A-a][0-9]_-]+@[[A-a][0-9]_-\.]+)",
|
r"'([[A-a][0-9]_-]+@[[A-a][0-9]_-\.]+)",
|
||||||
str(envelope.from_[0]),
|
str(envelope.from_[0]),
|
||||||
@ -418,16 +476,31 @@ class APRSDEmailThread(threads.APRSDThread):
|
|||||||
# LOG.debug("Message flags/tags: " + str(server.get_flags(msgid)[msgid]))
|
# LOG.debug("Message flags/tags: " + str(server.get_flags(msgid)[msgid]))
|
||||||
# if "APRS" not in 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
|
# in python3, imap tags are unicode. in py2 they're strings. so .decode them to handle both
|
||||||
taglist = [
|
try:
|
||||||
x.decode(errors="ignore")
|
taglist = [
|
||||||
for x in server.get_flags(msgid)[msgid]
|
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 "APRS" not in taglist:
|
||||||
# if msg not flagged as sent via aprs
|
# 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)
|
(body, from_addr) = parse_email(msgid, data, server)
|
||||||
# unset seen flag, will stay bold in email client
|
# 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:
|
if from_addr in shortcuts_inverted:
|
||||||
# reverse lookup of a shortcut
|
# reverse lookup of a shortcut
|
||||||
@ -441,14 +514,28 @@ class APRSDEmailThread(threads.APRSDThread):
|
|||||||
)
|
)
|
||||||
self.msg_queues["tx"].put(msg)
|
self.msg_queues["tx"].put(msg)
|
||||||
# flag message as sent via aprs
|
# flag message as sent via aprs
|
||||||
server.add_flags(msgid, ["APRS"])
|
try:
|
||||||
# unset seen flag, will stay bold in email client
|
server.add_flags(msgid, ["APRS"])
|
||||||
server.remove_flags(msgid, [imapclient.SEEN])
|
# 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 more often since we just received an email
|
||||||
check_email_delay = 60
|
check_email_delay = 60
|
||||||
|
|
||||||
# reset clock
|
# reset clock
|
||||||
|
LOG.debug("Done looping over Server.fetch, logging out.")
|
||||||
past = datetime.datetime.now()
|
past = datetime.datetime.now()
|
||||||
server.logout()
|
try:
|
||||||
|
server.logout()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception("IMAP failed to logout: ", e)
|
||||||
|
continue
|
||||||
else:
|
else:
|
||||||
# We haven't hit the email delay yet.
|
# We haven't hit the email delay yet.
|
||||||
# LOG.debug("Delta({}) < {}".format(now - past, check_email_delay))
|
# LOG.debug("Delta({}) < {}".format(now - past, check_email_delay))
|
||||||
@ -457,6 +544,3 @@ class APRSDEmailThread(threads.APRSDThread):
|
|||||||
# Remove ourselves from the global threads list
|
# Remove ourselves from the global threads list
|
||||||
threads.APRSDThreadList().remove(self)
|
threads.APRSDThreadList().remove(self)
|
||||||
LOG.info("Exiting")
|
LOG.info("Exiting")
|
||||||
|
|
||||||
|
|
||||||
# end check_email()
|
|
||||||
|
@ -1,21 +1,65 @@
|
|||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import messaging, stats
|
from aprsd import messaging, stats
|
||||||
import flask
|
import flask
|
||||||
import flask_classful
|
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):
|
class APRSDFlask(flask_classful.FlaskView):
|
||||||
config = None
|
config = None
|
||||||
|
|
||||||
def set_config(self, config):
|
def set_config(self, config):
|
||||||
|
global users
|
||||||
self.config = config
|
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):
|
def index(self):
|
||||||
return "Hello"
|
return "Hello"
|
||||||
# return flask.render_template("index.html", message=msg)
|
# 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):
|
def stats(self):
|
||||||
stats_obj = stats.APRSDStats()
|
stats_obj = stats.APRSDStats()
|
||||||
track = messaging.MsgTrack()
|
track = messaging.MsgTrack()
|
||||||
@ -30,9 +74,16 @@ class APRSDFlask(flask_classful.FlaskView):
|
|||||||
|
|
||||||
|
|
||||||
def init_flask(config):
|
def init_flask(config):
|
||||||
flask_app = flask.Flask("aprsd")
|
flask_app = flask.Flask(
|
||||||
|
"aprsd",
|
||||||
|
static_url_path="",
|
||||||
|
static_folder="web/static",
|
||||||
|
template_folder="web/templates",
|
||||||
|
)
|
||||||
server = APRSDFlask()
|
server = APRSDFlask()
|
||||||
server.set_config(config)
|
server.set_config(config)
|
||||||
# flask_app.route('/', methods=['GET'])(server.index)
|
# flask_app.route('/', methods=['GET'])(server.index)
|
||||||
flask_app.route("/stats", methods=["GET"])(server.stats)
|
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
|
return flask_app
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
# python included libs
|
# python included libs
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from logging import NullHandler
|
from logging import NullHandler
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
@ -27,12 +28,11 @@ import os
|
|||||||
import queue
|
import queue
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import threading
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
# local imports here
|
# local imports here
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import client, email, flask, messaging, plugin, stats, threads, utils
|
from aprsd import client, email, flask, messaging, plugin, stats, threads, trace, utils
|
||||||
import aprslib
|
import aprslib
|
||||||
from aprslib.exceptions import LoginError
|
from aprslib.exceptions import LoginError
|
||||||
import click
|
import click
|
||||||
@ -52,7 +52,9 @@ LOG_LEVELS = {
|
|||||||
|
|
||||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||||
|
|
||||||
server_event = threading.Event()
|
flask_enabled = False
|
||||||
|
|
||||||
|
# server_event = threading.Event()
|
||||||
|
|
||||||
# localization, please edit:
|
# localization, please edit:
|
||||||
# HOST = "noam.aprs2.net" # north america tier2 servers round robin
|
# HOST = "noam.aprs2.net" # north america tier2 servers round robin
|
||||||
@ -150,20 +152,23 @@ def install(append, case_insensitive, shell, path):
|
|||||||
|
|
||||||
|
|
||||||
def signal_handler(sig, frame):
|
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()
|
threads.APRSDThreadList().stop_all()
|
||||||
server_event.set()
|
if "subprocess" not in str(frame):
|
||||||
LOG.info("EXITING STATS")
|
LOG.info(
|
||||||
LOG.info(stats.APRSDStats())
|
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format(
|
||||||
# time.sleep(1)
|
datetime.datetime.now(),
|
||||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
),
|
||||||
|
)
|
||||||
|
time.sleep(5)
|
||||||
# end signal_handler
|
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
|
# Setup the logging faciility
|
||||||
@ -184,10 +189,21 @@ def setup_logging(config, loglevel, quiet):
|
|||||||
fh.setFormatter(log_formatter)
|
fh.setFormatter(log_formatter)
|
||||||
LOG.addHandler(fh)
|
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:
|
if not quiet:
|
||||||
sh = logging.StreamHandler(sys.stdout)
|
sh = logging.StreamHandler(sys.stdout)
|
||||||
sh.setFormatter(log_formatter)
|
sh.setFormatter(log_formatter)
|
||||||
LOG.addHandler(sh)
|
LOG.addHandler(sh)
|
||||||
|
if imap_logger:
|
||||||
|
imap_logger.addHandler(sh)
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
@main.command()
|
||||||
@ -394,9 +410,7 @@ def server(
|
|||||||
flush,
|
flush,
|
||||||
):
|
):
|
||||||
"""Start the aprsd server process."""
|
"""Start the aprsd server process."""
|
||||||
global event
|
global flask_enabled
|
||||||
|
|
||||||
event = threading.Event()
|
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
|
||||||
if not quiet:
|
if not quiet:
|
||||||
@ -410,6 +424,8 @@ def server(
|
|||||||
email.CONFIG = config
|
email.CONFIG = config
|
||||||
|
|
||||||
setup_logging(config, loglevel, quiet)
|
setup_logging(config, loglevel, quiet)
|
||||||
|
if config["aprsd"].get("trace", False):
|
||||||
|
trace.setup_tracing(["method", "api"])
|
||||||
LOG.info("APRSD Started version: {}".format(aprsd.__version__))
|
LOG.info("APRSD Started version: {}".format(aprsd.__version__))
|
||||||
stats.APRSDStats(config)
|
stats.APRSDStats(config)
|
||||||
|
|
||||||
@ -468,6 +484,7 @@ def server(
|
|||||||
web_enabled = False
|
web_enabled = False
|
||||||
|
|
||||||
if web_enabled:
|
if web_enabled:
|
||||||
|
flask_enabled = True
|
||||||
app = flask.init_flask(config)
|
app = flask.init_flask(config)
|
||||||
app.run(
|
app.run(
|
||||||
host=config["aprsd"]["web"]["host"],
|
host=config["aprsd"]["web"]["host"],
|
||||||
@ -475,10 +492,8 @@ def server(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# If there are items in the msgTracker, then save them
|
# If there are items in the msgTracker, then save them
|
||||||
tracker = messaging.MsgTrack()
|
|
||||||
tracker.save()
|
|
||||||
LOG.info(stats.APRSDStats())
|
|
||||||
LOG.info("APRSD Exiting.")
|
LOG.info("APRSD Exiting.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -53,6 +53,38 @@ class MsgTrack:
|
|||||||
cls._instance.lock = threading.Lock()
|
cls._instance.lock = threading.Lock()
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
|
def __getitem__(self, name):
|
||||||
|
with self.lock:
|
||||||
|
return self.track[name]
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
with self.lock:
|
||||||
|
return iter(self.track)
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
with self.lock:
|
||||||
|
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:
|
||||||
|
return len(self.track)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
with self.lock:
|
||||||
|
result = "{"
|
||||||
|
for key in self.track.keys():
|
||||||
|
result += "{}: {}, ".format(key, str(self.track[key]))
|
||||||
|
result += "}"
|
||||||
|
return result
|
||||||
|
|
||||||
def add(self, msg):
|
def add(self, msg):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
key = int(msg.id)
|
key = int(msg.id)
|
||||||
@ -71,24 +103,18 @@ class MsgTrack:
|
|||||||
if key in self.track.keys():
|
if key in self.track.keys():
|
||||||
del self.track[key]
|
del self.track[key]
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
with self.lock:
|
|
||||||
return len(self.track)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
with self.lock:
|
|
||||||
result = "{"
|
|
||||||
for key in self.track.keys():
|
|
||||||
result += "{}: {}, ".format(key, str(self.track[key]))
|
|
||||||
result += "}"
|
|
||||||
return result
|
|
||||||
|
|
||||||
def save(self):
|
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:
|
if len(self) > 0:
|
||||||
LOG.info("Saving {} tracking messages to disk".format(len(self)))
|
LOG.info("Saving {} tracking messages to disk".format(len(self)))
|
||||||
pickle.dump(self.dump(), open(utils.DEFAULT_SAVE_FILE, "wb+"))
|
pickle.dump(self.dump(), open(utils.DEFAULT_SAVE_FILE, "wb+"))
|
||||||
else:
|
else:
|
||||||
|
LOG.debug(
|
||||||
|
"Nothing to save, flushing old save file '{}'".format(
|
||||||
|
utils.DEFAULT_SAVE_FILE,
|
||||||
|
),
|
||||||
|
)
|
||||||
self.flush()
|
self.flush()
|
||||||
|
|
||||||
def dump(self):
|
def dump(self):
|
||||||
@ -229,8 +255,17 @@ class RawMessage(Message):
|
|||||||
super().__init__(None, None, msg_id=None)
|
super().__init__(None, None, msg_id=None)
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
def __repr__(self):
|
def dict(self):
|
||||||
return self.message
|
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):
|
def __str__(self):
|
||||||
return self.message
|
return self.message
|
||||||
@ -246,12 +281,12 @@ class RawMessage(Message):
|
|||||||
cl = client.get_client()
|
cl = client.get_client()
|
||||||
log_message(
|
log_message(
|
||||||
"Sending Message Direct",
|
"Sending Message Direct",
|
||||||
repr(self).rstrip("\n"),
|
str(self).rstrip("\n"),
|
||||||
self.message,
|
self.message,
|
||||||
tocall=self.tocall,
|
tocall=self.tocall,
|
||||||
fromcall=self.fromcall,
|
fromcall=self.fromcall,
|
||||||
)
|
)
|
||||||
cl.sendall(repr(self))
|
cl.sendall(str(self))
|
||||||
stats.APRSDStats().msgs_sent_inc()
|
stats.APRSDStats().msgs_sent_inc()
|
||||||
|
|
||||||
|
|
||||||
@ -267,7 +302,22 @@ class TextMessage(Message):
|
|||||||
# an ack? Some messages we don't want to do this ever.
|
# an ack? Some messages we don't want to do this ever.
|
||||||
self.allow_delay = allow_delay
|
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."""
|
"""Build raw string to send over the air."""
|
||||||
return "{}>APZ100::{}:{}{{{}\n".format(
|
return "{}>APZ100::{}:{}{{{}\n".format(
|
||||||
self.fromcall,
|
self.fromcall,
|
||||||
@ -276,19 +326,6 @@ class TextMessage(Message):
|
|||||||
str(self.id),
|
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):
|
def _filter_for_send(self):
|
||||||
"""Filter and format message string for FCC."""
|
"""Filter and format message string for FCC."""
|
||||||
# max? ftm400 displays 64, raw msg shows 74
|
# max? ftm400 displays 64, raw msg shows 74
|
||||||
@ -311,12 +348,12 @@ class TextMessage(Message):
|
|||||||
cl = client.get_client()
|
cl = client.get_client()
|
||||||
log_message(
|
log_message(
|
||||||
"Sending Message Direct",
|
"Sending Message Direct",
|
||||||
repr(self).rstrip("\n"),
|
str(self).rstrip("\n"),
|
||||||
self.message,
|
self.message,
|
||||||
tocall=self.tocall,
|
tocall=self.tocall,
|
||||||
fromcall=self.fromcall,
|
fromcall=self.fromcall,
|
||||||
)
|
)
|
||||||
cl.sendall(repr(self))
|
cl.sendall(str(self))
|
||||||
stats.APRSDStats().msgs_tx_inc()
|
stats.APRSDStats().msgs_tx_inc()
|
||||||
|
|
||||||
|
|
||||||
@ -370,13 +407,13 @@ class SendMessageThread(threads.APRSDThread):
|
|||||||
# tracking the time.
|
# tracking the time.
|
||||||
log_message(
|
log_message(
|
||||||
"Sending Message",
|
"Sending Message",
|
||||||
repr(msg).rstrip("\n"),
|
str(msg).rstrip("\n"),
|
||||||
msg.message,
|
msg.message,
|
||||||
tocall=self.msg.tocall,
|
tocall=self.msg.tocall,
|
||||||
retry_number=msg.last_send_attempt,
|
retry_number=msg.last_send_attempt,
|
||||||
msg_num=msg.id,
|
msg_num=msg.id,
|
||||||
)
|
)
|
||||||
cl.sendall(repr(msg))
|
cl.sendall(str(msg))
|
||||||
stats.APRSDStats().msgs_tx_inc()
|
stats.APRSDStats().msgs_tx_inc()
|
||||||
msg.last_send_time = datetime.datetime.now()
|
msg.last_send_time = datetime.datetime.now()
|
||||||
msg.last_send_attempt += 1
|
msg.last_send_attempt += 1
|
||||||
@ -392,29 +429,40 @@ class AckMessage(Message):
|
|||||||
def __init__(self, fromcall, tocall, msg_id):
|
def __init__(self, fromcall, tocall, msg_id):
|
||||||
super().__init__(fromcall, tocall, msg_id=msg_id)
|
super().__init__(fromcall, tocall, msg_id=msg_id)
|
||||||
|
|
||||||
def __repr__(self):
|
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(
|
return "{}>APZ100::{}:ack{}\n".format(
|
||||||
self.fromcall,
|
self.fromcall,
|
||||||
self.tocall.ljust(9),
|
self.tocall.ljust(9),
|
||||||
self.id,
|
self.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "From({}) TO({}) Ack ({})".format(self.fromcall, self.tocall, self.id)
|
|
||||||
|
|
||||||
def send_thread(self):
|
def send_thread(self):
|
||||||
"""Separate thread to send acks with retries."""
|
"""Separate thread to send acks with retries."""
|
||||||
cl = client.get_client()
|
cl = client.get_client()
|
||||||
for i in range(self.retry_count, 0, -1):
|
for i in range(self.retry_count, 0, -1):
|
||||||
log_message(
|
log_message(
|
||||||
"Sending ack",
|
"Sending ack",
|
||||||
repr(self).rstrip("\n"),
|
str(self).rstrip("\n"),
|
||||||
None,
|
None,
|
||||||
ack=self.id,
|
ack=self.id,
|
||||||
tocall=self.tocall,
|
tocall=self.tocall,
|
||||||
retry_number=i,
|
retry_number=i,
|
||||||
)
|
)
|
||||||
cl.sendall(repr(self))
|
cl.sendall(str(self))
|
||||||
stats.APRSDStats().ack_tx_inc()
|
stats.APRSDStats().ack_tx_inc()
|
||||||
# aprs duplicate detection is 30 secs?
|
# aprs duplicate detection is 30 secs?
|
||||||
# (21 only sends first, 28 skips middle)
|
# (21 only sends first, 28 skips middle)
|
||||||
@ -433,13 +481,13 @@ class AckMessage(Message):
|
|||||||
cl = client.get_client()
|
cl = client.get_client()
|
||||||
log_message(
|
log_message(
|
||||||
"Sending ack",
|
"Sending ack",
|
||||||
repr(self).rstrip("\n"),
|
str(self).rstrip("\n"),
|
||||||
None,
|
None,
|
||||||
ack=self.id,
|
ack=self.id,
|
||||||
tocall=self.tocall,
|
tocall=self.tocall,
|
||||||
fromcall=self.fromcall,
|
fromcall=self.fromcall,
|
||||||
)
|
)
|
||||||
cl.sendall(repr(self))
|
cl.sendall(str(self))
|
||||||
|
|
||||||
|
|
||||||
class SendAckThread(threads.APRSDThread):
|
class SendAckThread(threads.APRSDThread):
|
||||||
@ -476,13 +524,13 @@ class SendAckThread(threads.APRSDThread):
|
|||||||
cl = client.get_client()
|
cl = client.get_client()
|
||||||
log_message(
|
log_message(
|
||||||
"Sending ack",
|
"Sending ack",
|
||||||
repr(self.ack).rstrip("\n"),
|
str(self.ack).rstrip("\n"),
|
||||||
None,
|
None,
|
||||||
ack=self.ack.id,
|
ack=self.ack.id,
|
||||||
tocall=self.ack.tocall,
|
tocall=self.ack.tocall,
|
||||||
retry_number=self.ack.last_send_attempt,
|
retry_number=self.ack.last_send_attempt,
|
||||||
)
|
)
|
||||||
cl.sendall(repr(self.ack))
|
cl.sendall(str(self.ack))
|
||||||
stats.APRSDStats().ack_tx_inc()
|
stats.APRSDStats().ack_tx_inc()
|
||||||
self.ack.last_send_attempt += 1
|
self.ack.last_send_attempt += 1
|
||||||
self.ack.last_send_time = datetime.datetime.now()
|
self.ack.last_send_time = datetime.datetime.now()
|
||||||
|
@ -2,7 +2,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from aprsd import email, messaging, plugin
|
from aprsd import email, messaging, plugin, trace
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
@ -18,6 +18,7 @@ class EmailPlugin(plugin.APRSDPluginBase):
|
|||||||
# five mins {int:int}
|
# five mins {int:int}
|
||||||
email_sent_dict = {}
|
email_sent_dict = {}
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, fromcall, message, ack):
|
||||||
LOG.info("Email COMMAND")
|
LOG.info("Email COMMAND")
|
||||||
reply = None
|
reply = None
|
||||||
@ -79,6 +80,7 @@ class EmailPlugin(plugin.APRSDPluginBase):
|
|||||||
self.email_sent_dict.clear()
|
self.email_sent_dict.clear()
|
||||||
self.email_sent_dict[ack] = now
|
self.email_sent_dict[ack] = now
|
||||||
else:
|
else:
|
||||||
|
reply = messaging.NULL_MESSAGE
|
||||||
LOG.info(
|
LOG.info(
|
||||||
"Email for message number "
|
"Email for message number "
|
||||||
+ ack
|
+ ack
|
||||||
|
@ -2,7 +2,7 @@ import logging
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from aprsd import plugin
|
from aprsd import plugin, trace
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
@ -14,6 +14,7 @@ class FortunePlugin(plugin.APRSDPluginBase):
|
|||||||
command_regex = "^[fF]"
|
command_regex = "^[fF]"
|
||||||
command_name = "fortune"
|
command_name = "fortune"
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, fromcall, message, ack):
|
||||||
LOG.info("FortunePlugin")
|
LOG.info("FortunePlugin")
|
||||||
reply = None
|
reply = None
|
||||||
|
@ -2,7 +2,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from aprsd import plugin, plugin_utils, utils
|
from aprsd import plugin, plugin_utils, trace, utils
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
@ -14,6 +14,7 @@ class LocationPlugin(plugin.APRSDPluginBase):
|
|||||||
command_regex = "^[lL]"
|
command_regex = "^[lL]"
|
||||||
command_name = "location"
|
command_name = "location"
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, fromcall, message, ack):
|
||||||
LOG.info("Location Plugin")
|
LOG.info("Location Plugin")
|
||||||
# get last location of a callsign, get descriptive name from weather service
|
# get last location of a callsign, get descriptive name from weather service
|
||||||
@ -65,6 +66,10 @@ class LocationPlugin(plugin.APRSDPluginBase):
|
|||||||
LOG.error("Couldn't fetch forecast.weather.gov '{}'".format(ex))
|
LOG.error("Couldn't fetch forecast.weather.gov '{}'".format(ex))
|
||||||
wx_data = {"location": {"areaDescription": "Unknown Location"}}
|
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(
|
reply = "{}: {} {}' {},{} {}h ago".format(
|
||||||
searchcall,
|
searchcall,
|
||||||
wx_data["location"]["areaDescription"],
|
wx_data["location"]["areaDescription"],
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from aprsd import plugin
|
from aprsd import plugin, trace
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
@ -13,6 +13,7 @@ class PingPlugin(plugin.APRSDPluginBase):
|
|||||||
command_regex = "^[pP]"
|
command_regex = "^[pP]"
|
||||||
command_name = "ping"
|
command_name = "ping"
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, fromcall, message, ack):
|
||||||
LOG.info("PINGPlugin")
|
LOG.info("PINGPlugin")
|
||||||
stm = time.localtime()
|
stm = time.localtime()
|
||||||
|
@ -2,7 +2,7 @@ import datetime
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from aprsd import messaging, plugin
|
from aprsd import messaging, plugin, trace
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
@ -14,6 +14,7 @@ class QueryPlugin(plugin.APRSDPluginBase):
|
|||||||
command_regex = r"^\!.*"
|
command_regex = r"^\!.*"
|
||||||
command_name = "query"
|
command_name = "query"
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, fromcall, message, ack):
|
||||||
LOG.info("Query COMMAND")
|
LOG.info("Query COMMAND")
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from aprsd import fuzzyclock, plugin, plugin_utils, utils
|
from aprsd import fuzzyclock, plugin, plugin_utils, trace, utils
|
||||||
from opencage.geocoder import OpenCageGeocode
|
from opencage.geocoder import OpenCageGeocode
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
@ -38,6 +38,7 @@ class TimePlugin(plugin.APRSDPluginBase):
|
|||||||
|
|
||||||
return reply
|
return reply
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, fromcall, message, ack):
|
||||||
LOG.info("TIME COMMAND")
|
LOG.info("TIME COMMAND")
|
||||||
# So we can mock this in unit tests
|
# So we can mock this in unit tests
|
||||||
@ -52,6 +53,7 @@ class TimeOpenCageDataPlugin(TimePlugin):
|
|||||||
command_regex = "^[tT]"
|
command_regex = "^[tT]"
|
||||||
command_name = "Time"
|
command_name = "Time"
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, fromcall, message, ack):
|
||||||
api_key = self.config["services"]["aprs.fi"]["apiKey"]
|
api_key = self.config["services"]["aprs.fi"]["apiKey"]
|
||||||
try:
|
try:
|
||||||
@ -92,6 +94,7 @@ class TimeOWMPlugin(TimePlugin):
|
|||||||
command_regex = "^[tT]"
|
command_regex = "^[tT]"
|
||||||
command_name = "Time"
|
command_name = "Time"
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, fromcall, message, ack):
|
||||||
api_key = self.config["services"]["aprs.fi"]["apiKey"]
|
api_key = self.config["services"]["aprs.fi"]["apiKey"]
|
||||||
try:
|
try:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import plugin
|
from aprsd import plugin, trace
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
@ -17,6 +17,7 @@ class VersionPlugin(plugin.APRSDPluginBase):
|
|||||||
# five mins {int:int}
|
# five mins {int:int}
|
||||||
email_sent_dict = {}
|
email_sent_dict = {}
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, fromcall, message, ack):
|
||||||
LOG.info("Version COMMAND")
|
LOG.info("Version COMMAND")
|
||||||
return "APRSD version '{}'".format(aprsd.__version__)
|
return "APRSD version '{}'".format(aprsd.__version__)
|
||||||
|
@ -2,7 +2,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from aprsd import plugin, plugin_utils, utils
|
from aprsd import plugin, plugin_utils, trace, utils
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@ -25,6 +25,7 @@ class USWeatherPlugin(plugin.APRSDPluginBase):
|
|||||||
command_regex = "^[wW]"
|
command_regex = "^[wW]"
|
||||||
command_name = "weather"
|
command_name = "weather"
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, fromcall, message, ack):
|
||||||
LOG.info("Weather Plugin")
|
LOG.info("Weather Plugin")
|
||||||
try:
|
try:
|
||||||
@ -84,6 +85,7 @@ class USMetarPlugin(plugin.APRSDPluginBase):
|
|||||||
command_regex = "^[metar]"
|
command_regex = "^[metar]"
|
||||||
command_name = "Metar"
|
command_name = "Metar"
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, fromcall, message, ack):
|
||||||
LOG.info("WX Plugin '{}'".format(message))
|
LOG.info("WX Plugin '{}'".format(message))
|
||||||
a = re.search(r"^.*\s+(.*)", message)
|
a = re.search(r"^.*\s+(.*)", message)
|
||||||
@ -175,6 +177,7 @@ class OWMWeatherPlugin(plugin.APRSDPluginBase):
|
|||||||
command_regex = "^[wW]"
|
command_regex = "^[wW]"
|
||||||
command_name = "Weather"
|
command_name = "Weather"
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, fromcall, message, ack):
|
||||||
LOG.info("OWMWeather Plugin '{}'".format(message))
|
LOG.info("OWMWeather Plugin '{}'".format(message))
|
||||||
a = re.search(r"^.*\s+(.*)", message)
|
a = re.search(r"^.*\s+(.*)", message)
|
||||||
@ -295,6 +298,7 @@ class AVWXWeatherPlugin(plugin.APRSDPluginBase):
|
|||||||
command_regex = "^[metar]"
|
command_regex = "^[metar]"
|
||||||
command_name = "Weather"
|
command_name = "Weather"
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, fromcall, message, ack):
|
||||||
LOG.info("OWMWeather Plugin '{}'".format(message))
|
LOG.info("OWMWeather Plugin '{}'".format(message))
|
||||||
a = re.search(r"^.*\s+(.*)", message)
|
a = re.search(r"^.*\s+(.*)", message)
|
||||||
|
@ -4,8 +4,9 @@ import logging
|
|||||||
import queue
|
import queue
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import tracemalloc
|
||||||
|
|
||||||
from aprsd import client, messaging, plugin, stats
|
from aprsd import client, messaging, plugin, stats, trace
|
||||||
import aprslib
|
import aprslib
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@ -69,6 +70,7 @@ class KeepAliveThread(APRSDThread):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__("KeepAlive")
|
super().__init__("KeepAlive")
|
||||||
|
tracemalloc.start()
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
if self.cntr % 6 == 0:
|
if self.cntr % 6 == 0:
|
||||||
@ -81,14 +83,17 @@ class KeepAliveThread(APRSDThread):
|
|||||||
else:
|
else:
|
||||||
email_thread_time = "N/A"
|
email_thread_time = "N/A"
|
||||||
|
|
||||||
|
current, peak = tracemalloc.get_traced_memory()
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
"Uptime ({}) Tracker({}) "
|
"Uptime ({}) Tracker({}) "
|
||||||
"Msgs: TX:{} RX:{} EmailThread: {}".format(
|
"Msgs: TX:{} RX:{} EmailThread: {} RAM: Current:{} Peak:{}".format(
|
||||||
stats_obj.uptime,
|
stats_obj.uptime,
|
||||||
len(tracker),
|
len(tracker),
|
||||||
stats_obj.msgs_tx,
|
stats_obj.msgs_tx,
|
||||||
stats_obj.msgs_rx,
|
stats_obj.msgs_rx,
|
||||||
email_thread_time,
|
email_thread_time,
|
||||||
|
current,
|
||||||
|
peak,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.cntr += 1
|
self.cntr += 1
|
||||||
@ -219,11 +224,11 @@ class APRSDRXThread(APRSDThread):
|
|||||||
self.msg_queues["tx"].put(ack)
|
self.msg_queues["tx"].put(ack)
|
||||||
LOG.debug("Packet processing complete")
|
LOG.debug("Packet processing complete")
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def process_packet(self, packet):
|
def process_packet(self, packet):
|
||||||
"""Process a packet recieved from aprs-is server."""
|
"""Process a packet recieved from aprs-is server."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
LOG.info("Got message: {}".format(packet))
|
|
||||||
stats.APRSDStats().msgs_rx_inc()
|
stats.APRSDStats().msgs_rx_inc()
|
||||||
|
|
||||||
msg = packet.get("message_text", None)
|
msg = packet.get("message_text", None)
|
||||||
|
181
aprsd/trace.py
Normal file
181
aprsd/trace.py
Normal file
@ -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
|
@ -22,6 +22,7 @@ DEFAULT_CONFIG_DICT = {
|
|||||||
},
|
},
|
||||||
"aprsd": {
|
"aprsd": {
|
||||||
"logfile": "/tmp/aprsd.log",
|
"logfile": "/tmp/aprsd.log",
|
||||||
|
"trace": False,
|
||||||
"plugin_dir": "~/.config/aprsd/plugins",
|
"plugin_dir": "~/.config/aprsd/plugins",
|
||||||
"enabled_plugins": plugin.CORE_PLUGINS,
|
"enabled_plugins": plugin.CORE_PLUGINS,
|
||||||
"units": "imperial",
|
"units": "imperial",
|
||||||
@ -29,6 +30,9 @@ DEFAULT_CONFIG_DICT = {
|
|||||||
"enabled": True,
|
"enabled": True,
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"port": 8001,
|
"port": 8001,
|
||||||
|
"users": {
|
||||||
|
"admin": "aprsd",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
@ -43,6 +47,7 @@ DEFAULT_CONFIG_DICT = {
|
|||||||
"host": "smtp.gmail.com",
|
"host": "smtp.gmail.com",
|
||||||
"port": 465,
|
"port": 465,
|
||||||
"use_ssl": False,
|
"use_ssl": False,
|
||||||
|
"debug": False,
|
||||||
},
|
},
|
||||||
"imap": {
|
"imap": {
|
||||||
"login": "IMAP_USERNAME",
|
"login": "IMAP_USERNAME",
|
||||||
@ -50,6 +55,7 @@ DEFAULT_CONFIG_DICT = {
|
|||||||
"host": "imap.gmail.com",
|
"host": "imap.gmail.com",
|
||||||
"port": 993,
|
"port": 993,
|
||||||
"use_ssl": True,
|
"use_ssl": True,
|
||||||
|
"debug": False,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -297,6 +303,15 @@ def parse_config(config_file):
|
|||||||
["aprs", "password"],
|
["aprs", "password"],
|
||||||
default_fail=DEFAULT_CONFIG_DICT["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:
|
if config["aprsd"]["email"]["enabled"] is True:
|
||||||
# Check IMAP server settings
|
# Check IMAP server settings
|
||||||
check_option(config, ["aprsd", "email", "imap", "host"])
|
check_option(config, ["aprsd", "email", "imap", "host"])
|
||||||
|
57
aprsd/web/static/json-viewer/jquery.json-viewer.css
Normal file
57
aprsd/web/static/json-viewer/jquery.json-viewer.css
Normal file
@ -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;
|
||||||
|
}
|
158
aprsd/web/static/json-viewer/jquery.json-viewer.js
Normal file
158
aprsd/web/static/json-viewer/jquery.json-viewer.js
Normal file
@ -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);
|
15
aprsd/web/templates/messages.html
Normal file
15
aprsd/web/templates/messages.html
Normal file
@ -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 ..
|
|
@ -1,12 +1,12 @@
|
|||||||
FROM alpine:latest as aprsd
|
FROM alpine:latest as aprsd
|
||||||
|
|
||||||
# Dockerfile for building a container during aprsd development.
|
# Dockerfile for building a container during aprsd development.
|
||||||
|
ARG BRANCH
|
||||||
|
|
||||||
ENV VERSION=1.5.1
|
|
||||||
ENV APRS_USER=aprs
|
ENV APRS_USER=aprs
|
||||||
ENV HOME=/home/aprs
|
ENV HOME=/home/aprs
|
||||||
ENV APRSD=http://github.com/craigerl/aprsd.git
|
ENV APRSD=http://github.com/craigerl/aprsd.git
|
||||||
ENV APRSD_BRANCH="master"
|
ENV APRSD_BRANCH=$BRANCH
|
||||||
ENV VIRTUAL_ENV=$HOME/.venv3
|
ENV VIRTUAL_ENV=$HOME/.venv3
|
||||||
|
|
||||||
ENV INSTALL=$HOME/install
|
ENV INSTALL=$HOME/install
|
||||||
@ -46,5 +46,5 @@ RUN chown -R $APRS_USER:$APRS_USER /config
|
|||||||
ENV CONF default
|
ENV CONF default
|
||||||
USER $APRS_USER
|
USER $APRS_USER
|
||||||
|
|
||||||
ADD build/bin/run.sh $HOME/
|
ADD bin/run.sh $HOME/
|
||||||
ENTRYPOINT ["/home/aprs/run.sh"]
|
ENTRYPOINT ["/home/aprs/run.sh"]
|
4
docker/build.sh
Executable file
4
docker/build.sh
Executable file
@ -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 .
|
@ -13,4 +13,5 @@ pre-commit
|
|||||||
pytz
|
pytz
|
||||||
opencage
|
opencage
|
||||||
flask
|
flask
|
||||||
flask_classful
|
flask-classful
|
||||||
|
flask-httpauth
|
||||||
|
@ -37,10 +37,13 @@ filelock==3.0.12
|
|||||||
# virtualenv
|
# virtualenv
|
||||||
flask-classful==0.14.2
|
flask-classful==0.14.2
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
|
flask-httpauth==4.2.0
|
||||||
|
# via -r requirements.in
|
||||||
flask==1.1.2
|
flask==1.1.2
|
||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
# flask-classful
|
# flask-classful
|
||||||
|
# flask-httpauth
|
||||||
identify==1.5.13
|
identify==1.5.13
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
idna==2.10
|
idna==2.10
|
||||||
|
Loading…
x
Reference in New Issue
Block a user