mirror of
https://github.com/craigerl/aprsd.git
synced 2024-12-19 08:05:56 -05:00
Added flask messages web UI and basic auth
This patch fixes the CTRL-C signal_handler. This patch also adds the new Messages WEB UI page as well as the save url, which are both behind an http basic auth. The flask web service now has users in the config file aprsd: web: users: admin: <password>
This commit is contained in:
parent
0d18e54969
commit
aa290692ab
@ -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,7 +28,6 @@ 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
|
||||||
@ -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
|
||||||
@ -394,9 +399,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:
|
||||||
@ -468,6 +471,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 +479,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()
|
||||||
|
@ -29,6 +29,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,
|
||||||
@ -297,6 +300,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>
|
@ -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…
Reference in New Issue
Block a user