mirror of
https://github.com/craigerl/aprsd.git
synced 2025-03-10 22:38:33 -04:00
commit
923e1a7c3d
2
.github/workflows/python.yml
vendored
2
.github/workflows/python.yml
vendored
@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [2.7, 3.6, 3.7, 3.8, 3.9]
|
||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
|
63
aprsd/client.py
Normal file
63
aprsd/client.py
Normal file
@ -0,0 +1,63 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
import aprslib
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""Singleton client class that constructs the aprslib connection."""
|
||||
|
||||
_instance = None
|
||||
aprs_client = None
|
||||
config = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""This magic turns this into a singleton."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super(Client, cls).__new__(cls)
|
||||
# Put any initialization here.
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, config=None):
|
||||
"""Initialize the object instance."""
|
||||
if config:
|
||||
self.config = config
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
if not self.aprs_client:
|
||||
self.aprs_client = self.setup_connection()
|
||||
return self.aprs_client
|
||||
|
||||
def reset(self):
|
||||
"""Call this to force a rebuild/reconnect."""
|
||||
del self.aprs_client
|
||||
|
||||
def setup_connection(self):
|
||||
user = self.config["aprs"]["login"]
|
||||
password = self.config["aprs"]["password"]
|
||||
host = self.config["aprs"].get("host", "rotate.aprs.net")
|
||||
port = self.config["aprs"].get("port", 14580)
|
||||
connected = False
|
||||
while not connected:
|
||||
try:
|
||||
LOG.info("Creating aprslib client")
|
||||
aprs_client = aprslib.IS(user, passwd=password, host=host, port=port)
|
||||
# Force the logging to be the same
|
||||
aprs_client.logger = LOG
|
||||
aprs_client.connect()
|
||||
connected = True
|
||||
except Exception as e:
|
||||
LOG.error("Unable to connect to APRS-IS server.\n")
|
||||
print(str(e))
|
||||
time.sleep(5)
|
||||
continue
|
||||
LOG.debug("Logging in to APRS-IS with user '%s'" % user)
|
||||
return aprs_client
|
||||
|
||||
|
||||
def get_client():
|
||||
cl = Client()
|
||||
return cl.client
|
399
aprsd/email.py
Normal file
399
aprsd/email.py
Normal file
@ -0,0 +1,399 @@
|
||||
import datetime
|
||||
import email
|
||||
import imaplib
|
||||
import logging
|
||||
import re
|
||||
import smtplib
|
||||
import threading
|
||||
import time
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
import imapclient
|
||||
import six
|
||||
from validate_email import validate_email
|
||||
|
||||
from aprsd import messaging
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
# This gets forced set from main.py prior to being used internally
|
||||
CONFIG = None
|
||||
|
||||
|
||||
def start_thread():
|
||||
checkemailthread = threading.Thread(
|
||||
target=check_email_thread, name="check_email", args=()
|
||||
) # args must be tuple
|
||||
checkemailthread.start()
|
||||
|
||||
|
||||
def _imap_connect():
|
||||
imap_port = CONFIG["imap"].get("port", 143)
|
||||
use_ssl = CONFIG["imap"].get("use_ssl", False)
|
||||
host = CONFIG["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["imap"]["host"], port=imap_port, use_uid=True, ssl=use_ssl
|
||||
)
|
||||
except Exception:
|
||||
LOG.error("Failed to connect IMAP server")
|
||||
return
|
||||
|
||||
try:
|
||||
server.login(CONFIG["imap"]["login"], CONFIG["imap"]["password"])
|
||||
except (imaplib.IMAP4.error, Exception) as e:
|
||||
msg = getattr(e, "message", repr(e))
|
||||
LOG.error("Failed to login {}".format(msg))
|
||||
return
|
||||
|
||||
server.select_folder("INBOX")
|
||||
return server
|
||||
|
||||
|
||||
def _smtp_connect():
|
||||
host = CONFIG["smtp"]["host"]
|
||||
smtp_port = CONFIG["smtp"]["port"]
|
||||
use_ssl = CONFIG["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["imap"]["login"])
|
||||
)
|
||||
|
||||
try:
|
||||
if use_ssl:
|
||||
server = smtplib.SMTP_SSL(host=host, port=smtp_port)
|
||||
else:
|
||||
server = smtplib.SMTP(host=host, port=smtp_port)
|
||||
except Exception:
|
||||
LOG.error("Couldn't connect to SMTP Server")
|
||||
return
|
||||
|
||||
LOG.debug("Connected to smtp host {}".format(msg))
|
||||
|
||||
try:
|
||||
server.login(CONFIG["smtp"]["login"], CONFIG["smtp"]["password"])
|
||||
except Exception:
|
||||
LOG.error("Couldn't connect to SMTP Server")
|
||||
return
|
||||
|
||||
LOG.debug("Logged into SMTP server {}".format(msg))
|
||||
return server
|
||||
|
||||
|
||||
def validate_shortcuts(config):
|
||||
shortcuts = config.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:
|
||||
is_valid = validate_email(
|
||||
email_address=shortcuts[key],
|
||||
check_regex=True,
|
||||
check_mx=True,
|
||||
from_address=config["smtp"]["login"],
|
||||
helo_host=config["smtp"]["host"],
|
||||
smtp_timeout=10,
|
||||
dns_timeout=10,
|
||||
use_blacklist=False,
|
||||
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["shortcuts"][key]
|
||||
|
||||
LOG.info("Available shortcuts: {}".format(config["shortcuts"]))
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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("Got a message from '{}'".format(from_addr))
|
||||
m = server.fetch([msgid], ["RFC822"])
|
||||
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 = six.text_type(
|
||||
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 = six.text_type(
|
||||
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 = six.text_type(
|
||||
msg.get_payload(decode=True), "US-ASCII", "ignore"
|
||||
).encode("utf8", "replace")
|
||||
else:
|
||||
text = six.text_type(
|
||||
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
|
||||
|
||||
|
||||
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 = "%s-%s-%s" % (day, month, year)
|
||||
|
||||
shortcuts = CONFIG["shortcuts"]
|
||||
# swap key/value
|
||||
shortcuts_inverted = dict([[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
|
||||
|
||||
messages = server.search(["SINCE", today])
|
||||
# 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:
|
||||
for msgid, data in list(server.fetch(message, ["ENVELOPE"]).items()):
|
||||
# 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
|
||||
server.remove_flags(msgid, [imapclient.SEEN])
|
||||
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)
|
||||
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 %s:%s:%s" % (
|
||||
str(h).zfill(2),
|
||||
str(m).zfill(2),
|
||||
str(s).zfill(2),
|
||||
)
|
||||
messaging.send_message(fromcall, reply)
|
||||
|
||||
# check email more often since we're resending one now
|
||||
check_email_delay = 60
|
||||
|
||||
server.logout()
|
||||
# end resend_email()
|
||||
|
||||
|
||||
def check_email_thread():
|
||||
global check_email_delay
|
||||
|
||||
# LOG.debug("FIXME initial email delay is 10 seconds")
|
||||
check_email_delay = 60
|
||||
while True:
|
||||
# LOG.debug("Top of check_email_thread.")
|
||||
|
||||
time.sleep(check_email_delay)
|
||||
|
||||
# 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["shortcuts"]
|
||||
# swap key/value
|
||||
shortcuts_inverted = dict([[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 = "%s-%s-%s" % (day, month, year)
|
||||
|
||||
server = None
|
||||
try:
|
||||
server = _imap_connect()
|
||||
except Exception as e:
|
||||
LOG.exception("Failed to get IMAP server Can't check email.", e)
|
||||
|
||||
if not server:
|
||||
continue
|
||||
|
||||
messages = server.search(["SINCE", today])
|
||||
# LOG.debug("{} messages received today".format(len(messages)))
|
||||
|
||||
for msgid, data in server.fetch(messages, ["ENVELOPE"]).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
|
||||
taglist = [
|
||||
x.decode(errors="ignore") for x in server.get_flags(msgid)[msgid]
|
||||
]
|
||||
if "APRS" not in taglist:
|
||||
# if msg not flagged as sent via aprs
|
||||
server.fetch([msgid], ["RFC822"])
|
||||
(body, from_addr) = parse_email(msgid, data, server)
|
||||
# unset seen flag, will stay bold in email client
|
||||
server.remove_flags(msgid, [imapclient.SEEN])
|
||||
|
||||
if from_addr in shortcuts_inverted:
|
||||
# reverse lookup of a shortcut
|
||||
from_addr = shortcuts_inverted[from_addr]
|
||||
|
||||
reply = "-" + from_addr + " " + body.decode(errors="ignore")
|
||||
messaging.send_message(CONFIG["ham"]["callsign"], reply)
|
||||
# flag message as sent via aprs
|
||||
server.add_flags(msgid, ["APRS"])
|
||||
# unset seen flag, will stay bold in email client
|
||||
server.remove_flags(msgid, [imapclient.SEEN])
|
||||
# check email more often since we just received an email
|
||||
check_email_delay = 60
|
||||
|
||||
server.logout()
|
||||
|
||||
|
||||
# end check_email()
|
||||
|
||||
|
||||
def send_email(to_addr, content):
|
||||
global check_email_delay
|
||||
|
||||
LOG.info("Sending Email_________________")
|
||||
shortcuts = CONFIG["shortcuts"]
|
||||
if to_addr in shortcuts:
|
||||
LOG.info("To : " + to_addr)
|
||||
to_addr = shortcuts[to_addr]
|
||||
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["smtp"]["login"]
|
||||
msg["To"] = to_addr
|
||||
server = _smtp_connect()
|
||||
if server:
|
||||
try:
|
||||
server.sendmail(CONFIG["smtp"]["login"], [to_addr], msg.as_string())
|
||||
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
|
945
aprsd/main.py
945
aprsd/main.py
File diff suppressed because it is too large
Load Diff
247
aprsd/messaging.py
Normal file
247
aprsd/messaging.py
Normal file
@ -0,0 +1,247 @@
|
||||
import logging
|
||||
import pprint
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
|
||||
from aprsd import client
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
CONFIG = None
|
||||
|
||||
# current aprs radio message number, increments for each message we
|
||||
# send over rf {int}
|
||||
message_number = 0
|
||||
|
||||
# message_nubmer:ack combos so we stop sending a message after an
|
||||
# ack from radio {int:int}
|
||||
ack_dict = {}
|
||||
|
||||
# What to return from a plugin if we have processed the message
|
||||
# and it's ok, but don't send a usage string back
|
||||
NULL_MESSAGE = -1
|
||||
|
||||
|
||||
def send_ack_thread(tocall, ack, retry_count):
|
||||
cl = client.get_client()
|
||||
tocall = tocall.ljust(9) # pad to nine chars
|
||||
line = "{}>APRS::{}:ack{}\n".format(CONFIG["aprs"]["login"], tocall, ack)
|
||||
for i in range(retry_count, 0, -1):
|
||||
log_message(
|
||||
"Sending ack",
|
||||
line.rstrip("\n"),
|
||||
None,
|
||||
ack=ack,
|
||||
tocall=tocall,
|
||||
retry_number=i,
|
||||
)
|
||||
cl.sendall(line)
|
||||
# aprs duplicate detection is 30 secs?
|
||||
# (21 only sends first, 28 skips middle)
|
||||
time.sleep(31)
|
||||
# end_send_ack_thread
|
||||
|
||||
|
||||
def send_ack(tocall, ack):
|
||||
LOG.debug("Send ACK({}:{}) to radio.".format(tocall, ack))
|
||||
retry_count = 3
|
||||
thread = threading.Thread(
|
||||
target=send_ack_thread, name="send_ack", args=(tocall, ack, retry_count)
|
||||
)
|
||||
thread.start()
|
||||
# end send_ack()
|
||||
|
||||
|
||||
def send_message_thread(tocall, message, this_message_number, retry_count):
|
||||
cl = client.get_client()
|
||||
line = "{}>APRS::{}:{}{{{}\n".format(
|
||||
CONFIG["aprs"]["login"],
|
||||
tocall,
|
||||
message,
|
||||
str(this_message_number),
|
||||
)
|
||||
for i in range(retry_count, 0, -1):
|
||||
LOG.debug("DEBUG: send_message_thread msg:ack combos are: ")
|
||||
LOG.debug(pprint.pformat(ack_dict))
|
||||
if ack_dict[this_message_number] != 1:
|
||||
log_message(
|
||||
"Sending Message",
|
||||
line.rstrip("\n"),
|
||||
message,
|
||||
tocall=tocall,
|
||||
retry_number=i,
|
||||
)
|
||||
# tn.write(line)
|
||||
cl.sendall(line)
|
||||
# decaying repeats, 31 to 93 second intervals
|
||||
sleeptime = (retry_count - i + 1) * 31
|
||||
time.sleep(sleeptime)
|
||||
else:
|
||||
break
|
||||
return
|
||||
# end send_message_thread
|
||||
|
||||
|
||||
def send_message(tocall, message):
|
||||
global message_number
|
||||
global ack_dict
|
||||
|
||||
retry_count = 3
|
||||
if message_number > 98: # global
|
||||
message_number = 0
|
||||
message_number += 1
|
||||
if len(ack_dict) > 90:
|
||||
# empty ack dict if it's really big, could result in key error later
|
||||
LOG.debug(
|
||||
"DEBUG: Length of ack dictionary is big at %s clearing." % len(ack_dict)
|
||||
)
|
||||
ack_dict.clear()
|
||||
LOG.debug(pprint.pformat(ack_dict))
|
||||
LOG.debug(
|
||||
"DEBUG: Cleared ack dictionary, ack_dict length is now %s." % len(ack_dict)
|
||||
)
|
||||
ack_dict[message_number] = 0 # clear ack for this message number
|
||||
tocall = tocall.ljust(9) # pad to nine chars
|
||||
|
||||
# max? ftm400 displays 64, raw msg shows 74
|
||||
# and ftm400-send is max 64. setting this to
|
||||
# 67 displays 64 on the ftm400. (+3 {01 suffix)
|
||||
# feature req: break long ones into two msgs
|
||||
message = message[:67]
|
||||
# We all miss George Carlin
|
||||
message = re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message)
|
||||
thread = threading.Thread(
|
||||
target=send_message_thread,
|
||||
name="send_message",
|
||||
args=(tocall, message, message_number, retry_count),
|
||||
)
|
||||
thread.start()
|
||||
return ()
|
||||
# end send_message()
|
||||
|
||||
|
||||
def log_packet(packet):
|
||||
fromcall = packet.get("from", None)
|
||||
tocall = packet.get("to", None)
|
||||
|
||||
response_type = packet.get("response", None)
|
||||
msg = packet.get("message_text", None)
|
||||
msg_num = packet.get("msgNo", None)
|
||||
ack = packet.get("ack", None)
|
||||
|
||||
log_message(
|
||||
"Packet",
|
||||
packet["raw"],
|
||||
msg,
|
||||
fromcall=fromcall,
|
||||
tocall=tocall,
|
||||
ack=ack,
|
||||
packet_type=response_type,
|
||||
msg_num=msg_num,
|
||||
)
|
||||
|
||||
|
||||
def log_message(
|
||||
header,
|
||||
raw,
|
||||
message,
|
||||
tocall=None,
|
||||
fromcall=None,
|
||||
msg_num=None,
|
||||
retry_number=None,
|
||||
ack=None,
|
||||
packet_type=None,
|
||||
):
|
||||
"""
|
||||
|
||||
Log a message entry.
|
||||
|
||||
This builds a long string with newlines for the log entry, so that
|
||||
it's thread safe. If we log each item as a separate log.debug() call
|
||||
Then the message information could get multiplexed with other log
|
||||
messages. Each python log call is automatically synchronized.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
log_list = [""]
|
||||
if retry_number:
|
||||
# LOG.info(" {} _______________(TX:{})".format(header, retry_number))
|
||||
log_list.append(" {} _______________(TX:{})".format(header, retry_number))
|
||||
else:
|
||||
# LOG.info(" {} _______________".format(header))
|
||||
log_list.append(" {} _______________".format(header))
|
||||
|
||||
# LOG.info(" Raw : {}".format(raw))
|
||||
log_list.append(" Raw : {}".format(raw))
|
||||
|
||||
if packet_type:
|
||||
# LOG.info(" Packet : {}".format(packet_type))
|
||||
log_list.append(" Packet : {}".format(packet_type))
|
||||
if tocall:
|
||||
# LOG.info(" To : {}".format(tocall))
|
||||
log_list.append(" To : {}".format(tocall))
|
||||
if fromcall:
|
||||
# LOG.info(" From : {}".format(fromcall))
|
||||
log_list.append(" From : {}".format(fromcall))
|
||||
|
||||
if ack:
|
||||
# LOG.info(" Ack : {}".format(ack))
|
||||
log_list.append(" Ack : {}".format(ack))
|
||||
else:
|
||||
# LOG.info(" Message : {}".format(message))
|
||||
log_list.append(" Message : {}".format(message))
|
||||
if msg_num:
|
||||
# LOG.info(" Msg number : {}".format(msg_num))
|
||||
log_list.append(" Msg number : {}".format(msg_num))
|
||||
# LOG.info(" {} _______________ Complete".format(header))
|
||||
log_list.append(" {} _______________ Complete".format(header))
|
||||
|
||||
LOG.info("\n".join(log_list))
|
||||
|
||||
|
||||
def send_message_direct(tocall, message):
|
||||
"""Send a message without a separate thread."""
|
||||
cl = client.get_client()
|
||||
this_message_number = 1
|
||||
fromcall = CONFIG["aprs"]["login"]
|
||||
line = "{}>APRS::{}:{}{{{}\n".format(
|
||||
fromcall,
|
||||
tocall,
|
||||
message,
|
||||
str(this_message_number),
|
||||
)
|
||||
LOG.debug("DEBUG: send_message_thread msg:ack combos are: ")
|
||||
log_message(
|
||||
"Sending Message", line.rstrip("\n"), message, tocall=tocall, fromcall=fromcall
|
||||
)
|
||||
cl.sendall(line)
|
||||
|
||||
|
||||
def process_message(line):
|
||||
f = re.search("^(.*)>", line)
|
||||
fromcall = f.group(1)
|
||||
searchstring = "::%s[ ]*:(.*)" % CONFIG["aprs"]["login"]
|
||||
# verify this, callsign is padded out with spaces to colon
|
||||
m = re.search(searchstring, line)
|
||||
fullmessage = m.group(1)
|
||||
|
||||
ack_attached = re.search("(.*){([0-9A-Z]+)", fullmessage)
|
||||
# ack formats include: {1, {AB}, {12
|
||||
if ack_attached:
|
||||
# "{##" suffix means radio wants an ack back
|
||||
# message content
|
||||
message = ack_attached.group(1)
|
||||
# suffix number to use in ack
|
||||
ack_num = ack_attached.group(2)
|
||||
else:
|
||||
message = fullmessage
|
||||
# ack not requested, but lets send one as 0
|
||||
ack_num = "0"
|
||||
|
||||
log_message(
|
||||
"Received message", line, message, fromcall=fromcall, msg_num=str(ack_num)
|
||||
)
|
||||
|
||||
return (fromcall, message, ack_num)
|
||||
# end process_message()
|
298
aprsd/plugin.py
298
aprsd/plugin.py
@ -1,6 +1,7 @@
|
||||
# The base plugin class
|
||||
import abc
|
||||
import fnmatch
|
||||
import importlib
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
@ -14,6 +15,7 @@ import requests
|
||||
import six
|
||||
from thesmuggler import smuggle
|
||||
|
||||
from aprsd import email, messaging
|
||||
from aprsd.fuzzyclock import fuzzy
|
||||
|
||||
# setup the global logger
|
||||
@ -23,81 +25,42 @@ hookspec = pluggy.HookspecMarker("aprsd")
|
||||
hookimpl = pluggy.HookimplMarker("aprsd")
|
||||
|
||||
CORE_PLUGINS = [
|
||||
"FortunePlugin",
|
||||
"LocationPlugin",
|
||||
"PingPlugin",
|
||||
"TimePlugin",
|
||||
"WeatherPlugin",
|
||||
"aprsd.plugin.EmailPlugin",
|
||||
"aprsd.plugin.FortunePlugin",
|
||||
"aprsd.plugin.LocationPlugin",
|
||||
"aprsd.plugin.PingPlugin",
|
||||
"aprsd.plugin.TimePlugin",
|
||||
"aprsd.plugin.WeatherPlugin",
|
||||
]
|
||||
|
||||
|
||||
def setup_plugins(config):
|
||||
"""Create the plugin manager and register plugins."""
|
||||
|
||||
LOG.info("Loading Core APRSD Command Plugins")
|
||||
enabled_plugins = config["aprsd"].get("enabled_plugins", None)
|
||||
pm = pluggy.PluginManager("aprsd")
|
||||
pm.add_hookspecs(APRSDCommandSpec)
|
||||
for p_name in CORE_PLUGINS:
|
||||
plugin_obj = None
|
||||
if enabled_plugins:
|
||||
if p_name in enabled_plugins:
|
||||
plugin_obj = globals()[p_name](config)
|
||||
else:
|
||||
# Enabled plugins isn't set, so we default to loading all of
|
||||
# the core plugins.
|
||||
plugin_obj = globals()[p_name](config)
|
||||
|
||||
if plugin_obj:
|
||||
LOG.info(
|
||||
"Registering Command plugin '{}'({}) '{}'".format(
|
||||
p_name, plugin_obj.version, plugin_obj.command_regex
|
||||
)
|
||||
)
|
||||
pm.register(plugin_obj)
|
||||
|
||||
plugin_dir = config["aprsd"].get("plugin_dir", None)
|
||||
if plugin_dir:
|
||||
LOG.info("Trying to load custom plugins from '{}'".format(plugin_dir))
|
||||
cpm = PluginManager(config)
|
||||
plugins_list = cpm.load_plugins(plugin_dir)
|
||||
LOG.info("Discovered {} modules to load".format(len(plugins_list)))
|
||||
for o in plugins_list:
|
||||
plugin_obj = None
|
||||
if enabled_plugins:
|
||||
if o["name"] in enabled_plugins:
|
||||
plugin_obj = o["obj"]
|
||||
else:
|
||||
LOG.info(
|
||||
"'{}' plugin not listed in config aprsd:enabled_plugins".format(
|
||||
o["name"]
|
||||
)
|
||||
)
|
||||
else:
|
||||
# not setting enabled plugins means load all?
|
||||
plugin_obj = o["obj"]
|
||||
|
||||
if plugin_obj:
|
||||
LOG.info(
|
||||
"Registering Command plugin '{}'({}) '{}'".format(
|
||||
o["name"], o["obj"].version, o["obj"].command_regex
|
||||
)
|
||||
)
|
||||
pm.register(o["obj"])
|
||||
|
||||
else:
|
||||
LOG.info("Skipping Custom Plugins.")
|
||||
|
||||
LOG.info("Completed Plugin Loading.")
|
||||
return pm
|
||||
|
||||
|
||||
class PluginManager(object):
|
||||
def __init__(self, config):
|
||||
self.obj_list = []
|
||||
self.config = config
|
||||
# The singleton instance object for this class
|
||||
_instance = None
|
||||
|
||||
# the pluggy PluginManager
|
||||
_pluggy_pm = None
|
||||
|
||||
# aprsd config dict
|
||||
config = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""This magic turns this into a singleton."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super(PluginManager, cls).__new__(cls)
|
||||
# Put any initialization here.
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, config=None):
|
||||
self.obj_list = []
|
||||
if config:
|
||||
self.config = config
|
||||
|
||||
def load_plugins_from_path(self, module_path):
|
||||
if not os.path.exists(module_path):
|
||||
LOG.error("plugin path '{}' doesn't exist.".format(module_path))
|
||||
return None
|
||||
|
||||
def load_plugins(self, module_path):
|
||||
dir_path = os.path.realpath(module_path)
|
||||
pattern = "*.py"
|
||||
|
||||
@ -123,6 +86,108 @@ class PluginManager(object):
|
||||
|
||||
return False
|
||||
|
||||
def _create_class(self, module_class_string, super_cls: type = None, **kwargs):
|
||||
"""
|
||||
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 super_cls: expected super class for validity, None if bypass
|
||||
:param kwargs: parameters to pass
|
||||
:return:
|
||||
"""
|
||||
module_name, class_name = module_class_string.rsplit(".", 1)
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
except Exception as ex:
|
||||
LOG.error("Failed to load Plugin '{}' : '{}'".format(module_name, ex))
|
||||
return
|
||||
|
||||
assert hasattr(module, class_name), "class {} is not in {}".format(
|
||||
class_name, module_name
|
||||
)
|
||||
# click.echo('reading class {} from module {}'.format(
|
||||
# class_name, module_name))
|
||||
cls = getattr(module, class_name)
|
||||
if super_cls is not None:
|
||||
assert issubclass(cls, super_cls), "class {} should inherit from {}".format(
|
||||
class_name, super_cls.__name__
|
||||
)
|
||||
# click.echo('initialising {} with params {}'.format(class_name, kwargs))
|
||||
obj = cls(**kwargs)
|
||||
return obj
|
||||
|
||||
def _load_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, APRSDPluginBase, config=self.config
|
||||
)
|
||||
if plugin_obj:
|
||||
LOG.info(
|
||||
"Registering Command plugin '{}'({}) '{}'".format(
|
||||
plugin_name, plugin_obj.version, plugin_obj.command_regex
|
||||
)
|
||||
)
|
||||
self._pluggy_pm.register(plugin_obj)
|
||||
except Exception as ex:
|
||||
LOG.exception("Couldn't load plugin '{}'".format(plugin_name), ex)
|
||||
|
||||
def setup_plugins(self):
|
||||
"""Create the plugin manager and register plugins."""
|
||||
|
||||
LOG.info("Loading Core APRSD Command Plugins")
|
||||
enabled_plugins = self.config["aprsd"].get("enabled_plugins", None)
|
||||
self._pluggy_pm = pluggy.PluginManager("aprsd")
|
||||
self._pluggy_pm.add_hookspecs(APRSDCommandSpec)
|
||||
if enabled_plugins:
|
||||
for p_name in enabled_plugins:
|
||||
self._load_plugin(p_name)
|
||||
else:
|
||||
# Enabled plugins isn't set, so we default to loading all of
|
||||
# the core plugins.
|
||||
for p_name in CORE_PLUGINS:
|
||||
self._load_plugin(p_name)
|
||||
|
||||
plugin_dir = self.config["aprsd"].get("plugin_dir", None)
|
||||
if plugin_dir:
|
||||
LOG.info("Trying to load custom plugins from '{}'".format(plugin_dir))
|
||||
plugins_list = self.load_plugins_from_path(plugin_dir)
|
||||
if plugins_list:
|
||||
LOG.info("Discovered {} modules to load".format(len(plugins_list)))
|
||||
for o in plugins_list:
|
||||
plugin_obj = None
|
||||
# not setting enabled plugins means load all?
|
||||
plugin_obj = o["obj"]
|
||||
|
||||
if plugin_obj:
|
||||
LOG.info(
|
||||
"Registering Command plugin '{}'({}) '{}'".format(
|
||||
o["name"], o["obj"].version, o["obj"].command_regex
|
||||
)
|
||||
)
|
||||
self._pluggy_pm.register(o["obj"])
|
||||
|
||||
else:
|
||||
LOG.info("Skipping Custom Plugins directory.")
|
||||
LOG.info("Completed Plugin Loading.")
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
"""Execute all the pluguns run method."""
|
||||
return self._pluggy_pm.hook.run(*args, **kwargs)
|
||||
|
||||
def register(self, obj):
|
||||
"""Register the plugin."""
|
||||
self._pluggy_pm.register(obj)
|
||||
|
||||
def get_plugins(self):
|
||||
return self._pluggy_pm.get_plugins()
|
||||
|
||||
|
||||
class APRSDCommandSpec:
|
||||
"""A hook specification namespace."""
|
||||
@ -184,7 +249,6 @@ class FortunePlugin(APRSDPluginBase):
|
||||
["/usr/games/fortune", "-s", "-n 60"], stdout=subprocess.PIPE
|
||||
)
|
||||
reply = process.communicate()[0]
|
||||
# send_message(fromcall, reply.rstrip())
|
||||
reply = reply.decode(errors="ignore").rstrip()
|
||||
except Exception as ex:
|
||||
reply = "Fortune command failed '{}'".format(ex)
|
||||
@ -200,20 +264,20 @@ class LocationPlugin(APRSDPluginBase):
|
||||
command_regex = "^[lL]"
|
||||
command_name = "location"
|
||||
|
||||
config_items = {"apikey": "aprs.fi api key here"}
|
||||
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("Location Plugin")
|
||||
# get last location of a callsign, get descriptive name from weather service
|
||||
try:
|
||||
a = re.search(
|
||||
r"^.*\s+(.*)", message
|
||||
) # optional second argument is a callsign to search
|
||||
# optional second argument is a callsign to search
|
||||
a = re.search(r"^.*\s+(.*)", message)
|
||||
if a is not None:
|
||||
searchcall = a.group(1)
|
||||
searchcall = searchcall.upper()
|
||||
else:
|
||||
searchcall = (
|
||||
fromcall # if no second argument, search for calling station
|
||||
)
|
||||
# if no second argument, search for calling station
|
||||
searchcall = fromcall
|
||||
url = (
|
||||
"http://api.aprs.fi/api/get?name="
|
||||
+ searchcall
|
||||
@ -222,6 +286,7 @@ class LocationPlugin(APRSDPluginBase):
|
||||
response = requests.get(url)
|
||||
# aprs_data = json.loads(response.read())
|
||||
aprs_data = json.loads(response.text)
|
||||
LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
|
||||
lat = aprs_data["entries"][0]["lat"]
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
try: # altitude not always provided
|
||||
@ -230,9 +295,9 @@ class LocationPlugin(APRSDPluginBase):
|
||||
alt = 0
|
||||
altfeet = int(alt * 3.28084)
|
||||
aprs_lasttime_seconds = aprs_data["entries"][0]["lasttime"]
|
||||
aprs_lasttime_seconds = aprs_lasttime_seconds.encode(
|
||||
"ascii", errors="ignore"
|
||||
) # unicode to ascii
|
||||
# aprs_lasttime_seconds = aprs_lasttime_seconds.encode(
|
||||
# "ascii", errors="ignore"
|
||||
# ) # unicode to ascii
|
||||
delta_seconds = time.time() - int(aprs_lasttime_seconds)
|
||||
delta_hours = delta_seconds / 60 / 60
|
||||
url2 = (
|
||||
@ -342,3 +407,76 @@ class WeatherPlugin(APRSDPluginBase):
|
||||
reply = "Unable to find you (send beacon?)"
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
class EmailPlugin(APRSDPluginBase):
|
||||
"""Email Plugin."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^-.*"
|
||||
command_name = "email"
|
||||
|
||||
# message_number:time combos so we don't resend the same email in
|
||||
# five mins {int:int}
|
||||
email_sent_dict = {}
|
||||
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("Email COMMAND")
|
||||
reply = None
|
||||
|
||||
searchstring = "^" + self.config["ham"]["callsign"] + ".*"
|
||||
# only I can do email
|
||||
if re.search(searchstring, fromcall):
|
||||
# digits only, first one is number of emails to resend
|
||||
r = re.search("^-([0-9])[0-9]*$", message)
|
||||
if r is not None:
|
||||
LOG.debug("RESEND EMAIL")
|
||||
email.resend_email(r.group(1), fromcall)
|
||||
reply = messaging.NULL_MESSAGE
|
||||
# -user@address.com body of email
|
||||
elif re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message):
|
||||
# (same search again)
|
||||
a = re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message)
|
||||
if a is not None:
|
||||
to_addr = a.group(1)
|
||||
content = a.group(2)
|
||||
# send recipient link to aprs.fi map
|
||||
if content == "mapme":
|
||||
content = "Click for my location: http://aprs.fi/{}".format(
|
||||
self.config["ham"]["callsign"]
|
||||
)
|
||||
too_soon = 0
|
||||
now = time.time()
|
||||
# see if we sent this msg number recently
|
||||
if ack in self.email_sent_dict:
|
||||
timedelta = now - self.email_sent_dict[ack]
|
||||
if timedelta < 300: # five minutes
|
||||
too_soon = 1
|
||||
if not too_soon or ack == 0:
|
||||
LOG.info("Send email '{}'".format(content))
|
||||
send_result = email.send_email(to_addr, content)
|
||||
if send_result != 0:
|
||||
reply = "-{} failed".format(to_addr)
|
||||
# messaging.send_message(fromcall, "-" + to_addr + " failed")
|
||||
else:
|
||||
# clear email sent dictionary if somehow goes over 100
|
||||
if len(self.email_sent_dict) > 98:
|
||||
LOG.debug(
|
||||
"DEBUG: email_sent_dict is big ("
|
||||
+ str(len(self.email_sent_dict))
|
||||
+ ") clearing out."
|
||||
)
|
||||
self.email_sent_dict.clear()
|
||||
self.email_sent_dict[ack] = now
|
||||
reply = "mapme email sent"
|
||||
else:
|
||||
LOG.info(
|
||||
"Email for message number "
|
||||
+ ack
|
||||
+ " recently sent, not sending again."
|
||||
)
|
||||
else:
|
||||
reply = "Bad email address"
|
||||
# messaging.send_message(fromcall, "Bad email address")
|
||||
|
||||
return reply
|
||||
|
@ -1,8 +1,10 @@
|
||||
"""Utilities and helper functions."""
|
||||
|
||||
import errno
|
||||
import functools
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import click
|
||||
import yaml
|
||||
@ -15,7 +17,7 @@ DEFAULT_CONFIG_DICT = {
|
||||
"aprs": {
|
||||
"login": "someusername",
|
||||
"password": "somepassword",
|
||||
"host": "noam.aprs2.net",
|
||||
"host": "rotate.aprs.net",
|
||||
"port": 14580,
|
||||
"logfile": "/tmp/arsd.log",
|
||||
},
|
||||
@ -47,6 +49,17 @@ DEFAULT_CONFIG_DICT = {
|
||||
DEFAULT_CONFIG_FILE = "~/.config/aprsd/aprsd.yml"
|
||||
|
||||
|
||||
def synchronized(wrapped):
|
||||
lock = threading.Lock()
|
||||
|
||||
@functools.wraps(wrapped)
|
||||
def _wrap(*args, **kwargs):
|
||||
with lock:
|
||||
return wrapped(*args, **kwargs)
|
||||
|
||||
return _wrap
|
||||
|
||||
|
||||
def env(*vars, **kwargs):
|
||||
"""This returns the first environment variable set.
|
||||
if none are non-empty, defaults to '' or keyword arg default
|
||||
@ -152,8 +165,8 @@ def parse_config(config_file):
|
||||
)
|
||||
check_option(config, "aprs", "login")
|
||||
check_option(config, "aprs", "password")
|
||||
check_option(config, "aprs", "host")
|
||||
check_option(config, "aprs", "port")
|
||||
# check_option(config, "aprs", "host")
|
||||
# check_option(config, "aprs", "port")
|
||||
check_option(config, "aprs", "logfile", "./aprsd.log")
|
||||
check_option(config, "imap", "host")
|
||||
check_option(config, "imap", "login")
|
||||
|
@ -7,4 +7,3 @@ pep8-naming
|
||||
black
|
||||
isort
|
||||
Sphinx
|
||||
thesmuggler
|
||||
|
@ -10,7 +10,7 @@ attrs==20.3.0 # via pytest
|
||||
babel==2.9.0 # via sphinx
|
||||
black==20.8b1 # via -r dev-requirements.in
|
||||
certifi==2020.12.5 # via requests
|
||||
chardet==3.0.4 # via requests
|
||||
chardet==4.0.0 # via requests
|
||||
click==7.1.2 # via black
|
||||
coverage==5.3 # via pytest-cov
|
||||
distlib==0.3.1 # via virtualenv
|
||||
@ -37,10 +37,10 @@ pyflakes==2.2.0 # via flake8
|
||||
pygments==2.7.3 # via sphinx
|
||||
pyparsing==2.4.7 # via packaging
|
||||
pytest-cov==2.10.1 # via -r dev-requirements.in
|
||||
pytest==6.2.0 # via -r dev-requirements.in, pytest-cov
|
||||
pytest==6.2.1 # via -r dev-requirements.in, pytest-cov
|
||||
pytz==2020.4 # via babel
|
||||
regex==2020.11.13 # via black
|
||||
requests==2.25.0 # via sphinx
|
||||
requests==2.25.1 # via sphinx
|
||||
six==1.15.0 # via tox, virtualenv
|
||||
snowballstemmer==2.0.0 # via sphinx
|
||||
sphinx==3.3.1 # via -r dev-requirements.in
|
||||
@ -50,7 +50,6 @@ sphinxcontrib-htmlhelp==1.0.3 # via sphinx
|
||||
sphinxcontrib-jsmath==1.0.1 # via sphinx
|
||||
sphinxcontrib-qthelp==1.0.3 # via sphinx
|
||||
sphinxcontrib-serializinghtml==1.1.4 # via sphinx
|
||||
thesmuggler==1.0.1 # via -r dev-requirements.in
|
||||
toml==0.10.2 # via black, pytest, tox
|
||||
tox==3.20.1 # via -r dev-requirements.in
|
||||
typed-ast==1.4.1 # via black, mypy
|
||||
|
@ -7,3 +7,5 @@ pyyaml
|
||||
six
|
||||
requests
|
||||
thesmuggler
|
||||
aprslib
|
||||
py3-validate-email
|
||||
|
@ -4,18 +4,21 @@
|
||||
#
|
||||
# pip-compile
|
||||
#
|
||||
aprslib==0.6.47 # via -r requirements.in
|
||||
certifi==2020.12.5 # via requests
|
||||
chardet==3.0.4 # via requests
|
||||
chardet==4.0.0 # via requests
|
||||
click-completion==0.5.2 # via -r requirements.in
|
||||
click==7.1.2 # via -r requirements.in, click-completion
|
||||
idna==2.10 # via requests
|
||||
dnspython==2.0.0 # via py3-validate-email
|
||||
filelock==3.0.12 # via py3-validate-email
|
||||
idna==2.10 # via py3-validate-email, requests
|
||||
imapclient==2.1.0 # via -r requirements.in
|
||||
jinja2==2.11.2 # via click-completion
|
||||
markupsafe==1.1.1 # via jinja2
|
||||
pbr==5.5.1 # via -r requirements.in
|
||||
pluggy==0.13.1 # via -r requirements.in
|
||||
py3-validate-email==0.2.12 # via -r requirements.in
|
||||
pyyaml==5.3.1 # via -r requirements.in
|
||||
requests==2.25.0 # via -r requirements.in
|
||||
shellingham==1.3.2 # via click-completion
|
||||
six==1.15.0 # via -r requirements.in, click-completion, imapclient
|
||||
thesmuggler==1.0.1 # via -r requirements.in
|
||||
|
@ -1,3 +0,0 @@
|
||||
flake8
|
||||
pytest
|
||||
mock
|
@ -1,2 +0,0 @@
|
||||
flake8
|
||||
pytest
|
@ -4,7 +4,7 @@ import unittest
|
||||
|
||||
import pytest
|
||||
|
||||
from aprsd import main
|
||||
from aprsd import email
|
||||
|
||||
if sys.version_info >= (3, 2):
|
||||
from unittest import mock
|
||||
@ -13,11 +13,12 @@ else:
|
||||
|
||||
|
||||
class testMain(unittest.TestCase):
|
||||
@mock.patch("aprsd.main._imap_connect")
|
||||
@mock.patch("aprsd.main._smtp_connect")
|
||||
@mock.patch("aprsd.email._imap_connect")
|
||||
@mock.patch("aprsd.email._smtp_connect")
|
||||
def test_validate_email(self, imap_mock, smtp_mock):
|
||||
"""Test to make sure we fail."""
|
||||
imap_mock.return_value = None
|
||||
smtp_mock.return_value = {"smaiof": "fire"}
|
||||
config = mock.MagicMock()
|
||||
|
||||
main.validate_email()
|
||||
email.validate_email_config(config, True)
|
||||
|
23
tox.ini
23
tox.ini
@ -2,7 +2,7 @@
|
||||
minversion = 2.9.0
|
||||
skipdist = True
|
||||
skip_missing_interpreters = true
|
||||
envlist = py{27,36,37,38},pep8,fmt-check
|
||||
envlist = pep8,py{36,37,38},fmt-check
|
||||
|
||||
# Activate isolated build environment. tox will use a virtual environment
|
||||
# to build a source distribution from the source tree. For build tools and
|
||||
@ -14,7 +14,6 @@ setenv = VIRTUAL_ENV={envdir}
|
||||
usedevelop = True
|
||||
install_command = pip install {opts} {packages}
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
-r{toxinidir}/dev-requirements.txt
|
||||
commands =
|
||||
# Use -bb to enable BytesWarnings as error to catch str/bytes misuse.
|
||||
@ -23,29 +22,10 @@ commands =
|
||||
# --cov="{envsitepackagesdir}/aprsd" --cov-report=html --cov-report=term {posargs}
|
||||
{envpython} -bb -m pytest {posargs}
|
||||
|
||||
[testenv:py27]
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
usedevelop = True
|
||||
install_command = pip install {opts} {packages}
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements-py2.txt
|
||||
commands =
|
||||
# Use -bb to enable BytesWarnings as error to catch str/bytes misuse.
|
||||
# Use -Werror to treat warnings as errors.
|
||||
# {envpython} -bb -Werror -m pytest \
|
||||
# --cov="{envsitepackagesdir}/aprsd" --cov-report=html --cov-report=term {posargs}
|
||||
{envpython} -bb -Werror -m pytest
|
||||
|
||||
[testenv:docs]
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
commands = sphinx-build -b html docs/source docs/html
|
||||
|
||||
[testenv:pep8-27]
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements-py2.txt
|
||||
commands =
|
||||
flake8 {posargs} aprsd
|
||||
|
||||
[testenv:pep8]
|
||||
commands =
|
||||
flake8 {posargs} aprsd
|
||||
@ -78,7 +58,6 @@ exclude = .venv,.git,.tox,dist,doc,.ropeproject
|
||||
# This section is not needed if not using GitHub Actions for CI.
|
||||
[gh-actions]
|
||||
python =
|
||||
2.7: py27, pep8-27
|
||||
3.6: py36, pep8, fmt-check
|
||||
3.7: py38, pep8, fmt-check
|
||||
3.8: py38, pep8, fmt-check, type-check, docs
|
||||
|
Loading…
Reference in New Issue
Block a user