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.
Files changed (141) hide show
  1. aprsd/__init__.py +6 -4
  2. aprsd/cli_helper.py +151 -0
  3. aprsd/client/__init__.py +13 -0
  4. aprsd/client/aprsis.py +132 -0
  5. aprsd/client/base.py +105 -0
  6. aprsd/client/drivers/__init__.py +0 -0
  7. aprsd/client/drivers/aprsis.py +224 -0
  8. aprsd/client/drivers/fake.py +73 -0
  9. aprsd/client/drivers/kiss.py +119 -0
  10. aprsd/client/factory.py +88 -0
  11. aprsd/client/fake.py +48 -0
  12. aprsd/client/kiss.py +103 -0
  13. aprsd/client/stats.py +38 -0
  14. aprsd/cmds/__init__.py +0 -0
  15. aprsd/cmds/completion.py +22 -0
  16. aprsd/cmds/dev.py +162 -0
  17. aprsd/cmds/fetch_stats.py +156 -0
  18. aprsd/cmds/healthcheck.py +86 -0
  19. aprsd/cmds/list_plugins.py +319 -0
  20. aprsd/cmds/listen.py +230 -0
  21. aprsd/cmds/send_message.py +174 -0
  22. aprsd/cmds/server.py +142 -0
  23. aprsd/cmds/webchat.py +681 -0
  24. aprsd/conf/__init__.py +56 -0
  25. aprsd/conf/client.py +131 -0
  26. aprsd/conf/common.py +302 -0
  27. aprsd/conf/log.py +65 -0
  28. aprsd/conf/opts.py +80 -0
  29. aprsd/conf/plugin_common.py +191 -0
  30. aprsd/conf/plugin_email.py +105 -0
  31. aprsd/exception.py +13 -0
  32. aprsd/log/__init__.py +0 -0
  33. aprsd/log/log.py +138 -0
  34. aprsd/main.py +104 -867
  35. aprsd/messaging.py +4 -0
  36. aprsd/packets/__init__.py +12 -0
  37. aprsd/packets/collector.py +56 -0
  38. aprsd/packets/core.py +823 -0
  39. aprsd/packets/log.py +143 -0
  40. aprsd/packets/packet_list.py +116 -0
  41. aprsd/packets/seen_list.py +54 -0
  42. aprsd/packets/tracker.py +109 -0
  43. aprsd/packets/watch_list.py +122 -0
  44. aprsd/plugin.py +475 -284
  45. aprsd/plugin_utils.py +86 -0
  46. aprsd/plugins/__init__.py +0 -0
  47. aprsd/plugins/email.py +709 -0
  48. aprsd/plugins/fortune.py +61 -0
  49. aprsd/plugins/location.py +179 -0
  50. aprsd/plugins/notify.py +61 -0
  51. aprsd/plugins/ping.py +31 -0
  52. aprsd/plugins/time.py +115 -0
  53. aprsd/plugins/version.py +31 -0
  54. aprsd/plugins/weather.py +405 -0
  55. aprsd/stats/__init__.py +20 -0
  56. aprsd/stats/app.py +49 -0
  57. aprsd/stats/collector.py +38 -0
  58. aprsd/threads/__init__.py +11 -0
  59. aprsd/threads/aprsd.py +119 -0
  60. aprsd/threads/keep_alive.py +124 -0
  61. aprsd/threads/log_monitor.py +121 -0
  62. aprsd/threads/registry.py +56 -0
  63. aprsd/threads/rx.py +354 -0
  64. aprsd/threads/stats.py +44 -0
  65. aprsd/threads/tx.py +255 -0
  66. aprsd/utils/__init__.py +163 -0
  67. aprsd/utils/counter.py +51 -0
  68. aprsd/utils/json.py +80 -0
  69. aprsd/utils/objectstore.py +123 -0
  70. aprsd/utils/ring_buffer.py +40 -0
  71. aprsd/utils/trace.py +180 -0
  72. aprsd/web/__init__.py +0 -0
  73. aprsd/web/admin/__init__.py +0 -0
  74. aprsd/web/admin/static/css/index.css +84 -0
  75. aprsd/web/admin/static/css/prism.css +4 -0
  76. aprsd/web/admin/static/css/tabs.css +35 -0
  77. aprsd/web/admin/static/images/Untitled.png +0 -0
  78. aprsd/web/admin/static/images/aprs-symbols-16-0.png +0 -0
  79. aprsd/web/admin/static/images/aprs-symbols-16-1.png +0 -0
  80. aprsd/web/admin/static/images/aprs-symbols-64-0.png +0 -0
  81. aprsd/web/admin/static/images/aprs-symbols-64-1.png +0 -0
  82. aprsd/web/admin/static/images/aprs-symbols-64-2.png +0 -0
  83. aprsd/web/admin/static/js/charts.js +235 -0
  84. aprsd/web/admin/static/js/echarts.js +465 -0
  85. aprsd/web/admin/static/js/logs.js +26 -0
  86. aprsd/web/admin/static/js/main.js +231 -0
  87. aprsd/web/admin/static/js/prism.js +12 -0
  88. aprsd/web/admin/static/js/send-message.js +114 -0
  89. aprsd/web/admin/static/js/tabs.js +28 -0
  90. aprsd/web/admin/templates/index.html +196 -0
  91. aprsd/web/chat/static/css/chat.css +115 -0
  92. aprsd/web/chat/static/css/index.css +66 -0
  93. aprsd/web/chat/static/css/style.css.map +1 -0
  94. aprsd/web/chat/static/css/tabs.css +41 -0
  95. aprsd/web/chat/static/css/upstream/bootstrap.min.css +6 -0
  96. aprsd/web/chat/static/css/upstream/font.woff2 +0 -0
  97. aprsd/web/chat/static/css/upstream/google-fonts.css +23 -0
  98. aprsd/web/chat/static/css/upstream/jquery-ui.css +1311 -0
  99. aprsd/web/chat/static/css/upstream/jquery.toast.css +28 -0
  100. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff +0 -0
  101. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff2 +0 -0
  102. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff +0 -0
  103. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff2 +0 -0
  104. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff +0 -0
  105. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff2 +0 -0
  106. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff +0 -0
  107. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff2 +0 -0
  108. aprsd/web/chat/static/images/Untitled.png +0 -0
  109. aprsd/web/chat/static/images/aprs-symbols-16-0.png +0 -0
  110. aprsd/web/chat/static/images/aprs-symbols-16-1.png +0 -0
  111. aprsd/web/chat/static/images/aprs-symbols-64-0.png +0 -0
  112. aprsd/web/chat/static/images/aprs-symbols-64-1.png +0 -0
  113. aprsd/web/chat/static/images/aprs-symbols-64-2.png +0 -0
  114. aprsd/web/chat/static/images/globe.svg +3 -0
  115. aprsd/web/chat/static/js/gps.js +84 -0
  116. aprsd/web/chat/static/js/main.js +45 -0
  117. aprsd/web/chat/static/js/send-message.js +585 -0
  118. aprsd/web/chat/static/js/tabs.js +28 -0
  119. aprsd/web/chat/static/js/upstream/bootstrap.bundle.min.js +7 -0
  120. aprsd/web/chat/static/js/upstream/jquery-3.7.1.min.js +2 -0
  121. aprsd/web/chat/static/js/upstream/jquery-ui.min.js +13 -0
  122. aprsd/web/chat/static/js/upstream/jquery.toast.js +374 -0
  123. aprsd/web/chat/static/js/upstream/semantic.min.js +11 -0
  124. aprsd/web/chat/static/js/upstream/socket.io.min.js +7 -0
  125. aprsd/web/chat/templates/index.html +139 -0
  126. aprsd/wsgi.py +315 -0
  127. aprsd-3.4.1.dist-info/AUTHORS +13 -0
  128. aprsd-3.4.1.dist-info/LICENSE +175 -0
  129. aprsd-3.4.1.dist-info/METADATA +799 -0
  130. aprsd-3.4.1.dist-info/RECORD +134 -0
  131. {aprsd-1.0.0.dist-info → aprsd-3.4.1.dist-info}/WHEEL +1 -1
  132. aprsd-3.4.1.dist-info/entry_points.txt +8 -0
  133. aprsd/fake_aprs.py +0 -83
  134. aprsd/utils.py +0 -166
  135. aprsd-1.0.0.dist-info/AUTHORS +0 -6
  136. aprsd-1.0.0.dist-info/METADATA +0 -181
  137. aprsd-1.0.0.dist-info/RECORD +0 -13
  138. aprsd-1.0.0.dist-info/entry_points.txt +0 -4
  139. aprsd-1.0.0.dist-info/pbr.json +0 -1
  140. /aprsd/{fuzzyclock.py → utils/fuzzyclock.py} +0 -0
  141. {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 email
26
- import imaplib
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 click_completion
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 plugin, utils
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
- def main():
50
+ @click.pass_context
51
+ def cli(ctx):
122
52
  pass
123
53
 
124
54
 
125
- @main.command()
126
- @click.option(
127
- "-i", "--case-insensitive/--no-case-insensitive", help="Case insensitive completion"
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 _imap_connect():
284
- imap_port = CONFIG["imap"].get("port", 143)
285
- use_ssl = CONFIG["imap"].get("use_ssl", False)
286
- host = CONFIG["imap"]["host"]
287
- msg = "{}{}:{}".format("TLS " if use_ssl else "", host, imap_port)
288
- # LOG.debug("Connect to IMAP host {} with user '{}'".
289
- # format(msg, CONFIG['imap']['login']))
290
-
291
- try:
292
- server = imapclient.IMAPClient(
293
- CONFIG["imap"]["host"], port=imap_port, use_uid=True, ssl=use_ssl
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
- except Exception:
296
- LOG.error("Failed to connect IMAP server")
297
- return
298
-
299
- # LOG.debug("Connected to IMAP host {}".format(msg))
300
-
301
- try:
302
- server.login(CONFIG["imap"]["login"], CONFIG["imap"]["password"])
303
- except (imaplib.IMAP4.error, Exception) as e:
304
- msg = getattr(e, "message", repr(e))
305
- LOG.error("Failed to login {}".format(msg))
306
- return
307
-
308
- # LOG.debug("Logged in to IMAP, selecting INBOX")
309
- server.select_folder("INBOX")
310
- return server
311
-
312
-
313
- def _smtp_connect():
314
- host = CONFIG["smtp"]["host"]
315
- smtp_port = CONFIG["smtp"]["port"]
316
- use_ssl = CONFIG["smtp"].get("use_ssl", False)
317
- msg = "{}{}:{}".format("SSL " if use_ssl else "", host, smtp_port)
318
- LOG.debug(
319
- "Connect to SMTP host {} with user '{}'".format(msg, CONFIG["imap"]["login"])
320
- )
321
-
322
- try:
323
- if use_ssl:
324
- server = smtplib.SMTP_SSL(host=host, port=smtp_port)
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
- server = smtplib.SMTP(host=host, port=smtp_port)
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
- def validate_email():
344
- """function to simply ensure we can connect to email services.
124
+ return selected
345
125
 
346
- This helps with failing early during startup.
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
- def resend_email(count, fromcall):
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
- server = _imap_connect()
373
- except Exception as e:
374
- LOG.exception("Failed to Connect to IMAP. Cannot resend email ", e)
375
- return
376
-
377
- messages = server.search(["SINCE", today])
378
- # LOG.debug("%d messages received today" % len(messages))
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
- # let any threads do their thing, then ack
918
- time.sleep(1)
919
- # send an ack last
920
- send_ack(fromcall, ack)
921
- LOG.debug("Main loop end")
922
- # end while True
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__":