1
0
mirror of https://github.com/craigerl/aprsd.git synced 2025-06-26 05:55:26 -04:00

New Admin ui send message page working.

This commit is contained in:
Hemna 2021-08-26 20:58:07 -04:00
parent 6d3258e833
commit 23cbf32814
6 changed files with 449 additions and 71 deletions

View File

@ -130,8 +130,12 @@ class Aprsdis(aprslib.IS):
# sock.recv returns empty if the connection drops # sock.recv returns empty if the connection drops
if not short_buf: if not short_buf:
self.logger.error("socket.recv(): returned empty") if not blocking:
raise aprslib.ConnectionDrop("connection dropped") # 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: except OSError as e:
# self.logger.error("socket error on recv(): %s" % str(e)) # self.logger.error("socket error on recv(): %s" % str(e))
if "Resource temporarily unavailable" in str(e): if "Resource temporarily unavailable" in str(e):

View File

@ -4,8 +4,10 @@ import logging
from logging import NullHandler from logging import NullHandler
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
import sys import sys
import threading
import time import time
import aprslib
from aprslib.exceptions import LoginError from aprslib.exceptions import LoginError
import flask import flask
from flask import request from flask import request
@ -14,7 +16,7 @@ from flask_httpauth import HTTPBasicAuth
from werkzeug.security import check_password_hash, generate_password_hash from werkzeug.security import check_password_hash, generate_password_hash
import aprsd 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") LOG = logging.getLogger("APRSD")
@ -22,6 +24,72 @@ LOG = logging.getLogger("APRSD")
auth = HTTPBasicAuth() auth = HTTPBasicAuth()
users = None 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. # HTTPBasicAuth doesn't work on a class method.
# This has to be out here. Rely on the APRSDFlask # This has to be out here. Rely on the APRSDFlask
@ -34,6 +102,161 @@ def verify_password(username, password):
return username 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): class APRSDFlask(flask_classful.FlaskView):
config = None config = None
@ -118,69 +341,53 @@ class APRSDFlask(flask_classful.FlaskView):
return flask.render_template("messages.html", messages=json.dumps(msgs)) return flask.render_template("messages.html", messages=json.dumps(msgs))
def setup_connection(self): @auth.login_required
user = self.config["aprs"]["login"] def send_message_status(self):
password = self.config["aprs"]["password"] LOG.debug(request)
host = self.config["aprs"].get("host", "rotate.aprs.net") msgs = SentMessages()
port = self.config["aprs"].get("port", 14580) info = msgs.get_all()
connected = False return json.dumps(info)
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(self): def send_message(self):
LOG.debug(request)
if request.method == "POST": if request.method == "POST":
from_call = request.form["from_call"] info = {
to_call = request.form["to_call"] "from": request.form["from_call"],
message = request.form["message"] "to": request.form["to_call"],
LOG.info( "password": request.form["from_call_password"],
"From: '{}' To: '{}' Send '{}'".format( "message": request.form["message"],
from_call, "wait_reply": request.form["wait_reply"],
to_call, }
message, LOG.debug(info)
), msg = messaging.TextMessage(
info["from"], info["to"],
info["message"],
) )
msgs = SentMessages()
msgs.add(msg)
msgs.set_status(msg.id, "Sending")
try: send_message_t = SendMessageThread(self.config, info, msg)
aprsis_client = self.setup_connection() send_message_t.start()
except LoginError as e:
result = "Failed to setup Connection {}".format(e)
msg = messaging.TextMessage(from_call, to_call, message)
msg.send_direct(aprsis_client=aprsis_client) info["from"]
result = "Message sent" result = "sending"
msg_id = msg.id
result = {
"msg_id": msg_id,
"status": "sending",
}
return json.dumps(result)
else: else:
from_call = self.config["aprs"]["login"] result = "fail"
result = ""
return flask.render_template( return flask.render_template(
"send-message.html", "send-message.html",
from_call=from_call, callsign=self.config["aprs"]["login"],
result=result, version=aprsd.__version__,
) )
@auth.login_required @auth.login_required
def packets(self): def packets(self):
@ -292,6 +499,7 @@ def init_flask(config, loglevel, quiet):
flask_app.route("/messages", methods=["GET"])(server.messages) flask_app.route("/messages", methods=["GET"])(server.messages)
flask_app.route("/packets", methods=["GET"])(server.packets) flask_app.route("/packets", methods=["GET"])(server.packets)
flask_app.route("/send-message", methods=["GET", "POST"])(server.send_message) 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("/save", methods=["GET"])(server.save)
flask_app.route("/plugins", methods=["GET"])(server.plugins) flask_app.route("/plugins", methods=["GET"])(server.plugins)
return flask_app return flask_app

