mirror of
https://github.com/craigerl/aprsd.git
synced 2025-08-05 14:52:28 -04:00
Merge pull request #69 from craigerl/refactor_message_processing
Refactor Message processing and MORE
This commit is contained in:
commit
3dd48ebb86
@ -84,7 +84,7 @@ class Aprsdis(aprslib.IS):
|
|||||||
thread_stop = False
|
thread_stop = False
|
||||||
|
|
||||||
# timeout in seconds
|
# timeout in seconds
|
||||||
select_timeout = 10
|
select_timeout = 1
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.thread_stop = True
|
self.thread_stop = True
|
||||||
|
@ -14,7 +14,7 @@ import click_completion
|
|||||||
|
|
||||||
# local imports here
|
# local imports here
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import client, email, plugin, utils
|
from aprsd import client, plugin, utils
|
||||||
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
@ -179,7 +179,6 @@ def test_plugin(
|
|||||||
"""APRSD Plugin test app."""
|
"""APRSD Plugin test app."""
|
||||||
|
|
||||||
config = utils.parse_config(config_file)
|
config = utils.parse_config(config_file)
|
||||||
email.CONFIG = config
|
|
||||||
|
|
||||||
setup_logging(config, loglevel, False)
|
setup_logging(config, loglevel, False)
|
||||||
LOG.info(f"Test APRSD PLugin version: {aprsd.__version__}")
|
LOG.info(f"Test APRSD PLugin version: {aprsd.__version__}")
|
||||||
@ -189,11 +188,13 @@ def test_plugin(
|
|||||||
client.Client(config)
|
client.Client(config)
|
||||||
|
|
||||||
pm = plugin.PluginManager(config)
|
pm = plugin.PluginManager(config)
|
||||||
obj = pm._create_class(plugin_path, plugin.APRSDMessagePluginBase, config=config)
|
obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config)
|
||||||
|
|
||||||
packet = {"from": fromcall, "message_text": message, "msgNo": 1}
|
packet = {"from": fromcall, "message_text": message, "msgNo": 1}
|
||||||
|
|
||||||
reply = obj.run(packet)
|
reply = obj.filter(packet)
|
||||||
|
# Plugin might have threads, so lets stop them so we can exit.
|
||||||
|
obj.stop_threads()
|
||||||
LOG.info(f"Result = '{reply}'")
|
LOG.info(f"Result = '{reply}'")
|
||||||
|
|
||||||
|
|
||||||
|
548
aprsd/email.py
548
aprsd/email.py
@ -1,548 +0,0 @@
|
|||||||
import datetime
|
|
||||||
import email
|
|
||||||
from email.mime.text import MIMEText
|
|
||||||
import imaplib
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import smtplib
|
|
||||||
import time
|
|
||||||
|
|
||||||
import imapclient
|
|
||||||
from validate_email import validate_email
|
|
||||||
|
|
||||||
from aprsd import messaging, stats, threads, trace
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
|
||||||
|
|
||||||
# This gets forced set from main.py prior to being used internally
|
|
||||||
CONFIG = None
|
|
||||||
|
|
||||||
|
|
||||||
@trace.trace
|
|
||||||
def _imap_connect():
|
|
||||||
imap_port = CONFIG["aprsd"]["email"]["imap"].get("port", 143)
|
|
||||||
use_ssl = CONFIG["aprsd"]["email"]["imap"].get("use_ssl", False)
|
|
||||||
host = CONFIG["aprsd"]["email"]["imap"]["host"]
|
|
||||||
msg = "{}{}:{}".format("TLS " if use_ssl else "", host, imap_port)
|
|
||||||
# LOG.debug("Connect to IMAP host {} with user '{}'".
|
|
||||||
# format(msg, CONFIG['imap']['login']))
|
|
||||||
|
|
||||||
try:
|
|
||||||
server = imapclient.IMAPClient(
|
|
||||||
CONFIG["aprsd"]["email"]["imap"]["host"],
|
|
||||||
port=imap_port,
|
|
||||||
use_uid=True,
|
|
||||||
ssl=use_ssl,
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
LOG.error("Failed to connect IMAP server", e)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
server.login(
|
|
||||||
CONFIG["aprsd"]["email"]["imap"]["login"],
|
|
||||||
CONFIG["aprsd"]["email"]["imap"]["password"],
|
|
||||||
)
|
|
||||||
except (imaplib.IMAP4.error, Exception) as e:
|
|
||||||
msg = getattr(e, "message", repr(e))
|
|
||||||
LOG.error(f"Failed to login {msg}")
|
|
||||||
return
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@trace.trace
|
|
||||||
def _smtp_connect():
|
|
||||||
host = CONFIG["aprsd"]["email"]["smtp"]["host"]
|
|
||||||
smtp_port = CONFIG["aprsd"]["email"]["smtp"]["port"]
|
|
||||||
use_ssl = CONFIG["aprsd"]["email"]["smtp"].get("use_ssl", False)
|
|
||||||
msg = "{}{}:{}".format("SSL " if use_ssl else "", host, smtp_port)
|
|
||||||
LOG.debug(
|
|
||||||
"Connect to SMTP host {} with user '{}'".format(
|
|
||||||
msg,
|
|
||||||
CONFIG["aprsd"]["email"]["imap"]["login"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if use_ssl:
|
|
||||||
server = smtplib.SMTP_SSL(
|
|
||||||
host=host,
|
|
||||||
port=smtp_port,
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
server = smtplib.SMTP(
|
|
||||||
host=host,
|
|
||||||
port=smtp_port,
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
LOG.error("Couldn't connect to SMTP Server")
|
|
||||||
return
|
|
||||||
|
|
||||||
LOG.debug(f"Connected to smtp host {msg}")
|
|
||||||
|
|
||||||
debug = CONFIG["aprsd"]["email"]["smtp"].get("debug", False)
|
|
||||||
if debug:
|
|
||||||
server.set_debuglevel(5)
|
|
||||||
server.sendmail = trace.trace(server.sendmail)
|
|
||||||
|
|
||||||
try:
|
|
||||||
server.login(
|
|
||||||
CONFIG["aprsd"]["email"]["smtp"]["login"],
|
|
||||||
CONFIG["aprsd"]["email"]["smtp"]["password"],
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
LOG.error("Couldn't connect to SMTP Server")
|
|
||||||
return
|
|
||||||
|
|
||||||
LOG.debug(f"Logged into SMTP server {msg}")
|
|
||||||
return server
|
|
||||||
|
|
||||||
|
|
||||||
def validate_shortcuts(config):
|
|
||||||
shortcuts = config["aprsd"]["email"].get("shortcuts", None)
|
|
||||||
if not shortcuts:
|
|
||||||
return
|
|
||||||
|
|
||||||
LOG.info(
|
|
||||||
"Validating {} Email shortcuts. This can take up to 10 seconds"
|
|
||||||
" per shortcut".format(len(shortcuts)),
|
|
||||||
)
|
|
||||||
delete_keys = []
|
|
||||||
for key in shortcuts:
|
|
||||||
LOG.info(f"Validating {key}:{shortcuts[key]}")
|
|
||||||
is_valid = validate_email(
|
|
||||||
email_address=shortcuts[key],
|
|
||||||
check_regex=True,
|
|
||||||
check_mx=False,
|
|
||||||
from_address=config["aprsd"]["email"]["smtp"]["login"],
|
|
||||||
helo_host=config["aprsd"]["email"]["smtp"]["host"],
|
|
||||||
smtp_timeout=10,
|
|
||||||
dns_timeout=10,
|
|
||||||
use_blacklist=True,
|
|
||||||
debug=False,
|
|
||||||
)
|
|
||||||
if not is_valid:
|
|
||||||
LOG.error(
|
|
||||||
"'{}' is an invalid email address. Removing shortcut".format(
|
|
||||||
shortcuts[key],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
delete_keys.append(key)
|
|
||||||
|
|
||||||
for key in delete_keys:
|
|
||||||
del config["aprsd"]["email"]["shortcuts"][key]
|
|
||||||
|
|
||||||
LOG.info("Available shortcuts: {}".format(config["aprsd"]["email"]["shortcuts"]))
|
|
||||||
|
|
||||||
|
|
||||||
def get_email_from_shortcut(addr):
|
|
||||||
if CONFIG["aprsd"]["email"].get("shortcuts", False):
|
|
||||||
return CONFIG["aprsd"]["email"]["shortcuts"].get(addr, addr)
|
|
||||||
else:
|
|
||||||
return addr
|
|
||||||
|
|
||||||
|
|
||||||
def validate_email_config(config, disable_validation=False):
|
|
||||||
"""function to simply ensure we can connect to email services.
|
|
||||||
|
|
||||||
This helps with failing early during startup.
|
|
||||||
"""
|
|
||||||
LOG.info("Checking IMAP configuration")
|
|
||||||
imap_server = _imap_connect()
|
|
||||||
LOG.info("Checking SMTP configuration")
|
|
||||||
smtp_server = _smtp_connect()
|
|
||||||
|
|
||||||
# Now validate and flag any shortcuts as invalid
|
|
||||||
if not disable_validation:
|
|
||||||
validate_shortcuts(config)
|
|
||||||
else:
|
|
||||||
LOG.info("Shortcuts email validation is Disabled!!, you were warned.")
|
|
||||||
|
|
||||||
if imap_server and smtp_server:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@trace.trace
|
|
||||||
def parse_email(msgid, data, server):
|
|
||||||
envelope = data[b"ENVELOPE"]
|
|
||||||
# email address match
|
|
||||||
# use raw string to avoid invalid escape secquence errors r"string here"
|
|
||||||
f = re.search(r"([\.\w_-]+@[\.\w_-]+)", str(envelope.from_[0]))
|
|
||||||
if f is not None:
|
|
||||||
from_addr = f.group(1)
|
|
||||||
else:
|
|
||||||
from_addr = "noaddr"
|
|
||||||
LOG.debug(f"Got a message from '{from_addr}'")
|
|
||||||
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"))
|
|
||||||
if msg.is_multipart():
|
|
||||||
text = ""
|
|
||||||
html = None
|
|
||||||
# default in case body somehow isn't set below - happened once
|
|
||||||
body = b"* unreadable msg received"
|
|
||||||
# this uses the last text or html part in the email, phone companies often put content in an attachment
|
|
||||||
for part in msg.get_payload():
|
|
||||||
if part.get_content_charset() is None:
|
|
||||||
# or BREAK when we hit a text or html?
|
|
||||||
# We cannot know the character set,
|
|
||||||
# so return decoded "something"
|
|
||||||
LOG.debug("Email got unknown content type")
|
|
||||||
text = part.get_payload(decode=True)
|
|
||||||
continue
|
|
||||||
|
|
||||||
charset = part.get_content_charset()
|
|
||||||
|
|
||||||
if part.get_content_type() == "text/plain":
|
|
||||||
LOG.debug("Email got text/plain")
|
|
||||||
text = str(
|
|
||||||
part.get_payload(decode=True),
|
|
||||||
str(charset),
|
|
||||||
"ignore",
|
|
||||||
).encode("utf8", "replace")
|
|
||||||
|
|
||||||
if part.get_content_type() == "text/html":
|
|
||||||
LOG.debug("Email got text/html")
|
|
||||||
html = str(
|
|
||||||
part.get_payload(decode=True),
|
|
||||||
str(charset),
|
|
||||||
"ignore",
|
|
||||||
).encode("utf8", "replace")
|
|
||||||
|
|
||||||
if text is not None:
|
|
||||||
# strip removes white space fore and aft of string
|
|
||||||
body = text.strip()
|
|
||||||
else:
|
|
||||||
body = html.strip()
|
|
||||||
else: # message is not multipart
|
|
||||||
# email.uscc.net sends no charset, blows up unicode function below
|
|
||||||
LOG.debug("Email is not multipart")
|
|
||||||
if msg.get_content_charset() is None:
|
|
||||||
text = str(msg.get_payload(decode=True), "US-ASCII", "ignore").encode(
|
|
||||||
"utf8",
|
|
||||||
"replace",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
text = str(
|
|
||||||
msg.get_payload(decode=True),
|
|
||||||
msg.get_content_charset(),
|
|
||||||
"ignore",
|
|
||||||
).encode("utf8", "replace")
|
|
||||||
body = text.strip()
|
|
||||||
|
|
||||||
# FIXED: UnicodeDecodeError: 'ascii' codec can't decode byte 0xf0 in position 6: ordinal not in range(128)
|
|
||||||
# decode with errors='ignore'. be sure to encode it before we return it below, also with errors='ignore'
|
|
||||||
try:
|
|
||||||
body = body.decode(errors="ignore")
|
|
||||||
except Exception as e:
|
|
||||||
LOG.error("Unicode decode failure: " + str(e))
|
|
||||||
LOG.error("Unidoce decode failed: " + str(body))
|
|
||||||
body = "Unreadable unicode msg"
|
|
||||||
# strip all html tags
|
|
||||||
body = re.sub("<[^<]+?>", "", body)
|
|
||||||
# strip CR/LF, make it one line, .rstrip fails at this
|
|
||||||
body = body.replace("\n", " ").replace("\r", " ")
|
|
||||||
# ascii might be out of range, so encode it, removing any error characters
|
|
||||||
body = body.encode(errors="ignore")
|
|
||||||
return (body, from_addr)
|
|
||||||
|
|
||||||
|
|
||||||
# end parse_email
|
|
||||||
|
|
||||||
|
|
||||||
@trace.trace
|
|
||||||
def send_email(to_addr, content):
|
|
||||||
global check_email_delay
|
|
||||||
|
|
||||||
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
|
|
||||||
email_address = get_email_from_shortcut(to_addr)
|
|
||||||
LOG.info("Sending Email_________________")
|
|
||||||
|
|
||||||
if to_addr in shortcuts:
|
|
||||||
LOG.info("To : " + to_addr)
|
|
||||||
to_addr = email_address
|
|
||||||
LOG.info(" (" + to_addr + ")")
|
|
||||||
subject = CONFIG["ham"]["callsign"]
|
|
||||||
# content = content + "\n\n(NOTE: reply with one line)"
|
|
||||||
LOG.info("Subject : " + subject)
|
|
||||||
LOG.info("Body : " + content)
|
|
||||||
|
|
||||||
# check email more often since there's activity right now
|
|
||||||
check_email_delay = 60
|
|
||||||
|
|
||||||
msg = MIMEText(content)
|
|
||||||
msg["Subject"] = subject
|
|
||||||
msg["From"] = CONFIG["aprsd"]["email"]["smtp"]["login"]
|
|
||||||
msg["To"] = to_addr
|
|
||||||
server = _smtp_connect()
|
|
||||||
if server:
|
|
||||||
try:
|
|
||||||
server.sendmail(
|
|
||||||
CONFIG["aprsd"]["email"]["smtp"]["login"],
|
|
||||||
[to_addr],
|
|
||||||
msg.as_string(),
|
|
||||||
)
|
|
||||||
stats.APRSDStats().email_tx_inc()
|
|
||||||
except Exception as e:
|
|
||||||
msg = getattr(e, "message", repr(e))
|
|
||||||
LOG.error("Sendmail Error!!!! '{}'", msg)
|
|
||||||
server.quit()
|
|
||||||
return -1
|
|
||||||
server.quit()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
# end send_email
|
|
||||||
|
|
||||||
|
|
||||||
@trace.trace
|
|
||||||
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 = f"{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()
|
|
||||||
# end resend_email()
|
|
||||||
|
|
||||||
|
|
||||||
class APRSDEmailThread(threads.APRSDThread):
|
|
||||||
def __init__(self, msg_queues, config):
|
|
||||||
super().__init__("EmailThread")
|
|
||||||
self.msg_queues = msg_queues
|
|
||||||
self.config = config
|
|
||||||
|
|
||||||
@trace.trace
|
|
||||||
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 = f"{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(f"{len(messages)} messages received today")
|
|
||||||
|
|
||||||
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")
|
|
@ -61,6 +61,10 @@ class APRSDFlask(flask_classful.FlaskView):
|
|||||||
watch_count = 0
|
watch_count = 0
|
||||||
watch_age = 0
|
watch_age = 0
|
||||||
|
|
||||||
|
pm = plugin.PluginManager()
|
||||||
|
plugins = pm.get_plugins()
|
||||||
|
plugin_count = len(plugins)
|
||||||
|
|
||||||
return flask.render_template(
|
return flask.render_template(
|
||||||
"index.html",
|
"index.html",
|
||||||
initial_stats=stats,
|
initial_stats=stats,
|
||||||
@ -69,6 +73,7 @@ class APRSDFlask(flask_classful.FlaskView):
|
|||||||
config_json=json.dumps(self.config),
|
config_json=json.dumps(self.config),
|
||||||
watch_count=watch_count,
|
watch_count=watch_count,
|
||||||
watch_age=watch_age,
|
watch_age=watch_age,
|
||||||
|
plugin_count=plugin_count,
|
||||||
)
|
)
|
||||||
|
|
||||||
@auth.login_required
|
@auth.login_required
|
||||||
|
@ -294,7 +294,7 @@ def listen(
|
|||||||
|
|
||||||
# TODO(walt) - manually edit this list
|
# TODO(walt) - manually edit this list
|
||||||
# prior to running aprsd-listen listen
|
# prior to running aprsd-listen listen
|
||||||
watch_list = []
|
watch_list = ["k*"]
|
||||||
|
|
||||||
# build last seen list
|
# build last seen list
|
||||||
last_seen = {}
|
last_seen = {}
|
||||||
|
@ -25,7 +25,6 @@ import logging
|
|||||||
from logging import NullHandler
|
from logging import NullHandler
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
import os
|
import os
|
||||||
import queue
|
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@ -38,8 +37,7 @@ import click_completion
|
|||||||
# local imports here
|
# local imports here
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import (
|
from aprsd import (
|
||||||
client, email, flask, messaging, packets, plugin, stats, threads, trace,
|
client, flask, messaging, packets, plugin, stats, threads, trace, utils,
|
||||||
utils,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -159,7 +157,7 @@ def signal_handler(sig, frame):
|
|||||||
datetime.datetime.now(),
|
datetime.datetime.now(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
time.sleep(5)
|
time.sleep(1.5)
|
||||||
tracker = messaging.MsgTrack()
|
tracker = messaging.MsgTrack()
|
||||||
tracker.save()
|
tracker.save()
|
||||||
LOG.info(stats.APRSDStats())
|
LOG.info(stats.APRSDStats())
|
||||||
@ -227,12 +225,6 @@ def setup_logging(config, loglevel, quiet):
|
|||||||
def check_version(loglevel, config_file):
|
def check_version(loglevel, config_file):
|
||||||
config = utils.parse_config(config_file)
|
config = utils.parse_config(config_file)
|
||||||
|
|
||||||
# Force setting the config to the modules that need it
|
|
||||||
# TODO(Walt): convert these modules to classes that can
|
|
||||||
# Accept the config as a constructor param, instead of this
|
|
||||||
# hacky global setting
|
|
||||||
email.CONFIG = config
|
|
||||||
|
|
||||||
setup_logging(config, loglevel, False)
|
setup_logging(config, loglevel, False)
|
||||||
level, msg = utils._check_version()
|
level, msg = utils._check_version()
|
||||||
if level:
|
if level:
|
||||||
@ -414,12 +406,6 @@ def send_message(
|
|||||||
help="The log level to use for aprsd.log",
|
help="The log level to use for aprsd.log",
|
||||||
)
|
)
|
||||||
@click.option("--quiet", is_flag=True, default=False, help="Don't log to stdout")
|
@click.option("--quiet", is_flag=True, default=False, help="Don't log to stdout")
|
||||||
@click.option(
|
|
||||||
"--disable-validation",
|
|
||||||
is_flag=True,
|
|
||||||
default=False,
|
|
||||||
help="Disable email shortcut validation. Bad email addresses can result in broken email responses!!",
|
|
||||||
)
|
|
||||||
@click.option(
|
@click.option(
|
||||||
"-c",
|
"-c",
|
||||||
"--config",
|
"--config",
|
||||||
@ -440,7 +426,6 @@ def send_message(
|
|||||||
def server(
|
def server(
|
||||||
loglevel,
|
loglevel,
|
||||||
quiet,
|
quiet,
|
||||||
disable_validation,
|
|
||||||
config_file,
|
config_file,
|
||||||
flush,
|
flush,
|
||||||
):
|
):
|
||||||
@ -453,12 +438,6 @@ def server(
|
|||||||
|
|
||||||
config = utils.parse_config(config_file)
|
config = utils.parse_config(config_file)
|
||||||
|
|
||||||
# Force setting the config to the modules that need it
|
|
||||||
# TODO(Walt): convert these modules to classes that can
|
|
||||||
# Accept the config as a constructor param, instead of this
|
|
||||||
# hacky global setting
|
|
||||||
email.CONFIG = config
|
|
||||||
|
|
||||||
setup_logging(config, loglevel, quiet)
|
setup_logging(config, loglevel, quiet)
|
||||||
level, msg = utils._check_version()
|
level, msg = utils._check_version()
|
||||||
if level:
|
if level:
|
||||||
@ -479,27 +458,16 @@ def server(
|
|||||||
trace.setup_tracing(["method", "api"])
|
trace.setup_tracing(["method", "api"])
|
||||||
stats.APRSDStats(config)
|
stats.APRSDStats(config)
|
||||||
|
|
||||||
email_enabled = config["aprsd"]["email"].get("enabled", False)
|
|
||||||
|
|
||||||
if email_enabled:
|
|
||||||
# TODO(walt): Make email processing/checking optional?
|
|
||||||
# Maybe someone only wants this to process messages with plugins only.
|
|
||||||
valid = email.validate_email_config(config, disable_validation)
|
|
||||||
if not valid:
|
|
||||||
LOG.error("Failed to validate email config options")
|
|
||||||
sys.exit(-1)
|
|
||||||
else:
|
|
||||||
LOG.info("Email services not enabled.")
|
|
||||||
|
|
||||||
# Create the initial PM singleton and Register plugins
|
|
||||||
plugin_manager = plugin.PluginManager(config)
|
|
||||||
plugin_manager.setup_plugins()
|
|
||||||
try:
|
try:
|
||||||
cl = client.Client(config)
|
cl = client.Client(config)
|
||||||
cl.client
|
cl.client
|
||||||
except LoginError:
|
except LoginError:
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
|
# Create the initial PM singleton and Register plugins
|
||||||
|
plugin_manager = plugin.PluginManager(config)
|
||||||
|
plugin_manager.setup_plugins()
|
||||||
|
|
||||||
# Now load the msgTrack from disk if any
|
# Now load the msgTrack from disk if any
|
||||||
if flush:
|
if flush:
|
||||||
LOG.debug("Deleting saved MsgTrack.")
|
LOG.debug("Deleting saved MsgTrack.")
|
||||||
@ -509,38 +477,24 @@ def server(
|
|||||||
LOG.debug("Loading saved MsgTrack object.")
|
LOG.debug("Loading saved MsgTrack object.")
|
||||||
messaging.MsgTrack().load()
|
messaging.MsgTrack().load()
|
||||||
|
|
||||||
rx_notify_queue = queue.Queue(maxsize=20)
|
packets.PacketList(config=config)
|
||||||
rx_msg_queue = queue.Queue(maxsize=20)
|
|
||||||
tx_msg_queue = queue.Queue(maxsize=20)
|
rx_thread = threads.APRSDRXThread(
|
||||||
msg_queues = {
|
msg_queues=threads.msg_queues,
|
||||||
"rx": rx_msg_queue,
|
config=config,
|
||||||
"tx": tx_msg_queue,
|
)
|
||||||
"notify": rx_notify_queue,
|
|
||||||
}
|
rx_thread.start()
|
||||||
|
|
||||||
rx_thread = threads.APRSDRXThread(msg_queues=msg_queues, config=config)
|
|
||||||
tx_thread = threads.APRSDTXThread(msg_queues=msg_queues, config=config)
|
|
||||||
if "watch_list" in config["aprsd"] and config["aprsd"]["watch_list"].get(
|
if "watch_list" in config["aprsd"] and config["aprsd"]["watch_list"].get(
|
||||||
"enabled",
|
"enabled",
|
||||||
True,
|
True,
|
||||||
):
|
):
|
||||||
packets.PacketList(config)
|
packets.WatchList(config=config)
|
||||||
notify_thread = threads.APRSDNotifyThread(
|
|
||||||
msg_queues=msg_queues,
|
|
||||||
config=config,
|
|
||||||
)
|
|
||||||
notify_thread.start()
|
|
||||||
|
|
||||||
if email_enabled:
|
|
||||||
email_thread = email.APRSDEmailThread(msg_queues=msg_queues, config=config)
|
|
||||||
email_thread.start()
|
|
||||||
|
|
||||||
rx_thread.start()
|
|
||||||
tx_thread.start()
|
|
||||||
|
|
||||||
messaging.MsgTrack().restart()
|
messaging.MsgTrack().restart()
|
||||||
|
|
||||||
keepalive = threads.KeepAliveThread()
|
keepalive = threads.KeepAliveThread(config=config)
|
||||||
keepalive.start()
|
keepalive.start()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -9,7 +9,7 @@ import re
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from aprsd import client, stats, threads, trace, utils
|
from aprsd import client, packets, stats, threads, trace, utils
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@ -424,6 +424,7 @@ class SendMessageThread(threads.APRSDThread):
|
|||||||
)
|
)
|
||||||
cl.sendall(str(msg))
|
cl.sendall(str(msg))
|
||||||
stats.APRSDStats().msgs_tx_inc()
|
stats.APRSDStats().msgs_tx_inc()
|
||||||
|
packets.PacketList().add(msg.dict())
|
||||||
msg.last_send_time = datetime.datetime.now()
|
msg.last_send_time = datetime.datetime.now()
|
||||||
msg.last_send_attempt += 1
|
msg.last_send_attempt += 1
|
||||||
|
|
||||||
@ -467,8 +468,6 @@ class AckMessage(Message):
|
|||||||
thread = SendAckThread(self)
|
thread = SendAckThread(self)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
# end send_ack()
|
|
||||||
|
|
||||||
def send_direct(self):
|
def send_direct(self):
|
||||||
"""Send an ack message without a separate thread."""
|
"""Send an ack message without a separate thread."""
|
||||||
cl = client.get_client()
|
cl = client.get_client()
|
||||||
@ -527,6 +526,7 @@ class SendAckThread(threads.APRSDThread):
|
|||||||
)
|
)
|
||||||
cl.sendall(str(self.ack))
|
cl.sendall(str(self.ack))
|
||||||
stats.APRSDStats().ack_tx_inc()
|
stats.APRSDStats().ack_tx_inc()
|
||||||
|
packets.PacketList().add(self.ack.dict())
|
||||||
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()
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
@ -543,15 +543,8 @@ def log_packet(packet):
|
|||||||
ack = packet.get("ack", None)
|
ack = packet.get("ack", None)
|
||||||
|
|
||||||
log_message(
|
log_message(
|
||||||
"Packet",
|
"Packet", packet["raw"], msg, fromcall=fromcall, tocall=tocall,
|
||||||
packet["raw"],
|
ack=ack, packet_type=response_type, msg_num=msg_num, )
|
||||||
msg,
|
|
||||||
fromcall=fromcall,
|
|
||||||
tocall=tocall,
|
|
||||||
ack=ack,
|
|
||||||
packet_type=response_type,
|
|
||||||
msg_num=msg_num,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def log_message(
|
def log_message(
|
||||||
|
@ -27,7 +27,7 @@ class PacketList:
|
|||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super().__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
cls._instance.packet_list = utils.RingBuffer(100)
|
cls._instance.packet_list = utils.RingBuffer(1000)
|
||||||
cls._instance.lock = threading.Lock()
|
cls._instance.lock = threading.Lock()
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
@ -42,7 +42,10 @@ class PacketList:
|
|||||||
def add(self, packet):
|
def add(self, packet):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
packet["ts"] = time.time()
|
packet["ts"] = time.time()
|
||||||
if "from" in packet and packet["from"] == self.config["aprs"]["login"]:
|
if (
|
||||||
|
"fromcall" in packet
|
||||||
|
and packet["fromcall"] == self.config["aprs"]["login"]
|
||||||
|
):
|
||||||
self.total_tx += 1
|
self.total_tx += 1
|
||||||
else:
|
else:
|
||||||
self.total_recv += 1
|
self.total_recv += 1
|
||||||
@ -53,9 +56,11 @@ class PacketList:
|
|||||||
return self.packet_list.get()
|
return self.packet_list.get()
|
||||||
|
|
||||||
def total_received(self):
|
def total_received(self):
|
||||||
|
with self.lock:
|
||||||
return self.total_recv
|
return self.total_recv
|
||||||
|
|
||||||
def total_sent(self):
|
def total_sent(self):
|
||||||
|
with self.lock:
|
||||||
return self.total_tx
|
return self.total_tx
|
||||||
|
|
||||||
|
|
||||||
@ -156,3 +161,15 @@ def get_packet_type(packet):
|
|||||||
elif msg_format == "mic-e":
|
elif msg_format == "mic-e":
|
||||||
packet_type = PACKET_TYPE_MICE
|
packet_type = PACKET_TYPE_MICE
|
||||||
return packet_type
|
return packet_type
|
||||||
|
|
||||||
|
|
||||||
|
def is_message_packet(packet):
|
||||||
|
return get_packet_type(packet) == PACKET_TYPE_MESSAGE
|
||||||
|
|
||||||
|
|
||||||
|
def is_ack_packet(packet):
|
||||||
|
return get_packet_type(packet) == PACKET_TYPE_ACK
|
||||||
|
|
||||||
|
|
||||||
|
def is_mice_packet(packet):
|
||||||
|
return get_packet_type(packet) == PACKET_TYPE_MICE
|
||||||
|
268
aprsd/plugin.py
268
aprsd/plugin.py
@ -11,6 +11,8 @@ import threading
|
|||||||
import pluggy
|
import pluggy
|
||||||
from thesmuggler import smuggle
|
from thesmuggler import smuggle
|
||||||
|
|
||||||
|
from aprsd import client, messaging, packets, threads
|
||||||
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@ -39,12 +41,93 @@ class APRSDCommandSpec:
|
|||||||
"""A hook specification namespace."""
|
"""A hook specification namespace."""
|
||||||
|
|
||||||
@hookspec
|
@hookspec
|
||||||
def run(self, packet):
|
def filter(self, packet):
|
||||||
"""My special little hook that you can customize."""
|
"""My special little hook that you can customize."""
|
||||||
|
|
||||||
|
|
||||||
class APRSDNotificationPluginBase(metaclass=abc.ABCMeta):
|
class APRSDPluginBase(metaclass=abc.ABCMeta):
|
||||||
"""Base plugin class for all notification ased plugins.
|
"""The base class for all APRSD Plugins."""
|
||||||
|
|
||||||
|
config = None
|
||||||
|
rx_count = 0
|
||||||
|
tx_count = 0
|
||||||
|
version = "1.0"
|
||||||
|
|
||||||
|
# Holds the list of APRSDThreads that the plugin creates
|
||||||
|
threads = []
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
self.message_counter = 0
|
||||||
|
self.setup()
|
||||||
|
self.threads = self.create_threads()
|
||||||
|
if self.threads:
|
||||||
|
self.start_threads()
|
||||||
|
|
||||||
|
def start_threads(self):
|
||||||
|
if self.threads:
|
||||||
|
if not isinstance(self.threads, list):
|
||||||
|
self.threads = [self.threads]
|
||||||
|
|
||||||
|
try:
|
||||||
|
for thread in self.threads:
|
||||||
|
if isinstance(thread, threads.APRSDThread):
|
||||||
|
thread.start()
|
||||||
|
else:
|
||||||
|
LOG.error(
|
||||||
|
"Can't start thread {}:{}, Must be a child "
|
||||||
|
"of aprsd.threads.APRSDThread".format(
|
||||||
|
self,
|
||||||
|
thread,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
LOG.error(
|
||||||
|
"Failed to start threads for plugin {}".format(
|
||||||
|
self,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message_count(self):
|
||||||
|
return self.message_counter
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version(self):
|
||||||
|
"""Version"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
"""Do any plugin setup here."""
|
||||||
|
|
||||||
|
def create_threads(self):
|
||||||
|
"""Gives the plugin writer the ability start a background thread."""
|
||||||
|
return []
|
||||||
|
|
||||||
|
def rx_inc(self):
|
||||||
|
self.rx_count += 1
|
||||||
|
|
||||||
|
def tx_inc(self):
|
||||||
|
self.tx_count += 1
|
||||||
|
|
||||||
|
def stop_threads(self):
|
||||||
|
"""Stop any threads this plugin might have created."""
|
||||||
|
for thread in self.threads:
|
||||||
|
if isinstance(thread, threads.APRSDThread):
|
||||||
|
thread.stop()
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
@abc.abstractmethod
|
||||||
|
def filter(self, packet):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def process(self, packet):
|
||||||
|
"""This is called when the filter passes."""
|
||||||
|
|
||||||
|
|
||||||
|
class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
||||||
|
"""Base plugin class for all notification APRSD plugins.
|
||||||
|
|
||||||
All these plugins will get every packet seen by APRSD's
|
All these plugins will get every packet seen by APRSD's
|
||||||
registered list of HAM callsigns in the config file's
|
registered list of HAM callsigns in the config file's
|
||||||
@ -54,25 +137,43 @@ class APRSDNotificationPluginBase(metaclass=abc.ABCMeta):
|
|||||||
by a particular HAM callsign, write a plugin based off of
|
by a particular HAM callsign, write a plugin based off of
|
||||||
this class.
|
this class.
|
||||||
"""
|
"""
|
||||||
|
enabled = False
|
||||||
|
|
||||||
def __init__(self, config):
|
def setup(self):
|
||||||
"""The aprsd config object is stored."""
|
# if we have a watch list enabled, we need to add filtering
|
||||||
self.config = config
|
# to enable seeing packets from the watch list.
|
||||||
self.message_counter = 0
|
if "watch_list" in self.config["aprsd"] and self.config["aprsd"][
|
||||||
|
"watch_list"
|
||||||
|
].get("enabled", False):
|
||||||
|
# watch list is enabled
|
||||||
|
self.enabled = True
|
||||||
|
watch_list = self.config["aprsd"]["watch_list"].get(
|
||||||
|
"callsigns",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
# make sure the timeout is set or this doesn't work
|
||||||
|
if watch_list:
|
||||||
|
aprs_client = client.get_client()
|
||||||
|
filter_str = "b/{}".format("/".join(watch_list))
|
||||||
|
aprs_client.set_filter(filter_str)
|
||||||
|
else:
|
||||||
|
LOG.warning("Watch list enabled, but no callsigns set.")
|
||||||
|
|
||||||
@hookimpl
|
def filter(self, packet):
|
||||||
def run(self, packet):
|
wl = packets.WatchList()
|
||||||
return self.notify(packet)
|
result = messaging.NULL_MESSAGE
|
||||||
|
if wl.callsign_in_watchlist(packet["from"]):
|
||||||
|
# packet is from a callsign in the watch list
|
||||||
|
self.rx_inc()
|
||||||
|
result = self.process()
|
||||||
|
if result:
|
||||||
|
self.tx_inc()
|
||||||
|
wl.update_seen(packet)
|
||||||
|
|
||||||
@abc.abstractmethod
|
return result
|
||||||
def notify(self, packet):
|
|
||||||
"""This is the main method called when a packet is rx.
|
|
||||||
|
|
||||||
This will get called when a packet is seen by a callsign
|
|
||||||
registered in the watch list in the config file."""
|
|
||||||
|
|
||||||
|
|
||||||
class APRSDMessagePluginBase(metaclass=abc.ABCMeta):
|
class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
||||||
"""Base Message plugin class.
|
"""Base Message plugin class.
|
||||||
|
|
||||||
When you want to search for a particular command in an
|
When you want to search for a particular command in an
|
||||||
@ -80,11 +181,6 @@ class APRSDMessagePluginBase(metaclass=abc.ABCMeta):
|
|||||||
based off of this class.
|
based off of this class.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config):
|
|
||||||
"""The aprsd config object is stored."""
|
|
||||||
self.config = config
|
|
||||||
self.message_counter = 0
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def command_name(self):
|
def command_name(self):
|
||||||
"""The usage string help."""
|
"""The usage string help."""
|
||||||
@ -95,29 +191,28 @@ class APRSDMessagePluginBase(metaclass=abc.ABCMeta):
|
|||||||
"""The regex to match from the caller"""
|
"""The regex to match from the caller"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@property
|
|
||||||
def version(self):
|
|
||||||
"""Version"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@property
|
|
||||||
def message_count(self):
|
|
||||||
return self.message_counter
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def run(self, packet):
|
def filter(self, packet):
|
||||||
|
result = None
|
||||||
|
|
||||||
message = packet.get("message_text", None)
|
message = packet.get("message_text", None)
|
||||||
|
msg_format = packet.get("format", None)
|
||||||
|
tocall = packet.get("addresse", None)
|
||||||
|
|
||||||
|
# Only process messages destined for us
|
||||||
|
# and is an APRS message format and has a message.
|
||||||
|
if (
|
||||||
|
tocall == self.config["aprs"]["login"]
|
||||||
|
and msg_format == "message"
|
||||||
|
and message
|
||||||
|
):
|
||||||
if re.search(self.command_regex, message):
|
if re.search(self.command_regex, message):
|
||||||
self.message_counter += 1
|
self.rx_inc()
|
||||||
return self.command(packet)
|
result = self.process(packet)
|
||||||
|
if result:
|
||||||
|
self.tx_inc()
|
||||||
|
|
||||||
@abc.abstractmethod
|
return result
|
||||||
def command(self, packet):
|
|
||||||
"""This is the command that runs when the regex matches.
|
|
||||||
|
|
||||||
To reply with a message over the air, return a string
|
|
||||||
to send.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class PluginManager:
|
class PluginManager:
|
||||||
@ -125,10 +220,7 @@ class PluginManager:
|
|||||||
_instance = None
|
_instance = None
|
||||||
|
|
||||||
# the pluggy PluginManager for all Message plugins
|
# the pluggy PluginManager for all Message plugins
|
||||||
_pluggy_msg_pm = None
|
_pluggy_pm = None
|
||||||
|
|
||||||
# the pluggy PluginManager for all Notification plugins
|
|
||||||
_pluggy_notify_pm = None
|
|
||||||
|
|
||||||
# aprsd config dict
|
# aprsd config dict
|
||||||
config = None
|
config = None
|
||||||
@ -173,18 +265,20 @@ class PluginManager:
|
|||||||
|
|
||||||
def is_plugin(self, obj):
|
def is_plugin(self, obj):
|
||||||
for c in inspect.getmro(obj):
|
for c in inspect.getmro(obj):
|
||||||
if issubclass(c, APRSDMessagePluginBase) or issubclass(
|
if issubclass(c, APRSDPluginBase):
|
||||||
c,
|
|
||||||
APRSDNotificationPluginBase,
|
|
||||||
):
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _create_class(self, module_class_string, super_cls: type = None, **kwargs):
|
def _create_class(
|
||||||
|
self,
|
||||||
|
module_class_string,
|
||||||
|
super_cls: type = None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Method to create a class from a fqn python string.
|
Method to create a class from a fqn python string.
|
||||||
:param module_class_string: full name of the class to create an object of
|
:param module_class_string: full name of the class to create an object
|
||||||
:param super_cls: expected super class for validity, None if bypass
|
:param super_cls: expected super class for validity, None if bypass
|
||||||
:param kwargs: parameters to pass
|
:param kwargs: parameters to pass
|
||||||
:return:
|
:return:
|
||||||
@ -213,7 +307,7 @@ class PluginManager:
|
|||||||
obj = cls(**kwargs)
|
obj = cls(**kwargs)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def _load_msg_plugin(self, plugin_name):
|
def _load_plugin(self, plugin_name):
|
||||||
"""
|
"""
|
||||||
Given a python fully qualified class path.name,
|
Given a python fully qualified class path.name,
|
||||||
Try importing the path, then creating the object,
|
Try importing the path, then creating the object,
|
||||||
@ -223,61 +317,35 @@ class PluginManager:
|
|||||||
try:
|
try:
|
||||||
plugin_obj = self._create_class(
|
plugin_obj = self._create_class(
|
||||||
plugin_name,
|
plugin_name,
|
||||||
APRSDMessagePluginBase,
|
APRSDPluginBase,
|
||||||
config=self.config,
|
config=self.config,
|
||||||
)
|
)
|
||||||
if plugin_obj:
|
if plugin_obj:
|
||||||
LOG.info(
|
LOG.info(
|
||||||
"Registering Message plugin '{}'({}) '{}'".format(
|
"Registering plugin '{}'({})".format(
|
||||||
plugin_name,
|
|
||||||
plugin_obj.version,
|
|
||||||
plugin_obj.command_regex,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self._pluggy_msg_pm.register(plugin_obj)
|
|
||||||
except Exception as ex:
|
|
||||||
LOG.exception(f"Couldn't load plugin '{plugin_name}'", ex)
|
|
||||||
|
|
||||||
def _load_notify_plugin(self, plugin_name):
|
|
||||||
"""
|
|
||||||
Given a python fully qualified class path.name,
|
|
||||||
Try importing the path, then creating the object,
|
|
||||||
then registering it as a aprsd Command Plugin
|
|
||||||
"""
|
|
||||||
plugin_obj = None
|
|
||||||
try:
|
|
||||||
plugin_obj = self._create_class(
|
|
||||||
plugin_name,
|
|
||||||
APRSDNotificationPluginBase,
|
|
||||||
config=self.config,
|
|
||||||
)
|
|
||||||
if plugin_obj:
|
|
||||||
LOG.info(
|
|
||||||
"Registering Notification plugin '{}'({})".format(
|
|
||||||
plugin_name,
|
plugin_name,
|
||||||
plugin_obj.version,
|
plugin_obj.version,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self._pluggy_notify_pm.register(plugin_obj)
|
self._pluggy_pm.register(plugin_obj)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.exception(f"Couldn't load plugin '{plugin_name}'", ex)
|
LOG.exception(f"Couldn't load plugin '{plugin_name}'", ex)
|
||||||
|
|
||||||
def reload_plugins(self):
|
def reload_plugins(self):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
del self._pluggy_msg_pm
|
del self._pluggy_pm
|
||||||
del self._pluggy_notify_pm
|
|
||||||
self.setup_plugins()
|
self.setup_plugins()
|
||||||
|
|
||||||
def setup_plugins(self):
|
def setup_plugins(self):
|
||||||
"""Create the plugin manager and register plugins."""
|
"""Create the plugin manager and register plugins."""
|
||||||
|
|
||||||
LOG.info("Loading APRSD Message Plugins")
|
LOG.info("Loading APRSD Plugins")
|
||||||
enabled_msg_plugins = self.config["aprsd"].get("enabled_plugins", None)
|
enabled_plugins = self.config["aprsd"].get("enabled_plugins", None)
|
||||||
self._pluggy_msg_pm = pluggy.PluginManager("aprsd")
|
self._pluggy_pm = pluggy.PluginManager("aprsd")
|
||||||
self._pluggy_msg_pm.add_hookspecs(APRSDCommandSpec)
|
self._pluggy_pm.add_hookspecs(APRSDCommandSpec)
|
||||||
if enabled_msg_plugins:
|
if enabled_plugins:
|
||||||
for p_name in enabled_msg_plugins:
|
for p_name in enabled_plugins:
|
||||||
self._load_msg_plugin(p_name)
|
self._load_plugin(p_name)
|
||||||
else:
|
else:
|
||||||
# Enabled plugins isn't set, so we default to loading all of
|
# Enabled plugins isn't set, so we default to loading all of
|
||||||
# the core plugins.
|
# the core plugins.
|
||||||
@ -285,34 +353,24 @@ class PluginManager:
|
|||||||
self._load_plugin(p_name)
|
self._load_plugin(p_name)
|
||||||
|
|
||||||
if self.config["aprsd"]["watch_list"].get("enabled", False):
|
if self.config["aprsd"]["watch_list"].get("enabled", False):
|
||||||
LOG.info("Loading APRSD Notification Plugins")
|
LOG.info("Loading APRSD WatchList Plugins")
|
||||||
enabled_notify_plugins = self.config["aprsd"]["watch_list"].get(
|
enabled_notify_plugins = self.config["aprsd"]["watch_list"].get(
|
||||||
"enabled_plugins",
|
"enabled_plugins",
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
self._pluggy_notify_pm = pluggy.PluginManager("aprsd")
|
|
||||||
self._pluggy_notify_pm.add_hookspecs(APRSDCommandSpec)
|
|
||||||
if enabled_notify_plugins:
|
if enabled_notify_plugins:
|
||||||
for p_name in enabled_notify_plugins:
|
for p_name in enabled_notify_plugins:
|
||||||
self._load_notify_plugin(p_name)
|
self._load_plugin(p_name)
|
||||||
|
|
||||||
else:
|
|
||||||
LOG.info("Skipping Custom Plugins directory.")
|
|
||||||
LOG.info("Completed Plugin Loading.")
|
LOG.info("Completed Plugin Loading.")
|
||||||
|
|
||||||
def run(self, packet):
|
def run(self, packet):
|
||||||
"""Execute all the pluguns run method."""
|
"""Execute all the pluguns run method."""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
return self._pluggy_msg_pm.hook.run(packet=packet)
|
return self._pluggy_pm.hook.filter(packet=packet)
|
||||||
|
|
||||||
def notify(self, packet):
|
|
||||||
"""Execute all the notify pluguns run method."""
|
|
||||||
with self.lock:
|
|
||||||
return self._pluggy_notify_pm.hook.run(packet=packet)
|
|
||||||
|
|
||||||
def register_msg(self, obj):
|
def register_msg(self, obj):
|
||||||
"""Register the plugin."""
|
"""Register the plugin."""
|
||||||
self._pluggy_msg_pm.register(obj)
|
self._pluggy_pm.register(obj)
|
||||||
|
|
||||||
def get_msg_plugins(self):
|
def get_plugins(self):
|
||||||
return self._pluggy_msg_pm.get_plugins()
|
return self._pluggy_pm.get_plugins()
|
||||||
|
@ -1,14 +1,26 @@
|
|||||||
|
import datetime
|
||||||
|
import email
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
import imaplib
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import smtplib
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from aprsd import email, messaging, plugin, trace
|
import imapclient
|
||||||
|
from validate_email import validate_email
|
||||||
|
|
||||||
|
from aprsd import messaging, plugin, stats, threads, trace
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
# This gets forced set from main.py prior to being used internally
|
||||||
|
CONFIG = {}
|
||||||
|
check_email_delay = 60
|
||||||
|
|
||||||
class EmailPlugin(plugin.APRSDMessagePluginBase):
|
|
||||||
|
class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
"""Email Plugin."""
|
"""Email Plugin."""
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
@ -18,10 +30,40 @@ class EmailPlugin(plugin.APRSDMessagePluginBase):
|
|||||||
# message_number:time combos so we don't resend the same email in
|
# message_number:time combos so we don't resend the same email in
|
||||||
# five mins {int:int}
|
# five mins {int:int}
|
||||||
email_sent_dict = {}
|
email_sent_dict = {}
|
||||||
|
enabled = False
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
"""Ensure that email is enabled and start the thread."""
|
||||||
|
global CONFIG
|
||||||
|
CONFIG = self.config
|
||||||
|
|
||||||
|
email_enabled = self.config["aprsd"]["email"].get("enabled", False)
|
||||||
|
validation = self.config["aprsd"]["email"].get("validate", False)
|
||||||
|
|
||||||
|
if email_enabled:
|
||||||
|
valid = validate_email_config(self.config, validation)
|
||||||
|
if not valid:
|
||||||
|
LOG.error("Failed to validate email config options.")
|
||||||
|
LOG.error("EmailPlugin DISABLED!!!!")
|
||||||
|
else:
|
||||||
|
self.enabled = True
|
||||||
|
else:
|
||||||
|
LOG.info("Email services not enabled.")
|
||||||
|
|
||||||
|
def create_threads(self):
|
||||||
|
if self.enabled:
|
||||||
|
return APRSDEmailThread(
|
||||||
|
msg_queues=threads.msg_queues,
|
||||||
|
config=self.config,
|
||||||
|
)
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, packet):
|
def process(self, packet):
|
||||||
LOG.info("Email COMMAND")
|
LOG.info("Email COMMAND")
|
||||||
|
if not self.enabled:
|
||||||
|
# Email has not been enabled
|
||||||
|
# so the plugin will just NOOP
|
||||||
|
return messaging.NULL_MESSAGE
|
||||||
|
|
||||||
fromcall = packet.get("from")
|
fromcall = packet.get("from")
|
||||||
message = packet.get("message_text", None)
|
message = packet.get("message_text", None)
|
||||||
@ -39,7 +81,7 @@ class EmailPlugin(plugin.APRSDMessagePluginBase):
|
|||||||
r = re.search("^-([0-9])[0-9]*$", message)
|
r = re.search("^-([0-9])[0-9]*$", message)
|
||||||
if r is not None:
|
if r is not None:
|
||||||
LOG.debug("RESEND EMAIL")
|
LOG.debug("RESEND EMAIL")
|
||||||
email.resend_email(r.group(1), fromcall)
|
resend_email(r.group(1), fromcall)
|
||||||
reply = messaging.NULL_MESSAGE
|
reply = messaging.NULL_MESSAGE
|
||||||
# -user@address.com body of email
|
# -user@address.com body of email
|
||||||
elif re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message):
|
elif re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message):
|
||||||
@ -49,14 +91,16 @@ class EmailPlugin(plugin.APRSDMessagePluginBase):
|
|||||||
to_addr = a.group(1)
|
to_addr = a.group(1)
|
||||||
content = a.group(2)
|
content = a.group(2)
|
||||||
|
|
||||||
email_address = email.get_email_from_shortcut(to_addr)
|
email_address = get_email_from_shortcut(to_addr)
|
||||||
if not email_address:
|
if not email_address:
|
||||||
reply = "Bad email address"
|
reply = "Bad email address"
|
||||||
return reply
|
return reply
|
||||||
|
|
||||||
# send recipient link to aprs.fi map
|
# send recipient link to aprs.fi map
|
||||||
if content == "mapme":
|
if content == "mapme":
|
||||||
content = "Click for my location: http://aprs.fi/{}".format(
|
content = (
|
||||||
|
"Click for my location: http://aprs.fi/{}" ""
|
||||||
|
).format(
|
||||||
self.config["ham"]["callsign"],
|
self.config["ham"]["callsign"],
|
||||||
)
|
)
|
||||||
too_soon = 0
|
too_soon = 0
|
||||||
@ -74,9 +118,9 @@ class EmailPlugin(plugin.APRSDMessagePluginBase):
|
|||||||
reply = messaging.NULL_MESSAGE
|
reply = messaging.NULL_MESSAGE
|
||||||
if send_result != 0:
|
if send_result != 0:
|
||||||
reply = f"-{to_addr} failed"
|
reply = f"-{to_addr} failed"
|
||||||
# messaging.send_message(fromcall, "-" + to_addr + " failed")
|
|
||||||
else:
|
else:
|
||||||
# clear email sent dictionary if somehow goes over 100
|
# clear email sent dictionary if somehow goes
|
||||||
|
# over 100
|
||||||
if len(self.email_sent_dict) > 98:
|
if len(self.email_sent_dict) > 98:
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
"DEBUG: email_sent_dict is big ("
|
"DEBUG: email_sent_dict is big ("
|
||||||
@ -97,3 +141,540 @@ class EmailPlugin(plugin.APRSDMessagePluginBase):
|
|||||||
# messaging.send_message(fromcall, "Bad email address")
|
# messaging.send_message(fromcall, "Bad email address")
|
||||||
|
|
||||||
return reply
|
return reply
|
||||||
|
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
|
def _imap_connect():
|
||||||
|
global CONFIG
|
||||||
|
imap_port = CONFIG["aprsd"]["email"]["imap"].get("port", 143)
|
||||||
|
use_ssl = CONFIG["aprsd"]["email"]["imap"].get("use_ssl", False)
|
||||||
|
# host = CONFIG["aprsd"]["email"]["imap"]["host"]
|
||||||
|
# msg = "{}{}:{}".format("TLS " if use_ssl else "", host, imap_port)
|
||||||
|
# LOG.debug("Connect to IMAP host {} with user '{}'".
|
||||||
|
# format(msg, CONFIG['imap']['login']))
|
||||||
|
|
||||||
|
try:
|
||||||
|
server = imapclient.IMAPClient(
|
||||||
|
CONFIG["aprsd"]["email"]["imap"]["host"],
|
||||||
|
port=imap_port,
|
||||||
|
use_uid=True,
|
||||||
|
ssl=use_ssl,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error("Failed to connect IMAP server", e)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
server.login(
|
||||||
|
CONFIG["aprsd"]["email"]["imap"]["login"],
|
||||||
|
CONFIG["aprsd"]["email"]["imap"]["password"],
|
||||||
|
)
|
||||||
|
except (imaplib.IMAP4.error, Exception) as e:
|
||||||
|
msg = getattr(e, "message", repr(e))
|
||||||
|
LOG.error(f"Failed to login {msg}")
|
||||||
|
return
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
|
def _smtp_connect():
|
||||||
|
host = CONFIG["aprsd"]["email"]["smtp"]["host"]
|
||||||
|
smtp_port = CONFIG["aprsd"]["email"]["smtp"]["port"]
|
||||||
|
use_ssl = CONFIG["aprsd"]["email"]["smtp"].get("use_ssl", False)
|
||||||
|
msg = "{}{}:{}".format("SSL " if use_ssl else "", host, smtp_port)
|
||||||
|
LOG.debug(
|
||||||
|
"Connect to SMTP host {} with user '{}'".format(
|
||||||
|
msg,
|
||||||
|
CONFIG["aprsd"]["email"]["imap"]["login"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if use_ssl:
|
||||||
|
server = smtplib.SMTP_SSL(
|
||||||
|
host=host,
|
||||||
|
port=smtp_port,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
server = smtplib.SMTP(
|
||||||
|
host=host,
|
||||||
|
port=smtp_port,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
LOG.error("Couldn't connect to SMTP Server")
|
||||||
|
return
|
||||||
|
|
||||||
|
LOG.debug(f"Connected to smtp host {msg}")
|
||||||
|
|
||||||
|
debug = CONFIG["aprsd"]["email"]["smtp"].get("debug", False)
|
||||||
|
if debug:
|
||||||
|
server.set_debuglevel(5)
|
||||||
|
server.sendmail = trace.trace(server.sendmail)
|
||||||
|
|
||||||
|
try:
|
||||||
|
server.login(
|
||||||
|
CONFIG["aprsd"]["email"]["smtp"]["login"],
|
||||||
|
CONFIG["aprsd"]["email"]["smtp"]["password"],
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
LOG.error("Couldn't connect to SMTP Server")
|
||||||
|
return
|
||||||
|
|
||||||
|
LOG.debug(f"Logged into SMTP server {msg}")
|
||||||
|
return server
|
||||||
|
|
||||||
|
|
||||||
|
def validate_shortcuts(config):
|
||||||
|
shortcuts = config["aprsd"]["email"].get("shortcuts", None)
|
||||||
|
if not shortcuts:
|
||||||
|
return
|
||||||
|
|
||||||
|
LOG.info(
|
||||||
|
"Validating {} Email shortcuts. This can take up to 10 seconds"
|
||||||
|
" per shortcut".format(len(shortcuts)),
|
||||||
|
)
|
||||||
|
delete_keys = []
|
||||||
|
for key in shortcuts:
|
||||||
|
LOG.info(f"Validating {key}:{shortcuts[key]}")
|
||||||
|
is_valid = validate_email(
|
||||||
|
email_address=shortcuts[key],
|
||||||
|
check_regex=True,
|
||||||
|
check_mx=False,
|
||||||
|
from_address=config["aprsd"]["email"]["smtp"]["login"],
|
||||||
|
helo_host=config["aprsd"]["email"]["smtp"]["host"],
|
||||||
|
smtp_timeout=10,
|
||||||
|
dns_timeout=10,
|
||||||
|
use_blacklist=True,
|
||||||
|
debug=False,
|
||||||
|
)
|
||||||
|
if not is_valid:
|
||||||
|
LOG.error(
|
||||||
|
"'{}' is an invalid email address. Removing shortcut".format(
|
||||||
|
shortcuts[key],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
delete_keys.append(key)
|
||||||
|
|
||||||
|
for key in delete_keys:
|
||||||
|
del config["aprsd"]["email"]["shortcuts"][key]
|
||||||
|
|
||||||
|
LOG.info(
|
||||||
|
"Available shortcuts: {}".format(
|
||||||
|
config["aprsd"]["email"]["shortcuts"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_email_from_shortcut(addr):
|
||||||
|
if CONFIG["aprsd"]["email"].get("shortcuts", False):
|
||||||
|
return CONFIG["aprsd"]["email"]["shortcuts"].get(addr, addr)
|
||||||
|
else:
|
||||||
|
return addr
|
||||||
|
|
||||||
|
|
||||||
|
def validate_email_config(config, disable_validation=False):
|
||||||
|
"""function to simply ensure we can connect to email services.
|
||||||
|
|
||||||
|
This helps with failing early during startup.
|
||||||
|
"""
|
||||||
|
LOG.info("Checking IMAP configuration")
|
||||||
|
imap_server = _imap_connect()
|
||||||
|
LOG.info("Checking SMTP configuration")
|
||||||
|
smtp_server = _smtp_connect()
|
||||||
|
|
||||||
|
# Now validate and flag any shortcuts as invalid
|
||||||
|
if not disable_validation:
|
||||||
|
validate_shortcuts(config)
|
||||||
|
else:
|
||||||
|
LOG.info("Shortcuts email validation is Disabled!!, you were warned.")
|
||||||
|
|
||||||
|
if imap_server and smtp_server:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
|
def parse_email(msgid, data, server):
|
||||||
|
envelope = data[b"ENVELOPE"]
|
||||||
|
# email address match
|
||||||
|
# use raw string to avoid invalid escape secquence errors r"string here"
|
||||||
|
f = re.search(r"([\.\w_-]+@[\.\w_-]+)", str(envelope.from_[0]))
|
||||||
|
if f is not None:
|
||||||
|
from_addr = f.group(1)
|
||||||
|
else:
|
||||||
|
from_addr = "noaddr"
|
||||||
|
LOG.debug(f"Got a message from '{from_addr}'")
|
||||||
|
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"))
|
||||||
|
if msg.is_multipart():
|
||||||
|
text = ""
|
||||||
|
html = None
|
||||||
|
# default in case body somehow isn't set below - happened once
|
||||||
|
body = b"* unreadable msg received"
|
||||||
|
# this uses the last text or html part in the email,
|
||||||
|
# phone companies often put content in an attachment
|
||||||
|
for part in msg.get_payload():
|
||||||
|
if part.get_content_charset() is None:
|
||||||
|
# or BREAK when we hit a text or html?
|
||||||
|
# We cannot know the character set,
|
||||||
|
# so return decoded "something"
|
||||||
|
LOG.debug("Email got unknown content type")
|
||||||
|
text = part.get_payload(decode=True)
|
||||||
|
continue
|
||||||
|
|
||||||
|
charset = part.get_content_charset()
|
||||||
|
|
||||||
|
if part.get_content_type() == "text/plain":
|
||||||
|
LOG.debug("Email got text/plain")
|
||||||
|
text = str(
|
||||||
|
part.get_payload(decode=True),
|
||||||
|
str(charset),
|
||||||
|
"ignore",
|
||||||
|
).encode("utf8", "replace")
|
||||||
|
|
||||||
|
if part.get_content_type() == "text/html":
|
||||||
|
LOG.debug("Email got text/html")
|
||||||
|
html = str(
|
||||||
|
part.get_payload(decode=True),
|
||||||
|
str(charset),
|
||||||
|
"ignore",
|
||||||
|
).encode("utf8", "replace")
|
||||||
|
|
||||||
|
if text is not None:
|
||||||
|
# strip removes white space fore and aft of string
|
||||||
|
body = text.strip()
|
||||||
|
else:
|
||||||
|
body = html.strip()
|
||||||
|
else: # message is not multipart
|
||||||
|
# email.uscc.net sends no charset, blows up unicode function below
|
||||||
|
LOG.debug("Email is not multipart")
|
||||||
|
if msg.get_content_charset() is None:
|
||||||
|
text = str(msg.get_payload(decode=True), "US-ASCII", "ignore").encode(
|
||||||
|
"utf8",
|
||||||
|
"replace",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
text = str(
|
||||||
|
msg.get_payload(decode=True),
|
||||||
|
msg.get_content_charset(),
|
||||||
|
"ignore",
|
||||||
|
).encode("utf8", "replace")
|
||||||
|
body = text.strip()
|
||||||
|
|
||||||
|
# FIXED: UnicodeDecodeError: 'ascii' codec can't decode byte 0xf0
|
||||||
|
# in position 6: ordinal not in range(128)
|
||||||
|
# decode with errors='ignore'. be sure to encode it before we return
|
||||||
|
# it below, also with errors='ignore'
|
||||||
|
try:
|
||||||
|
body = body.decode(errors="ignore")
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error("Unicode decode failure: " + str(e))
|
||||||
|
LOG.error("Unidoce decode failed: " + str(body))
|
||||||
|
body = "Unreadable unicode msg"
|
||||||
|
# strip all html tags
|
||||||
|
body = re.sub("<[^<]+?>", "", body)
|
||||||
|
# strip CR/LF, make it one line, .rstrip fails at this
|
||||||
|
body = body.replace("\n", " ").replace("\r", " ")
|
||||||
|
# ascii might be out of range, so encode it, removing any error characters
|
||||||
|
body = body.encode(errors="ignore")
|
||||||
|
return body, from_addr
|
||||||
|
|
||||||
|
|
||||||
|
# end parse_email
|
||||||
|
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
|
def send_email(to_addr, content):
|
||||||
|
global check_email_delay
|
||||||
|
|
||||||
|
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
|
||||||
|
email_address = get_email_from_shortcut(to_addr)
|
||||||
|
LOG.info("Sending Email_________________")
|
||||||
|
|
||||||
|
if to_addr in shortcuts:
|
||||||
|
LOG.info("To : " + to_addr)
|
||||||
|
to_addr = email_address
|
||||||
|
LOG.info(" (" + to_addr + ")")
|
||||||
|
subject = CONFIG["ham"]["callsign"]
|
||||||
|
# content = content + "\n\n(NOTE: reply with one line)"
|
||||||
|
LOG.info("Subject : " + subject)
|
||||||
|
LOG.info("Body : " + content)
|
||||||
|
|
||||||
|
# check email more often since there's activity right now
|
||||||
|
check_email_delay = 60
|
||||||
|
|
||||||
|
msg = MIMEText(content)
|
||||||
|
msg["Subject"] = subject
|
||||||
|
msg["From"] = CONFIG["aprsd"]["email"]["smtp"]["login"]
|
||||||
|
msg["To"] = to_addr
|
||||||
|
server = _smtp_connect()
|
||||||
|
if server:
|
||||||
|
try:
|
||||||
|
server.sendmail(
|
||||||
|
CONFIG["aprsd"]["email"]["smtp"]["login"],
|
||||||
|
[to_addr],
|
||||||
|
msg.as_string(),
|
||||||
|
)
|
||||||
|
stats.APRSDStats().email_tx_inc()
|
||||||
|
except Exception as e:
|
||||||
|
msg = getattr(e, "message", repr(e))
|
||||||
|
LOG.error("Sendmail Error!!!! '{}'", msg)
|
||||||
|
server.quit()
|
||||||
|
return -1
|
||||||
|
server.quit()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
|
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 = f"{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()
|
||||||
|
# end resend_email()
|
||||||
|
|
||||||
|
|
||||||
|
class APRSDEmailThread(threads.APRSDThread):
|
||||||
|
def __init__(self, msg_queues, config):
|
||||||
|
super().__init__("EmailThread")
|
||||||
|
self.msg_queues = msg_queues
|
||||||
|
self.config = config
|
||||||
|
self.past = datetime.datetime.now()
|
||||||
|
|
||||||
|
def loop(self):
|
||||||
|
global check_email_delay
|
||||||
|
|
||||||
|
LOG.debug("Starting Loop")
|
||||||
|
|
||||||
|
check_email_delay = 60
|
||||||
|
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 - self.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 = f"{day}-{month}-{year}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
server = _imap_connect()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception("IMAP failed to connect.", e)
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
messages = server.search(["SINCE", today])
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception(
|
||||||
|
"IMAP failed to search for messages since today.",
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
LOG.debug(f"{len(messages)} messages received today")
|
||||||
|
|
||||||
|
try:
|
||||||
|
_msgs = server.fetch(messages, ["ENVELOPE"])
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception("IMAP failed to fetch/flag messages: ", e)
|
||||||
|
return True
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
msg.send()
|
||||||
|
# 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.")
|
||||||
|
self.past = datetime.datetime.now()
|
||||||
|
try:
|
||||||
|
server.logout()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception("IMAP failed to logout: ", e)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# We haven't hit the email delay yet.
|
||||||
|
# LOG.debug("Delta({}) < {}".format(now - past, check_email_delay))
|
||||||
|
return True
|
||||||
|
|
||||||
|
return True
|
||||||
|
@ -8,7 +8,7 @@ from aprsd import plugin, trace
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class FortunePlugin(plugin.APRSDMessagePluginBase):
|
class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
"""Fortune."""
|
"""Fortune."""
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
@ -16,7 +16,7 @@ class FortunePlugin(plugin.APRSDMessagePluginBase):
|
|||||||
command_name = "fortune"
|
command_name = "fortune"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, packet):
|
def process(self, packet):
|
||||||
LOG.info("FortunePlugin")
|
LOG.info("FortunePlugin")
|
||||||
|
|
||||||
# fromcall = packet.get("from")
|
# fromcall = packet.get("from")
|
||||||
|
@ -8,7 +8,7 @@ from aprsd import plugin, plugin_utils, trace, utils
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class LocationPlugin(plugin.APRSDMessagePluginBase):
|
class LocationPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
"""Location!"""
|
"""Location!"""
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
@ -16,7 +16,7 @@ class LocationPlugin(plugin.APRSDMessagePluginBase):
|
|||||||
command_name = "location"
|
command_name = "location"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, packet):
|
def process(self, packet):
|
||||||
LOG.info("Location Plugin")
|
LOG.info("Location Plugin")
|
||||||
fromcall = packet.get("from")
|
fromcall = packet.get("from")
|
||||||
message = packet.get("message_text", None)
|
message = packet.get("message_text", None)
|
||||||
|
@ -6,7 +6,7 @@ from aprsd import messaging, packets, plugin
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class NotifySeenPlugin(plugin.APRSDNotificationPluginBase):
|
class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
|
||||||
"""Notification plugin to send seen message for callsign.
|
"""Notification plugin to send seen message for callsign.
|
||||||
|
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ class NotifySeenPlugin(plugin.APRSDNotificationPluginBase):
|
|||||||
"""The aprsd config object is stored."""
|
"""The aprsd config object is stored."""
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
|
|
||||||
def notify(self, packet):
|
def process(self, packet):
|
||||||
LOG.info("BaseNotifyPlugin")
|
LOG.info("BaseNotifyPlugin")
|
||||||
|
|
||||||
notify_callsign = self.config["aprsd"]["watch_list"]["alert_callsign"]
|
notify_callsign = self.config["aprsd"]["watch_list"]["alert_callsign"]
|
||||||
|
@ -7,7 +7,7 @@ from aprsd import plugin, trace
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class PingPlugin(plugin.APRSDMessagePluginBase):
|
class PingPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
"""Ping."""
|
"""Ping."""
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
@ -15,8 +15,8 @@ class PingPlugin(plugin.APRSDMessagePluginBase):
|
|||||||
command_name = "ping"
|
command_name = "ping"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, packet):
|
def process(self, packet):
|
||||||
LOG.info("PINGPlugin")
|
LOG.info("PingPlugin")
|
||||||
# fromcall = packet.get("from")
|
# fromcall = packet.get("from")
|
||||||
# message = packet.get("message_text", None)
|
# message = packet.get("message_text", None)
|
||||||
# ack = packet.get("msgNo", "0")
|
# ack = packet.get("msgNo", "0")
|
||||||
|
@ -8,7 +8,7 @@ from aprsd import messaging, plugin, trace
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class QueryPlugin(plugin.APRSDMessagePluginBase):
|
class QueryPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
"""Query command."""
|
"""Query command."""
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
@ -16,7 +16,7 @@ class QueryPlugin(plugin.APRSDMessagePluginBase):
|
|||||||
command_name = "query"
|
command_name = "query"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, packet):
|
def process(self, packet):
|
||||||
LOG.info("Query COMMAND")
|
LOG.info("Query COMMAND")
|
||||||
|
|
||||||
fromcall = packet.get("from")
|
fromcall = packet.get("from")
|
||||||
|
@ -9,7 +9,7 @@ from aprsd import plugin, trace
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class StockPlugin(plugin.APRSDMessagePluginBase):
|
class StockPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
"""Stock market plugin for fetching stock quotes"""
|
"""Stock market plugin for fetching stock quotes"""
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
@ -17,7 +17,7 @@ class StockPlugin(plugin.APRSDMessagePluginBase):
|
|||||||
command_name = "stock"
|
command_name = "stock"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, packet):
|
def process(self, packet):
|
||||||
LOG.info("StockPlugin")
|
LOG.info("StockPlugin")
|
||||||
|
|
||||||
# fromcall = packet.get("from")
|
# fromcall = packet.get("from")
|
||||||
|
@ -11,7 +11,7 @@ from aprsd import fuzzyclock, plugin, plugin_utils, trace, utils
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class TimePlugin(plugin.APRSDMessagePluginBase):
|
class TimePlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
"""Time command."""
|
"""Time command."""
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
@ -42,7 +42,7 @@ class TimePlugin(plugin.APRSDMessagePluginBase):
|
|||||||
return reply
|
return reply
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, packet):
|
def process(self, packet):
|
||||||
LOG.info("TIME COMMAND")
|
LOG.info("TIME COMMAND")
|
||||||
# So we can mock this in unit tests
|
# So we can mock this in unit tests
|
||||||
localzone = self._get_local_tz()
|
localzone = self._get_local_tz()
|
||||||
@ -57,7 +57,7 @@ class TimeOpenCageDataPlugin(TimePlugin):
|
|||||||
command_name = "Time"
|
command_name = "Time"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, packet):
|
def process(self, packet):
|
||||||
fromcall = packet.get("from")
|
fromcall = packet.get("from")
|
||||||
message = packet.get("message_text", None)
|
message = packet.get("message_text", None)
|
||||||
# ack = packet.get("msgNo", "0")
|
# ack = packet.get("msgNo", "0")
|
||||||
@ -123,7 +123,7 @@ class TimeOWMPlugin(TimePlugin):
|
|||||||
command_name = "Time"
|
command_name = "Time"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, packet):
|
def process(self, packet):
|
||||||
fromcall = packet.get("from")
|
fromcall = packet.get("from")
|
||||||
message = packet.get("message_text", None)
|
message = packet.get("message_text", None)
|
||||||
# ack = packet.get("msgNo", "0")
|
# ack = packet.get("msgNo", "0")
|
||||||
|
@ -7,7 +7,7 @@ from aprsd import plugin, stats, trace
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class VersionPlugin(plugin.APRSDMessagePluginBase):
|
class VersionPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
"""Version of APRSD Plugin."""
|
"""Version of APRSD Plugin."""
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
@ -19,7 +19,7 @@ class VersionPlugin(plugin.APRSDMessagePluginBase):
|
|||||||
email_sent_dict = {}
|
email_sent_dict = {}
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, packet):
|
def process(self, packet):
|
||||||
LOG.info("Version COMMAND")
|
LOG.info("Version COMMAND")
|
||||||
# fromcall = packet.get("from")
|
# fromcall = packet.get("from")
|
||||||
# message = packet.get("message_text", None)
|
# message = packet.get("message_text", None)
|
||||||
|
@ -10,7 +10,7 @@ from aprsd import plugin, plugin_utils, trace, utils
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class USWeatherPlugin(plugin.APRSDMessagePluginBase):
|
class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
"""USWeather Command
|
"""USWeather Command
|
||||||
|
|
||||||
Returns a weather report for the calling weather station
|
Returns a weather report for the calling weather station
|
||||||
@ -28,7 +28,7 @@ class USWeatherPlugin(plugin.APRSDMessagePluginBase):
|
|||||||
command_name = "weather"
|
command_name = "weather"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, packet):
|
def process(self, packet):
|
||||||
LOG.info("Weather Plugin")
|
LOG.info("Weather Plugin")
|
||||||
fromcall = packet.get("from")
|
fromcall = packet.get("from")
|
||||||
# message = packet.get("message_text", None)
|
# message = packet.get("message_text", None)
|
||||||
@ -71,7 +71,7 @@ class USWeatherPlugin(plugin.APRSDMessagePluginBase):
|
|||||||
return reply
|
return reply
|
||||||
|
|
||||||
|
|
||||||
class USMetarPlugin(plugin.APRSDMessagePluginBase):
|
class USMetarPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
"""METAR Command
|
"""METAR Command
|
||||||
|
|
||||||
This provides a METAR weather report from a station near the caller
|
This provides a METAR weather report from a station near the caller
|
||||||
@ -91,7 +91,7 @@ class USMetarPlugin(plugin.APRSDMessagePluginBase):
|
|||||||
command_name = "Metar"
|
command_name = "Metar"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, packet):
|
def process(self, packet):
|
||||||
fromcall = packet.get("from")
|
fromcall = packet.get("from")
|
||||||
message = packet.get("message_text", None)
|
message = packet.get("message_text", None)
|
||||||
# ack = packet.get("msgNo", "0")
|
# ack = packet.get("msgNo", "0")
|
||||||
@ -162,7 +162,7 @@ class USMetarPlugin(plugin.APRSDMessagePluginBase):
|
|||||||
return reply
|
return reply
|
||||||
|
|
||||||
|
|
||||||
class OWMWeatherPlugin(plugin.APRSDMessagePluginBase):
|
class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
"""OpenWeatherMap Weather Command
|
"""OpenWeatherMap Weather Command
|
||||||
|
|
||||||
This provides weather near the caller or callsign.
|
This provides weather near the caller or callsign.
|
||||||
@ -186,7 +186,7 @@ class OWMWeatherPlugin(plugin.APRSDMessagePluginBase):
|
|||||||
command_name = "Weather"
|
command_name = "Weather"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, packet):
|
def process(self, packet):
|
||||||
fromcall = packet.get("from")
|
fromcall = packet.get("from")
|
||||||
message = packet.get("message_text", None)
|
message = packet.get("message_text", None)
|
||||||
# ack = packet.get("msgNo", "0")
|
# ack = packet.get("msgNo", "0")
|
||||||
@ -282,7 +282,7 @@ class OWMWeatherPlugin(plugin.APRSDMessagePluginBase):
|
|||||||
return reply
|
return reply
|
||||||
|
|
||||||
|
|
||||||
class AVWXWeatherPlugin(plugin.APRSDMessagePluginBase):
|
class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
"""AVWXWeatherMap Weather Command
|
"""AVWXWeatherMap Weather Command
|
||||||
|
|
||||||
Fetches a METAR weather report for the nearest
|
Fetches a METAR weather report for the nearest
|
||||||
@ -310,7 +310,7 @@ class AVWXWeatherPlugin(plugin.APRSDMessagePluginBase):
|
|||||||
command_name = "Weather"
|
command_name = "Weather"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, packet):
|
def process(self, packet):
|
||||||
fromcall = packet.get("from")
|
fromcall = packet.get("from")
|
||||||
message = packet.get("message_text", None)
|
message = packet.get("message_text", None)
|
||||||
# ack = packet.get("msgNo", "0")
|
# ack = packet.get("msgNo", "0")
|
||||||
|
@ -183,7 +183,7 @@ class APRSDStats:
|
|||||||
last_aprsis_keepalive = "never"
|
last_aprsis_keepalive = "never"
|
||||||
|
|
||||||
pm = plugin.PluginManager()
|
pm = plugin.PluginManager()
|
||||||
plugins = pm.get_msg_plugins()
|
plugins = pm.get_plugins()
|
||||||
plugin_stats = {}
|
plugin_stats = {}
|
||||||
|
|
||||||
def full_name_with_qualname(obj):
|
def full_name_with_qualname(obj):
|
||||||
@ -193,7 +193,10 @@ class APRSDStats:
|
|||||||
)
|
)
|
||||||
|
|
||||||
for p in plugins:
|
for p in plugins:
|
||||||
plugin_stats[full_name_with_qualname(p)] = p.message_count
|
plugin_stats[full_name_with_qualname(p)] = {
|
||||||
|
"rx": p.rx_count,
|
||||||
|
"tx": p.tx_count,
|
||||||
|
}
|
||||||
|
|
||||||
wl = packets.WatchList()
|
wl = packets.WatchList()
|
||||||
|
|
||||||
|
232
aprsd/threads.py
232
aprsd/threads.py
@ -14,9 +14,13 @@ from aprsd import client, messaging, packets, plugin, stats, utils
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
RX_THREAD = "RX"
|
RX_THREAD = "RX"
|
||||||
TX_THREAD = "TX"
|
|
||||||
EMAIL_THREAD = "Email"
|
EMAIL_THREAD = "Email"
|
||||||
|
|
||||||
|
rx_msg_queue = queue.Queue(maxsize=20)
|
||||||
|
msg_queues = {
|
||||||
|
"rx": rx_msg_queue,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class APRSDThreadList:
|
class APRSDThreadList:
|
||||||
"""Singleton class that keeps track of application wide threads."""
|
"""Singleton class that keeps track of application wide threads."""
|
||||||
@ -45,8 +49,13 @@ class APRSDThreadList:
|
|||||||
"""Iterate over all threads and call stop on them."""
|
"""Iterate over all threads and call stop on them."""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
for th in self.threads_list:
|
for th in self.threads_list:
|
||||||
|
LOG.debug(f"Stopping Thread {th.name}")
|
||||||
th.stop()
|
th.stop()
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
with self.lock:
|
||||||
|
return len(self.threads_list)
|
||||||
|
|
||||||
|
|
||||||
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
@ -57,6 +66,10 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
|||||||
def stop(self):
|
def stop(self):
|
||||||
self.thread_stop = True
|
self.thread_stop = True
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def loop(self):
|
||||||
|
pass
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
LOG.debug("Starting")
|
LOG.debug("Starting")
|
||||||
while not self.thread_stop:
|
while not self.thread_stop:
|
||||||
@ -71,15 +84,17 @@ class KeepAliveThread(APRSDThread):
|
|||||||
cntr = 0
|
cntr = 0
|
||||||
checker_time = datetime.datetime.now()
|
checker_time = datetime.datetime.now()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, config):
|
||||||
tracemalloc.start()
|
tracemalloc.start()
|
||||||
super().__init__("KeepAlive")
|
super().__init__("KeepAlive")
|
||||||
|
self.config = config
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
if self.cntr % 6 == 0:
|
if self.cntr % 6 == 0:
|
||||||
tracker = messaging.MsgTrack()
|
tracker = messaging.MsgTrack()
|
||||||
stats_obj = stats.APRSDStats()
|
stats_obj = stats.APRSDStats()
|
||||||
packets_list = packets.PacketList().packet_list
|
pl = packets.PacketList()
|
||||||
|
thread_list = APRSDThreadList()
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
last_email = stats_obj.email_thread_time
|
last_email = stats_obj.email_thread_time
|
||||||
if last_email:
|
if last_email:
|
||||||
@ -92,18 +107,24 @@ class KeepAliveThread(APRSDThread):
|
|||||||
current, peak = tracemalloc.get_traced_memory()
|
current, peak = tracemalloc.get_traced_memory()
|
||||||
stats_obj.set_memory(current)
|
stats_obj.set_memory(current)
|
||||||
stats_obj.set_memory_peak(peak)
|
stats_obj.set_memory_peak(peak)
|
||||||
keepalive = "Uptime {} Tracker {} " "Msgs TX:{} RX:{} Last:{} Email:{} Packets:{} RAM Current:{} Peak:{}".format(
|
keepalive = (
|
||||||
|
"{} - Uptime {} RX:{} TX:{} Tracker:{} Msgs TX:{} RX:{} "
|
||||||
|
"Last:{} Email: {} - RAM Current:{} Peak:{} Threads:{}"
|
||||||
|
).format(
|
||||||
|
self.config["aprs"]["login"],
|
||||||
utils.strfdelta(stats_obj.uptime),
|
utils.strfdelta(stats_obj.uptime),
|
||||||
|
pl.total_recv,
|
||||||
|
pl.total_tx,
|
||||||
len(tracker),
|
len(tracker),
|
||||||
stats_obj.msgs_tx,
|
stats_obj.msgs_tx,
|
||||||
stats_obj.msgs_rx,
|
stats_obj.msgs_rx,
|
||||||
last_msg_time,
|
last_msg_time,
|
||||||
email_thread_time,
|
email_thread_time,
|
||||||
len(packets_list),
|
|
||||||
utils.human_size(current),
|
utils.human_size(current),
|
||||||
utils.human_size(peak),
|
utils.human_size(peak),
|
||||||
|
len(thread_list),
|
||||||
)
|
)
|
||||||
LOG.debug(keepalive)
|
LOG.info(keepalive)
|
||||||
# Check version every hour
|
# Check version every hour
|
||||||
delta = now - self.checker_time
|
delta = now - self.checker_time
|
||||||
if delta > datetime.timedelta(hours=1):
|
if delta > datetime.timedelta(hours=1):
|
||||||
@ -112,52 +133,7 @@ class KeepAliveThread(APRSDThread):
|
|||||||
if level:
|
if level:
|
||||||
LOG.warning(msg)
|
LOG.warning(msg)
|
||||||
self.cntr += 1
|
self.cntr += 1
|
||||||
time.sleep(10)
|
time.sleep(1)
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class APRSDNotifyThread(APRSDThread):
|
|
||||||
last_seen = {}
|
|
||||||
|
|
||||||
def __init__(self, msg_queues, config):
|
|
||||||
super().__init__("NOTIFY_MSG")
|
|
||||||
self.msg_queues = msg_queues
|
|
||||||
self.config = config
|
|
||||||
packets.WatchList(config=config)
|
|
||||||
|
|
||||||
def loop(self):
|
|
||||||
try:
|
|
||||||
packet = self.msg_queues["notify"].get(timeout=5)
|
|
||||||
wl = packets.WatchList()
|
|
||||||
if wl.callsign_in_watchlist(packet["from"]):
|
|
||||||
# NOW WE RUN through the notify plugins.
|
|
||||||
# If they return a msg, then we queue it for sending.
|
|
||||||
pm = plugin.PluginManager()
|
|
||||||
results = pm.notify(packet)
|
|
||||||
for reply in results:
|
|
||||||
if reply is not messaging.NULL_MESSAGE:
|
|
||||||
watch_list_conf = self.config["aprsd"]["watch_list"]
|
|
||||||
|
|
||||||
msg = messaging.TextMessage(
|
|
||||||
self.config["aprs"]["login"],
|
|
||||||
watch_list_conf["alert_callsign"],
|
|
||||||
reply,
|
|
||||||
)
|
|
||||||
self.msg_queues["tx"].put(msg)
|
|
||||||
|
|
||||||
wl.update_seen(packet)
|
|
||||||
else:
|
|
||||||
LOG.debug(
|
|
||||||
"Ignoring packet from '{}'. Not in watch list.".format(
|
|
||||||
packet["from"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Allows stats object to have latest info from the last_seen dict
|
|
||||||
LOG.debug("Packet processing complete")
|
|
||||||
except queue.Empty:
|
|
||||||
pass
|
|
||||||
# Continue to loop
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -174,23 +150,6 @@ class APRSDRXThread(APRSDThread):
|
|||||||
def loop(self):
|
def loop(self):
|
||||||
aprs_client = client.get_client()
|
aprs_client = client.get_client()
|
||||||
|
|
||||||
# if we have a watch list enabled, we need to add filtering
|
|
||||||
# to enable seeing packets from the watch list.
|
|
||||||
if "watch_list" in self.config["aprsd"] and self.config["aprsd"][
|
|
||||||
"watch_list"
|
|
||||||
].get("enabled", False):
|
|
||||||
# watch list is enabled
|
|
||||||
watch_list = self.config["aprsd"]["watch_list"].get(
|
|
||||||
"callsigns",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
# make sure the timeout is set or this doesn't work
|
|
||||||
if watch_list:
|
|
||||||
filter_str = "p/{}".format("/".join(watch_list))
|
|
||||||
aprs_client.set_filter(filter_str)
|
|
||||||
else:
|
|
||||||
LOG.warning("Watch list enabled, but no callsigns set.")
|
|
||||||
|
|
||||||
# setup the consumer of messages and block until a messages
|
# setup the consumer of messages and block until a messages
|
||||||
try:
|
try:
|
||||||
# This will register a packet consumer with aprslib
|
# This will register a packet consumer with aprslib
|
||||||
@ -214,6 +173,19 @@ class APRSDRXThread(APRSDThread):
|
|||||||
# Continue to loop
|
# Continue to loop
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def process_packet(self, packet):
|
||||||
|
thread = APRSDProcessPacketThread(packet=packet, config=self.config)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
class APRSDProcessPacketThread(APRSDThread):
|
||||||
|
|
||||||
|
def __init__(self, packet, config):
|
||||||
|
self.packet = packet
|
||||||
|
self.config = config
|
||||||
|
name = self.packet["raw"][:10]
|
||||||
|
super().__init__(f"RX_PACKET-{name}")
|
||||||
|
|
||||||
def process_ack_packet(self, packet):
|
def process_ack_packet(self, packet):
|
||||||
ack_num = packet.get("msgNo")
|
ack_num = packet.get("msgNo")
|
||||||
LOG.info(f"Got ack for message {ack_num}")
|
LOG.info(f"Got ack for message {ack_num}")
|
||||||
@ -229,35 +201,53 @@ class APRSDRXThread(APRSDThread):
|
|||||||
stats.APRSDStats().ack_rx_inc()
|
stats.APRSDStats().ack_rx_inc()
|
||||||
return
|
return
|
||||||
|
|
||||||
def process_mic_e_packet(self, packet):
|
def loop(self):
|
||||||
LOG.info("Mic-E Packet detected. Currenlty unsupported.")
|
"""Process a packet recieved from aprs-is server."""
|
||||||
messaging.log_packet(packet)
|
packet = self.packet
|
||||||
stats.APRSDStats().msgs_mice_inc()
|
packets.PacketList().add(packet)
|
||||||
return
|
|
||||||
|
|
||||||
def process_message_packet(self, packet):
|
|
||||||
fromcall = packet["from"]
|
fromcall = packet["from"]
|
||||||
message = packet.get("message_text", None)
|
tocall = packet.get("addresse", None)
|
||||||
|
msg = packet.get("message_text", None)
|
||||||
msg_id = packet.get("msgNo", "0")
|
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(
|
messaging.log_message(
|
||||||
"Received Message",
|
"Received Message",
|
||||||
packet["raw"],
|
packet["raw"],
|
||||||
message,
|
msg,
|
||||||
fromcall=fromcall,
|
fromcall=fromcall,
|
||||||
msg_num=msg_id,
|
msg_num=msg_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
found_command = False
|
# Only ack messages that were sent directly to us
|
||||||
# Get singleton of the PM
|
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()
|
pm = plugin.PluginManager()
|
||||||
try:
|
try:
|
||||||
results = pm.run(packet)
|
results = pm.run(packet)
|
||||||
|
replied = False
|
||||||
for reply in results:
|
for reply in results:
|
||||||
if isinstance(reply, list):
|
if isinstance(reply, list):
|
||||||
# one of the plugins wants to send multiple messages
|
# one of the plugins wants to send multiple messages
|
||||||
found_command = True
|
replied = True
|
||||||
for subreply in reply:
|
for subreply in reply:
|
||||||
LOG.debug(f"Sending '{subreply}'")
|
LOG.debug(f"Sending '{subreply}'")
|
||||||
|
|
||||||
@ -266,13 +256,14 @@ class APRSDRXThread(APRSDThread):
|
|||||||
fromcall,
|
fromcall,
|
||||||
subreply,
|
subreply,
|
||||||
)
|
)
|
||||||
self.msg_queues["tx"].put(msg)
|
msg.send()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
found_command = True
|
replied = True
|
||||||
# A plugin can return a null message flag which signals
|
# A plugin can return a null message flag which signals
|
||||||
# us that they processed the message correctly, but have
|
# us that they processed the message correctly, but have
|
||||||
# nothing to reply with, so we avoid replying with a usage string
|
# nothing to reply with, so we avoid replying with a
|
||||||
|
# usage string
|
||||||
if reply is not messaging.NULL_MESSAGE:
|
if reply is not messaging.NULL_MESSAGE:
|
||||||
LOG.debug(f"Sending '{reply}'")
|
LOG.debug(f"Sending '{reply}'")
|
||||||
|
|
||||||
@ -281,16 +272,11 @@ class APRSDRXThread(APRSDThread):
|
|||||||
fromcall,
|
fromcall,
|
||||||
reply,
|
reply,
|
||||||
)
|
)
|
||||||
self.msg_queues["tx"].put(msg)
|
msg.send()
|
||||||
else:
|
|
||||||
LOG.debug("Got NULL MESSAGE from plugin")
|
|
||||||
|
|
||||||
if not found_command:
|
# If the message was for us and we didn't have a
|
||||||
plugins = pm.get_msg_plugins()
|
# response, then we send a usage statement.
|
||||||
names = [x.command_name for x in plugins]
|
if tocall == self.config["aprs"]["login"] and not replied:
|
||||||
names.sort()
|
|
||||||
|
|
||||||
# reply = "Usage: {}".format(", ".join(names))
|
|
||||||
reply = "Usage: weather, locate [call], time, fortune, ping"
|
reply = "Usage: weather, locate [call], time, fortune, ping"
|
||||||
|
|
||||||
msg = messaging.TextMessage(
|
msg = messaging.TextMessage(
|
||||||
@ -298,64 +284,19 @@ class APRSDRXThread(APRSDThread):
|
|||||||
fromcall,
|
fromcall,
|
||||||
reply,
|
reply,
|
||||||
)
|
)
|
||||||
self.msg_queues["tx"].put(msg)
|
msg.send()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.exception("Plugin failed!!!", ex)
|
LOG.exception("Plugin failed!!!", ex)
|
||||||
|
# Do we need to send a reply?
|
||||||
|
if tocall == self.config["aprs"]["login"]:
|
||||||
reply = "A Plugin failed! try again?"
|
reply = "A Plugin failed! try again?"
|
||||||
msg = messaging.TextMessage(self.config["aprs"]["login"], fromcall, reply)
|
msg = messaging.TextMessage(
|
||||||
self.msg_queues["tx"].put(msg)
|
|
||||||
|
|
||||||
# let any threads do their thing, then ack
|
|
||||||
# send an ack last
|
|
||||||
ack = messaging.AckMessage(
|
|
||||||
self.config["aprs"]["login"],
|
self.config["aprs"]["login"],
|
||||||
fromcall,
|
fromcall,
|
||||||
msg_id=msg_id,
|
reply,
|
||||||
)
|
)
|
||||||
self.msg_queues["tx"].put(ack)
|
msg.send()
|
||||||
|
|
||||||
def process_packet(self, packet):
|
|
||||||
"""Process a packet recieved from aprs-is server."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
LOG.debug("Adding packet to notify queue {}".format(packet["raw"]))
|
|
||||||
self.msg_queues["notify"].put(packet)
|
|
||||||
packets.PacketList().add(packet)
|
|
||||||
|
|
||||||
# since we can see packets from anyone now with the
|
|
||||||
# watch list, we need to filter messages directly only to us.
|
|
||||||
tocall = packet.get("addresse", None)
|
|
||||||
if tocall == self.config["aprs"]["login"]:
|
|
||||||
stats.APRSDStats().msgs_rx_inc()
|
|
||||||
packets.PacketList().add(packet)
|
|
||||||
|
|
||||||
msg = packet.get("message_text", None)
|
|
||||||
msg_format = packet.get("format", None)
|
|
||||||
msg_response = packet.get("response", None)
|
|
||||||
if msg_format == "message" and msg:
|
|
||||||
# we want to send the message through the
|
|
||||||
# plugins
|
|
||||||
self.process_message_packet(packet)
|
|
||||||
return
|
|
||||||
elif msg_response == "ack":
|
|
||||||
self.process_ack_packet(packet)
|
|
||||||
return
|
|
||||||
|
|
||||||
if msg_format == "mic-e":
|
|
||||||
# process a mic-e packet
|
|
||||||
self.process_mic_e_packet(packet)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
LOG.debug(
|
|
||||||
"Ignoring '{}' packet from '{}' to '{}'".format(
|
|
||||||
packets.get_packet_type(packet),
|
|
||||||
packet["from"],
|
|
||||||
tocall,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
except (aprslib.ParseError, aprslib.UnknownFormat) as exp:
|
|
||||||
LOG.exception("Failed to parse packet from aprs-is", exp)
|
|
||||||
LOG.debug("Packet processing complete")
|
LOG.debug("Packet processing complete")
|
||||||
|
|
||||||
|
|
||||||
@ -367,8 +308,7 @@ class APRSDTXThread(APRSDThread):
|
|||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
try:
|
try:
|
||||||
msg = self.msg_queues["tx"].get(timeout=5)
|
msg = self.msg_queues["tx"].get(timeout=1)
|
||||||
packets.PacketList().add(msg.dict())
|
|
||||||
msg.send()
|
msg.send()
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
pass
|
pass
|
||||||
|
@ -394,8 +394,14 @@ def human_size(bytes, units=None):
|
|||||||
return str(bytes) + units[0] if bytes < 1024 else human_size(bytes >> 10, units[1:])
|
return str(bytes) + units[0] if bytes < 1024 else human_size(bytes >> 10, units[1:])
|
||||||
|
|
||||||
|
|
||||||
def strfdelta(tdelta, fmt="{hours}:{minutes}:{seconds}"):
|
def strfdelta(tdelta, fmt="{hours:{width}}:{minutes:{width}}:{seconds:{width}}"):
|
||||||
d = {"days": tdelta.days}
|
d = {
|
||||||
|
"days": tdelta.days,
|
||||||
|
"width": "02",
|
||||||
|
}
|
||||||
|
if tdelta.days > 0:
|
||||||
|
fmt = "{days} days " + fmt
|
||||||
|
|
||||||
d["hours"], rem = divmod(tdelta.seconds, 3600)
|
d["hours"], rem = divmod(tdelta.seconds, 3600)
|
||||||
d["minutes"], d["seconds"] = divmod(rem, 60)
|
d["minutes"], d["seconds"] = divmod(rem, 60)
|
||||||
return fmt.format(**d)
|
return fmt.format(**d)
|
||||||
@ -460,6 +466,9 @@ class RingBuffer:
|
|||||||
"""return list of elements in correct order"""
|
"""return list of elements in correct order"""
|
||||||
return self.data[self.cur :] + self.data[: self.cur]
|
return self.data[self.cur :] + self.data[: self.cur]
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.data)
|
||||||
|
|
||||||
def append(self, x):
|
def append(self, x):
|
||||||
"""append an element at the end of the buffer"""
|
"""append an element at the end of the buffer"""
|
||||||
|
|
||||||
|
@ -58,6 +58,23 @@ function update_watchlist_from_packet(callsign, val) {
|
|||||||
//console.log(watchlist)
|
//console.log(watchlist)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update_plugins( data ) {
|
||||||
|
var plugindiv = $("#pluginDiv");
|
||||||
|
var html_str = '<table class="ui celled striped table"><thead><tr><th>Plugin Name</th><th>Processed Packets</th><th>Sent Packets</th></tr></thead><tbody>'
|
||||||
|
plugindiv.html('')
|
||||||
|
|
||||||
|
var plugins = data["stats"]["plugins"];
|
||||||
|
var keys = Object.keys(plugins);
|
||||||
|
keys.sort();
|
||||||
|
for (var i=0; i<keys.length; i++) { // now lets iterate in sort order
|
||||||
|
var key = keys[i];
|
||||||
|
var val = plugins[key];
|
||||||
|
html_str += '<tr><td class="collapsing">' + key + '</td><td>' + val["rx"] + '</td><td>' + val["tx"] + '</td></tr>';
|
||||||
|
}
|
||||||
|
html_str += "</tbody></table>";
|
||||||
|
plugindiv.append(html_str);
|
||||||
|
}
|
||||||
|
|
||||||
function update_packets( data ) {
|
function update_packets( data ) {
|
||||||
var packetsdiv = $("#packetsDiv");
|
var packetsdiv = $("#packetsDiv");
|
||||||
//nuke the contents first, then add to it.
|
//nuke the contents first, then add to it.
|
||||||
@ -120,6 +137,7 @@ function start_update() {
|
|||||||
success: function(data) {
|
success: function(data) {
|
||||||
update_stats(data);
|
update_stats(data);
|
||||||
update_watchlist(data);
|
update_watchlist(data);
|
||||||
|
update_plugins(data);
|
||||||
},
|
},
|
||||||
complete: function() {
|
complete: function() {
|
||||||
setTimeout(statsworker, 10000);
|
setTimeout(statsworker, 10000);
|
||||||
|
@ -71,6 +71,7 @@
|
|||||||
<div class="active item" data-tab="charts-tab">Charts</div>
|
<div class="active item" data-tab="charts-tab">Charts</div>
|
||||||
<div class="item" data-tab="msgs-tab">Messages</div>
|
<div class="item" data-tab="msgs-tab">Messages</div>
|
||||||
<div class="item" data-tab="watch-tab">Watch List</div>
|
<div class="item" data-tab="watch-tab">Watch List</div>
|
||||||
|
<div class="item" data-tab="plugin-tab">Plugins</div>
|
||||||
<div class="item" data-tab="config-tab">Config</div>
|
<div class="item" data-tab="config-tab">Config</div>
|
||||||
<div class="item" data-tab="raw-tab">Raw JSON</div>
|
<div class="item" data-tab="raw-tab">Raw JSON</div>
|
||||||
</div>
|
</div>
|
||||||
@ -129,6 +130,13 @@
|
|||||||
<div id="watchDiv" class="ui mini text">Loading</div>
|
<div id="watchDiv" class="ui mini text">Loading</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="ui bottom attached tab segment" data-tab="plugin-tab">
|
||||||
|
<h3 class="ui dividing header">
|
||||||
|
Plugins Loaded (<span id="plugin_count">{{ plugin_count }}</span>)
|
||||||
|
</h3>
|
||||||
|
<div id="pluginDiv" class="ui mini text">Loading</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="ui bottom attached tab segment" data-tab="config-tab">
|
<div class="ui bottom attached tab segment" data-tab="config-tab">
|
||||||
<h3 class="ui dividing header">Config</h3>
|
<h3 class="ui dividing header">Config</h3>
|
||||||
<pre id="configjson" class="language-json">{{ config_json|safe }}</pre>
|
<pre id="configjson" class="language-json">{{ config_json|safe }}</pre>
|
||||||
|
67
tests/fake.py
Normal file
67
tests/fake.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
from aprsd import packets, plugin, threads
|
||||||
|
|
||||||
|
|
||||||
|
FAKE_MESSAGE_TEXT = "fake MeSSage"
|
||||||
|
FAKE_FROM_CALLSIGN = "KFART"
|
||||||
|
FAKE_TO_CALLSIGN = "KMINE"
|
||||||
|
|
||||||
|
|
||||||
|
def fake_packet(
|
||||||
|
fromcall=FAKE_FROM_CALLSIGN,
|
||||||
|
tocall=FAKE_TO_CALLSIGN,
|
||||||
|
message=None,
|
||||||
|
msg_number=None,
|
||||||
|
message_format=packets.PACKET_TYPE_MESSAGE,
|
||||||
|
):
|
||||||
|
packet = {
|
||||||
|
"from": fromcall,
|
||||||
|
"addresse": tocall,
|
||||||
|
"format": message_format,
|
||||||
|
}
|
||||||
|
if message:
|
||||||
|
packet["message_text"] = message
|
||||||
|
|
||||||
|
if msg_number:
|
||||||
|
packet["msgNo"] = msg_number
|
||||||
|
|
||||||
|
return packet
|
||||||
|
|
||||||
|
|
||||||
|
class FakeBaseNoThreadsPlugin(plugin.APRSDPluginBase):
|
||||||
|
version = "1.0"
|
||||||
|
|
||||||
|
def filter(self, packet):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process(self, packet):
|
||||||
|
return "process"
|
||||||
|
|
||||||
|
|
||||||
|
class FakeThread(threads.APRSDThread):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("FakeThread")
|
||||||
|
|
||||||
|
def loop(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class FakeBaseThreadsPlugin(plugin.APRSDPluginBase):
|
||||||
|
version = "1.0"
|
||||||
|
|
||||||
|
def filter(self, packet):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process(self, packet):
|
||||||
|
return "process"
|
||||||
|
|
||||||
|
def create_threads(self):
|
||||||
|
return FakeThread()
|
||||||
|
|
||||||
|
|
||||||
|
class FakeRegexCommandPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
|
version = "1.0"
|
||||||
|
command_regex = "^[fF]"
|
||||||
|
command_name = "fake"
|
||||||
|
|
||||||
|
def process(self, packet):
|
||||||
|
return FAKE_MESSAGE_TEXT
|
@ -1,6 +1,6 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from aprsd import email
|
from aprsd.plugins import email
|
||||||
|
|
||||||
|
|
||||||
class TestEmail(unittest.TestCase):
|
class TestEmail(unittest.TestCase):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from aprsd import email
|
from aprsd.plugins import email
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info >= (3, 2):
|
if sys.version_info >= (3, 2):
|
||||||
@ -11,8 +11,8 @@ else:
|
|||||||
|
|
||||||
|
|
||||||
class TestMain(unittest.TestCase):
|
class TestMain(unittest.TestCase):
|
||||||
@mock.patch("aprsd.email._imap_connect")
|
@mock.patch("aprsd.plugins.email._imap_connect")
|
||||||
@mock.patch("aprsd.email._smtp_connect")
|
@mock.patch("aprsd.plugins.email._smtp_connect")
|
||||||
def test_validate_email(self, imap_mock, smtp_mock):
|
def test_validate_email(self, imap_mock, smtp_mock):
|
||||||
"""Test to make sure we fail."""
|
"""Test to make sure we fail."""
|
||||||
imap_mock.return_value = None
|
imap_mock.return_value = None
|
||||||
|
@ -4,7 +4,7 @@ from unittest import mock
|
|||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import messaging, stats, utils
|
from aprsd import messaging, packets, stats, utils
|
||||||
from aprsd.fuzzyclock import fuzzy
|
from aprsd.fuzzyclock import fuzzy
|
||||||
from aprsd.plugins import fortune as fortune_plugin
|
from aprsd.plugins import fortune as fortune_plugin
|
||||||
from aprsd.plugins import ping as ping_plugin
|
from aprsd.plugins import ping as ping_plugin
|
||||||
@ -12,33 +12,123 @@ from aprsd.plugins import query as query_plugin
|
|||||||
from aprsd.plugins import time as time_plugin
|
from aprsd.plugins import time as time_plugin
|
||||||
from aprsd.plugins import version as version_plugin
|
from aprsd.plugins import version as version_plugin
|
||||||
|
|
||||||
|
from . import fake
|
||||||
|
|
||||||
|
|
||||||
class TestPlugin(unittest.TestCase):
|
class TestPlugin(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.fromcall = "KFART"
|
self.fromcall = fake.FAKE_FROM_CALLSIGN
|
||||||
self.ack = 1
|
self.ack = 1
|
||||||
self.config = utils.DEFAULT_CONFIG_DICT
|
self.config = utils.DEFAULT_CONFIG_DICT
|
||||||
self.config["ham"]["callsign"] = self.fromcall
|
self.config["ham"]["callsign"] = self.fromcall
|
||||||
|
self.config["aprs"]["login"] = fake.FAKE_TO_CALLSIGN
|
||||||
# Inintialize the stats object with the config
|
# Inintialize the stats object with the config
|
||||||
stats.APRSDStats(self.config)
|
stats.APRSDStats(self.config)
|
||||||
|
|
||||||
def fake_packet(self, fromcall="KFART", message=None, msg_number=None):
|
@mock.patch.object(fake.FakeBaseNoThreadsPlugin, "process")
|
||||||
packet = {"from": fromcall}
|
def test_base_plugin_no_threads(self, mock_process):
|
||||||
if message:
|
p = fake.FakeBaseNoThreadsPlugin(self.config)
|
||||||
packet["message_text"] = message
|
|
||||||
|
|
||||||
if msg_number:
|
expected = []
|
||||||
packet["msgNo"] = msg_number
|
actual = p.create_threads()
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
return packet
|
expected = "1.0"
|
||||||
|
actual = p.version
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
expected = 0
|
||||||
|
actual = p.message_counter
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
expected = None
|
||||||
|
actual = p.filter(fake.fake_packet())
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
mock_process.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch.object(fake.FakeBaseThreadsPlugin, "create_threads")
|
||||||
|
def test_base_plugin_threads_created(self, mock_create):
|
||||||
|
fake.FakeBaseThreadsPlugin(self.config)
|
||||||
|
mock_create.assert_called_once()
|
||||||
|
|
||||||
|
def test_base_plugin_threads(self):
|
||||||
|
p = fake.FakeBaseThreadsPlugin(self.config)
|
||||||
|
actual = p.create_threads()
|
||||||
|
self.assertTrue(isinstance(actual, fake.FakeThread))
|
||||||
|
p.stop_threads()
|
||||||
|
|
||||||
|
@mock.patch.object(fake.FakeRegexCommandPlugin, "process")
|
||||||
|
def test_regex_base_not_called(self, mock_process):
|
||||||
|
p = fake.FakeRegexCommandPlugin(self.config)
|
||||||
|
packet = fake.fake_packet(message="a")
|
||||||
|
expected = None
|
||||||
|
actual = p.filter(packet)
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
mock_process.assert_not_called()
|
||||||
|
|
||||||
|
packet = fake.fake_packet(tocall="notMe", message="f")
|
||||||
|
expected = None
|
||||||
|
actual = p.filter(packet)
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
mock_process.assert_not_called()
|
||||||
|
|
||||||
|
packet = fake.fake_packet(
|
||||||
|
message="F",
|
||||||
|
message_format=packets.PACKET_TYPE_MICE,
|
||||||
|
)
|
||||||
|
expected = None
|
||||||
|
actual = p.filter(packet)
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
mock_process.assert_not_called()
|
||||||
|
|
||||||
|
packet = fake.fake_packet(
|
||||||
|
message="f",
|
||||||
|
message_format=packets.PACKET_TYPE_ACK,
|
||||||
|
)
|
||||||
|
expected = None
|
||||||
|
actual = p.filter(packet)
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
mock_process.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch.object(fake.FakeRegexCommandPlugin, "process")
|
||||||
|
def test_regex_base_assert_called(self, mock_process):
|
||||||
|
p = fake.FakeRegexCommandPlugin(self.config)
|
||||||
|
packet = fake.fake_packet(message="f")
|
||||||
|
p.filter(packet)
|
||||||
|
mock_process.assert_called_once()
|
||||||
|
|
||||||
|
def test_regex_base_process_called(self):
|
||||||
|
p = fake.FakeRegexCommandPlugin(self.config)
|
||||||
|
|
||||||
|
packet = fake.fake_packet(message="f")
|
||||||
|
expected = fake.FAKE_MESSAGE_TEXT
|
||||||
|
actual = p.filter(packet)
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
packet = fake.fake_packet(message="F")
|
||||||
|
expected = fake.FAKE_MESSAGE_TEXT
|
||||||
|
actual = p.filter(packet)
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
packet = fake.fake_packet(message="fake")
|
||||||
|
expected = fake.FAKE_MESSAGE_TEXT
|
||||||
|
actual = p.filter(packet)
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
packet = fake.fake_packet(message="FAKE")
|
||||||
|
expected = fake.FAKE_MESSAGE_TEXT
|
||||||
|
actual = p.filter(packet)
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFortunePlugin(TestPlugin):
|
||||||
@mock.patch("shutil.which")
|
@mock.patch("shutil.which")
|
||||||
def test_fortune_fail(self, mock_which):
|
def test_fortune_fail(self, mock_which):
|
||||||
fortune = fortune_plugin.FortunePlugin(self.config)
|
fortune = fortune_plugin.FortunePlugin(self.config)
|
||||||
mock_which.return_value = None
|
mock_which.return_value = None
|
||||||
expected = "Fortune command not installed"
|
expected = "Fortune command not installed"
|
||||||
packet = self.fake_packet(message="fortune")
|
packet = fake.fake_packet(message="fortune")
|
||||||
actual = fortune.run(packet)
|
actual = fortune.filter(packet)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
@mock.patch("subprocess.check_output")
|
@mock.patch("subprocess.check_output")
|
||||||
@ -50,17 +140,19 @@ class TestPlugin(unittest.TestCase):
|
|||||||
mock_output.return_value = "Funny fortune"
|
mock_output.return_value = "Funny fortune"
|
||||||
|
|
||||||
expected = "Funny fortune"
|
expected = "Funny fortune"
|
||||||
packet = self.fake_packet(message="fortune")
|
packet = fake.fake_packet(message="fortune")
|
||||||
actual = fortune.run(packet)
|
actual = fortune.filter(packet)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
|
||||||
|
class TestQueryPlugin(TestPlugin):
|
||||||
@mock.patch("aprsd.messaging.MsgTrack.flush")
|
@mock.patch("aprsd.messaging.MsgTrack.flush")
|
||||||
def test_query_flush(self, mock_flush):
|
def test_query_flush(self, mock_flush):
|
||||||
packet = self.fake_packet(message="!delete")
|
packet = fake.fake_packet(message="!delete")
|
||||||
query = query_plugin.QueryPlugin(self.config)
|
query = query_plugin.QueryPlugin(self.config)
|
||||||
|
|
||||||
expected = "Deleted ALL pending msgs."
|
expected = "Deleted ALL pending msgs."
|
||||||
actual = query.run(packet)
|
actual = query.filter(packet)
|
||||||
mock_flush.assert_called_once()
|
mock_flush.assert_called_once()
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
@ -68,11 +160,11 @@ class TestPlugin(unittest.TestCase):
|
|||||||
def test_query_restart_delayed(self, mock_restart):
|
def test_query_restart_delayed(self, mock_restart):
|
||||||
track = messaging.MsgTrack()
|
track = messaging.MsgTrack()
|
||||||
track.track = {}
|
track.track = {}
|
||||||
packet = self.fake_packet(message="!4")
|
packet = fake.fake_packet(message="!4")
|
||||||
query = query_plugin.QueryPlugin(self.config)
|
query = query_plugin.QueryPlugin(self.config)
|
||||||
|
|
||||||
expected = "No pending msgs to resend"
|
expected = "No pending msgs to resend"
|
||||||
actual = query.run(packet)
|
actual = query.filter(packet)
|
||||||
mock_restart.assert_not_called()
|
mock_restart.assert_not_called()
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
mock_restart.reset_mock()
|
mock_restart.reset_mock()
|
||||||
@ -80,9 +172,11 @@ class TestPlugin(unittest.TestCase):
|
|||||||
# add a message
|
# add a message
|
||||||
msg = messaging.TextMessage(self.fromcall, "testing", self.ack)
|
msg = messaging.TextMessage(self.fromcall, "testing", self.ack)
|
||||||
track.add(msg)
|
track.add(msg)
|
||||||
actual = query.run(packet)
|
actual = query.filter(packet)
|
||||||
mock_restart.assert_called_once()
|
mock_restart.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
class TestTimePlugins(TestPlugin):
|
||||||
@mock.patch("aprsd.plugins.time.TimePlugin._get_local_tz")
|
@mock.patch("aprsd.plugins.time.TimePlugin._get_local_tz")
|
||||||
@mock.patch("aprsd.plugins.time.TimePlugin._get_utcnow")
|
@mock.patch("aprsd.plugins.time.TimePlugin._get_utcnow")
|
||||||
def test_time(self, mock_utcnow, mock_localtz):
|
def test_time(self, mock_utcnow, mock_localtz):
|
||||||
@ -100,19 +194,17 @@ class TestPlugin(unittest.TestCase):
|
|||||||
fake_time.tm_sec = 13
|
fake_time.tm_sec = 13
|
||||||
time = time_plugin.TimePlugin(self.config)
|
time = time_plugin.TimePlugin(self.config)
|
||||||
|
|
||||||
packet = self.fake_packet(
|
packet = fake.fake_packet(
|
||||||
fromcall="KFART",
|
|
||||||
message="location",
|
message="location",
|
||||||
msg_number=1,
|
msg_number=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
actual = time.run(packet)
|
actual = time.filter(packet)
|
||||||
self.assertEqual(None, actual)
|
self.assertEqual(None, actual)
|
||||||
|
|
||||||
cur_time = fuzzy(h, m, 1)
|
cur_time = fuzzy(h, m, 1)
|
||||||
|
|
||||||
packet = self.fake_packet(
|
packet = fake.fake_packet(
|
||||||
fromcall="KFART",
|
|
||||||
message="time",
|
message="time",
|
||||||
msg_number=1,
|
msg_number=1,
|
||||||
)
|
)
|
||||||
@ -121,9 +213,11 @@ class TestPlugin(unittest.TestCase):
|
|||||||
cur_time,
|
cur_time,
|
||||||
local_short_str,
|
local_short_str,
|
||||||
)
|
)
|
||||||
actual = time.run(packet)
|
actual = time.filter(packet)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPingPlugin(TestPlugin):
|
||||||
@mock.patch("time.localtime")
|
@mock.patch("time.localtime")
|
||||||
def test_ping(self, mock_time):
|
def test_ping(self, mock_time):
|
||||||
fake_time = mock.MagicMock()
|
fake_time = mock.MagicMock()
|
||||||
@ -134,13 +228,12 @@ class TestPlugin(unittest.TestCase):
|
|||||||
|
|
||||||
ping = ping_plugin.PingPlugin(self.config)
|
ping = ping_plugin.PingPlugin(self.config)
|
||||||
|
|
||||||
packet = self.fake_packet(
|
packet = fake.fake_packet(
|
||||||
fromcall="KFART",
|
|
||||||
message="location",
|
message="location",
|
||||||
msg_number=1,
|
msg_number=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
result = ping.run(packet)
|
result = ping.filter(packet)
|
||||||
self.assertEqual(None, result)
|
self.assertEqual(None, result)
|
||||||
|
|
||||||
def ping_str(h, m, s):
|
def ping_str(h, m, s):
|
||||||
@ -153,49 +246,46 @@ class TestPlugin(unittest.TestCase):
|
|||||||
+ str(s).zfill(2)
|
+ str(s).zfill(2)
|
||||||
)
|
)
|
||||||
|
|
||||||
packet = self.fake_packet(
|
packet = fake.fake_packet(
|
||||||
fromcall="KFART",
|
|
||||||
message="Ping",
|
message="Ping",
|
||||||
msg_number=1,
|
msg_number=1,
|
||||||
)
|
)
|
||||||
actual = ping.run(packet)
|
actual = ping.filter(packet)
|
||||||
expected = ping_str(h, m, s)
|
expected = ping_str(h, m, s)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
packet = self.fake_packet(
|
packet = fake.fake_packet(
|
||||||
fromcall="KFART",
|
|
||||||
message="ping",
|
message="ping",
|
||||||
msg_number=1,
|
msg_number=1,
|
||||||
)
|
)
|
||||||
actual = ping.run(packet)
|
actual = ping.filter(packet)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
@mock.patch("aprsd.plugin.PluginManager.get_msg_plugins")
|
|
||||||
|
class TestVersionPlugin(TestPlugin):
|
||||||
|
@mock.patch("aprsd.plugin.PluginManager.get_plugins")
|
||||||
def test_version(self, mock_get_plugins):
|
def test_version(self, mock_get_plugins):
|
||||||
expected = f"APRSD ver:{aprsd.__version__} uptime:0:0:0"
|
expected = f"APRSD ver:{aprsd.__version__} uptime:00:00:00"
|
||||||
version = version_plugin.VersionPlugin(self.config)
|
version = version_plugin.VersionPlugin(self.config)
|
||||||
|
|
||||||
packet = self.fake_packet(
|
packet = fake.fake_packet(
|
||||||
fromcall="KFART",
|
|
||||||
message="No",
|
message="No",
|
||||||
msg_number=1,
|
msg_number=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
actual = version.run(packet)
|
actual = version.filter(packet)
|
||||||
self.assertEqual(None, actual)
|
self.assertEqual(None, actual)
|
||||||
|
|
||||||
packet = self.fake_packet(
|
packet = fake.fake_packet(
|
||||||
fromcall="KFART",
|
|
||||||
message="version",
|
message="version",
|
||||||
msg_number=1,
|
msg_number=1,
|
||||||
)
|
)
|
||||||
actual = version.run(packet)
|
actual = version.filter(packet)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
packet = self.fake_packet(
|
packet = fake.fake_packet(
|
||||||
fromcall="KFART",
|
|
||||||
message="Version",
|
message="Version",
|
||||||
msg_number=1,
|
msg_number=1,
|
||||||
)
|
)
|
||||||
actual = version.run(packet)
|
actual = version.filter(packet)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
2
tox.ini
2
tox.ini
@ -2,7 +2,7 @@
|
|||||||
minversion = 2.9.0
|
minversion = 2.9.0
|
||||||
skipdist = True
|
skipdist = True
|
||||||
skip_missing_interpreters = true
|
skip_missing_interpreters = true
|
||||||
envlist = pre-commit,pep8,fmt-check,py{36,37,38}
|
envlist = pre-commit,pep8,py{36,37,38}
|
||||||
|
|
||||||
# Activate isolated build environment. tox will use a virtual environment
|
# Activate isolated build environment. tox will use a virtual environment
|
||||||
# to build a source distribution from the source tree. For build tools and
|
# to build a source distribution from the source tree. For build tools and
|
||||||
|
Loading…
x
Reference in New Issue
Block a user