From 7d0006b0a6c4b05c14214609fcfe718ae7f0c146 Mon Sep 17 00:00:00 2001 From: Hemna Date: Fri, 5 Nov 2021 13:36:33 -0400 Subject: [PATCH] Refactor the cli This patch refactors the cli to incorporate the dev, send-message, listen commands into the main aprsd app. This also moves the command line completion installer/show into it's own subgroup. --- aprsd/aprsd.py | 197 ++++++++++++++ aprsd/cli_helper.py | 35 +++ aprsd/clients/__init__.py | 0 aprsd/cmds/__init__.py | 0 aprsd/cmds/completion.py | 35 +++ aprsd/cmds/dev.py | 104 +++++++ aprsd/cmds/listen.py | 152 +++++++++++ aprsd/cmds/send_message.py | 162 +++++++++++ aprsd/cmds/server.py | 119 ++++++++ aprsd/dev.py | 251 ----------------- aprsd/listen.py | 394 --------------------------- aprsd/main.py | 536 ------------------------------------- setup.cfg | 4 +- 13 files changed, 805 insertions(+), 1184 deletions(-) create mode 100644 aprsd/aprsd.py create mode 100644 aprsd/cli_helper.py create mode 100644 aprsd/clients/__init__.py create mode 100644 aprsd/cmds/__init__.py create mode 100644 aprsd/cmds/completion.py create mode 100644 aprsd/cmds/dev.py create mode 100644 aprsd/cmds/listen.py create mode 100644 aprsd/cmds/send_message.py create mode 100644 aprsd/cmds/server.py delete mode 100644 aprsd/dev.py delete mode 100644 aprsd/listen.py delete mode 100644 aprsd/main.py diff --git a/aprsd/aprsd.py b/aprsd/aprsd.py new file mode 100644 index 0000000..2a33154 --- /dev/null +++ b/aprsd/aprsd.py @@ -0,0 +1,197 @@ +# +# Listen on amateur radio aprs-is network for messages and respond to them. +# You must have an amateur radio callsign to use this software. You must +# create an ~/.aprsd/config.yml file with all of the required settings. To +# generate an example config.yml, just run aprsd, then copy the sample config +# to ~/.aprsd/config.yml and edit the settings. +# +# APRS messages: +# l(ocation) = descriptive location of calling station +# w(eather) = temp, (hi/low) forecast, later forecast +# t(ime) = respond with the current time +# f(ortune) = respond with a short fortune +# -email_addr email text = send an email +# -2 = display the last 2 emails received +# p(ing) = respond with Pong!/time +# anything else = respond with usage +# +# (C)2018 Craig Lamparter +# License GPLv2 +# + +# python included libs +import datetime +import logging +from logging import NullHandler +from logging.handlers import RotatingFileHandler +import os +import signal +import sys +import time + +import click +import click_completion + +# local imports here +import aprsd +from aprsd import config as aprsd_config +from aprsd import messaging, packets, stats, threads, utils +from aprsd.cli_helper import AliasedGroup + + +# setup the global logger +# logging.basicConfig(level=logging.DEBUG) # level=10 +LOG = logging.getLogger("APRSD") +CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) +flask_enabled = False + + +def custom_startswith(string, incomplete): + """A custom completion match that supports case insensitive matching.""" + if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"): + string = string.lower() + incomplete = incomplete.lower() + return string.startswith(incomplete) + + +click_completion.core.startswith = custom_startswith +click_completion.init() + + +@click.group(cls=AliasedGroup, context_settings=CONTEXT_SETTINGS) +@click.option( + "--loglevel", + default="INFO", + show_default=True, + type=click.Choice( + ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], + case_sensitive=False, + ), + show_choices=True, + help="The log level to use for aprsd.log", +) +@click.option( + "-c", + "--config", + "config_file", + show_default=True, + default=aprsd_config.DEFAULT_CONFIG_FILE, + help="The aprsd config file to use for options.", +) +@click.option( + "--quiet", + is_flag=True, + default=False, + help="Don't log to stdout", +) +@click.version_option() +@click.pass_context +def cli(ctx, loglevel, config_file, quiet): + ctx.ensure_object(dict) + ctx.obj["loglevel"] = loglevel + ctx.obj["config_file"] = config_file + ctx.obj["quiet"] = quiet + ctx.obj["config"] = aprsd_config.parse_config(config_file) + setup_logging(ctx.obj["config"], loglevel, quiet) + +def main(): + from .cmds import completion, dev, listen, send_message, server # noqa + cli() + + +def signal_handler(sig, frame): + global flask_enabled + + click.echo("signal_handler: called") + threads.APRSDThreadList().stop_all() + if "subprocess" not in str(frame): + LOG.info( + "Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format( + datetime.datetime.now(), + ), + ) + time.sleep(1.5) + messaging.MsgTrack().save() + packets.WatchList().save() + packets.SeenList().save() + LOG.info(stats.APRSDStats()) + # signal.signal(signal.SIGTERM, sys.exit(0)) + # sys.exit(0) + if flask_enabled: + signal.signal(signal.SIGTERM, sys.exit(0)) + + +# Setup the logging faciility +# to disable logging to stdout, but still log to file +# use the --quiet option on the cmdln +def setup_logging(config, loglevel, quiet): + log_level = aprsd_config.LOG_LEVELS[loglevel] + LOG.setLevel(log_level) + log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT) + date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT) + log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) + log_file = config["aprsd"].get("logfile", None) + if log_file: + fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4) + else: + fh = NullHandler() + + fh.setFormatter(log_formatter) + LOG.addHandler(fh) + + imap_logger = None + if config.get("aprsd.email.enabled", default=False) and config.get("aprsd.email.imap.debug", default=False): + + imap_logger = logging.getLogger("imapclient.imaplib") + imap_logger.setLevel(log_level) + imap_logger.addHandler(fh) + + if config.get("aprsd.web.enabled", default=False): + qh = logging.handlers.QueueHandler(threads.logging_queue) + q_log_formatter = logging.Formatter( + fmt=aprsd_config.QUEUE_LOG_FORMAT, + datefmt=aprsd_config.QUEUE_DATE_FORMAT, + ) + qh.setFormatter(q_log_formatter) + LOG.addHandler(qh) + + if not quiet: + sh = logging.StreamHandler(sys.stdout) + sh.setFormatter(log_formatter) + LOG.addHandler(sh) + if imap_logger: + imap_logger.addHandler(sh) + + +@cli.command() +@click.pass_context +def check_version(ctx): + """Check this version against the latest in pypi.org.""" + config_file = ctx.obj["config_file"] + loglevel = ctx.obj["loglevel"] + config = aprsd_config.parse_config(config_file) + + setup_logging(config, loglevel, False) + level, msg = utils._check_version() + if level: + LOG.warning(msg) + else: + LOG.info(msg) + + +@cli.command() +@click.pass_context +def sample_config(ctx): + """This dumps the config to stdout.""" + click.echo(aprsd_config.dump_default_cfg()) + + +@cli.command() +@click.pass_context +def version(ctx): + """Show the APRSD version.""" + click.echo(f"APRSD Version : {aprsd.__version__}") + + +if __name__ == "__main__": + main() diff --git a/aprsd/cli_helper.py b/aprsd/cli_helper.py new file mode 100644 index 0000000..ef83aae --- /dev/null +++ b/aprsd/cli_helper.py @@ -0,0 +1,35 @@ +import click + + +class AliasedGroup(click.Group): + def command(self, *args, **kwargs): + """A shortcut decorator for declaring and attaching a command to + the group. This takes the same arguments as :func:`command` but + immediately registers the created command with this instance by + calling into :meth:`add_command`. + Copied from `click` and extended for `aliases`. + """ + def decorator(f): + aliases = kwargs.pop("aliases", []) + cmd = click.decorators.command(*args, **kwargs)(f) + self.add_command(cmd) + for alias in aliases: + self.add_command(cmd, name=alias) + return cmd + return decorator + + def group(self, *args, **kwargs): + """A shortcut decorator for declaring and attaching a group to + the group. This takes the same arguments as :func:`group` but + immediately registers the created command with this instance by + calling into :meth:`add_command`. + Copied from `click` and extended for `aliases`. + """ + def decorator(f): + aliases = kwargs.pop("aliases", []) + cmd = click.decorators.group(*args, **kwargs)(f) + self.add_command(cmd) + for alias in aliases: + self.add_command(cmd, name=alias) + return cmd + return decorator diff --git a/aprsd/clients/__init__.py b/aprsd/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aprsd/cmds/__init__.py b/aprsd/cmds/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aprsd/cmds/completion.py b/aprsd/cmds/completion.py new file mode 100644 index 0000000..55621a3 --- /dev/null +++ b/aprsd/cmds/completion.py @@ -0,0 +1,35 @@ +import click +import click_completion + +from ..aprsd import cli + + +CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) + +@cli.group(help="Click Completion subcommands", context_settings=CONTEXT_SETTINGS) +@click.pass_context +def completion(ctx): + pass + + +# show dumps out the completion code for a particular shell +@completion.command(help="Show completion code for shell") +@click.option("-i", "--case-insensitive/--no-case-insensitive", help="Case insensitive completion") +@click.argument("shell", required=False, type=click_completion.DocumentedChoice(click_completion.core.shells)) +def show(shell, case_insensitive): + """Show the click-completion-command completion code""" + extra_env = {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} if case_insensitive else {} + click.echo(click_completion.core.get_code(shell, extra_env=extra_env)) + + +# install will install the completion code for a particular shell +@completion.command(help="Install completion code for a shell") +@click.option("--append/--overwrite", help="Append the completion code to the file", default=None) +@click.option("-i", "--case-insensitive/--no-case-insensitive", help="Case insensitive completion") +@click.argument("shell", required=False, type=click_completion.DocumentedChoice(click_completion.core.shells)) +@click.argument("path", required=False) +def install(append, case_insensitive, shell, path): + """Install the click-completion-command completion""" + extra_env = {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} if case_insensitive else {} + shell, path = click_completion.core.install(shell=shell, path=path, append=append, extra_env=extra_env) + click.echo(f"{shell} completion installed in {path}") diff --git a/aprsd/cmds/dev.py b/aprsd/cmds/dev.py new file mode 100644 index 0000000..5a910e5 --- /dev/null +++ b/aprsd/cmds/dev.py @@ -0,0 +1,104 @@ +# +# Dev.py is used to help develop plugins +# +# +# python included libs +import logging + +import click + +# local imports here +from aprsd import client, plugin + +from ..aprsd import cli + + +LOG = logging.getLogger("APRSD") +CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) + + +@cli.group(help="Development tools", context_settings=CONTEXT_SETTINGS) +@click.pass_context +def dev(ctx): + pass + + +@dev.command() +@click.option( + "-p", + "--plugin", + "plugin_path", + show_default=True, + default=None, + help="The plugin to run. Ex: aprsd.plugins.ping.PingPlugin", +) +@click.option( + "-a", + "--all", + "load_all", + show_default=True, + is_flag=True, + default=False, + help="Load all the plugins in config?", +) +@click.option( + "-n", + "--num", + "number", + show_default=True, + default=1, + help="Number of times to call the plugin", +) +@click.argument("fromcall") +@click.argument("message", nargs=-1, required=True) +@click.pass_context +def test_plugin( + ctx, + plugin_path, + load_all, + number, + fromcall, + message, +): + """APRSD Plugin test app.""" + config = ctx.obj["config"] + + if not plugin_path: + click.echo(ctx.get_help()) + click.echo("") + ctx.fail("Failed to provide -p option to test a plugin") + ctx.exit() + + if type(message) is tuple: + message = " ".join(message) + client.Client(config) + + pm = plugin.PluginManager(config) + if load_all: + pm.setup_plugins() + else: + pm._init() + obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config) + # Register the plugin they wanted tested. + LOG.info( + "Testing plugin {} Version {}".format( + obj.__class__, obj.version, + ), + ) + pm._pluggy_pm.register(obj) + login = config["aprs"]["login"] + + packet = { + "from": fromcall, "addresse": login, + "message_text": message, + "format": "message", + "msgNo": 1, + } + LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'") + + for x in range(number): + reply = pm.run(packet) + # Plugin might have threads, so lets stop them so we can exit. + # obj.stop_threads() + LOG.info(f"Result{x} = '{reply}'") + pm.stop() diff --git a/aprsd/cmds/listen.py b/aprsd/cmds/listen.py new file mode 100644 index 0000000..69f258d --- /dev/null +++ b/aprsd/cmds/listen.py @@ -0,0 +1,152 @@ +# +# License GPLv2 +# + +# python included libs +import datetime +import logging +import sys +import time + +import aprslib +import click + +# local imports here +import aprsd +from aprsd import client, messaging, packets, stats, threads, trace, utils + +from ..aprsd import cli + + +# setup the global logger +# logging.basicConfig(level=logging.DEBUG) # level=10 +LOG = logging.getLogger("APRSD") + + +def signal_handler(sig, frame): + threads.APRSDThreadList().stop_all() + if "subprocess" not in str(frame): + LOG.info( + "Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format( + datetime.datetime.now(), + ), + ) + time.sleep(5) + LOG.info(stats.APRSDStats()) + + +@cli.command() +@click.option( + "--aprs-login", + envvar="APRS_LOGIN", + show_envvar=True, + help="What callsign to send the message from.", +) +@click.option( + "--aprs-password", + envvar="APRS_PASSWORD", + show_envvar=True, + help="the APRS-IS password for APRS_LOGIN", +) +@click.argument( + "filter", + nargs=-1, + required=True, +) +@click.pass_context +def listen( + ctx, + aprs_login, + aprs_password, + filter, +): + """Listen to packets on the APRS-IS Network based on FILTER. + + FILTER is the APRS Filter to use. see http://www.aprs-is.net/javAPRSFilter.aspx + + """ + config = ctx.obj["config"] + + if not aprs_login: + click.echo(ctx.get_help()) + click.echo("") + ctx.fail("Must set --aprs_login or APRS_LOGIN") + ctx.exit() + + if not aprs_password: + click.echo(ctx.get_help()) + click.echo("") + ctx.fail("Must set --aprs-password or APRS_PASSWORD") + ctx.exit() + + config["aprs"]["login"] = aprs_login + config["aprs"]["password"] = aprs_password + + LOG.info(f"APRSD Listen Started version: {aprsd.__version__}") + + flat_config = utils.flatten_dict(config) + LOG.info("Using CONFIG values:") + for x in flat_config: + if "password" in x or "aprsd.web.users.admin" in x: + LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX") + else: + LOG.info(f"{x} = {flat_config[x]}") + + stats.APRSDStats(config) + + # Try and load saved MsgTrack list + LOG.debug("Loading saved MsgTrack object.") + messaging.MsgTrack(config=config).load() + packets.WatchList(config=config).load() + packets.SeenList(config=config).load() + + @trace.trace + def rx_packet(packet): + resp = packet.get("response", None) + if resp == "ack": + ack_num = packet.get("msgNo") + LOG.info(f"We saw an ACK {ack_num} Ignoring") + messaging.log_packet(packet) + else: + message = packet.get("message_text", None) + fromcall = packet["from"] + msg_number = packet.get("msgNo", "0") + messaging.log_message( + "Received Message", + packet["raw"], + message, + fromcall=fromcall, + ack=msg_number, + ) + + # Initialize the client factory and create + # The correct client object ready for use + client.ClientFactory.setup(config) + # Make sure we have 1 client transport enabled + if not client.factory.is_client_enabled(): + LOG.error("No Clients are enabled in config.") + sys.exit(-1) + + # Creates the client object + LOG.info("Creating client connection") + client.factory.create().client + aprs_client = client.factory.create().client + + LOG.debug(f"Filter by '{filter}'") + aprs_client.set_filter(filter) + + while True: + try: + # This will register a packet consumer with aprslib + # When new packets come in the consumer will process + # the packet + aprs_client.consumer(rx_packet, raw=False) + except aprslib.exceptions.ConnectionDrop: + LOG.error("Connection dropped, reconnecting") + time.sleep(5) + # Force the deletion of the client object connected to aprs + # This will cause a reconnect, next time client.get_client() + # is called + aprs_client.reset() + except aprslib.exceptions.UnknownFormat: + LOG.error("Got a Bad packet") diff --git a/aprsd/cmds/send_message.py b/aprsd/cmds/send_message.py new file mode 100644 index 0000000..89f770c --- /dev/null +++ b/aprsd/cmds/send_message.py @@ -0,0 +1,162 @@ +import logging +import sys +import time + +import aprslib +from aprslib.exceptions import LoginError +import click + +import aprsd +from aprsd import client, messaging, packets + +from ..aprsd import cli + + +LOG = logging.getLogger("APRSD") + + +@cli.command() +@click.option( + "--aprs-login", + envvar="APRS_LOGIN", + show_envvar=True, + help="What callsign to send the message from.", +) +@click.option( + "--aprs-password", + envvar="APRS_PASSWORD", + show_envvar=True, + help="the APRS-IS password for APRS_LOGIN", +) +@click.option( + "--no-ack", + "-n", + is_flag=True, + show_default=True, + default=False, + help="Don't wait for an ack, just sent it to APRS-IS and bail.", +) +@click.option( + "--wait-response", + "-w", + is_flag=True, + show_default=True, + default=False, + help="Wait for a response to the message?", +) +@click.option("--raw", default=None, help="Send a raw message. Implies --no-ack") +@click.argument("tocallsign", required=False) +@click.argument("command", nargs=-1, required=False) +@click.pass_context +def send_message( + ctx, + aprs_login, + aprs_password, + no_ack, + wait_response, + raw, + tocallsign, + command, +): + """Send a message to a callsign via APRS_IS.""" + global got_ack, got_response + config = ctx.obj["config"] + quiet = ctx.obj["quiet"] + + if not aprs_login: + click.echo("Must set --aprs_login or APRS_LOGIN") + return + + if not aprs_password: + click.echo("Must set --aprs-password or APRS_PASSWORD") + return + + config["aprs"]["login"] = aprs_login + config["aprs"]["password"] = aprs_password + + LOG.info(f"APRSD LISTEN Started version: {aprsd.__version__}") + if type(command) is tuple: + command = " ".join(command) + if not quiet: + if raw: + LOG.info(f"L'{aprs_login}' R'{raw}'") + else: + LOG.info(f"L'{aprs_login}' To'{tocallsign}' C'{command}'") + + got_ack = False + got_response = False + + def rx_packet(packet): + global got_ack, got_response + # LOG.debug("Got packet back {}".format(packet)) + resp = packet.get("response", None) + if resp == "ack": + ack_num = packet.get("msgNo") + LOG.info(f"We got ack for our sent message {ack_num}") + messaging.log_packet(packet) + got_ack = True + else: + message = packet.get("message_text", None) + fromcall = packet["from"] + msg_number = packet.get("msgNo", "0") + messaging.log_message( + "Received Message", + packet["raw"], + message, + fromcall=fromcall, + ack=msg_number, + ) + got_response = True + # Send the ack back? + ack = messaging.AckMessage( + config["aprs"]["login"], + fromcall, + msg_id=msg_number, + ) + ack.send_direct() + + if got_ack: + if wait_response: + if got_response: + sys.exit(0) + else: + sys.exit(0) + + try: + client.ClientFactory.setup(config) + client.factory.create().client + except LoginError: + sys.exit(-1) + + packets.PacketList(config=config) + packets.WatchList(config=config) + + # Send a message + # then we setup a consumer to rx messages + # We should get an ack back as well as a new message + # we should bail after we get the ack and send an ack back for the + # message + if raw: + msg = messaging.RawMessage(raw) + msg.send_direct() + sys.exit(0) + else: + msg = messaging.TextMessage(aprs_login, tocallsign, command) + msg.send_direct() + + if no_ack: + sys.exit(0) + + try: + # This will register a packet consumer with aprslib + # When new packets come in the consumer will process + # the packet + aprs_client = client.factory.create().client + aprs_client.consumer(rx_packet, raw=False) + except aprslib.exceptions.ConnectionDrop: + LOG.error("Connection dropped, reconnecting") + time.sleep(5) + # Force the deletion of the client object connected to aprs + # This will cause a reconnect, next time client.get_client() + # is called + aprs_client.reset() diff --git a/aprsd/cmds/server.py b/aprsd/cmds/server.py new file mode 100644 index 0000000..bb08f7d --- /dev/null +++ b/aprsd/cmds/server.py @@ -0,0 +1,119 @@ +import logging +import signal +import sys + +import click + +import aprsd +from aprsd import ( + client, flask, messaging, packets, plugin, stats, threads, trace, utils, +) +from aprsd import aprsd as aprsd_main + +from ..aprsd import cli + + +LOG = logging.getLogger("APRSD") + + +# main() ### +@cli.command() +@click.option( + "-f", + "--flush", + "flush", + is_flag=True, + show_default=True, + default=False, + help="Flush out all old aged messages on disk.", +) +@click.pass_context +def server(ctx, flush): + """Start the aprsd server process.""" + ctx.obj["config_file"] + loglevel = ctx.obj["loglevel"] + quiet = ctx.obj["quiet"] + config = ctx.obj["config"] + + signal.signal(signal.SIGINT, aprsd_main.signal_handler) + signal.signal(signal.SIGTERM, aprsd_main.signal_handler) + + if not quiet: + click.echo("Load config") + + level, msg = utils._check_version() + if level: + LOG.warning(msg) + else: + LOG.info(msg) + LOG.info(f"APRSD Started version: {aprsd.__version__}") + + flat_config = utils.flatten_dict(config) + LOG.info("Using CONFIG values:") + for x in flat_config: + if "password" in x or "aprsd.web.users.admin" in x: + LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX") + else: + LOG.info(f"{x} = {flat_config[x]}") + + if config["aprsd"].get("trace", False): + trace.setup_tracing(["method", "api"]) + stats.APRSDStats(config) + + # Initialize the client factory and create + # The correct client object ready for use + client.ClientFactory.setup(config) + # Make sure we have 1 client transport enabled + if not client.factory.is_client_enabled(): + LOG.error("No Clients are enabled in config.") + sys.exit(-1) + + # Creates the client object + LOG.info("Creating client connection") + client.factory.create().client + + # Now load the msgTrack from disk if any + packets.PacketList(config=config) + if flush: + LOG.debug("Deleting saved MsgTrack.") + messaging.MsgTrack(config=config).flush() + packets.WatchList(config=config) + packets.SeenList(config=config) + else: + # Try and load saved MsgTrack list + LOG.debug("Loading saved MsgTrack object.") + messaging.MsgTrack(config=config).load() + packets.WatchList(config=config).load() + packets.SeenList(config=config).load() + + # Create the initial PM singleton and Register plugins + LOG.info("Loading Plugin Manager and registering plugins") + plugin_manager = plugin.PluginManager(config) + plugin_manager.setup_plugins() + + rx_thread = threads.APRSDRXThread( + msg_queues=threads.msg_queues, + config=config, + ) + rx_thread.start() + + messaging.MsgTrack().restart() + + keepalive = threads.KeepAliveThread(config=config) + keepalive.start() + + web_enabled = config.get("aprsd.web.enabled", default=False) + + if web_enabled: + aprsd_main.flask_enabled = True + (socketio, app) = flask.init_flask(config, loglevel, quiet) + socketio.run( + app, + host=config["aprsd"]["web"]["host"], + port=config["aprsd"]["web"]["port"], + ) + + # If there are items in the msgTracker, then save them + LOG.info("APRSD Exiting.") + return 0 +import logging diff --git a/aprsd/dev.py b/aprsd/dev.py deleted file mode 100644 index eb952e2..0000000 --- a/aprsd/dev.py +++ /dev/null @@ -1,251 +0,0 @@ -# -# Dev.py is used to help develop plugins -# -# -# python included libs -import logging -from logging import NullHandler -from logging.handlers import RotatingFileHandler -import os -import sys - -import click -import click_completion - -# local imports here -import aprsd -from aprsd import client -from aprsd import config as aprsd_config -from aprsd import plugin - - -# setup the global logger -# logging.basicConfig(level=logging.DEBUG) # level=10 -LOG = logging.getLogger("APRSD") - -LOG_LEVELS = { - "CRITICAL": logging.CRITICAL, - "ERROR": logging.ERROR, - "WARNING": logging.WARNING, - "INFO": logging.INFO, - "DEBUG": logging.DEBUG, -} - -CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) - - -def custom_startswith(string, incomplete): - """A custom completion match that supports case insensitive matching.""" - if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"): - string = string.lower() - incomplete = incomplete.lower() - return string.startswith(incomplete) - - -click_completion.core.startswith = custom_startswith -click_completion.init() - - -cmd_help = """Shell completion for click-completion-command -Available shell types: -\b - %s -Default type: auto -""" % "\n ".join( - f"{k:<12} {click_completion.core.shells[k]}" - for k in sorted(click_completion.core.shells.keys()) -) - - -@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS) -@click.version_option() -def main(): - pass - - -@main.command() -@click.option( - "-i", - "--case-insensitive/--no-case-insensitive", - help="Case insensitive completion", -) -@click.argument( - "shell", - required=False, - type=click_completion.DocumentedChoice(click_completion.core.shells), -) -def show(shell, case_insensitive): - """Show the click-completion-command completion code""" - extra_env = ( - {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} - if case_insensitive - else {} - ) - click.echo(click_completion.core.get_code(shell, extra_env=extra_env)) - - -@main.command() -@click.option( - "--append/--overwrite", - help="Append the completion code to the file", - default=None, -) -@click.option( - "-i", - "--case-insensitive/--no-case-insensitive", - help="Case insensitive completion", -) -@click.argument( - "shell", - required=False, - type=click_completion.DocumentedChoice(click_completion.core.shells), -) -@click.argument("path", required=False) -def install(append, case_insensitive, shell, path): - """Install the click-completion-command completion""" - extra_env = ( - {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} - if case_insensitive - else {} - ) - shell, path = click_completion.core.install( - shell=shell, - path=path, - append=append, - extra_env=extra_env, - ) - click.echo(f"{shell} completion installed in {path}") - - -# Setup the logging faciility -# to disable logging to stdout, but still log to file -# use the --quiet option on the cmdln -def setup_logging(config, loglevel, quiet): - log_level = aprsd_config.LOG_LEVELS[loglevel] - LOG.setLevel(log_level) - log_format = config["aprsd"].get( - "logformat", - aprsd_config.DEFAULT_LOG_FORMAT, - ) - date_format = config["aprsd"].get( - "dateformat", - aprsd_config.DEFAULT_DATE_FORMAT, - ) - log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) - log_file = config["aprsd"].get("logfile", None) - if log_file: - fh = RotatingFileHandler( - log_file, maxBytes=(10248576 * 5), - backupCount=4, - ) - else: - fh = NullHandler() - - fh.setFormatter(log_formatter) - LOG.addHandler(fh) - - if not quiet: - sh = logging.StreamHandler(sys.stdout) - sh.setFormatter(log_formatter) - LOG.addHandler(sh) - - -@main.command() -@click.option( - "--loglevel", - default="DEBUG", - show_default=True, - type=click.Choice( - ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], - case_sensitive=False, - ), - show_choices=True, - help="The log level to use for aprsd.log", -) -@click.option( - "-c", - "--config", - "config_file", - show_default=True, - default=aprsd_config.DEFAULT_CONFIG_FILE, - help="The aprsd config file to use for options.", -) -@click.option( - "-p", - "--plugin", - "plugin_path", - show_default=True, - default="aprsd.plugins.wx.WxPlugin", - help="The plugin to run", -) -@click.option( - "-a", - "--all", - "load_all", - show_default=True, - is_flag=True, - default=False, - help="Load all the plugins in config?", -) -@click.option( - "-n", - "--num", - "number", - show_default=True, - default=1, - help="Number of times to call the plugin", -) -@click.argument("fromcall") -@click.argument("message", nargs=-1, required=True) -def test_plugin( - loglevel, - config_file, - plugin_path, - load_all, - number, - fromcall, - message, -): - """APRSD Plugin test app.""" - - config = aprsd_config.parse_config(config_file) - setup_logging(config, loglevel, False) - - LOG.info(f"Test APRSD Plgin version: {aprsd.__version__}") - if type(message) is tuple: - message = " ".join(message) - client.Client(config) - - pm = plugin.PluginManager(config) - if load_all: - pm.setup_plugins() - else: - pm._init() - obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config) - # Register the plugin they wanted tested. - LOG.info( - "Testing plugin {} Version {}".format( - obj.__class__, obj.version, - ), - ) - pm._pluggy_pm.register(obj) - login = config["aprs"]["login"] - - packet = { - "from": fromcall, "addresse": login, - "message_text": message, - "format": "message", - "msgNo": 1, - } - LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'") - - for x in range(number): - reply = pm.run(packet) - # Plugin might have threads, so lets stop them so we can exit. - # obj.stop_threads() - LOG.info(f"Result{x} = '{reply}'") - pm.stop() - - -if __name__ == "__main__": - main() diff --git a/aprsd/listen.py b/aprsd/listen.py deleted file mode 100644 index 40d90b7..0000000 --- a/aprsd/listen.py +++ /dev/null @@ -1,394 +0,0 @@ -# -# Listen on amateur radio aprs-is network for messages and respond to them. -# You must have an amateur radio callsign to use this software. You must -# create an ~/.aprsd/config.yml file with all of the required settings. To -# generate an example config.yml, just run aprsd, then copy the sample config -# to ~/.aprsd/config.yml and edit the settings. -# -# APRS messages: -# l(ocation) = descriptive location of calling station -# w(eather) = temp, (hi/low) forecast, later forecast -# t(ime) = respond with the current time -# f(ortune) = respond with a short fortune -# -email_addr email text = send an email -# -2 = display the last 2 emails received -# p(ing) = respond with Pong!/time -# anything else = respond with usage -# -# (C)2018 Craig Lamparter -# License GPLv2 -# - -# python included libs -import datetime -import logging -from logging import NullHandler -from logging.handlers import RotatingFileHandler -import os -import signal -import sys -import time - -import aprslib -from aprslib.exceptions import LoginError -import click -import click_completion - -# local imports here -import aprsd -from aprsd import client -from aprsd import config as aprsd_config -from aprsd import messaging, stats, threads, trace, utils - - -# setup the global logger -# logging.basicConfig(level=logging.DEBUG) # level=10 -LOG = logging.getLogger("APRSD") - - -CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) - -flask_enabled = False - -# server_event = threading.Event() - -# localization, please edit: -# HOST = "noam.aprs2.net" # north america tier2 servers round robin -# USER = "KM6XXX-9" # callsign of this aprs client with SSID -# PASS = "99999" # google how to generate this -# BASECALLSIGN = "KM6XXX" # callsign of radio in the field to send email -# shortcuts = { -# "aa" : "5551239999@vtext.com", -# "cl" : "craiglamparter@somedomain.org", -# "wb" : "5553909472@vtext.com" -# } - - -def custom_startswith(string, incomplete): - """A custom completion match that supports case insensitive matching.""" - if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"): - string = string.lower() - incomplete = incomplete.lower() - return string.startswith(incomplete) - - -click_completion.core.startswith = custom_startswith -click_completion.init() - - -cmd_help = """Shell completion for click-completion-command -Available shell types: -\b - %s -Default type: auto -""" % "\n ".join( - f"{k:<12} {click_completion.core.shells[k]}" - for k in sorted(click_completion.core.shells.keys()) -) - - -@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS) -@click.version_option() -def main(): - pass - - -@main.command() -@click.option( - "-i", - "--case-insensitive/--no-case-insensitive", - help="Case insensitive completion", -) -@click.argument( - "shell", - required=False, - type=click_completion.DocumentedChoice(click_completion.core.shells), -) -def show(shell, case_insensitive): - """Show the click-completion-command completion code""" - extra_env = ( - {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} - if case_insensitive - else {} - ) - click.echo(click_completion.core.get_code(shell, extra_env=extra_env)) - - -@main.command() -@click.option( - "--append/--overwrite", - help="Append the completion code to the file", - default=None, -) -@click.option( - "-i", - "--case-insensitive/--no-case-insensitive", - help="Case insensitive completion", -) -@click.argument( - "shell", - required=False, - type=click_completion.DocumentedChoice(click_completion.core.shells), -) -@click.argument("path", required=False) -def install(append, case_insensitive, shell, path): - """Install the click-completion-command completion""" - extra_env = ( - {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} - if case_insensitive - else {} - ) - shell, path = click_completion.core.install( - shell=shell, - path=path, - append=append, - extra_env=extra_env, - ) - click.echo(f"{shell} completion installed in {path}") - - -def signal_handler(sig, frame): - global flask_enabled - - threads.APRSDThreadList().stop_all() - if "subprocess" not in str(frame): - LOG.info( - "Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format( - datetime.datetime.now(), - ), - ) - time.sleep(5) - tracker = messaging.MsgTrack() - tracker.save() - LOG.info(stats.APRSDStats()) - # signal.signal(signal.SIGTERM, sys.exit(0)) - # sys.exit(0) - if flask_enabled: - signal.signal(signal.SIGTERM, sys.exit(0)) - - -# Setup the logging faciility -# to disable logging to stdout, but still log to file -# use the --quiet option on the cmdln -def setup_logging(config, loglevel, quiet): - log_level = aprsd_config.LOG_LEVELS[loglevel] - LOG.setLevel(log_level) - log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT) - date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT) - log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) - log_file = config["aprsd"].get("logfile", None) - if log_file: - fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4) - else: - fh = NullHandler() - - fh.setFormatter(log_formatter) - LOG.addHandler(fh) - - imap_logger = None - if config["aprsd"]["email"].get("enabled", False) and config["aprsd"]["email"][ - "imap" - ].get("debug", False): - - imap_logger = logging.getLogger("imapclient.imaplib") - imap_logger.setLevel(log_level) - imap_logger.addHandler(fh) - - if not quiet: - sh = logging.StreamHandler(sys.stdout) - sh.setFormatter(log_formatter) - LOG.addHandler(sh) - if imap_logger: - imap_logger.addHandler(sh) - - -@main.command() -@click.option( - "--loglevel", - default="DEBUG", - show_default=True, - type=click.Choice( - ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], - case_sensitive=False, - ), - show_choices=True, - 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( - "-c", - "--config", - "config_file", - show_default=True, - default=aprsd_config.DEFAULT_CONFIG_FILE, - help="The aprsd config file to use for options.", -) -@click.option( - "--aprs-login", - envvar="APRS_LOGIN", - show_envvar=True, - help="What callsign to send the message from.", -) -@click.option( - "--aprs-password", - envvar="APRS_PASSWORD", - show_envvar=True, - help="the APRS-IS password for APRS_LOGIN", -) -@click.option( - "--no-ack", - "-n", - is_flag=True, - show_default=True, - default=False, - help="Don't wait for an ack, just sent it to APRS-IS and bail.", -) -@click.option("--raw", default=None, help="Send a raw message. Implies --no-ack") -@click.argument("tocallsign", required=False) -@click.argument("command", nargs=-1, required=False) -def listen( - loglevel, - quiet, - config_file, - aprs_login, - aprs_password, - no_ack, - raw, - tocallsign, - command, -): - """Send a message to a callsign via APRS_IS.""" - global got_ack, got_response - - config = aprsd_config.parse_config(config_file) - if not aprs_login: - click.echo("Must set --aprs_login or APRS_LOGIN") - return - - if not aprs_password: - click.echo("Must set --aprs-password or APRS_PASSWORD") - return - - config["aprs"]["login"] = aprs_login - config["aprs"]["password"] = aprs_password - messaging.CONFIG = config - - setup_logging(config, loglevel, quiet) - LOG.info(f"APRSD TEST Started version: {aprsd.__version__}") - if type(command) is tuple: - command = " ".join(command) - if not quiet: - if raw: - LOG.info(f"L'{aprs_login}' R'{raw}'") - else: - LOG.info(f"L'{aprs_login}' To'{tocallsign}' C'{command}'") - - flat_config = utils.flatten_dict(config) - LOG.info("Using CONFIG values:") - for x in flat_config: - if "password" in x or "aprsd.web.users.admin" in x: - LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX") - else: - LOG.info(f"{x} = {flat_config[x]}") - - got_ack = False - got_response = False - - # TODO(walt) - manually edit this list - # prior to running aprsd-listen listen - watch_list = ["k*"] - - # build last seen list - last_seen = {} - for callsign in watch_list: - call = callsign.replace("*", "") - last_seen[call] = datetime.datetime.now() - - LOG.debug("Last seen list") - LOG.debug(last_seen) - - @trace.trace - def rx_packet(packet): - global got_ack, got_response - LOG.debug("Got packet back {}".format(packet["raw"])) - - if packet["from"] in last_seen: - now = datetime.datetime.now() - age = str(now - last_seen[packet["from"]]) - - delta = utils.parse_delta_str(age) - d = datetime.timedelta(**delta) - - max_timeout = { - "seconds": config["aprsd"]["watch_list"]["alert_time_seconds"], - } - max_delta = datetime.timedelta(**max_timeout) - if d > max_delta: - LOG.debug( - "NOTIFY!!! {} last seen {} max age={}".format( - packet["from"], - age, - max_delta, - ), - ) - else: - LOG.debug(f"Not old enough to notify {d} < {max_delta}") - LOG.debug("Update last seen from {}".format(packet["from"])) - last_seen[packet["from"]] = now - else: - LOG.debug( - "ignoring packet because {} not in watch list".format(packet["from"]), - ) - - resp = packet.get("response", None) - if resp == "ack": - ack_num = packet.get("msgNo") - LOG.info(f"We saw an ACK {ack_num} Ignoring") - # messaging.log_packet(packet) - got_ack = True - else: - message = packet.get("message_text", None) - fromcall = packet["from"] - msg_number = packet.get("msgNo", "0") - messaging.log_message( - "Received Message", - packet["raw"], - message, - fromcall=fromcall, - ack=msg_number, - ) - - try: - cl = client.Client(config) - cl.setup_connection() - except LoginError: - sys.exit(-1) - - aprs_client = client.get_client() - - # filter_str = 'b/{}'.format('/'.join(watch_list)) - # LOG.debug("Filter by '{}'".format(filter_str)) - # aprs_client.set_filter(filter_str) - filter_str = "p/{}".format("/".join(watch_list)) - LOG.debug(f"Filter by '{filter_str}'") - aprs_client.set_filter(filter_str) - - while True: - try: - # This will register a packet consumer with aprslib - # When new packets come in the consumer will process - # the packet - aprs_client.consumer(rx_packet, raw=False) - except aprslib.exceptions.ConnectionDrop: - LOG.error("Connection dropped, reconnecting") - time.sleep(5) - # Force the deletion of the client object connected to aprs - # This will cause a reconnect, next time client.get_client() - # is called - cl.reset() - except aprslib.exceptions.UnknownFormat: - LOG.error("Got a shitty packet") - - -if __name__ == "__main__": - main() diff --git a/aprsd/main.py b/aprsd/main.py deleted file mode 100644 index 78ff12c..0000000 --- a/aprsd/main.py +++ /dev/null @@ -1,536 +0,0 @@ -# -# Listen on amateur radio aprs-is network for messages and respond to them. -# You must have an amateur radio callsign to use this software. You must -# create an ~/.aprsd/config.yml file with all of the required settings. To -# generate an example config.yml, just run aprsd, then copy the sample config -# to ~/.aprsd/config.yml and edit the settings. -# -# APRS messages: -# l(ocation) = descriptive location of calling station -# w(eather) = temp, (hi/low) forecast, later forecast -# t(ime) = respond with the current time -# f(ortune) = respond with a short fortune -# -email_addr email text = send an email -# -2 = display the last 2 emails received -# p(ing) = respond with Pong!/time -# anything else = respond with usage -# -# (C)2018 Craig Lamparter -# License GPLv2 -# - -# python included libs -import datetime -import logging -from logging import NullHandler -from logging.handlers import RotatingFileHandler -import os -import signal -import sys -import time - -import aprslib -from aprslib.exceptions import LoginError -import click -import click_completion - -# local imports here -import aprsd -from aprsd import ( - flask, messaging, packets, plugin, stats, threads, trace, utils, -) -from aprsd import client -from aprsd import config as aprsd_config - - -# setup the global logger -# logging.basicConfig(level=logging.DEBUG) # level=10 -LOG = logging.getLogger("APRSD") - - -CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) -flask_enabled = False - - -def custom_startswith(string, incomplete): - """A custom completion match that supports case insensitive matching.""" - if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"): - string = string.lower() - incomplete = incomplete.lower() - return string.startswith(incomplete) - - -click_completion.core.startswith = custom_startswith -click_completion.init() - - -cmd_help = """Shell completion for click-completion-command -Available shell types: -\b - %s -Default type: auto -""" % "\n ".join( - f"{k:<12} {click_completion.core.shells[k]}" - for k in sorted(click_completion.core.shells.keys()) -) - - -@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS) -@click.version_option() -def main(): - pass - - -@main.command() -@click.option( - "-i", - "--case-insensitive/--no-case-insensitive", - help="Case insensitive completion", -) -@click.argument( - "shell", - required=False, - type=click_completion.DocumentedChoice(click_completion.core.shells), -) -def show(shell, case_insensitive): - """Show the click-completion-command completion code""" - extra_env = ( - {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} - if case_insensitive - else {} - ) - click.echo(click_completion.core.get_code(shell, extra_env=extra_env)) - - -@main.command() -@click.option( - "--append/--overwrite", - help="Append the completion code to the file", - default=None, -) -@click.option( - "-i", - "--case-insensitive/--no-case-insensitive", - help="Case insensitive completion", -) -@click.argument( - "shell", - required=False, - type=click_completion.DocumentedChoice(click_completion.core.shells), -) -@click.argument("path", required=False) -def install(append, case_insensitive, shell, path): - """Install the click-completion-command completion""" - extra_env = ( - {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} - if case_insensitive - else {} - ) - shell, path = click_completion.core.install( - shell=shell, - path=path, - append=append, - extra_env=extra_env, - ) - click.echo(f"{shell} completion installed in {path}") - - -def signal_handler(sig, frame): - global flask_enabled - - threads.APRSDThreadList().stop_all() - if "subprocess" not in str(frame): - LOG.info( - "Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format( - datetime.datetime.now(), - ), - ) - time.sleep(1.5) - messaging.MsgTrack().save() - packets.WatchList().save() - packets.SeenList().save() - LOG.info(stats.APRSDStats()) - # signal.signal(signal.SIGTERM, sys.exit(0)) - # sys.exit(0) - if flask_enabled: - signal.signal(signal.SIGTERM, sys.exit(0)) - - -# Setup the logging faciility -# to disable logging to stdout, but still log to file -# use the --quiet option on the cmdln -def setup_logging(config, loglevel, quiet): - log_level = aprsd_config.LOG_LEVELS[loglevel] - LOG.setLevel(log_level) - log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT) - date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT) - log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) - log_file = config["aprsd"].get("logfile", None) - if log_file: - fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4) - else: - fh = NullHandler() - - fh.setFormatter(log_formatter) - LOG.addHandler(fh) - - imap_logger = None - if config.get("aprsd.email.enabled", default=False) and config.get("aprsd.email.imap.debug", default=False): - - imap_logger = logging.getLogger("imapclient.imaplib") - imap_logger.setLevel(log_level) - imap_logger.addHandler(fh) - - if config.get("aprsd.web.enabled", default=False): - qh = logging.handlers.QueueHandler(threads.logging_queue) - q_log_formatter = logging.Formatter( - fmt=aprsd_config.QUEUE_LOG_FORMAT, - datefmt=aprsd_config.QUEUE_DATE_FORMAT, - ) - qh.setFormatter(q_log_formatter) - LOG.addHandler(qh) - - if not quiet: - sh = logging.StreamHandler(sys.stdout) - sh.setFormatter(log_formatter) - LOG.addHandler(sh) - if imap_logger: - imap_logger.addHandler(sh) - - -@main.command() -@click.option( - "--loglevel", - default="INFO", - show_default=True, - type=click.Choice( - ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], - case_sensitive=False, - ), - show_choices=True, - help="The log level to use for aprsd.log", -) -@click.option( - "-c", - "--config", - "config_file", - show_default=True, - default=aprsd_config.DEFAULT_CONFIG_FILE, - help="The aprsd config file to use for options.", -) -def check_version(loglevel, config_file): - config = aprsd_config.parse_config(config_file) - - setup_logging(config, loglevel, False) - level, msg = utils._check_version() - if level: - LOG.warning(msg) - else: - LOG.info(msg) - - -@main.command() -def sample_config(): - """This dumps the config to stdout.""" - click.echo(aprsd_config.dump_default_cfg()) - - -@main.command() -@click.option( - "--loglevel", - default="DEBUG", - show_default=True, - type=click.Choice( - ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], - case_sensitive=False, - ), - show_choices=True, - 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( - "-c", - "--config", - "config_file", - show_default=True, - default=aprsd_config.DEFAULT_CONFIG_FILE, - help="The aprsd config file to use for options.", -) -@click.option( - "--aprs-login", - envvar="APRS_LOGIN", - show_envvar=True, - help="What callsign to send the message from.", -) -@click.option( - "--aprs-password", - envvar="APRS_PASSWORD", - show_envvar=True, - help="the APRS-IS password for APRS_LOGIN", -) -@click.option( - "--no-ack", - "-n", - is_flag=True, - show_default=True, - default=False, - help="Don't wait for an ack, just sent it to APRS-IS and bail.", -) -@click.option( - "--wait-response", - "-w", - is_flag=True, - show_default=True, - default=False, - help="Wait for a response to the message?", -) -@click.option("--raw", default=None, help="Send a raw message. Implies --no-ack") -@click.argument("tocallsign", required=False) -@click.argument("command", nargs=-1, required=False) -def send_message( - loglevel, - quiet, - config_file, - aprs_login, - aprs_password, - no_ack, - wait_response, - raw, - tocallsign, - command, -): - """Send a message to a callsign via APRS_IS.""" - global got_ack, got_response - - config = aprsd_config.parse_config(config_file) - if not aprs_login: - click.echo("Must set --aprs_login or APRS_LOGIN") - return - - if not aprs_password: - click.echo("Must set --aprs-password or APRS_PASSWORD") - return - - config["aprs"]["login"] = aprs_login - config["aprs"]["password"] = aprs_password - messaging.CONFIG = config - - setup_logging(config, loglevel, quiet) - LOG.info(f"APRSD Started version: {aprsd.__version__}") - if type(command) is tuple: - command = " ".join(command) - if not quiet: - if raw: - LOG.info(f"L'{aprs_login}' R'{raw}'") - else: - LOG.info(f"L'{aprs_login}' To'{tocallsign}' C'{command}'") - - packets.WatchList(config=config) - packets.SeenList(config=config) - - got_ack = False - got_response = False - - def rx_packet(packet): - global got_ack, got_response - # LOG.debug("Got packet back {}".format(packet)) - resp = packet.get("response", None) - if resp == "ack": - ack_num = packet.get("msgNo") - LOG.info(f"We got ack for our sent message {ack_num}") - messaging.log_packet(packet) - got_ack = True - else: - message = packet.get("message_text", None) - fromcall = packet["from"] - msg_number = packet.get("msgNo", "0") - messaging.log_message( - "Received Message", - packet["raw"], - message, - fromcall=fromcall, - ack=msg_number, - ) - got_response = True - # Send the ack back? - ack = messaging.AckMessage( - config["aprs"]["login"], - fromcall, - msg_id=msg_number, - ) - ack.send_direct() - - if got_ack: - if wait_response: - if got_response: - sys.exit(0) - else: - sys.exit(0) - - try: - client.ClientFactory.setup(config) - client.factory.create().client - except LoginError: - sys.exit(-1) - - packets.PacketList(config=config) - packets.WatchList(config=config) - - # Send a message - # then we setup a consumer to rx messages - # We should get an ack back as well as a new message - # we should bail after we get the ack and send an ack back for the - # message - if raw: - msg = messaging.RawMessage(raw) - msg.send_direct() - sys.exit(0) - else: - msg = messaging.TextMessage(aprs_login, tocallsign, command) - msg.send_direct() - - if no_ack: - sys.exit(0) - - try: - # This will register a packet consumer with aprslib - # When new packets come in the consumer will process - # the packet - aprs_client = client.factory.create().client - aprs_client.consumer(rx_packet, raw=False) - except aprslib.exceptions.ConnectionDrop: - LOG.error("Connection dropped, reconnecting") - time.sleep(5) - # Force the deletion of the client object connected to aprs - # This will cause a reconnect, next time client.get_client() - # is called - aprs_client.reset() - - -# main() ### -@main.command() -@click.option( - "--loglevel", - default="INFO", - show_default=True, - type=click.Choice( - ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], - case_sensitive=False, - ), - show_choices=True, - 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( - "-c", - "--config", - "config_file", - show_default=True, - default=aprsd_config.DEFAULT_CONFIG_FILE, - help="The aprsd config file to use for options.", -) -@click.option( - "-f", - "--flush", - "flush", - is_flag=True, - show_default=True, - default=False, - help="Flush out all old aged messages on disk.", -) -def server( - loglevel, - quiet, - config_file, - flush, -): - """Start the aprsd server process.""" - global flask_enabled - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - if not quiet: - click.echo("Load config") - - config = aprsd_config.parse_config(config_file) - - setup_logging(config, loglevel, quiet) - level, msg = utils._check_version() - if level: - LOG.warning(msg) - else: - LOG.info(msg) - LOG.info(f"APRSD Started version: {aprsd.__version__}") - - flat_config = utils.flatten_dict(config) - LOG.info("Using CONFIG values:") - for x in flat_config: - if "password" in x or "aprsd.web.users.admin" in x: - LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX") - else: - LOG.info(f"{x} = {flat_config[x]}") - - if config["aprsd"].get("trace", False): - trace.setup_tracing(["method", "api"]) - stats.APRSDStats(config) - - # Initialize the client factory and create - # The correct client object ready for use - client.ClientFactory.setup(config) - # Make sure we have 1 client transport enabled - if not client.factory.is_client_enabled(): - LOG.error("No Clients are enabled in config.") - sys.exit(-1) - - # Creates the client object - LOG.info("Creating client connection") - client.factory.create().client - - # Now load the msgTrack from disk if any - packets.PacketList(config=config) - if flush: - LOG.debug("Deleting saved MsgTrack.") - messaging.MsgTrack(config=config).flush() - packets.WatchList(config=config) - packets.SeenList(config=config) - else: - # Try and load saved MsgTrack list - LOG.debug("Loading saved MsgTrack object.") - messaging.MsgTrack(config=config).load() - packets.WatchList(config=config).load() - packets.SeenList(config=config).load() - - # Create the initial PM singleton and Register plugins - LOG.info("Loading Plugin Manager and registering plugins") - plugin_manager = plugin.PluginManager(config) - plugin_manager.setup_plugins() - - rx_thread = threads.APRSDRXThread( - msg_queues=threads.msg_queues, - config=config, - ) - rx_thread.start() - - messaging.MsgTrack().restart() - - keepalive = threads.KeepAliveThread(config=config) - keepalive.start() - - web_enabled = config.get("aprsd.web.enabled", default=False) - - if web_enabled: - flask_enabled = True - (socketio, app) = flask.init_flask(config, loglevel, quiet) - socketio.run( - app, - host=config["aprsd"]["web"]["host"], - port=config["aprsd"]["web"]["port"], - ) - - # If there are items in the msgTracker, then save them - LOG.info("APRSD Exiting.") - return 0 - - -if __name__ == "__main__": - main() diff --git a/setup.cfg b/setup.cfg index bad2b0c..89e33bd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,9 +33,7 @@ packages = [entry_points] console_scripts = - aprsd = aprsd.main:main - aprsd-listen = aprsd.listen:main - aprsd-dev = aprsd.dev:main + aprsd = aprsd.aprsd:main aprsd-healthcheck = aprsd.healthcheck:main fake_aprs = aprsd.fake_aprs:main