diff --git a/aprsd/email.py b/aprsd/email.py index 800674a..49d68a9 100644 --- a/aprsd/email.py +++ b/aprsd/email.py @@ -18,16 +18,16 @@ CONFIG = None def _imap_connect(): - imap_port = CONFIG["imap"].get("port", 143) - use_ssl = CONFIG["imap"].get("use_ssl", False) - host = CONFIG["imap"]["host"] + 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["imap"]["host"], + CONFIG["aprsd"]["email"]["imap"]["host"], port=imap_port, use_uid=True, ssl=use_ssl, @@ -37,7 +37,10 @@ def _imap_connect(): return try: - server.login(CONFIG["imap"]["login"], CONFIG["imap"]["password"]) + 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("Failed to login {}".format(msg)) @@ -48,12 +51,15 @@ def _imap_connect(): def _smtp_connect(): - host = CONFIG["smtp"]["host"] - smtp_port = CONFIG["smtp"]["port"] - use_ssl = CONFIG["smtp"].get("use_ssl", False) + 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["imap"]["login"]), + "Connect to SMTP host {} with user '{}'".format( + msg, + CONFIG["aprsd"]["email"]["imap"]["login"], + ), ) try: @@ -68,7 +74,10 @@ def _smtp_connect(): LOG.debug("Connected to smtp host {}".format(msg)) try: - server.login(CONFIG["smtp"]["login"], CONFIG["smtp"]["password"]) + server.login( + CONFIG["aprsd"]["email"]["smtp"]["login"], + CONFIG["aprsd"]["email"]["smtp"]["password"], + ) except Exception: LOG.error("Couldn't connect to SMTP Server") return @@ -93,8 +102,8 @@ def validate_shortcuts(config): email_address=shortcuts[key], check_regex=True, check_mx=False, - from_address=config["smtp"]["login"], - helo_host=config["smtp"]["host"], + from_address=config["aprsd"]["email"]["smtp"]["login"], + helo_host=config["aprsd"]["email"]["smtp"]["host"], smtp_timeout=10, dns_timeout=10, use_blacklist=True, @@ -109,14 +118,14 @@ def validate_shortcuts(config): delete_keys.append(key) for key in delete_keys: - del config["shortcuts"][key] + del config["aprsd"]["email"]["shortcuts"][key] LOG.info("Available shortcuts: {}".format(config["shortcuts"])) def get_email_from_shortcut(addr): - if CONFIG.get("shortcuts", False): - return CONFIG["shortcuts"].get(addr, addr) + if CONFIG["aprsd"]["email"].get("shortcuts", False): + return CONFIG["aprsd"]["email"]["shortcuts"].get(addr, addr) else: return addr @@ -232,7 +241,7 @@ def parse_email(msgid, data, server): def send_email(to_addr, content): global check_email_delay - shortcuts = CONFIG["shortcuts"] + shortcuts = CONFIG["aprsd"]["email"]["shortcuts"] email_address = get_email_from_shortcut(to_addr) LOG.info("Sending Email_________________") @@ -250,12 +259,16 @@ def send_email(to_addr, content): msg = MIMEText(content) msg["Subject"] = subject - msg["From"] = CONFIG["smtp"]["login"] + msg["From"] = CONFIG["aprsd"]["email"]["smtp"]["login"] msg["To"] = to_addr server = _smtp_connect() if server: try: - server.sendmail(CONFIG["smtp"]["login"], [to_addr], msg.as_string()) + server.sendmail( + CONFIG["aprsd"]["email"]["smtp"]["login"], + [to_addr], + msg.as_string(), + ) except Exception as e: msg = getattr(e, "message", repr(e)) LOG.error("Sendmail Error!!!! '{}'", msg) @@ -305,7 +318,11 @@ def resend_email(count, fromcall): # 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 = messaging.TextMessage( + CONFIG["aprsd"]["email"]["aprs"]["login"], + fromcall, + reply, + ) msg.send() msgexists = True @@ -362,7 +379,7 @@ class APRSDEmailThread(threads.APRSDThread): check_email_delay += 1 LOG.debug("check_email_delay is " + str(check_email_delay) + " seconds") - shortcuts = CONFIG["shortcuts"] + shortcuts = CONFIG["aprsd"]["email"]["shortcuts"] # swap key/value shortcuts_inverted = {v: k for k, v in shortcuts.items()} diff --git a/aprsd/main.py b/aprsd/main.py index 0f19016..618bc78 100644 --- a/aprsd/main.py +++ b/aprsd/main.py @@ -37,7 +37,6 @@ import aprslib from aprslib.exceptions import LoginError import click import click_completion -import yaml # setup the global logger # logging.basicConfig(level=logging.DEBUG) # level=10 @@ -174,7 +173,7 @@ def setup_logging(config, loglevel, quiet): log_format = "[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s]" " %(message)s" date_format = "%m/%d/%Y %I:%M:%S %p" log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) - log_file = config["aprs"].get("logfile", None) + log_file = config["aprsd"].get("logfile", None) if log_file: fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4) else: @@ -192,7 +191,7 @@ def setup_logging(config, loglevel, quiet): @main.command() def sample_config(): """This dumps the config to stdout.""" - click.echo(utils.add_config_comments(yaml.dump(utils.DEFAULT_CONFIG_DICT))) + click.echo(utils.dump_default_cfg()) @main.command() diff --git a/aprsd/plugins/location.py b/aprsd/plugins/location.py index d8bd5d7..63db646 100644 --- a/aprsd/plugins/location.py +++ b/aprsd/plugins/location.py @@ -18,12 +18,13 @@ class LocationPlugin(plugin.APRSDPluginBase): LOG.info("Location Plugin") # get last location of a callsign, get descriptive name from weather service try: - utils.check_config_option(self.config, "aprs.fi", "apiKey") + utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"]) except Exception as ex: LOG.error("Failed to find config aprs.fi:apikey {}".format(ex)) return "No aprs.fi apikey found" - api_key = self.config["aprs.fi"]["apiKey"] + api_key = self.config["services"]["aprs.fi"]["apiKey"] + # optional second argument is a callsign to search a = re.search(r"^.*\s+(.*)", message) if a is not None: diff --git a/aprsd/plugins/time.py b/aprsd/plugins/time.py index 7807b99..396d9c0 100644 --- a/aprsd/plugins/time.py +++ b/aprsd/plugins/time.py @@ -53,7 +53,7 @@ class TimeOpenCageDataPlugin(TimePlugin): command_name = "Time" def command(self, fromcall, message, ack): - api_key = self.config["aprs.fi"]["apiKey"] + api_key = self.config["services"]["aprs.fi"]["apiKey"] try: aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall) except Exception as ex: @@ -93,7 +93,7 @@ class TimeOWMPlugin(TimePlugin): command_name = "Time" def command(self, fromcall, message, ack): - api_key = self.config["aprs.fi"]["apiKey"] + api_key = self.config["services"]["aprs.fi"]["apiKey"] try: aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall) except Exception as ex: @@ -105,12 +105,15 @@ class TimeOWMPlugin(TimePlugin): lon = aprs_data["entries"][0]["lng"] try: - utils.check_config_option(self.config, "openweathermap", "apiKey") + utils.check_config_option( + self.config, + ["services", "openweathermap", "apiKey"], + ) except Exception as ex: LOG.error("Failed to find config openweathermap:apiKey {}".format(ex)) return "No openweathermap apiKey found" - api_key = self.config["openweathermap"]["apiKey"] + api_key = self.config["services"]["openweathermap"]["apiKey"] try: results = plugin_utils.fetch_openweathermap(api_key, lat, lon) except Exception as ex: diff --git a/aprsd/plugins/weather.py b/aprsd/plugins/weather.py index 048ca6d..0dedc28 100644 --- a/aprsd/plugins/weather.py +++ b/aprsd/plugins/weather.py @@ -27,7 +27,13 @@ class USWeatherPlugin(plugin.APRSDPluginBase): def command(self, fromcall, message, ack): LOG.info("Weather Plugin") - api_key = self.config["aprs.fi"]["apiKey"] + try: + utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"]) + except Exception as ex: + LOG.error("Failed to find config aprs.fi:apikey {}".format(ex)) + return "No aprs.fi apikey found" + + api_key = self.config["services"]["aprs.fi"]["apiKey"] try: aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall) except Exception as ex: @@ -98,7 +104,17 @@ class USMetarPlugin(plugin.APRSDPluginBase): # if no second argument, search for calling station fromcall = fromcall - api_key = self.config["aprs.fi"]["apiKey"] + try: + utils.check_config_option( + self.config, + ["services", "aprs.fi", "apiKey"], + ) + except Exception as ex: + LOG.error("Failed to find config aprs.fi:apikey {}".format(ex)) + return "No aprs.fi apikey found" + + api_key = self.config["services"]["aprs.fi"]["apiKey"] + try: aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall) except Exception as ex: @@ -168,7 +184,13 @@ class OWMWeatherPlugin(plugin.APRSDPluginBase): else: searchcall = fromcall - api_key = self.config["aprs.fi"]["apiKey"] + try: + utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"]) + except Exception as ex: + LOG.error("Failed to find config aprs.fi:apikey {}".format(ex)) + return "No aprs.fi apikey found" + + api_key = self.config["services"]["aprs.fi"]["apiKey"] try: aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall) except Exception as ex: @@ -184,20 +206,23 @@ class OWMWeatherPlugin(plugin.APRSDPluginBase): lon = aprs_data["entries"][0]["lng"] try: - utils.check_config_option(self.config, "openweathermap", "apiKey") + utils.check_config_option( + self.config, + ["services", "openweathermap", "apiKey"], + ) except Exception as ex: LOG.error("Failed to find config openweathermap:apiKey {}".format(ex)) return "No openweathermap apiKey found" try: - utils.check_config_option(self.config, "aprsd", "units") + utils.check_config_option(self.config, ["aprsd", "units"]) except Exception: LOG.debug("Couldn't find untis in aprsd:services:units") units = "metric" else: units = self.config["aprsd"]["units"] - api_key = self.config["openweathermap"]["apiKey"] + api_key = self.config["services"]["openweathermap"]["apiKey"] try: wx_data = plugin_utils.fetch_openweathermap( api_key, @@ -279,7 +304,13 @@ class AVWXWeatherPlugin(plugin.APRSDPluginBase): else: searchcall = fromcall - api_key = self.config["aprs.fi"]["apiKey"] + try: + utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"]) + except Exception as ex: + LOG.error("Failed to find config aprs.fi:apikey {}".format(ex)) + return "No aprs.fi apikey found" + + api_key = self.config["services"]["aprs.fi"]["apiKey"] try: aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall) except Exception as ex: @@ -295,20 +326,20 @@ class AVWXWeatherPlugin(plugin.APRSDPluginBase): lon = aprs_data["entries"][0]["lng"] try: - utils.check_config_option(self.config, "avwx", "apiKey") + utils.check_config_option(self.config, ["services", "avwx", "apiKey"]) except Exception as ex: LOG.error("Failed to find config avwx:apiKey {}".format(ex)) return "No avwx apiKey found" try: - utils.check_config_option(self.config, "avwx", "base_url") + utils.check_config_option(self.config, ["services", "avwx", "base_url"]) except Exception as ex: LOG.debut("Didn't find avwx:base_url {}".format(ex)) base_url = "https://avwx.rest" else: - base_url = self.config["avwx"]["base_url"] + base_url = self.config["services"]["avwx"]["base_url"] - api_key = self.config["avwx"]["apiKey"] + api_key = self.config["services"]["avwx"]["apiKey"] token = "TOKEN {}".format(api_key) headers = {"Authorization": token} try: diff --git a/aprsd/utils.py b/aprsd/utils.py index d088657..9824667 100644 --- a/aprsd/utils.py +++ b/aprsd/utils.py @@ -17,37 +17,42 @@ DEFAULT_CONFIG_DICT = { "aprs": { "login": "CALLSIGN", "password": "00000", - "host": "rotate.aprs.net", + "host": "rotate.aprs2.net", "port": 14580, - "logfile": "/tmp/aprsd.log", - }, - "aprs.fi": {"apiKey": "set me"}, - "openweathermap": {"apiKey": "set me"}, - "opencagedata": {"apiKey": "set me"}, - "avwx": {"base_url": "http://host:port", "apiKey": "set me"}, - "shortcuts": { - "aa": "5551239999@vtext.com", - "cl": "craiglamparter@somedomain.org", - "wb": "555309@vtext.com", - }, - "smtp": { - "login": "SMTP_USERNAME", - "password": "SMTP_PASSWORD", - "host": "smtp.gmail.com", - "port": 465, - "use_ssl": False, - }, - "imap": { - "login": "IMAP_USERNAME", - "password": "IMAP_PASSWORD", - "host": "imap.gmail.com", - "port": 993, - "use_ssl": True, }, "aprsd": { + "logfile": "/tmp/aprsd.log", "plugin_dir": "~/.config/aprsd/plugins", "enabled_plugins": plugin.CORE_PLUGINS, "units": "imperial", + "email": { + "enabled": True, + "shortcuts": { + "aa": "5551239999@vtext.com", + "cl": "craiglamparter@somedomain.org", + "wb": "555309@vtext.com", + }, + "smtp": { + "login": "SMTP_USERNAME", + "password": "SMTP_PASSWORD", + "host": "smtp.gmail.com", + "port": 465, + "use_ssl": False, + }, + "imap": { + "login": "IMAP_USERNAME", + "password": "IMAP_PASSWORD", + "host": "imap.gmail.com", + "port": 993, + "use_ssl": True, + }, + }, + }, + "services": { + "aprs.fi": {"apiKey": "APIKEYVALUE"}, + "openweathermap": {"apiKey": "APIKEYVALUE"}, + "opencagedata": {"apiKey": "APIKEYVALUE"}, + "avwx": {"base_url": "http://host:port", "apiKey": "APIKEYVALUE"}, }, } @@ -105,14 +110,33 @@ def end_substr(original, substr): return idx +def dump_default_cfg(): + return add_config_comments( + yaml.dump( + DEFAULT_CONFIG_DICT, + indent=4, + ), + ) + + def add_config_comments(raw_yaml): + end_idx = end_substr(raw_yaml, "aprs:") + if end_idx != -1: + # lets insert a comment + raw_yaml = insert_str( + raw_yaml, + "\n # Get the passcode for your callsign here: " + "\n # https://apps.magicbug.co.uk/passcode", + end_idx, + ) + end_idx = end_substr(raw_yaml, "aprs.fi:") if end_idx != -1: # lets insert a comment raw_yaml = insert_str( raw_yaml, - "\n # Get the apiKey from your aprs.fi account here: " - "\n # http://aprs.fi/account", + "\n # Get the apiKey from your aprs.fi account here: " + "\n # http://aprs.fi/account", end_idx, ) @@ -121,9 +145,9 @@ def add_config_comments(raw_yaml): # lets insert a comment raw_yaml = insert_str( raw_yaml, - "\n # (Optional for TimeOpenCageDataPlugin) " - "\n # Get the apiKey from your opencagedata account here: " - "\n # https://opencagedata.com/dashboard#api-keys", + "\n # (Optional for TimeOpenCageDataPlugin) " + "\n # Get the apiKey from your opencagedata account here: " + "\n # https://opencagedata.com/dashboard#api-keys", end_idx, ) @@ -132,10 +156,10 @@ def add_config_comments(raw_yaml): # lets insert a comment raw_yaml = insert_str( raw_yaml, - "\n # (Optional for OWMWeatherPlugin) " - "\n # Get the apiKey from your " - "\n # openweathermap account here: " - "\n # https://home.openweathermap.org/api_keys", + "\n # (Optional for OWMWeatherPlugin) " + "\n # Get the apiKey from your " + "\n # openweathermap account here: " + "\n # https://home.openweathermap.org/api_keys", end_idx, ) @@ -144,10 +168,10 @@ def add_config_comments(raw_yaml): # lets insert a comment raw_yaml = insert_str( raw_yaml, - "\n # (Optional for AVWXWeatherPlugin) " - "\n # Use hosted avwx-api here: https://avwx.rest " - "\n # or deploy your own from here: " - "\n # https://github.com/avwx-rest/avwx-api", + "\n # (Optional for AVWXWeatherPlugin) " + "\n # Use hosted avwx-api here: https://avwx.rest " + "\n # or deploy your own from here: " + "\n # https://github.com/avwx-rest/avwx-api", end_idx, ) @@ -163,8 +187,7 @@ def create_default_config(): click.echo("Config dir '{}' doesn't exist, creating.".format(config_dir)) mkdir_p(config_dir) with open(config_file_expanded, "w+") as cf: - raw_yaml = yaml.dump(DEFAULT_CONFIG_DICT) - cf.write(add_config_comments(raw_yaml)) + cf.write(dump_default_cfg()) def get_config(config_file): @@ -194,33 +217,32 @@ def get_config(config_file): sys.exit(-1) -def check_config_option(config, section, name=None, default=None, default_fail=None): - if section in config: +def conf_option_exists(conf, chain): + _key = chain.pop(0) + if _key in conf: + return conf_option_exists(conf[_key], chain) if chain else conf[_key] - if name and name not in config[section]: - if not default: - raise Exception( - "'{}' was not in '{}' section of config file".format( - name, - section, - ), - ) - else: - config[section][name] = default - else: - if ( - default_fail - and name in config[section] - and config[section][name] == default_fail - ): + +def check_config_option(config, chain, default_fail=None): + result = conf_option_exists(config, chain.copy()) + if not result: + raise Exception( + "'{}' was not in config file".format( + chain, + ), + ) + else: + if default_fail: + if result == default_fail: # We have to fail and bail if the user hasn't edited # this config option. raise Exception( - "Config file needs to be edited from provided defaults.", + "Config file needs to be edited from provided defaults for {}.".format( + chain, + ), ) - else: - raise Exception("'%s' section wasn't in config file" % section) - return config + else: + return config # This method tries to parse the config yaml file @@ -235,41 +257,68 @@ def parse_config(config_file): click.echo(msg) sys.exit(-1) - def check_option(config, section, name=None, default=None, default_fail=None): + def check_option(config, chain, default_fail=None): try: - config = check_config_option(config, section, name, default, default_fail) + config = check_config_option(config, chain, default_fail=default_fail) except Exception as ex: fail(repr(ex)) else: return config config = get_config(config_file) - check_option(config, "shortcuts") + # special check here to make sure user has edited the config file # and changed the ham callsign check_option( config, - "ham", - "callsign", + [ + "ham", + "callsign", + ], default_fail=DEFAULT_CONFIG_DICT["ham"]["callsign"], ) check_option( config, - "aprs.fi", - "apiKey", - default_fail=DEFAULT_CONFIG_DICT["aprs.fi"]["apiKey"], + ["services", "aprs.fi", "apiKey"], + default_fail=DEFAULT_CONFIG_DICT["services"]["aprs.fi"]["apiKey"], ) - check_option(config, "aprs", "login") - check_option(config, "aprs", "password") - # 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") - check_option(config, "imap", "password") - check_option(config, "smtp", "host") - check_option(config, "smtp", "port") - check_option(config, "smtp", "login") - check_option(config, "smtp", "password") + check_option( + config, + ["aprs", "login"], + default_fail=DEFAULT_CONFIG_DICT["aprs"]["login"], + ) + check_option( + config, + ["aprs", "password"], + default_fail=DEFAULT_CONFIG_DICT["aprs"]["password"], + ) + if config["aprsd"]["email"]["enabled"] is True: + # Check IMAP server settings + check_option(config, ["aprsd", "email", "imap", "host"]) + check_option(config, ["aprsd", "email", "imap", "port"]) + check_option( + config, + ["aprsd", "email", "imap", "login"], + default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["login"], + ) + check_option( + config, + ["aprsd", "email", "imap", "password"], + default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["password"], + ) + + # Check SMTP server settings + check_option(config, ["aprsd", "email", "smtp", "host"]) + check_option(config, ["aprsd", "email", "smtp", "port"]) + check_option( + config, + ["aprsd", "email", "smtp", "login"], + default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["login"], + ) + check_option( + config, + ["aprsd", "email", "smtp", "password"], + default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["password"], + ) return config diff --git a/tests/test_email.py b/tests/test_email.py index ce9b5ac..77a1c6d 100644 --- a/tests/test_email.py +++ b/tests/test_email.py @@ -5,21 +5,21 @@ from aprsd import email class TestEmail(unittest.TestCase): def test_get_email_from_shortcut(self): - email.CONFIG = {"shortcuts": {}} + email.CONFIG = {"aprsd": {"email": {"shortcuts": {}}}} email_address = "something@something.com" addr = "-{}".format(email_address) actual = email.get_email_from_shortcut(addr) self.assertEqual(addr, actual) - email.CONFIG = {"nothing": "nothing"} + email.CONFIG = {"aprsd": {"email": {"nothing": "nothing"}}} actual = email.get_email_from_shortcut(addr) self.assertEqual(addr, actual) - email.CONFIG = {"shortcuts": {"not_used": "empty"}} + email.CONFIG = {"aprsd": {"email": {"shortcuts": {"not_used": "empty"}}}} actual = email.get_email_from_shortcut(addr) self.assertEqual(addr, actual) - email.CONFIG = {"shortcuts": {"-wb": email_address}} + email.CONFIG = {"aprsd": {"email": {"shortcuts": {"-wb": email_address}}}} short = "-wb" actual = email.get_email_from_shortcut(short) self.assertEqual(email_address, actual)