View File

@ -391,6 +391,7 @@ class TextMessage(Message):
) )
cl.send(self) cl.send(self)
stats.APRSDStats().msgs_tx_inc() stats.APRSDStats().msgs_tx_inc()
packets.PacketList().add(self.dict())
class SendMessageThread(threads.APRSDThread): class SendMessageThread(threads.APRSDThread):

View File

@ -69,6 +69,7 @@ class WatchList:
_instance = None _instance = None
callsigns = {} callsigns = {}
config = None
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls._instance is None: if cls._instance is None:
@ -97,7 +98,7 @@ class WatchList:
} }
def is_enabled(self): 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) return self.config["aprsd"]["watch_list"].get("enabled", False)
else: else:
return False return False

View File

@ -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 = '<div class="ui title" id="' + title_id + '"><i class="dropdown icon"></i>';
msg_html += '<div class="ui active inline loader" id="' + loader_id +'" data-content="Waiting for Ack"></div>&nbsp;';
msg_html += '<i class="thumbs down outline icon" id="' + reply_id + '" data-content="Waiting for Reply"></i>&nbsp;' + from_to + '</div>';
msg_html += '<div class="content"><p class="transition hidden"><pre id="' + pretty_id + '" class="language-json">' + json_pretty + '</p></p></div>'
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');
}

View File

@ -1,24 +1,114 @@
<html> <html>
<head> <head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css"> <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/prism.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/components/prism-json.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/themes/prism-tomorrow.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
<script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.js"></script>
<link rel="stylesheet" href="/static/css/index.css">
<link rel="stylesheet" href="/static/css/tabs.css">
<script src="/static/js/send-message.js"></script>
<script language="JavaScript">
$(document).ready(function() {
$("#sendform").submit(function(event) {
event.preventDefault();
var $checkboxes = $(this).find('input[type=checkbox]');
//loop through the checkboxes and change to hidden fields
$checkboxes.each(function() {
if ($(this)[0].checked) {
$(this).attr('type', 'hidden');
$(this).val(1);
} else {
$(this).attr('type', 'hidden');
$(this).val(0);
}
});
var posting = $.post("/send-message", $("#sendform").serialize());
posting.done(function(data, status){
console.log("Data: " + data + "\nStatus: " + status);
});
//loop through the checkboxes and change to hidden fields
$checkboxes.each(function() {
$(this).attr('type', 'checkbox');
});
});
(function sent_msg_worker() {
$.ajax({
url: "/send-message-status",
type: 'GET',
dataType: 'json',
success: function(data) {
update_messages(data);
},
complete: function() {
setTimeout(sent_msg_worker, 1000);
}
});
})();
});
</script>
</head> </head>
<body> <body>
<form action="send-message" method="POST" name="send-message"> <div class='ui text container'>
<p><label for="from_call">From Callsign:</label> <h1 class='ui dividing header'>APRSD {{ version }}</h1>
<input type="text" name="from_call" id="fcall" value="{{ from_call }}"></p> </div>
<p><label for="to_call">To Callsign:</label> <div class='ui grid text container'>
<input type="text" name="to_call" id="tcall"></p> <div class='left floated ten wide column'>
<span style='color: green'>{{ callsign }}</span>
connected to
<span style='color: blue' id='aprsis'>NONE</span>
</div>
<p><label for="message">Message:</label> <div class='right floated four wide column'>
<input type="text" name="message" id="message"></p> <span id='uptime'>NONE</span>
</div>
</div>
<h3 class="ui dividing header">Send Message Form</h3>
<form id="sendform" name="sendmsg" action="">
<p><label for="from_call">From Callsign:</label>
<input type="text" name="from_call" id="fcall" value="WB4BOR"></p>
<p><label for="from_call_password">Password:</label>
<input type="text" name="from_call_password" value="24496"></p>
<p><label for="to_call">To Callsign:</label>
<input type="text" name="to_call" id="tcall" value="WB4BOR-11"></p>
<p><label for="message">Message:</label>
<input type="text" name="message" id="message" value="ping"></p>
<p><label for="wait">Wait for Reply?</label>
<input type="checkbox" name="wait_reply" id="wait_reply" value="off" checked>
</p>
<input type="submit" name="submit" class="button" id="send_msg" value="Send" />
</form>
<h3 class="ui dividing header">Messages (<span id="msgs_count">0</span>)</h3>
<div class="ui styled fluid accordion" id="accordion">
<div id="msgsDiv" class="ui mini text">Messages</div>
</div>
<input value="Submit" type="submit">
</form>
</body> </body>