From 23cbf32814368fa504a1c7ce5fc1012fb899cfa0 Mon Sep 17 00:00:00 2001 From: Hemna Date: Thu, 26 Aug 2021 20:58:07 -0400 Subject: [PATCH] New Admin ui send message page working. --- aprsd/client.py | 8 +- aprsd/flask.py | 320 +++++++++++++++++++++----- aprsd/messaging.py | 1 + aprsd/packets.py | 3 +- aprsd/web/static/js/send-message.js | 74 ++++++ aprsd/web/templates/send-message.html | 114 ++++++++- 6 files changed, 449 insertions(+), 71 deletions(-) create mode 100644 aprsd/web/static/js/send-message.js diff --git a/aprsd/client.py b/aprsd/client.py index 157f4ef..7b65703 100644 --- a/aprsd/client.py +++ b/aprsd/client.py @@ -130,8 +130,12 @@ class Aprsdis(aprslib.IS): # sock.recv returns empty if the connection drops if not short_buf: - self.logger.error("socket.recv(): returned empty") - raise aprslib.ConnectionDrop("connection dropped") + if not blocking: + # We could just not be blocking, so empty is expected + continue + else: + self.logger.error("socket.recv(): returned empty") + raise aprslib.ConnectionDrop("connection dropped") except OSError as e: # self.logger.error("socket error on recv(): %s" % str(e)) if "Resource temporarily unavailable" in str(e): diff --git a/aprsd/flask.py b/aprsd/flask.py index e87e695..839d42e 100644 --- a/aprsd/flask.py +++ b/aprsd/flask.py @@ -4,8 +4,10 @@ import logging from logging import NullHandler from logging.handlers import RotatingFileHandler import sys +import threading import time +import aprslib from aprslib.exceptions import LoginError import flask from flask import request @@ -14,7 +16,7 @@ from flask_httpauth import HTTPBasicAuth from werkzeug.security import check_password_hash, generate_password_hash import aprsd -from aprsd import client, kissclient, messaging, packets, plugin, stats, utils +from aprsd import client, kissclient, messaging, packets, plugin, stats, threads, utils LOG = logging.getLogger("APRSD") @@ -22,6 +24,72 @@ LOG = logging.getLogger("APRSD") auth = HTTPBasicAuth() users = None +class SentMessages: + _instance = None + lock = None + + msgs = {} + + def __new__(cls, *args, **kwargs): + """This magic turns this into a singleton.""" + if cls._instance is None: + cls._instance = super().__new__(cls) + # Put any initialization here. + cls.lock = threading.Lock() + return cls._instance + + def add(self, msg): + with self.lock: + self.msgs[msg.id] = self._create(msg.id) + self.msgs[msg.id]["from"] = msg.fromcall + self.msgs[msg.id]["to"] = msg.tocall + self.msgs[msg.id]["message"] = msg.message.rstrip("\n") + self.msgs[msg.id]["raw"] = str(msg).rstrip("\n") + + + def _create(self, id): + return { + "id": id, + "ts": time.time(), + "ack": False, + "from": None, + "to": None, + "raw": None, + "message": None, + "status": None, + "last_update": None, + "reply": None, + } + + def __len__(self): + with self.lock: + return len(self.msgs.keys()) + + def get(self, id): + with self.lock: + if id in self.msgs: + return self.msgs[id] + + def get_all(self): + with self.lock: + return self.msgs + + def set_status(self, id, status): + with self.lock: + self.msgs[id]["last_update"] = str(datetime.datetime.now()) + self.msgs[id]["status"] = status + + def ack(self, id): + """The message got an ack!""" + with self.lock: + self.msgs[id]["last_update"] = str(datetime.datetime.now()) + self.msgs[id]["ack"] = True + + def reply(self, id, packet): + """We got a packet back from the sent message.""" + with self.lock: + self.msgs[id]["reply"] = packet + # HTTPBasicAuth doesn't work on a class method. # This has to be out here. Rely on the APRSDFlask @@ -34,6 +102,161 @@ def verify_password(username, password): return username +class SendMessageThread(threads.APRSDThread): + """Thread for sending a message from web.""" + + aprsis_client = None + request = None + got_ack = False + + def __init__(self, config, info, msg): + self.config = config + self.request = info + self.msg = msg + msg = "({} -> {}) : {}".format( + info["from"], + info["to"], + info["message"], + ) + super().__init__(f"WEB_SEND_MSG-{msg}") + + def setup_connection(self): + user = self.request["from"] + password = self.request["password"] + host = self.config["aprs"].get("host", "rotate.aprs.net") + port = self.config["aprs"].get("port", 14580) + connected = False + backoff = 1 + while not connected: + try: + LOG.info("Creating aprslib client") + aprs_client = client.Aprsdis( + user, + passwd=password, + host=host, + port=port, + ) + # Force the logging to be the same + aprs_client.logger = LOG + aprs_client.connect() + connected = True + backoff = 1 + except LoginError as e: + LOG.error(f"Failed to login to APRS-IS Server '{e}'") + connected = False + raise e + except Exception as e: + LOG.error(f"Unable to connect to APRS-IS server. '{e}' ") + time.sleep(backoff) + backoff = backoff * 2 + continue + LOG.debug(f"Logging in to APRS-IS with user '{user}'") + return aprs_client + + def run(self): + LOG.debug("Starting") + from_call = self.request["from"] + self.request["password"] + to_call = self.request["to"] + message = self.request["message"] + LOG.info( + "From: '{}' To: '{}' Send '{}'".format( + from_call, + to_call, + message, + ), + ) + + try: + self.aprs_client = self.setup_connection() + except LoginError as e: + f"Failed to setup Connection {e}" + + self.msg.send_direct(aprsis_client=self.aprs_client) + SentMessages().set_status(self.msg.id, "Sent") + + while not self.thread_stop: + can_loop = self.loop() + if not can_loop: + self.stop() + threads.APRSDThreadList().remove(self) + LOG.debug("Exiting") + + def rx_packet(self, packet): + global got_ack, got_response + # LOG.debug("Got packet back {}".format(packet)) + resp = packet.get("response", None) + if resp == "ack": + ack_num = packet.get("msgNo") + LOG.info(f"We got ack for our sent message {ack_num}") + messaging.log_packet(packet) + SentMessages().ack(self.msg.id) + stats.APRSDStats().ack_rx_inc() + self.got_ack = True + if self.request["wait_reply"] == "0": + # We aren't waiting for a reply, so we can bail + self.thread_stop = self.aprs_client.thread_stop = True + else: + packets.PacketList().add(packet) + stats.APRSDStats().msgs_rx_inc() + message = packet.get("message_text", None) + fromcall = packet["from"] + msg_number = packet.get("msgNo", "0") + messaging.log_message( + "Received Message", + packet["raw"], + message, + fromcall=fromcall, + ack=msg_number, + ) + got_response = True + SentMessages().reply(self.msg.id, packet) + SentMessages().set_status(self.msg.id, "Got Reply") + + # Send the ack back? + ack = messaging.AckMessage( + self.request["from"], + fromcall, + msg_id=msg_number, + ) + ack.send_direct() + SentMessages().set_status(self.msg.id, "Ack Sent") + + # Now we can exit, since we are done. + if self.got_ack: + self.thread_stop = self.aprs_client.thread_stop = True + + def loop(self): + LOG.debug("LOOP Start") + try: + # This will register a packet consumer with aprslib + # When new packets come in the consumer will process + # the packet + self.aprs_client.consumer(self.rx_packet, raw=False, blocking=False) + except aprslib.exceptions.ConnectionDrop: + LOG.error("Connection dropped, reconnecting") + time.sleep(5) + # Force the deletion of the client object connected to aprs + # This will cause a reconnect, next time client.get_client() + # is called + del self.aprs_client + connecting = True + counter = 0; + while connecting: + try: + self.aprs_client = self.setup_connection() + connecting = False + except Exception: + LOG.error("Couldn't connect") + counter += 1 + if counter >= 3: + LOG.error("Reached reconnect limit.") + return False + + LOG.debug("LOOP END") + return True + + class APRSDFlask(flask_classful.FlaskView): config = None @@ -118,69 +341,53 @@ class APRSDFlask(flask_classful.FlaskView): return flask.render_template("messages.html", messages=json.dumps(msgs)) - def setup_connection(self): - user = self.config["aprs"]["login"] - password = self.config["aprs"]["password"] - host = self.config["aprs"].get("host", "rotate.aprs.net") - port = self.config["aprs"].get("port", 14580) - connected = False - backoff = 1 - while not connected: - try: - LOG.info("Creating aprslib client") - aprs_client = client.Aprsdis( - user, - passwd=password, - host=host, - port=port, - ) - # Force the logging to be the same - aprs_client.logger = LOG - aprs_client.connect() - connected = True - backoff = 1 - except LoginError as e: - LOG.error("Failed to login to APRS-IS Server '{}'".format(e)) - connected = False - raise e - except Exception as e: - LOG.error("Unable to connect to APRS-IS server. '{}' ".format(e)) - time.sleep(backoff) - backoff = backoff * 2 - continue - LOG.debug("Logging in to APRS-IS with user '%s'" % user) - return aprs_client + @auth.login_required + def send_message_status(self): + LOG.debug(request) + msgs = SentMessages() + info = msgs.get_all() + return json.dumps(info) + @auth.login_required def send_message(self): + LOG.debug(request) if request.method == "POST": - from_call = request.form["from_call"] - to_call = request.form["to_call"] - message = request.form["message"] - LOG.info( - "From: '{}' To: '{}' Send '{}'".format( - from_call, - to_call, - message, - ), + info = { + "from": request.form["from_call"], + "to": request.form["to_call"], + "password": request.form["from_call_password"], + "message": request.form["message"], + "wait_reply": request.form["wait_reply"], + } + LOG.debug(info) + msg = messaging.TextMessage( + info["from"], info["to"], + info["message"], ) + msgs = SentMessages() + msgs.add(msg) + msgs.set_status(msg.id, "Sending") - try: - aprsis_client = self.setup_connection() - except LoginError as e: - result = "Failed to setup Connection {}".format(e) + send_message_t = SendMessageThread(self.config, info, msg) + send_message_t.start() - msg = messaging.TextMessage(from_call, to_call, message) - msg.send_direct(aprsis_client=aprsis_client) - result = "Message sent" + + info["from"] + result = "sending" + msg_id = msg.id + result = { + "msg_id": msg_id, + "status": "sending", + } + return json.dumps(result) else: - from_call = self.config["aprs"]["login"] - result = "" + result = "fail" - return flask.render_template( - "send-message.html", - from_call=from_call, - result=result, - ) + return flask.render_template( + "send-message.html", + callsign=self.config["aprs"]["login"], + version=aprsd.__version__, + ) @auth.login_required def packets(self): @@ -292,6 +499,7 @@ def init_flask(config, loglevel, quiet): flask_app.route("/messages", methods=["GET"])(server.messages) flask_app.route("/packets", methods=["GET"])(server.packets) flask_app.route("/send-message", methods=["GET", "POST"])(server.send_message) + flask_app.route("/send-message-status", methods=["GET"])(server.send_message_status) flask_app.route("/save", methods=["GET"])(server.save) flask_app.route("/plugins", methods=["GET"])(server.plugins) return flask_app diff --git a/aprsd/messaging.py b/aprsd/messaging.py index 3e51f0f..8ae987b 100644 --- a/aprsd/messaging.py +++ b/aprsd/messaging.py @@ -391,6 +391,7 @@ class TextMessage(Message): ) cl.send(self) stats.APRSDStats().msgs_tx_inc() + packets.PacketList().add(self.dict()) class SendMessageThread(threads.APRSDThread): diff --git a/aprsd/packets.py b/aprsd/packets.py index ef5e3f5..8e80432 100644 --- a/aprsd/packets.py +++ b/aprsd/packets.py @@ -69,6 +69,7 @@ class WatchList: _instance = None callsigns = {} + config = None def __new__(cls, *args, **kwargs): if cls._instance is None: @@ -97,7 +98,7 @@ class WatchList: } def is_enabled(self): - if "watch_list" in self.config["aprsd"]: + if self.config and "watch_list" in self.config["aprsd"]: return self.config["aprsd"]["watch_list"].get("enabled", False) else: return False diff --git a/aprsd/web/static/js/send-message.js b/aprsd/web/static/js/send-message.js new file mode 100644 index 0000000..92afbd4 --- /dev/null +++ b/aprsd/web/static/js/send-message.js @@ -0,0 +1,74 @@ +msgs_list = {}; + +function size_dict(d){c=0; for (i in d) ++c; return c} + +function update_messages(data) { + msgs_cnt = size_dict(data); + $('#msgs_count').html(msgs_cnt); + + var msgsdiv = $("#msgsDiv"); + //nuke the contents first, then add to it. + if (size_dict(msgs_list) == 0 && size_dict(data) > 0) { + msgsdiv.html('') + } + + jQuery.each(data, function(i, val) { + if ( msgs_list.hasOwnProperty(val["ts"]) == false ) { + // Store the packet + msgs_list[val["ts"]] = val; + ts_str = val["ts"].toString(); + ts = ts_str.split(".")[0]*1000; + var d = new Date(ts).toLocaleDateString("en-US") + var t = new Date(ts).toLocaleTimeString("en-US") + + from = val['from'] + title_id = 'title_tx' + var from_to = d + " " + t + "    " + from + " > " + + if (val.hasOwnProperty('to')) { + from_to = from_to + val['to'] + } + from_to = from_to + "  -  " + val['raw'] + + id = ts_str.split('.')[0] + pretty_id = "pretty_" + id + loader_id = "loader_" + id + reply_id = "reply_" + id + json_pretty = Prism.highlight(JSON.stringify(val, null, '\t'), Prism.languages.json, 'json'); + msg_html = '
'; + msg_html += '
 '; + msg_html += ' ' + from_to + '
