aprsd 1.0.0__py3-none-any.whl → 3.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aprsd/__init__.py +6 -4
- aprsd/cli_helper.py +151 -0
- aprsd/client/__init__.py +13 -0
- aprsd/client/aprsis.py +132 -0
- aprsd/client/base.py +105 -0
- aprsd/client/drivers/__init__.py +0 -0
- aprsd/client/drivers/aprsis.py +224 -0
- aprsd/client/drivers/fake.py +73 -0
- aprsd/client/drivers/kiss.py +119 -0
- aprsd/client/factory.py +88 -0
- aprsd/client/fake.py +48 -0
- aprsd/client/kiss.py +103 -0
- aprsd/client/stats.py +38 -0
- aprsd/cmds/__init__.py +0 -0
- aprsd/cmds/completion.py +22 -0
- aprsd/cmds/dev.py +162 -0
- aprsd/cmds/fetch_stats.py +156 -0
- aprsd/cmds/healthcheck.py +86 -0
- aprsd/cmds/list_plugins.py +319 -0
- aprsd/cmds/listen.py +230 -0
- aprsd/cmds/send_message.py +174 -0
- aprsd/cmds/server.py +142 -0
- aprsd/cmds/webchat.py +681 -0
- aprsd/conf/__init__.py +56 -0
- aprsd/conf/client.py +131 -0
- aprsd/conf/common.py +302 -0
- aprsd/conf/log.py +65 -0
- aprsd/conf/opts.py +80 -0
- aprsd/conf/plugin_common.py +191 -0
- aprsd/conf/plugin_email.py +105 -0
- aprsd/exception.py +13 -0
- aprsd/log/__init__.py +0 -0
- aprsd/log/log.py +138 -0
- aprsd/main.py +104 -867
- aprsd/messaging.py +4 -0
- aprsd/packets/__init__.py +12 -0
- aprsd/packets/collector.py +56 -0
- aprsd/packets/core.py +823 -0
- aprsd/packets/log.py +143 -0
- aprsd/packets/packet_list.py +116 -0
- aprsd/packets/seen_list.py +54 -0
- aprsd/packets/tracker.py +109 -0
- aprsd/packets/watch_list.py +122 -0
- aprsd/plugin.py +475 -284
- aprsd/plugin_utils.py +86 -0
- aprsd/plugins/__init__.py +0 -0
- aprsd/plugins/email.py +709 -0
- aprsd/plugins/fortune.py +61 -0
- aprsd/plugins/location.py +179 -0
- aprsd/plugins/notify.py +61 -0
- aprsd/plugins/ping.py +31 -0
- aprsd/plugins/time.py +115 -0
- aprsd/plugins/version.py +31 -0
- aprsd/plugins/weather.py +405 -0
- aprsd/stats/__init__.py +20 -0
- aprsd/stats/app.py +49 -0
- aprsd/stats/collector.py +38 -0
- aprsd/threads/__init__.py +11 -0
- aprsd/threads/aprsd.py +119 -0
- aprsd/threads/keep_alive.py +124 -0
- aprsd/threads/log_monitor.py +121 -0
- aprsd/threads/registry.py +56 -0
- aprsd/threads/rx.py +354 -0
- aprsd/threads/stats.py +44 -0
- aprsd/threads/tx.py +255 -0
- aprsd/utils/__init__.py +163 -0
- aprsd/utils/counter.py +51 -0
- aprsd/utils/json.py +80 -0
- aprsd/utils/objectstore.py +123 -0
- aprsd/utils/ring_buffer.py +40 -0
- aprsd/utils/trace.py +180 -0
- aprsd/web/__init__.py +0 -0
- aprsd/web/admin/__init__.py +0 -0
- aprsd/web/admin/static/css/index.css +84 -0
- aprsd/web/admin/static/css/prism.css +4 -0
- aprsd/web/admin/static/css/tabs.css +35 -0
- aprsd/web/admin/static/images/Untitled.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-16-0.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-16-1.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-64-0.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-64-1.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-64-2.png +0 -0
- aprsd/web/admin/static/js/charts.js +235 -0
- aprsd/web/admin/static/js/echarts.js +465 -0
- aprsd/web/admin/static/js/logs.js +26 -0
- aprsd/web/admin/static/js/main.js +231 -0
- aprsd/web/admin/static/js/prism.js +12 -0
- aprsd/web/admin/static/js/send-message.js +114 -0
- aprsd/web/admin/static/js/tabs.js +28 -0
- aprsd/web/admin/templates/index.html +196 -0
- aprsd/web/chat/static/css/chat.css +115 -0
- aprsd/web/chat/static/css/index.css +66 -0
- aprsd/web/chat/static/css/style.css.map +1 -0
- aprsd/web/chat/static/css/tabs.css +41 -0
- aprsd/web/chat/static/css/upstream/bootstrap.min.css +6 -0
- aprsd/web/chat/static/css/upstream/font.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/google-fonts.css +23 -0
- aprsd/web/chat/static/css/upstream/jquery-ui.css +1311 -0
- aprsd/web/chat/static/css/upstream/jquery.toast.css +28 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff2 +0 -0
- aprsd/web/chat/static/images/Untitled.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-16-0.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-16-1.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-64-0.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-64-1.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-64-2.png +0 -0
- aprsd/web/chat/static/images/globe.svg +3 -0
- aprsd/web/chat/static/js/gps.js +84 -0
- aprsd/web/chat/static/js/main.js +45 -0
- aprsd/web/chat/static/js/send-message.js +585 -0
- aprsd/web/chat/static/js/tabs.js +28 -0
- aprsd/web/chat/static/js/upstream/bootstrap.bundle.min.js +7 -0
- aprsd/web/chat/static/js/upstream/jquery-3.7.1.min.js +2 -0
- aprsd/web/chat/static/js/upstream/jquery-ui.min.js +13 -0
- aprsd/web/chat/static/js/upstream/jquery.toast.js +374 -0
- aprsd/web/chat/static/js/upstream/semantic.min.js +11 -0
- aprsd/web/chat/static/js/upstream/socket.io.min.js +7 -0
- aprsd/web/chat/templates/index.html +139 -0
- aprsd/wsgi.py +315 -0
- aprsd-3.4.1.dist-info/AUTHORS +13 -0
- aprsd-3.4.1.dist-info/LICENSE +175 -0
- aprsd-3.4.1.dist-info/METADATA +799 -0
- aprsd-3.4.1.dist-info/RECORD +134 -0
- {aprsd-1.0.0.dist-info → aprsd-3.4.1.dist-info}/WHEEL +1 -1
- aprsd-3.4.1.dist-info/entry_points.txt +8 -0
- aprsd/fake_aprs.py +0 -83
- aprsd/utils.py +0 -166
- aprsd-1.0.0.dist-info/AUTHORS +0 -6
- aprsd-1.0.0.dist-info/METADATA +0 -181
- aprsd-1.0.0.dist-info/RECORD +0 -13
- aprsd-1.0.0.dist-info/entry_points.txt +0 -4
- aprsd-1.0.0.dist-info/pbr.json +0 -1
- /aprsd/{fuzzyclock.py → utils/fuzzyclock.py} +0 -0
- {aprsd-1.0.0.dist-info → aprsd-3.4.1.dist-info}/top_level.txt +0 -0
aprsd/main.py
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
1
|
#
|
3
2
|
# Listen on amateur radio aprs-is network for messages and respond to them.
|
4
3
|
# You must have an amateur radio callsign to use this software. You must
|
@@ -22,904 +21,142 @@
|
|
22
21
|
|
23
22
|
# python included libs
|
24
23
|
import datetime
|
25
|
-
import
|
26
|
-
import
|
24
|
+
import importlib.metadata as imp
|
25
|
+
from importlib.metadata import version as metadata_version
|
27
26
|
import logging
|
28
|
-
import os
|
29
|
-
import pprint
|
30
|
-
import re
|
31
|
-
import select
|
32
27
|
import signal
|
33
|
-
import smtplib
|
34
|
-
import socket
|
35
28
|
import sys
|
36
|
-
import threading
|
37
29
|
import time
|
38
|
-
from email.mime.text import MIMEText
|
39
|
-
from logging.handlers import RotatingFileHandler
|
40
30
|
|
41
31
|
import click
|
42
|
-
import
|
43
|
-
import imapclient
|
44
|
-
import six
|
45
|
-
import yaml
|
32
|
+
from oslo_config import cfg, generator
|
46
33
|
|
47
34
|
# local imports here
|
48
35
|
import aprsd
|
49
|
-
from aprsd import
|
36
|
+
from aprsd import cli_helper, packets, threads, utils
|
37
|
+
from aprsd.stats import collector
|
38
|
+
|
50
39
|
|
51
40
|
# setup the global logger
|
41
|
+
# log.basicConfig(level=log.DEBUG) # level=10
|
42
|
+
CONF = cfg.CONF
|
52
43
|
LOG = logging.getLogger("APRSD")
|
44
|
+
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
45
|
+
flask_enabled = False
|
53
46
|
|
54
|
-
# global for the config yaml
|
55
|
-
CONFIG = None
|
56
|
-
|
57
|
-
# localization, please edit:
|
58
|
-
# HOST = "noam.aprs2.net" # north america tier2 servers round robin
|
59
|
-
# USER = "KM6XXX-9" # callsign of this aprs client with SSID
|
60
|
-
# PASS = "99999" # google how to generate this
|
61
|
-
# BASECALLSIGN = "KM6XXX" # callsign of radio in the field to send email
|
62
|
-
# shortcuts = {
|
63
|
-
# "aa" : "5551239999@vtext.com",
|
64
|
-
# "cl" : "craiglamparter@somedomain.org",
|
65
|
-
# "wb" : "5553909472@vtext.com"
|
66
|
-
# }
|
67
|
-
|
68
|
-
# globals - tell me a better way to update data being used by threads
|
69
|
-
|
70
|
-
# message_number:time combos so we don't resend the same email in
|
71
|
-
# five mins {int:int}
|
72
|
-
email_sent_dict = {}
|
73
|
-
|
74
|
-
# message_nubmer:ack combos so we stop sending a message after an
|
75
|
-
# ack from radio {int:int}
|
76
|
-
ack_dict = {}
|
77
|
-
|
78
|
-
# current aprs radio message number, increments for each message we
|
79
|
-
# send over rf {int}
|
80
|
-
message_number = 0
|
81
|
-
|
82
|
-
# global telnet connection object -- not needed anymore
|
83
|
-
# tn = None
|
84
|
-
|
85
|
-
# ## set default encoding for python, so body.decode doesn't blow up in email thread
|
86
|
-
# reload(sys)
|
87
|
-
# sys.setdefaultencoding('utf8')
|
88
|
-
|
89
|
-
# import locale
|
90
|
-
# def getpreferredencoding(do_setlocale = True):
|
91
|
-
# return "utf-8"
|
92
|
-
# locale.getpreferredencoding = getpreferredencoding
|
93
|
-
# ## default encoding failed attempts....
|
94
47
|
|
95
|
-
|
96
|
-
def custom_startswith(string, incomplete):
|
97
|
-
"""A custom completion match that supports case insensitive matching."""
|
98
|
-
if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"):
|
99
|
-
string = string.lower()
|
100
|
-
incomplete = incomplete.lower()
|
101
|
-
return string.startswith(incomplete)
|
102
|
-
|
103
|
-
|
104
|
-
click_completion.core.startswith = custom_startswith
|
105
|
-
click_completion.init()
|
106
|
-
|
107
|
-
|
108
|
-
cmd_help = """Shell completion for click-completion-command
|
109
|
-
Available shell types:
|
110
|
-
\b
|
111
|
-
%s
|
112
|
-
Default type: auto
|
113
|
-
""" % "\n ".join(
|
114
|
-
"{:<12} {}".format(k, click_completion.core.shells[k])
|
115
|
-
for k in sorted(click_completion.core.shells.keys())
|
116
|
-
)
|
117
|
-
|
118
|
-
|
119
|
-
@click.group(help=cmd_help)
|
48
|
+
@click.group(cls=cli_helper.AliasedGroup, context_settings=CONTEXT_SETTINGS)
|
120
49
|
@click.version_option()
|
121
|
-
|
50
|
+
@click.pass_context
|
51
|
+
def cli(ctx):
|
122
52
|
pass
|
123
53
|
|
124
54
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
@click.argument(
|
130
|
-
"shell",
|
131
|
-
required=False,
|
132
|
-
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
133
|
-
)
|
134
|
-
def show(shell, case_insensitive):
|
135
|
-
"""Show the click-completion-command completion code"""
|
136
|
-
extra_env = (
|
137
|
-
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
138
|
-
if case_insensitive
|
139
|
-
else {}
|
140
|
-
)
|
141
|
-
click.echo(click_completion.core.get_code(shell, extra_env=extra_env))
|
142
|
-
|
143
|
-
|
144
|
-
@main.command()
|
145
|
-
@click.option(
|
146
|
-
"--append/--overwrite", help="Append the completion code to the file", default=None
|
147
|
-
)
|
148
|
-
@click.option(
|
149
|
-
"-i", "--case-insensitive/--no-case-insensitive", help="Case insensitive completion"
|
150
|
-
)
|
151
|
-
@click.argument(
|
152
|
-
"shell",
|
153
|
-
required=False,
|
154
|
-
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
155
|
-
)
|
156
|
-
@click.argument("path", required=False)
|
157
|
-
def install(append, case_insensitive, shell, path):
|
158
|
-
"""Install the click-completion-command completion"""
|
159
|
-
extra_env = (
|
160
|
-
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
161
|
-
if case_insensitive
|
162
|
-
else {}
|
163
|
-
)
|
164
|
-
shell, path = click_completion.core.install(
|
165
|
-
shell=shell, path=path, append=append, extra_env=extra_env
|
55
|
+
def load_commands():
|
56
|
+
from .cmds import ( # noqa
|
57
|
+
completion, dev, fetch_stats, healthcheck, list_plugins, listen,
|
58
|
+
send_message, server, webchat,
|
166
59
|
)
|
167
|
-
click.echo("%s completion installed in %s" % (shell, path))
|
168
|
-
|
169
|
-
|
170
|
-
def setup_connection():
|
171
|
-
global sock
|
172
|
-
connected = False
|
173
|
-
while not connected:
|
174
|
-
try:
|
175
|
-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
176
|
-
sock.settimeout(300)
|
177
|
-
sock.connect((CONFIG["aprs"]["host"], 14580))
|
178
|
-
connected = True
|
179
|
-
LOG.debug("Connected to server: " + CONFIG["aprs"]["host"])
|
180
|
-
# sock_file = sock.makefile(mode="r")
|
181
|
-
# sock_file = sock.makefile(mode='r', encoding=None, errors=None, newline=None)
|
182
|
-
# sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # disable nagle algorithm
|
183
|
-
# sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 512) # buffer size
|
184
|
-
except Exception as e:
|
185
|
-
LOG.error("Unable to connect to APRS-IS server.\n")
|
186
|
-
print(str(e))
|
187
|
-
time.sleep(5)
|
188
|
-
continue
|
189
|
-
# os._exit(1)
|
190
|
-
user = CONFIG["aprs"]["login"]
|
191
|
-
password = CONFIG["aprs"]["password"]
|
192
|
-
LOG.debug("Logging in to APRS-IS with user '%s'" % user)
|
193
|
-
msg = "user {} pass {} vers aprsd {}\n".format(user, password, aprsd.__version__)
|
194
|
-
sock.send(msg.encode())
|
195
|
-
return sock
|
196
|
-
|
197
|
-
|
198
|
-
def signal_handler(signal, frame):
|
199
|
-
LOG.info("Ctrl+C, exiting.")
|
200
|
-
# sys.exit(0) # thread ignores this
|
201
|
-
os._exit(0)
|
202
|
-
|
203
|
-
|
204
|
-
# end signal_handler
|
205
|
-
|
206
|
-
|
207
|
-
def parse_email(msgid, data, server):
|
208
|
-
envelope = data[b"ENVELOPE"]
|
209
|
-
# email address match
|
210
|
-
# use raw string to avoid invalid escape secquence errors r"string here"
|
211
|
-
f = re.search(r"([\.\w_-]+@[\.\w_-]+)", str(envelope.from_[0]))
|
212
|
-
if f is not None:
|
213
|
-
from_addr = f.group(1)
|
214
|
-
else:
|
215
|
-
from_addr = "noaddr"
|
216
|
-
LOG.debug("Got a message from '{}'".format(from_addr))
|
217
|
-
m = server.fetch([msgid], ["RFC822"])
|
218
|
-
msg = email.message_from_string(m[msgid][b"RFC822"].decode(errors="ignore"))
|
219
|
-
if msg.is_multipart():
|
220
|
-
text = ""
|
221
|
-
html = None
|
222
|
-
# default in case body somehow isn't set below - happened once
|
223
|
-
body = "* unreadable msg received"
|
224
|
-
# this uses the last text or html part in the email, phone companies often put content in an attachment
|
225
|
-
for part in msg.get_payload():
|
226
|
-
if (
|
227
|
-
part.get_content_charset() is None
|
228
|
-
): # or BREAK when we hit a text or html?
|
229
|
-
# We cannot know the character set,
|
230
|
-
# so return decoded "something"
|
231
|
-
text = part.get_payload(decode=True)
|
232
|
-
continue
|
233
|
-
|
234
|
-
charset = part.get_content_charset()
|
235
|
-
|
236
|
-
if part.get_content_type() == "text/plain":
|
237
|
-
text = six.text_type(
|
238
|
-
part.get_payload(decode=True), str(charset), "ignore"
|
239
|
-
).encode("utf8", "replace")
|
240
|
-
|
241
|
-
if part.get_content_type() == "text/html":
|
242
|
-
html = six.text_type(
|
243
|
-
part.get_payload(decode=True), str(charset), "ignore"
|
244
|
-
).encode("utf8", "replace")
|
245
|
-
|
246
|
-
if text is not None:
|
247
|
-
# strip removes white space fore and aft of string
|
248
|
-
body = text.strip()
|
249
|
-
else:
|
250
|
-
body = html.strip()
|
251
|
-
else: # message is not multipart
|
252
|
-
# email.uscc.net sends no charset, blows up unicode function below
|
253
|
-
if msg.get_content_charset() is None:
|
254
|
-
text = six.text_type(
|
255
|
-
msg.get_payload(decode=True), "US-ASCII", "ignore"
|
256
|
-
).encode("utf8", "replace")
|
257
|
-
else:
|
258
|
-
text = six.text_type(
|
259
|
-
msg.get_payload(decode=True), msg.get_content_charset(), "ignore"
|
260
|
-
).encode("utf8", "replace")
|
261
|
-
body = text.strip()
|
262
|
-
|
263
|
-
# FIXED: UnicodeDecodeError: 'ascii' codec can't decode byte 0xf0 in position 6: ordinal not in range(128)
|
264
|
-
# decode with errors='ignore'. be sure to encode it before we return it below, also with errors='ignore'
|
265
|
-
try:
|
266
|
-
body = body.decode(errors="ignore")
|
267
|
-
except Exception as e:
|
268
|
-
LOG.error("Unicode decode failure: " + str(e))
|
269
|
-
LOG.error("Unidoce decode failed: " + str(body))
|
270
|
-
body = "Unreadable unicode msg"
|
271
|
-
# strip all html tags
|
272
|
-
body = re.sub("<[^<]+?>", "", body)
|
273
|
-
# strip CR/LF, make it one line, .rstrip fails at this
|
274
|
-
body = body.replace("\n", " ").replace("\r", " ")
|
275
|
-
# ascii might be out of range, so encode it, removing any error characters
|
276
|
-
body = body.encode(errors="ignore")
|
277
|
-
return (body, from_addr)
|
278
|
-
|
279
|
-
|
280
|
-
# end parse_email
|
281
60
|
|
282
61
|
|
283
|
-
def
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
62
|
+
def main():
|
63
|
+
# First import all the possible commands for the CLI
|
64
|
+
# The commands themselves live in the cmds directory
|
65
|
+
load_commands()
|
66
|
+
utils.load_entry_points("aprsd.extension")
|
67
|
+
cli(auto_envvar_prefix="APRSD")
|
68
|
+
|
69
|
+
|
70
|
+
def signal_handler(sig, frame):
|
71
|
+
global flask_enabled
|
72
|
+
|
73
|
+
click.echo("signal_handler: called")
|
74
|
+
threads.APRSDThreadList().stop_all()
|
75
|
+
if "subprocess" not in str(frame):
|
76
|
+
LOG.info(
|
77
|
+
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format(
|
78
|
+
datetime.datetime.now(),
|
79
|
+
),
|
294
80
|
)
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
81
|
+
time.sleep(1.5)
|
82
|
+
packets.PacketTrack().save()
|
83
|
+
packets.WatchList().save()
|
84
|
+
packets.SeenList().save()
|
85
|
+
packets.PacketList().save()
|
86
|
+
LOG.info(collector.Collector().collect())
|
87
|
+
# signal.signal(signal.SIGTERM, sys.exit(0))
|
88
|
+
# sys.exit(0)
|
89
|
+
|
90
|
+
if flask_enabled:
|
91
|
+
signal.signal(signal.SIGTERM, sys.exit(0))
|
92
|
+
|
93
|
+
|
94
|
+
@cli.command()
|
95
|
+
@cli_helper.add_options(cli_helper.common_options)
|
96
|
+
@click.pass_context
|
97
|
+
@cli_helper.process_standard_options_no_config
|
98
|
+
def check_version(ctx):
|
99
|
+
"""Check this version against the latest in pypi.org."""
|
100
|
+
level, msg = utils._check_version()
|
101
|
+
if level:
|
102
|
+
click.secho(msg, fg="yellow")
|
103
|
+
else:
|
104
|
+
click.secho(msg, fg="green")
|
105
|
+
|
106
|
+
|
107
|
+
@cli.command()
|
108
|
+
@click.pass_context
|
109
|
+
def sample_config(ctx):
|
110
|
+
"""Generate a sample Config file from aprsd and all installed plugins."""
|
111
|
+
|
112
|
+
def _get_selected_entry_points():
|
113
|
+
import sys
|
114
|
+
if sys.version_info < (3, 10):
|
115
|
+
all = imp.entry_points()
|
116
|
+
selected = []
|
117
|
+
if "oslo.config.opts" in all:
|
118
|
+
for x in all["oslo.config.opts"]:
|
119
|
+
if x.group == "oslo.config.opts":
|
120
|
+
selected.append(x)
|
325
121
|
else:
|
326
|
-
|
327
|
-
except Exception:
|
328
|
-
LOG.error("Couldn't connect to SMTP Server")
|
329
|
-
return
|
330
|
-
|
331
|
-
LOG.debug("Connected to smtp host {}".format(msg))
|
332
|
-
|
333
|
-
try:
|
334
|
-
server.login(CONFIG["smtp"]["login"], CONFIG["smtp"]["password"])
|
335
|
-
except Exception:
|
336
|
-
LOG.error("Couldn't connect to SMTP Server")
|
337
|
-
return
|
338
|
-
|
339
|
-
LOG.debug("Logged into SMTP server {}".format(msg))
|
340
|
-
return server
|
341
|
-
|
122
|
+
selected = imp.entry_points(group="oslo.config.opts")
|
342
123
|
|
343
|
-
|
344
|
-
"""function to simply ensure we can connect to email services.
|
124
|
+
return selected
|
345
125
|
|
346
|
-
|
347
|
-
|
348
|
-
LOG.info("Checking IMAP configuration")
|
349
|
-
imap_server = _imap_connect()
|
350
|
-
LOG.info("Checking SMTP configuration")
|
351
|
-
smtp_server = _smtp_connect()
|
352
|
-
|
353
|
-
if imap_server and smtp_server:
|
354
|
-
return True
|
355
|
-
else:
|
356
|
-
return False
|
126
|
+
def get_namespaces():
|
127
|
+
args = []
|
357
128
|
|
129
|
+
# selected = imp.entry_points(group="oslo.config.opts")
|
130
|
+
selected = _get_selected_entry_points()
|
131
|
+
for entry in selected:
|
132
|
+
if "aprsd" in entry.name:
|
133
|
+
args.append("--namespace")
|
134
|
+
args.append(entry.name)
|
358
135
|
|
359
|
-
|
360
|
-
global check_email_delay
|
361
|
-
date = datetime.datetime.now()
|
362
|
-
month = date.strftime("%B")[:3] # Nov, Mar, Apr
|
363
|
-
day = date.day
|
364
|
-
year = date.year
|
365
|
-
today = "%s-%s-%s" % (day, month, year)
|
366
|
-
|
367
|
-
shortcuts = CONFIG["shortcuts"]
|
368
|
-
# swap key/value
|
369
|
-
shortcuts_inverted = dict([[v, k] for k, v in shortcuts.items()])
|
136
|
+
return args
|
370
137
|
|
138
|
+
args = get_namespaces()
|
139
|
+
config_version = metadata_version("oslo.config")
|
140
|
+
logging.basicConfig(level=logging.WARN)
|
141
|
+
conf = cfg.ConfigOpts()
|
142
|
+
generator.register_cli_opts(conf)
|
371
143
|
try:
|
372
|
-
|
373
|
-
except
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
msgexists = False
|
381
|
-
|
382
|
-
messages.sort(reverse=True)
|
383
|
-
del messages[int(count) :] # only the latest "count" messages
|
384
|
-
for message in messages:
|
385
|
-
for msgid, data in list(server.fetch(message, ["ENVELOPE"]).items()):
|
386
|
-
# one at a time, otherwise order is random
|
387
|
-
(body, from_addr) = parse_email(msgid, data, server)
|
388
|
-
# unset seen flag, will stay bold in email client
|
389
|
-
server.remove_flags(msgid, [imapclient.SEEN])
|
390
|
-
if from_addr in shortcuts_inverted:
|
391
|
-
# reverse lookup of a shortcut
|
392
|
-
from_addr = shortcuts_inverted[from_addr]
|
393
|
-
# asterisk indicates a resend
|
394
|
-
reply = "-" + from_addr + " * " + body.decode(errors="ignore")
|
395
|
-
send_message(fromcall, reply)
|
396
|
-
msgexists = True
|
397
|
-
|
398
|
-
if msgexists is not True:
|
399
|
-
stm = time.localtime()
|
400
|
-
h = stm.tm_hour
|
401
|
-
m = stm.tm_min
|
402
|
-
s = stm.tm_sec
|
403
|
-
# append time as a kind of serial number to prevent FT1XDR from
|
404
|
-
# thinking this is a duplicate message.
|
405
|
-
# The FT1XDR pretty much ignores the aprs message number in this
|
406
|
-
# regard. The FTM400 gets it right.
|
407
|
-
reply = "No new msg %s:%s:%s" % (
|
408
|
-
str(h).zfill(2),
|
409
|
-
str(m).zfill(2),
|
410
|
-
str(s).zfill(2),
|
411
|
-
)
|
412
|
-
send_message(fromcall, reply)
|
413
|
-
|
414
|
-
# check email more often since we're resending one now
|
415
|
-
check_email_delay = 60
|
416
|
-
|
417
|
-
server.logout()
|
418
|
-
# end resend_email()
|
419
|
-
|
420
|
-
|
421
|
-
def check_email_thread():
|
422
|
-
global check_email_delay
|
423
|
-
|
424
|
-
# LOG.debug("FIXME initial email delay is 10 seconds")
|
425
|
-
check_email_delay = 60
|
426
|
-
while True:
|
427
|
-
# LOG.debug("Top of check_email_thread.")
|
428
|
-
|
429
|
-
time.sleep(check_email_delay)
|
430
|
-
|
431
|
-
# slowly increase delay every iteration, max out at 300 seconds
|
432
|
-
# any send/receive/resend activity will reset this to 60 seconds
|
433
|
-
if check_email_delay < 300:
|
434
|
-
check_email_delay += 1
|
435
|
-
LOG.debug("check_email_delay is " + str(check_email_delay) + " seconds")
|
436
|
-
|
437
|
-
shortcuts = CONFIG["shortcuts"]
|
438
|
-
# swap key/value
|
439
|
-
shortcuts_inverted = dict([[v, k] for k, v in shortcuts.items()])
|
440
|
-
|
441
|
-
date = datetime.datetime.now()
|
442
|
-
month = date.strftime("%B")[:3] # Nov, Mar, Apr
|
443
|
-
day = date.day
|
444
|
-
year = date.year
|
445
|
-
today = "%s-%s-%s" % (day, month, year)
|
446
|
-
|
447
|
-
server = None
|
448
|
-
try:
|
449
|
-
server = _imap_connect()
|
450
|
-
except Exception as e:
|
451
|
-
LOG.exception("Failed to get IMAP server Can't check email.", e)
|
452
|
-
|
453
|
-
if not server:
|
454
|
-
continue
|
455
|
-
|
456
|
-
messages = server.search(["SINCE", today])
|
457
|
-
# LOG.debug("{} messages received today".format(len(messages)))
|
458
|
-
|
459
|
-
for msgid, data in server.fetch(messages, ["ENVELOPE"]).items():
|
460
|
-
envelope = data[b"ENVELOPE"]
|
461
|
-
# LOG.debug('ID:%d "%s" (%s)' % (msgid, envelope.subject.decode(), envelope.date))
|
462
|
-
f = re.search(
|
463
|
-
r"'([[A-a][0-9]_-]+@[[A-a][0-9]_-\.]+)", str(envelope.from_[0])
|
464
|
-
)
|
465
|
-
if f is not None:
|
466
|
-
from_addr = f.group(1)
|
467
|
-
else:
|
468
|
-
from_addr = "noaddr"
|
469
|
-
|
470
|
-
# LOG.debug("Message flags/tags: " + str(server.get_flags(msgid)[msgid]))
|
471
|
-
# if "APRS" not in server.get_flags(msgid)[msgid]:
|
472
|
-
# in python3, imap tags are unicode. in py2 they're strings. so .decode them to handle both
|
473
|
-
taglist = [
|
474
|
-
x.decode(errors="ignore") for x in server.get_flags(msgid)[msgid]
|
475
|
-
]
|
476
|
-
if "APRS" not in taglist:
|
477
|
-
# if msg not flagged as sent via aprs
|
478
|
-
server.fetch([msgid], ["RFC822"])
|
479
|
-
(body, from_addr) = parse_email(msgid, data, server)
|
480
|
-
# unset seen flag, will stay bold in email client
|
481
|
-
server.remove_flags(msgid, [imapclient.SEEN])
|
482
|
-
|
483
|
-
if from_addr in shortcuts_inverted:
|
484
|
-
# reverse lookup of a shortcut
|
485
|
-
from_addr = shortcuts_inverted[from_addr]
|
486
|
-
|
487
|
-
reply = "-" + from_addr + " " + body.decode(errors="ignore")
|
488
|
-
send_message(CONFIG["ham"]["callsign"], reply)
|
489
|
-
# flag message as sent via aprs
|
490
|
-
server.add_flags(msgid, ["APRS"])
|
491
|
-
# unset seen flag, will stay bold in email client
|
492
|
-
server.remove_flags(msgid, [imapclient.SEEN])
|
493
|
-
# check email more often since we just received an email
|
494
|
-
check_email_delay = 60
|
495
|
-
|
496
|
-
server.logout()
|
497
|
-
|
498
|
-
|
499
|
-
# end check_email()
|
500
|
-
|
501
|
-
|
502
|
-
def send_ack_thread(tocall, ack, retry_count):
|
503
|
-
tocall = tocall.ljust(9) # pad to nine chars
|
504
|
-
line = "{}>APRS::{}:ack{}\n".format(CONFIG["aprs"]["login"], tocall, ack)
|
505
|
-
for i in range(retry_count, 0, -1):
|
506
|
-
LOG.info("Sending ack __________________ Tx({})".format(i))
|
507
|
-
LOG.info("Raw : {}".format(line.rstrip("\n")))
|
508
|
-
LOG.info("To : {}".format(tocall))
|
509
|
-
LOG.info("Ack number : {}".format(ack))
|
510
|
-
sock.send(line.encode())
|
511
|
-
# aprs duplicate detection is 30 secs?
|
512
|
-
# (21 only sends first, 28 skips middle)
|
513
|
-
time.sleep(31)
|
514
|
-
# end_send_ack_thread
|
515
|
-
|
516
|
-
|
517
|
-
def send_ack(tocall, ack):
|
518
|
-
LOG.debug("Send ACK({}:{}) to radio.".format(tocall, ack))
|
519
|
-
retry_count = 3
|
520
|
-
thread = threading.Thread(
|
521
|
-
target=send_ack_thread, name="send_ack", args=(tocall, ack, retry_count)
|
522
|
-
)
|
523
|
-
thread.start()
|
524
|
-
# end send_ack()
|
525
|
-
|
526
|
-
|
527
|
-
def send_message_thread(tocall, message, this_message_number, retry_count):
|
528
|
-
global ack_dict
|
529
|
-
# line = (CONFIG['aprs']['login'] + ">APRS::" + tocall + ":" + message
|
530
|
-
# + "{" + str(this_message_number) + "\n")
|
531
|
-
# line = ("{}>APRS::{}:{}{{{}\n".format( CONFIG['aprs']['login'], tocall, message.encode(errors='ignore'), str(this_message_number),))
|
532
|
-
line = "{}>APRS::{}:{}{{{}\n".format(
|
533
|
-
CONFIG["aprs"]["login"],
|
534
|
-
tocall,
|
535
|
-
message,
|
536
|
-
str(this_message_number),
|
537
|
-
)
|
538
|
-
for i in range(retry_count, 0, -1):
|
539
|
-
LOG.debug("DEBUG: send_message_thread msg:ack combos are: ")
|
540
|
-
LOG.debug(pprint.pformat(ack_dict))
|
541
|
-
if ack_dict[this_message_number] != 1:
|
542
|
-
LOG.info(
|
543
|
-
"Sending message_______________ {}(Tx{})".format(
|
544
|
-
str(this_message_number), str(i)
|
545
|
-
)
|
546
|
-
)
|
547
|
-
LOG.info("Raw : {}".format(line.rstrip("\n")))
|
548
|
-
LOG.info("To : {}".format(tocall))
|
549
|
-
# LOG.info("Message : {}".format(message.encode(errors='ignore')))
|
550
|
-
LOG.info("Message : {}".format(message))
|
551
|
-
# tn.write(line)
|
552
|
-
sock.send(line.encode())
|
553
|
-
# decaying repeats, 31 to 93 second intervals
|
554
|
-
sleeptime = (retry_count - i + 1) * 31
|
555
|
-
time.sleep(sleeptime)
|
556
|
-
else:
|
557
|
-
break
|
144
|
+
conf(args, version=config_version)
|
145
|
+
except cfg.RequiredOptError:
|
146
|
+
conf.print_help()
|
147
|
+
if not sys.argv[1:]:
|
148
|
+
raise SystemExit
|
149
|
+
raise
|
150
|
+
generator.generate(conf)
|
558
151
|
return
|
559
|
-
# end send_message_thread
|
560
|
-
|
561
|
-
|
562
|
-
def send_message(tocall, message):
|
563
|
-
global message_number
|
564
|
-
global ack_dict
|
565
|
-
retry_count = 3
|
566
|
-
if message_number > 98: # global
|
567
|
-
message_number = 0
|
568
|
-
message_number += 1
|
569
|
-
if len(ack_dict) > 90:
|
570
|
-
# empty ack dict if it's really big, could result in key error later
|
571
|
-
LOG.debug(
|
572
|
-
"DEBUG: Length of ack dictionary is big at %s clearing." % len(ack_dict)
|
573
|
-
)
|
574
|
-
ack_dict.clear()
|
575
|
-
LOG.debug(pprint.pformat(ack_dict))
|
576
|
-
LOG.debug(
|
577
|
-
"DEBUG: Cleared ack dictionary, ack_dict length is now %s." % len(ack_dict)
|
578
|
-
)
|
579
|
-
ack_dict[message_number] = 0 # clear ack for this message number
|
580
|
-
tocall = tocall.ljust(9) # pad to nine chars
|
581
|
-
|
582
|
-
# max? ftm400 displays 64, raw msg shows 74
|
583
|
-
# and ftm400-send is max 64. setting this to
|
584
|
-
# 67 displays 64 on the ftm400. (+3 {01 suffix)
|
585
|
-
# feature req: break long ones into two msgs
|
586
|
-
message = message[:67]
|
587
|
-
# We all miss George Carlin
|
588
|
-
message = re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message)
|
589
|
-
thread = threading.Thread(
|
590
|
-
target=send_message_thread,
|
591
|
-
name="send_message",
|
592
|
-
args=(tocall, message, message_number, retry_count),
|
593
|
-
)
|
594
|
-
thread.start()
|
595
|
-
return ()
|
596
|
-
# end send_message()
|
597
|
-
|
598
|
-
|
599
|
-
def process_message(line):
|
600
|
-
f = re.search("^(.*)>", line)
|
601
|
-
fromcall = f.group(1)
|
602
|
-
searchstring = "::%s[ ]*:(.*)" % CONFIG["aprs"]["login"]
|
603
|
-
# verify this, callsign is padded out with spaces to colon
|
604
|
-
m = re.search(searchstring, line)
|
605
|
-
fullmessage = m.group(1)
|
606
|
-
|
607
|
-
ack_attached = re.search("(.*){([0-9A-Z]+)", fullmessage)
|
608
|
-
# ack formats include: {1, {AB}, {12
|
609
|
-
if ack_attached:
|
610
|
-
# "{##" suffix means radio wants an ack back
|
611
|
-
# message content
|
612
|
-
message = ack_attached.group(1)
|
613
|
-
# suffix number to use in ack
|
614
|
-
ack_num = ack_attached.group(2)
|
615
|
-
else:
|
616
|
-
message = fullmessage
|
617
|
-
# ack not requested, but lets send one as 0
|
618
|
-
ack_num = "0"
|
619
|
-
|
620
|
-
LOG.info("Received message______________")
|
621
|
-
LOG.info("Raw : " + line)
|
622
|
-
LOG.info("From : " + fromcall)
|
623
|
-
LOG.info("Message : " + message)
|
624
|
-
LOG.info("Msg number : " + str(ack_num))
|
625
|
-
|
626
|
-
return (fromcall, message, ack_num)
|
627
|
-
# end process_message()
|
628
|
-
|
629
|
-
|
630
|
-
def send_email(to_addr, content):
|
631
|
-
global check_email_delay
|
632
|
-
|
633
|
-
LOG.info("Sending Email_________________")
|
634
|
-
shortcuts = CONFIG["shortcuts"]
|
635
|
-
if to_addr in shortcuts:
|
636
|
-
LOG.info("To : " + to_addr)
|
637
|
-
to_addr = shortcuts[to_addr]
|
638
|
-
LOG.info(" (" + to_addr + ")")
|
639
|
-
subject = CONFIG["ham"]["callsign"]
|
640
|
-
# content = content + "\n\n(NOTE: reply with one line)"
|
641
|
-
LOG.info("Subject : " + subject)
|
642
|
-
LOG.info("Body : " + content)
|
643
|
-
|
644
|
-
# check email more often since there's activity right now
|
645
|
-
check_email_delay = 60
|
646
|
-
|
647
|
-
msg = MIMEText(content)
|
648
|
-
msg["Subject"] = subject
|
649
|
-
msg["From"] = CONFIG["smtp"]["login"]
|
650
|
-
msg["To"] = to_addr
|
651
|
-
server = _smtp_connect()
|
652
|
-
if server:
|
653
|
-
try:
|
654
|
-
server.sendmail(CONFIG["smtp"]["login"], [to_addr], msg.as_string())
|
655
|
-
except Exception as e:
|
656
|
-
msg = getattr(e, "message", repr(e))
|
657
|
-
LOG.error("Sendmail Error!!!! '{}'", msg)
|
658
|
-
server.quit()
|
659
|
-
return -1
|
660
|
-
server.quit()
|
661
|
-
return 0
|
662
|
-
# end send_email
|
663
|
-
|
664
|
-
|
665
|
-
# Setup the logging faciility
|
666
|
-
# to disable logging to stdout, but still log to file
|
667
|
-
# use the --quiet option on the cmdln
|
668
|
-
def setup_logging(loglevel, quiet):
|
669
|
-
levels = {
|
670
|
-
"CRITICAL": logging.CRITICAL,
|
671
|
-
"ERROR": logging.ERROR,
|
672
|
-
"WARNING": logging.WARNING,
|
673
|
-
"INFO": logging.INFO,
|
674
|
-
"DEBUG": logging.DEBUG,
|
675
|
-
}
|
676
|
-
log_level = levels[loglevel]
|
677
|
-
|
678
|
-
LOG.setLevel(log_level)
|
679
|
-
log_format = "%(asctime)s [%(threadName)-12s] [%(levelname)-5.5s]" " %(message)s"
|
680
|
-
date_format = "%m/%d/%Y %I:%M:%S %p"
|
681
|
-
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
682
|
-
fh = RotatingFileHandler(
|
683
|
-
CONFIG["aprs"]["logfile"], maxBytes=(10248576 * 5), backupCount=4
|
684
|
-
)
|
685
|
-
fh.setFormatter(log_formatter)
|
686
|
-
LOG.addHandler(fh)
|
687
|
-
|
688
|
-
if not quiet:
|
689
|
-
sh = logging.StreamHandler(sys.stdout)
|
690
|
-
sh.setFormatter(log_formatter)
|
691
|
-
LOG.addHandler(sh)
|
692
|
-
|
693
|
-
|
694
|
-
@main.command()
|
695
|
-
def sample_config():
|
696
|
-
"""This dumps the config to stdout."""
|
697
|
-
click.echo(yaml.dump(utils.DEFAULT_CONFIG_DICT))
|
698
|
-
|
699
|
-
|
700
|
-
COMMAND_ENVELOPE = {
|
701
|
-
"email": {"command": "^-.*", "function": "command_email"},
|
702
|
-
}
|
703
|
-
|
704
|
-
|
705
|
-
def command_email(fromcall, message, ack):
|
706
|
-
LOG.info("Email COMMAND")
|
707
|
-
|
708
|
-
searchstring = "^" + CONFIG["ham"]["callsign"] + ".*"
|
709
|
-
# only I can do email
|
710
|
-
if re.search(searchstring, fromcall):
|
711
|
-
# digits only, first one is number of emails to resend
|
712
|
-
r = re.search("^-([0-9])[0-9]*$", message)
|
713
|
-
if r is not None:
|
714
|
-
resend_email(r.group(1), fromcall)
|
715
|
-
# -user@address.com body of email
|
716
|
-
elif re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message):
|
717
|
-
# (same search again)
|
718
|
-
a = re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message)
|
719
|
-
if a is not None:
|
720
|
-
to_addr = a.group(1)
|
721
|
-
content = a.group(2)
|
722
|
-
# send recipient link to aprs.fi map
|
723
|
-
if content == "mapme":
|
724
|
-
content = "Click for my location: http://aprs.fi/{}".format(
|
725
|
-
CONFIG["ham"]["callsign"]
|
726
|
-
)
|
727
|
-
too_soon = 0
|
728
|
-
now = time.time()
|
729
|
-
# see if we sent this msg number recently
|
730
|
-
if ack in email_sent_dict:
|
731
|
-
timedelta = now - email_sent_dict[ack]
|
732
|
-
if timedelta < 300: # five minutes
|
733
|
-
too_soon = 1
|
734
|
-
if not too_soon or ack == 0:
|
735
|
-
send_result = send_email(to_addr, content)
|
736
|
-
if send_result != 0:
|
737
|
-
send_message(fromcall, "-" + to_addr + " failed")
|
738
|
-
else:
|
739
|
-
# send_message(fromcall, "-" + to_addr + " sent")
|
740
|
-
if (
|
741
|
-
len(email_sent_dict) > 98
|
742
|
-
): # clear email sent dictionary if somehow goes over 100
|
743
|
-
LOG.debug(
|
744
|
-
"DEBUG: email_sent_dict is big ("
|
745
|
-
+ str(len(email_sent_dict))
|
746
|
-
+ ") clearing out."
|
747
|
-
)
|
748
|
-
email_sent_dict.clear()
|
749
|
-
email_sent_dict[ack] = now
|
750
|
-
else:
|
751
|
-
LOG.info(
|
752
|
-
"Email for message number "
|
753
|
-
+ ack
|
754
|
-
+ " recently sent, not sending again."
|
755
|
-
)
|
756
|
-
else:
|
757
|
-
send_message(fromcall, "Bad email address")
|
758
|
-
|
759
|
-
return (fromcall, message, ack)
|
760
|
-
|
761
|
-
|
762
|
-
# main() ###
|
763
|
-
@main.command()
|
764
|
-
@click.option(
|
765
|
-
"--loglevel",
|
766
|
-
default="DEBUG",
|
767
|
-
show_default=True,
|
768
|
-
type=click.Choice(
|
769
|
-
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], case_sensitive=False
|
770
|
-
),
|
771
|
-
show_choices=True,
|
772
|
-
help="The log level to use for aprsd.log",
|
773
|
-
)
|
774
|
-
@click.option("--quiet", is_flag=True, default=False, help="Don't log to stdout")
|
775
|
-
@click.option(
|
776
|
-
"-c",
|
777
|
-
"--config",
|
778
|
-
"config_file",
|
779
|
-
show_default=True,
|
780
|
-
default=utils.DEFAULT_CONFIG_FILE,
|
781
|
-
help="The aprsd config file to use for options.",
|
782
|
-
)
|
783
|
-
def server(loglevel, quiet, config_file):
|
784
|
-
"""Start the aprsd server process."""
|
785
|
-
global CONFIG
|
786
|
-
|
787
|
-
CONFIG = utils.parse_config(config_file)
|
788
|
-
signal.signal(signal.SIGINT, signal_handler)
|
789
|
-
setup_logging(loglevel, quiet)
|
790
|
-
LOG.info("APRSD Started version: {}".format(aprsd.__version__))
|
791
|
-
|
792
|
-
time.sleep(2)
|
793
|
-
client_sock = setup_connection()
|
794
|
-
valid = validate_email()
|
795
|
-
if not valid:
|
796
|
-
LOG.error("Failed to validate email config options")
|
797
|
-
sys.exit(-1)
|
798
|
-
|
799
|
-
user = CONFIG["aprs"]["login"]
|
800
|
-
LOG.debug("Looking for messages for user '{}'".format(user))
|
801
|
-
# password = CONFIG["aprs"]["password"]
|
802
|
-
# LOG.debug("LOGIN to APRSD with user '%s'" % user)
|
803
|
-
# msg = ("user {} pass {} vers aprsd {}\n".format(user, password, aprsd.__version__))
|
804
|
-
# sock.send(msg.encode())
|
805
|
-
|
806
|
-
time.sleep(2)
|
807
|
-
|
808
|
-
checkemailthread = threading.Thread(
|
809
|
-
target=check_email_thread, name="check_email", args=()
|
810
|
-
) # args must be tuple
|
811
|
-
checkemailthread.start()
|
812
|
-
|
813
|
-
read_sockets = [client_sock]
|
814
|
-
|
815
|
-
# Register plugins
|
816
|
-
pm = plugin.setup_plugins(CONFIG)
|
817
|
-
|
818
|
-
fromcall = message = ack = None
|
819
|
-
while True:
|
820
|
-
LOG.debug("Main loop start")
|
821
|
-
reconnect = False
|
822
|
-
message = None
|
823
|
-
try:
|
824
|
-
readable, writable, exceptional = select.select(read_sockets, [], [])
|
825
|
-
|
826
|
-
for s in readable:
|
827
|
-
data = s.recv(10240).decode().strip()
|
828
|
-
if data:
|
829
|
-
LOG.info("APRS-IS({}): {}".format(len(data), data))
|
830
|
-
searchstring = "::%s" % user
|
831
|
-
if re.search(searchstring, data):
|
832
|
-
LOG.debug(
|
833
|
-
"main: found message addressed to us begin process_message"
|
834
|
-
)
|
835
|
-
(fromcall, message, ack) = process_message(data)
|
836
|
-
else:
|
837
|
-
LOG.error("Connection Failed. retrying to connect")
|
838
|
-
read_sockets.remove(s)
|
839
|
-
s.close()
|
840
|
-
time.sleep(2)
|
841
|
-
client_sock = setup_connection()
|
842
|
-
read_sockets.append(client_sock)
|
843
|
-
reconnect = True
|
844
|
-
|
845
|
-
for s in exceptional:
|
846
|
-
LOG.error("Connection Failed. retrying to connect")
|
847
|
-
read_sockets.remove(s)
|
848
|
-
s.close()
|
849
|
-
time.sleep(2)
|
850
|
-
client_sock = setup_connection()
|
851
|
-
read_sockets.append(client_sock)
|
852
|
-
reconnect = True
|
853
|
-
|
854
|
-
if reconnect:
|
855
|
-
# start the loop over
|
856
|
-
LOG.warning("Starting Main loop over.")
|
857
|
-
continue
|
858
|
-
|
859
|
-
except Exception as e:
|
860
|
-
LOG.exception(e)
|
861
|
-
LOG.error("%s" % str(e))
|
862
|
-
if (
|
863
|
-
str(e) == "closed_socket"
|
864
|
-
or str(e) == "timed out"
|
865
|
-
or str(e) == "Temporary failure in name resolution"
|
866
|
-
or str(e) == "Network is unreachable"
|
867
|
-
):
|
868
|
-
LOG.error("Attempting to reconnect.")
|
869
|
-
sock.shutdown(0)
|
870
|
-
sock.close()
|
871
|
-
client_sock = setup_connection()
|
872
|
-
continue
|
873
|
-
LOG.error("Unexpected error: " + str(e))
|
874
|
-
LOG.error("Continuing anyway.")
|
875
|
-
time.sleep(5)
|
876
|
-
continue # don't know what failed, so wait and then continue main loop again
|
877
|
-
|
878
|
-
if not message:
|
879
|
-
continue
|
880
|
-
|
881
|
-
LOG.debug("Process the command. '{}'".format(message))
|
882
|
-
|
883
|
-
# ACK (ack##)
|
884
|
-
# Custom command due to needing to avoid send_ack
|
885
|
-
if re.search("^ack[0-9]+", message):
|
886
|
-
LOG.debug("ACK")
|
887
|
-
# put message_number:1 in dict to record the ack
|
888
|
-
a = re.search("^ack([0-9]+)", message)
|
889
|
-
ack_dict.update({int(a.group(1)): 1})
|
890
|
-
continue # break out of this so we don't ack an ack at the end
|
891
|
-
|
892
|
-
# call our `myhook` hook
|
893
|
-
found_command = False
|
894
|
-
results = pm.hook.run(fromcall=fromcall, message=message, ack=ack)
|
895
|
-
for reply in results:
|
896
|
-
found_command = True
|
897
|
-
send_message(fromcall, reply)
|
898
|
-
|
899
|
-
# it's not an ack, so try and process user input
|
900
|
-
for key in COMMAND_ENVELOPE:
|
901
|
-
if re.search(COMMAND_ENVELOPE[key]["command"], message):
|
902
|
-
# now call the registered function
|
903
|
-
funct = COMMAND_ENVELOPE[key]["function"]
|
904
|
-
(fromcall, message, ack) = globals()[funct](fromcall, message, ack)
|
905
|
-
found_command = True
|
906
|
-
|
907
|
-
if not found_command:
|
908
|
-
plugins = pm.get_plugins()
|
909
|
-
names = [x.command_name for x in plugins]
|
910
|
-
for k in COMMAND_ENVELOPE.keys():
|
911
|
-
names.append(k)
|
912
|
-
names.sort()
|
913
152
|
|
914
|
-
reply = "Usage: {}".format(", ".join(names))
|
915
|
-
send_message(fromcall, reply)
|
916
153
|
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
154
|
+
@cli.command()
|
155
|
+
@click.pass_context
|
156
|
+
def version(ctx):
|
157
|
+
"""Show the APRSD version."""
|
158
|
+
click.echo(click.style("APRSD Version : ", fg="white"), nl=False)
|
159
|
+
click.secho(f"{aprsd.__version__}", fg="yellow", bold=True)
|
923
160
|
|
924
161
|
|
925
162
|
if __name__ == "__main__":
|