def test_len(self): """Test getting length of tracked messages.""" track = self._clean_track() fromcall = "KFART" tocall = "KHELP" message = "somthing" msg = messaging.TextMessage(fromcall, tocall, message) track.add(msg) self.assertEqual(1, len(track)) msg2 = messaging.TextMessage(tocall, fromcall, message) track.add(msg2) self.assertEqual(2, len(track)) track.remove(msg.id) self.assertEqual(1, len(track))
def test_add(self): track = self._clean_track() fromcall = "KFART" tocall = "KHELP" message = "somthing" msg = messaging.TextMessage(fromcall, tocall, message) track.add(msg) self.assertEqual(msg, track.get(msg.id))
def test__resend(self, mock_send): """Test the _resend method.""" track = self._clean_track() fromcall = "KFART" tocall = "KHELP" message = "somthing" msg = messaging.TextMessage(fromcall, tocall, message) msg.last_send_attempt = 3 track.add(msg) track._resend(msg) msg.send.assert_called_with() self.assertEqual(0, msg.last_send_attempt)
def test_query_restart_delayed(self, mock_restart): track = messaging.MsgTrack() track.data = {} packet = fake.fake_packet(message="!4") query = query_plugin.QueryPlugin(self.config) expected = "No pending msgs to resend" actual = query.filter(packet) mock_restart.assert_not_called() self.assertEqual(expected, actual) mock_restart.reset_mock() # add a message msg = messaging.TextMessage(self.fromcall, "testing", self.ack) track.add(msg) actual = query.filter(packet) mock_restart.assert_called_once()
def on_send(self, data): global socketio LOG.debug(f"WS: on_send {data}") self.request = data msg = messaging.TextMessage( data["from"], data["to"], data["message"], ) self.msg = msg msgs = SentMessages() msgs.add(msg) msgs.set_status(msg.id, "Sending") socketio.emit( "sent", SentMessages().get(self.msg.id), namespace="/sendmsg", ) socketio.start_background_task(self._start, self._config, data, msg, self) LOG.warning("WS: on_send: exit")
def process(self, packet): LOG.info("NotifySeenPlugin") notify_callsign = self.config["aprsd"]["watch_list"]["alert_callsign"] fromcall = packet.get("from") wl = packets.WatchList() age = wl.age(fromcall) if wl.is_old(packet["from"]): LOG.info( "NOTIFY {} last seen {} max age={}".format( fromcall, age, wl.max_delta(), ), ) packet_type = packets.get_packet_type(packet) # we shouldn't notify the alert user that they are online. if fromcall != notify_callsign: msg = messaging.TextMessage( self.config["aprs"]["login"], notify_callsign, f"{fromcall} was just seen by type:'{packet_type}'", # We don't need to keep this around if it doesn't go thru allow_delay=False, ) return msg else: LOG.debug( "fromcall and notify_callsign are the same, not notifying") return messaging.NULL_MESSAGE else: LOG.debug( "Not old enough to notify on callsign '{}' : {} < {}".format( fromcall, age, wl.max_delta(), ), ) return messaging.NULL_MESSAGE
def send_message( ctx, aprs_login, aprs_password, no_ack, wait_response, raw, tocallsign, command, ): """Send a message to a callsign via APRS_IS.""" global got_ack, got_response config = ctx.obj["config"] quiet = ctx.obj["quiet"] if not aprs_login: if not config.exists("aprs.login"): click.echo("Must set --aprs_login or APRS_LOGIN") ctx.exit(-1) return else: config["aprs"]["login"] = aprs_login if not aprs_password: if not config.exists("aprs.password"): click.echo("Must set --aprs-password or APRS_PASSWORD") ctx.exit(-1) return else: config["aprs"]["password"] = aprs_password LOG.info(f"APRSD LISTEN Started version: {aprsd.__version__}") if type(command) is tuple: command = " ".join(command) if not quiet: if raw: LOG.info(f"L'{aprs_login}' R'{raw}'") else: LOG.info(f"L'{aprs_login}' To'{tocallsign}' C'{command}'") packets.PacketList(config=config) packets.WatchList(config=config) packets.SeenList(config=config) got_ack = False got_response = False def rx_packet(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) got_ack = True else: 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 # Send the ack back? ack = messaging.AckMessage( config["aprs"]["login"], fromcall, msg_id=msg_number, ) ack.send_direct() if got_ack: if wait_response: if got_response: sys.exit(0) else: sys.exit(0) try: client.ClientFactory.setup(config) client.factory.create().client except LoginError: sys.exit(-1) # Send a message # then we setup a consumer to rx messages # We should get an ack back as well as a new message # we should bail after we get the ack and send an ack back for the # message if raw: msg = messaging.RawMessage(raw) msg.send_direct() sys.exit(0) else: msg = messaging.TextMessage(aprs_login, tocallsign, command) msg.send_direct() if no_ack: sys.exit(0) try: # This will register a packet consumer with aprslib # When new packets come in the consumer will process # the packet aprs_client = client.factory.create().client aprs_client.consumer(rx_packet, raw=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 aprs_client.reset()
def test_restart_delayed(self, mock_send): """Test the _resend method.""" track = self._clean_track() fromcall = "KFART" tocall = "KHELP" message1 = "something" message2 = "something another" message3 = "something another again" mock1_send = mock.MagicMock() mock2_send = mock.MagicMock() mock3_send = mock.MagicMock() msg1 = messaging.TextMessage(fromcall, tocall, message1) msg1.last_send_attempt = 3 msg1.last_send_time = datetime.datetime.now() msg1.send = mock1_send track.add(msg1) msg2 = messaging.TextMessage(tocall, fromcall, message2) msg2.last_send_attempt = 3 msg2.last_send_time = datetime.datetime.now() msg2.send = mock2_send track.add(msg2) track.restart_delayed(count=None) msg1.send.assert_called_once() self.assertEqual(0, msg1.last_send_attempt) msg2.send.assert_called_once() self.assertEqual(0, msg2.last_send_attempt) msg1.last_send_attempt = 3 msg1.send.reset_mock() msg2.last_send_attempt = 3 msg2.send.reset_mock() track.restart_delayed(count=1) msg1.send.assert_not_called() msg2.send.assert_called_once() self.assertEqual(3, msg1.last_send_attempt) self.assertEqual(0, msg2.last_send_attempt) msg3 = messaging.TextMessage(tocall, fromcall, message3) msg3.last_send_attempt = 3 msg3.last_send_time = datetime.datetime.now() msg3.send = mock3_send track.add(msg3) msg1.last_send_attempt = 3 msg1.send.reset_mock() msg2.last_send_attempt = 3 msg2.send.reset_mock() msg3.last_send_attempt = 3 msg3.send.reset_mock() track.restart_delayed(count=2) msg1.send.assert_not_called() msg2.send.assert_called_once() msg3.send.assert_called_once() self.assertEqual(3, msg1.last_send_attempt) self.assertEqual(0, msg2.last_send_attempt) self.assertEqual(0, msg3.last_send_attempt) msg1.last_send_attempt = 3 msg1.send.reset_mock() msg2.last_send_attempt = 3 msg2.send.reset_mock() msg3.last_send_attempt = 3 msg3.send.reset_mock() track.restart_delayed(count=2, most_recent=False) msg1.send.assert_called_once() msg2.send.assert_called_once() msg3.send.assert_not_called() self.assertEqual(0, msg1.last_send_attempt) self.assertEqual(0, msg2.last_send_attempt) self.assertEqual(3, msg3.last_send_attempt)
def run(self): global check_email_delay LOG.debug("Starting") check_email_delay = 60 past = datetime.datetime.now() while not self.thread_stop: time.sleep(5) stats.APRSDStats().email_thread_update() # always sleep for 5 seconds and see if we need to check email # This allows CTRL-C to stop the execution of this loop sooner # than check_email_delay time now = datetime.datetime.now() if now - past > datetime.timedelta(seconds=check_email_delay): # It's time to check email # slowly increase delay every iteration, max out at 300 seconds # any send/receive/resend activity will reset this to 60 seconds if check_email_delay < 300: check_email_delay += 1 LOG.debug("check_email_delay is " + str(check_email_delay) + " seconds") shortcuts = CONFIG["aprsd"]["email"]["shortcuts"] # swap key/value shortcuts_inverted = {v: k for k, v in shortcuts.items()} date = datetime.datetime.now() month = date.strftime("%B")[:3] # Nov, Mar, Apr day = date.day year = date.year today = "{}-{}-{}".format(day, month, year) server = None try: server = _imap_connect() except Exception as e: LOG.exception("IMAP failed to connect.", e) if not server: continue 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))) try: _msgs = server.fetch(messages, ["ENVELOPE"]) except Exception as e: LOG.exception("IMAP failed to fetch/flag messages: ", e) continue for msgid, data in _msgs.items(): envelope = data[b"ENVELOPE"] LOG.debug( 'ID:%d "%s" (%s)' % (msgid, envelope.subject.decode(), envelope.date), ) f = re.search( r"'([[A-a][0-9]_-]+@[[A-a][0-9]_-\.]+)", str(envelope.from_[0]), ) if f is not None: from_addr = f.group(1) else: from_addr = "noaddr" # LOG.debug("Message flags/tags: " + str(server.get_flags(msgid)[msgid])) # if "APRS" not in server.get_flags(msgid)[msgid]: # in python3, imap tags are unicode. in py2 they're strings. so .decode them to handle both try: taglist = [ x.decode(errors="ignore") for x in server.get_flags(msgid)[msgid] ] except Exception as e: LOG.exception("Failed to get flags.", e) break if "APRS" not in taglist: # if msg not flagged as sent via aprs try: server.fetch([msgid], ["RFC822"]) except Exception as e: LOG.exception( "Failed single server fetch for RFC822", e) break (body, from_addr) = parse_email(msgid, data, server) # unset seen flag, will stay bold in email client try: server.remove_flags(msgid, [imapclient.SEEN]) except Exception as e: LOG.exception("Failed to remove flags SEEN", e) # Not much we can do here, so lets try and # send the aprs message anyway if from_addr in shortcuts_inverted: # reverse lookup of a shortcut from_addr = shortcuts_inverted[from_addr] reply = "-" + from_addr + " " + body.decode( errors="ignore") msg = messaging.TextMessage( self.config["aprs"]["login"], self.config["ham"]["callsign"], reply, ) self.msg_queues["tx"].put(msg) # flag message as sent via aprs try: server.add_flags(msgid, ["APRS"]) # unset seen flag, will stay bold in email client except Exception as e: LOG.exception("Couldn't add APRS flag to email", e) try: server.remove_flags(msgid, [imapclient.SEEN]) except Exception as e: LOG.exception( "Couldn't remove seen flag from email", e) # check email more often since we just received an email check_email_delay = 60 # reset clock LOG.debug("Done looping over Server.fetch, logging out.") past = datetime.datetime.now() try: server.logout() except Exception as e: LOG.exception("IMAP failed to logout: ", e) continue else: # We haven't hit the email delay yet. # LOG.debug("Delta({}) < {}".format(now - past, check_email_delay)) pass # Remove ourselves from the global threads list threads.APRSDThreadList().remove(self) LOG.info("Exiting")
def resend_email(count, fromcall): global check_email_delay date = datetime.datetime.now() month = date.strftime("%B")[:3] # Nov, Mar, Apr day = date.day year = date.year today = "{}-{}-{}".format(day, month, year) shortcuts = CONFIG["aprsd"]["email"]["shortcuts"] # swap key/value shortcuts_inverted = {v: k for k, v in shortcuts.items()} try: server = _imap_connect() except Exception as e: LOG.exception("Failed to Connect to IMAP. Cannot resend email ", e) return try: messages = server.search(["SINCE", today]) except Exception as e: LOG.exception("Couldn't search for emails in resend_email ", e) return # LOG.debug("%d messages received today" % len(messages)) msgexists = False messages.sort(reverse=True) del messages[int(count):] # only the latest "count" messages for message in messages: try: parts = server.fetch(message, ["ENVELOPE"]).items() except Exception as e: LOG.exception("Couldn't fetch email parts in resend_email", e) continue for msgid, data in list(parts): # one at a time, otherwise order is random (body, from_addr) = parse_email(msgid, data, server) # unset seen flag, will stay bold in email client try: server.remove_flags(msgid, [imapclient.SEEN]) except Exception as e: LOG.exception("Failed to remove SEEN flag in resend_email", e) if from_addr in shortcuts_inverted: # reverse lookup of a shortcut from_addr = shortcuts_inverted[from_addr] # asterisk indicates a resend reply = "-" + from_addr + " * " + body.decode(errors="ignore") # messaging.send_message(fromcall, reply) msg = messaging.TextMessage( CONFIG["aprs"]["login"], fromcall, reply, ) msg.send() msgexists = True if msgexists is not True: stm = time.localtime() h = stm.tm_hour m = stm.tm_min s = stm.tm_sec # append time as a kind of serial number to prevent FT1XDR from # thinking this is a duplicate message. # The FT1XDR pretty much ignores the aprs message number in this # regard. The FTM400 gets it right. reply = "No new msg {}:{}:{}".format( str(h).zfill(2), str(m).zfill(2), str(s).zfill(2), ) # messaging.send_message(fromcall, reply) msg = messaging.TextMessage(CONFIG["aprs"]["login"], fromcall, reply) msg.send() # check email more often since we're resending one now check_email_delay = 60 server.logout()
def loop(self): """Process a packet recieved from aprs-is server.""" packet = self.packet packets.PacketList().add(packet) fromcall = packet["from"] tocall = packet.get("addresse", None) msg = packet.get("message_text", None) msg_id = packet.get("msgNo", "0") msg_response = packet.get("response", None) # LOG.debug(f"Got packet from '{fromcall}' - {packet}") # We don't put ack packets destined for us through the # plugins. if tocall == self.config["aprs"]["login"] and msg_response == "ack": self.process_ack_packet(packet) else: # It's not an ACK for us, so lets run it through # the plugins. messaging.log_message( "Received Message", packet["raw"], msg, fromcall=fromcall, msg_num=msg_id, ) # Only ack messages that were sent directly to us if tocall == self.config["aprs"]["login"]: stats.APRSDStats().msgs_rx_inc() # let any threads do their thing, then ack # send an ack last ack = messaging.AckMessage( self.config["aprs"]["login"], fromcall, msg_id=msg_id, ) ack.send() pm = plugin.PluginManager() try: results = pm.run(packet) wl = packets.WatchList() wl.update_seen(packet) replied = False for reply in results: if isinstance(reply, list): # one of the plugins wants to send multiple messages replied = True for subreply in reply: LOG.debug(f"Sending '{subreply}'") if isinstance(subreply, messaging.Message): subreply.send() else: msg = messaging.TextMessage( self.config["aprs"]["login"], fromcall, subreply, ) msg.send() elif isinstance(reply, messaging.Message): # We have a message based object. LOG.debug(f"Sending '{reply}'") reply.send() replied = True else: replied = True # A plugin can return a null message flag which signals # us that they processed the message correctly, but have # nothing to reply with, so we avoid replying with a # usage string if reply is not messaging.NULL_MESSAGE: LOG.debug(f"Sending '{reply}'") msg = messaging.TextMessage( self.config["aprs"]["login"], fromcall, reply, ) msg.send() # If the message was for us and we didn't have a # response, then we send a usage statement. if tocall == self.config["aprs"]["login"] and not replied: LOG.warning("Sending help!") msg = messaging.TextMessage( self.config["aprs"]["login"], fromcall, "Unknown command! Send 'help' message for help", ) msg.send() except Exception as ex: LOG.error("Plugin failed!!!") LOG.exception(ex) # Do we need to send a reply? if tocall == self.config["aprs"]["login"]: reply = "A Plugin failed! try again?" msg = messaging.TextMessage( self.config["aprs"]["login"], fromcall, reply, ) msg.send() LOG.debug("Packet processing complete")
def send_message( loglevel, quiet, config_file, aprs_login, aprs_password, no_ack, raw, tocallsign, command, ): """Send a message to a callsign via APRS_IS.""" global got_ack, got_response config = utils.parse_config(config_file) if not aprs_login: click.echo("Must set --aprs_login or APRS_LOGIN") return if not aprs_password: click.echo("Must set --aprs-password or APRS_PASSWORD") return config["aprs"]["login"] = aprs_login config["aprs"]["password"] = aprs_password messaging.CONFIG = config setup_logging(config, loglevel, quiet) LOG.info("APRSD Started version: {}".format(aprsd.__version__)) if type(command) is tuple: command = " ".join(command) if not quiet: if raw: LOG.info("L'{}' R'{}'".format(aprs_login, raw)) else: LOG.info("L'{}' To'{}' C'{}'".format(aprs_login, tocallsign, command)) got_ack = False got_response = False def rx_packet(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("We got ack for our sent message {}".format(ack_num)) messaging.log_packet(packet) got_ack = True else: 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 # Send the ack back? ack = messaging.AckMessage( config["aprs"]["login"], fromcall, msg_id=msg_number, ) ack.send_direct() if got_ack and got_response: sys.exit(0) try: cl = client.Client(config) cl.setup_connection() except LoginError: sys.exit(-1) # Send a message # then we setup a consumer to rx messages # We should get an ack back as well as a new message # we should bail after we get the ack and send an ack back for the # message if raw: msg = messaging.RawMessage(raw) msg.send_direct() sys.exit(0) else: msg = messaging.TextMessage(aprs_login, tocallsign, command) msg.send_direct() if no_ack: sys.exit(0) try: # This will register a packet consumer with aprslib # When new packets come in the consumer will process # the packet aprs_client = client.get_client() aprs_client.consumer(rx_packet, raw=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 cl.reset()