'; + msg_html += '
' + json_pretty + '

' + msgsdiv.prepend(msg_html); + } else { + // We have an existing entry + msgs_list[val["ts"]] = val; + ts_str = val["ts"].toString(); + id = ts_str.split('.')[0] + pretty_id = "pretty_" + id + loader_id = "loader_" + id + reply_id = "reply_" + id + var pretty_pre = $("#" + pretty_id); + if (val['ack'] == true) { + var loader_div = $('#' + loader_id); + loader_div.removeClass('ui active inline loader'); + loader_div.addClass('ui disabled loader'); + loader_div.attr('data-content', 'Got reply'); + } + + if (val['reply'] !== null) { + var reply_div = $('#' + reply_id); + reply_div.removeClass("thumbs down outline icon"); + reply_div.addClass('thumbs up outline icon'); + reply_div.attr('data-content', 'Got Reply'); + } + + pretty_pre.html(''); + json_pretty = Prism.highlight(JSON.stringify(val, null, '\t'), Prism.languages.json, 'json'); + pretty_pre.html(json_pretty); + } + }); + + $('.ui.accordion').accordion('refresh'); + +} diff --git a/aprsd/web/templates/send-message.html b/aprsd/web/templates/send-message.html index d0aeb8c..1f9f354 100644 --- a/aprsd/web/templates/send-message.html +++ b/aprsd/web/templates/send-message.html @@ -1,24 +1,114 @@ - - - + + + + + + + + + + + + + + + + -
-

-

+
+

APRSD {{ version }}

+
-

-

+
+
+ {{ callsign }} + connected to + NONE +
-

-

+
+ NONE +
+
+ +

Send Message Form

+ +

+

+

+

+ +

+

+ +

+

+ +

+ +

+ + +
+ +

Messages (0)

+
+
Messages
+
- -