aprsd 1.0.0__py3-none-any.whl → 3.4.2__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 (140) 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 +228 -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 +231 -0
  21. aprsd/cmds/send_message.py +171 -0
  22. aprsd/cmds/server.py +137 -0
  23. aprsd/cmds/webchat.py +674 -0
  24. aprsd/conf/__init__.py +56 -0
  25. aprsd/conf/client.py +131 -0
  26. aprsd/conf/common.py +301 -0
  27. aprsd/conf/log.py +65 -0
  28. aprsd/conf/opts.py +80 -0
  29. aprsd/conf/plugin_common.py +182 -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/packets/__init__.py +20 -0
  36. aprsd/packets/collector.py +79 -0
  37. aprsd/packets/core.py +823 -0
  38. aprsd/packets/log.py +161 -0
  39. aprsd/packets/packet_list.py +110 -0
  40. aprsd/packets/seen_list.py +49 -0
  41. aprsd/packets/tracker.py +103 -0
  42. aprsd/packets/watch_list.py +119 -0
  43. aprsd/plugin.py +474 -284
  44. aprsd/plugin_utils.py +86 -0
  45. aprsd/plugins/__init__.py +0 -0
  46. aprsd/plugins/email.py +709 -0
  47. aprsd/plugins/fortune.py +61 -0
  48. aprsd/plugins/location.py +179 -0
  49. aprsd/plugins/notify.py +61 -0
  50. aprsd/plugins/ping.py +31 -0
  51. aprsd/plugins/time.py +115 -0
  52. aprsd/plugins/version.py +31 -0
  53. aprsd/plugins/weather.py +405 -0
  54. aprsd/stats/__init__.py +20 -0
  55. aprsd/stats/app.py +49 -0
  56. aprsd/stats/collector.py +37 -0
  57. aprsd/threads/__init__.py +11 -0
  58. aprsd/threads/aprsd.py +119 -0
  59. aprsd/threads/keep_alive.py +131 -0
  60. aprsd/threads/log_monitor.py +121 -0
  61. aprsd/threads/registry.py +56 -0
  62. aprsd/threads/rx.py +354 -0
  63. aprsd/threads/stats.py +44 -0
  64. aprsd/threads/tx.py +255 -0
  65. aprsd/utils/__init__.py +218 -0
  66. aprsd/utils/counter.py +51 -0
  67. aprsd/utils/json.py +80 -0
  68. aprsd/utils/objectstore.py +123 -0
  69. aprsd/utils/ring_buffer.py +40 -0
  70. aprsd/utils/trace.py +180 -0
  71. aprsd/web/__init__.py +0 -0
  72. aprsd/web/admin/__init__.py +0 -0
  73. aprsd/web/admin/static/css/index.css +84 -0
  74. aprsd/web/admin/static/css/prism.css +4 -0
  75. aprsd/web/admin/static/css/tabs.css +35 -0
  76. aprsd/web/admin/static/images/Untitled.png +0 -0
  77. aprsd/web/admin/static/images/aprs-symbols-16-0.png +0 -0
  78. aprsd/web/admin/static/images/aprs-symbols-16-1.png +0 -0
  79. aprsd/web/admin/static/images/aprs-symbols-64-0.png +0 -0
  80. aprsd/web/admin/static/images/aprs-symbols-64-1.png +0 -0
  81. aprsd/web/admin/static/images/aprs-symbols-64-2.png +0 -0
  82. aprsd/web/admin/static/js/charts.js +235 -0
  83. aprsd/web/admin/static/js/echarts.js +465 -0
  84. aprsd/web/admin/static/js/logs.js +26 -0
  85. aprsd/web/admin/static/js/main.js +231 -0
  86. aprsd/web/admin/static/js/prism.js +12 -0
  87. aprsd/web/admin/static/js/send-message.js +114 -0
  88. aprsd/web/admin/static/js/tabs.js +28 -0
  89. aprsd/web/admin/templates/index.html +196 -0
  90. aprsd/web/chat/static/css/chat.css +115 -0
  91. aprsd/web/chat/static/css/index.css +66 -0
  92. aprsd/web/chat/static/css/style.css.map +1 -0
  93. aprsd/web/chat/static/css/tabs.css +41 -0
  94. aprsd/web/chat/static/css/upstream/bootstrap.min.css +6 -0
  95. aprsd/web/chat/static/css/upstream/font.woff2 +0 -0
  96. aprsd/web/chat/static/css/upstream/google-fonts.css +23 -0
  97. aprsd/web/chat/static/css/upstream/jquery-ui.css +1311 -0
  98. aprsd/web/chat/static/css/upstream/jquery.toast.css +28 -0
  99. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff +0 -0
  100. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff2 +0 -0
  101. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff +0 -0
  102. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff2 +0 -0
  103. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff +0 -0
  104. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff2 +0 -0
  105. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff +0 -0
  106. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff2 +0 -0
  107. aprsd/web/chat/static/images/Untitled.png +0 -0
  108. aprsd/web/chat/static/images/aprs-symbols-16-0.png +0 -0
  109. aprsd/web/chat/static/images/aprs-symbols-16-1.png +0 -0
  110. aprsd/web/chat/static/images/aprs-symbols-64-0.png +0 -0
  111. aprsd/web/chat/static/images/aprs-symbols-64-1.png +0 -0
  112. aprsd/web/chat/static/images/aprs-symbols-64-2.png +0 -0
  113. aprsd/web/chat/static/images/globe.svg +3 -0
  114. aprsd/web/chat/static/js/gps.js +84 -0
  115. aprsd/web/chat/static/js/main.js +45 -0
  116. aprsd/web/chat/static/js/send-message.js +585 -0
  117. aprsd/web/chat/static/js/tabs.js +28 -0
  118. aprsd/web/chat/static/js/upstream/bootstrap.bundle.min.js +7 -0
  119. aprsd/web/chat/static/js/upstream/jquery-3.7.1.min.js +2 -0
  120. aprsd/web/chat/static/js/upstream/jquery-ui.min.js +13 -0
  121. aprsd/web/chat/static/js/upstream/jquery.toast.js +374 -0
  122. aprsd/web/chat/static/js/upstream/semantic.min.js +11 -0
  123. aprsd/web/chat/static/js/upstream/socket.io.min.js +7 -0
  124. aprsd/web/chat/templates/index.html +139 -0
  125. aprsd/wsgi.py +315 -0
  126. aprsd-3.4.2.dist-info/AUTHORS +13 -0
  127. aprsd-3.4.2.dist-info/LICENSE +175 -0
  128. aprsd-3.4.2.dist-info/METADATA +793 -0
  129. aprsd-3.4.2.dist-info/RECORD +133 -0
  130. {aprsd-1.0.0.dist-info → aprsd-3.4.2.dist-info}/WHEEL +1 -1
  131. aprsd-3.4.2.dist-info/entry_points.txt +8 -0
  132. aprsd/fake_aprs.py +0 -83
  133. aprsd/utils.py +0 -166
  134. aprsd-1.0.0.dist-info/AUTHORS +0 -6
  135. aprsd-1.0.0.dist-info/METADATA +0 -181
  136. aprsd-1.0.0.dist-info/RECORD +0 -13
  137. aprsd-1.0.0.dist-info/entry_points.txt +0 -4
  138. aprsd-1.0.0.dist-info/pbr.json +0 -1
  139. /aprsd/{fuzzyclock.py → utils/fuzzyclock.py} +0 -0
  140. {aprsd-1.0.0.dist-info → aprsd-3.4.2.dist-info}/top_level.txt +0 -0
aprsd/cmds/webchat.py ADDED
@@ -0,0 +1,674 @@
1
+ import datetime
2
+ import json
3
+ import logging
4
+ import math
5
+ import signal
6
+ import sys
7
+ import threading
8
+ import time
9
+
10
+ import click
11
+ import flask
12
+ from flask import request
13
+ from flask_httpauth import HTTPBasicAuth
14
+ from flask_socketio import Namespace, SocketIO
15
+ from geopy.distance import geodesic
16
+ from oslo_config import cfg
17
+ from werkzeug.security import check_password_hash, generate_password_hash
18
+ import wrapt
19
+
20
+ import aprsd
21
+ from aprsd import (
22
+ cli_helper, client, packets, plugin_utils, stats, threads, utils,
23
+ )
24
+ from aprsd.client import client_factory, kiss
25
+ from aprsd.main import cli
26
+ from aprsd.threads import aprsd as aprsd_threads
27
+ from aprsd.threads import keep_alive, rx, tx
28
+ from aprsd.utils import trace
29
+
30
+
31
+ CONF = cfg.CONF
32
+ LOG = logging.getLogger()
33
+ auth = HTTPBasicAuth()
34
+ users = {}
35
+ socketio = None
36
+
37
+ # List of callsigns that we don't want to track/fetch their location
38
+ callsign_no_track = [
39
+ "REPEAT", "WB4BOR-11", "APDW16", "WXNOW", "WXBOT", "BLN0", "BLN1", "BLN2",
40
+ "BLN3", "BLN4", "BLN5", "BLN6", "BLN7", "BLN8", "BLN9",
41
+ ]
42
+
43
+ # Callsign location information
44
+ # callsign: {lat: 0.0, long: 0.0, last_update: datetime}
45
+ callsign_locations = {}
46
+
47
+ flask_app = flask.Flask(
48
+ "aprsd",
49
+ static_url_path="/static",
50
+ static_folder="web/chat/static",
51
+ template_folder="web/chat/templates",
52
+ )
53
+
54
+
55
+ def signal_handler(sig, frame):
56
+
57
+ click.echo("signal_handler: called")
58
+ LOG.info(
59
+ f"Ctrl+C, Sending all threads({len(threads.APRSDThreadList())}) exit! "
60
+ f"Can take up to 10 seconds {datetime.datetime.now()}",
61
+ )
62
+ threads.APRSDThreadList().stop_all()
63
+ if "subprocess" not in str(frame):
64
+ time.sleep(1.5)
65
+ stats.stats_collector.collect()
66
+ LOG.info("Telling flask to bail.")
67
+ signal.signal(signal.SIGTERM, sys.exit(0))
68
+
69
+
70
+ class SentMessages:
71
+
72
+ _instance = None
73
+ lock = threading.Lock()
74
+
75
+ data = {}
76
+
77
+ def __new__(cls, *args, **kwargs):
78
+ """This magic turns this into a singleton."""
79
+ if cls._instance is None:
80
+ cls._instance = super().__new__(cls)
81
+ return cls._instance
82
+
83
+ def is_initialized(self):
84
+ return True
85
+
86
+ @wrapt.synchronized(lock)
87
+ def add(self, msg):
88
+ self.data[msg.msgNo] = msg.__dict__
89
+
90
+ @wrapt.synchronized(lock)
91
+ def __len__(self):
92
+ return len(self.data.keys())
93
+
94
+ @wrapt.synchronized(lock)
95
+ def get(self, id):
96
+ if id in self.data:
97
+ return self.data[id]
98
+
99
+ @wrapt.synchronized(lock)
100
+ def get_all(self):
101
+ return self.data
102
+
103
+ @wrapt.synchronized(lock)
104
+ def set_status(self, id, status):
105
+ if id in self.data:
106
+ self.data[id]["last_update"] = str(datetime.datetime.now())
107
+ self.data[id]["status"] = status
108
+
109
+ @wrapt.synchronized(lock)
110
+ def ack(self, id):
111
+ """The message got an ack!"""
112
+ if id in self.data:
113
+ self.data[id]["last_update"] = str(datetime.datetime.now())
114
+ self.data[id]["ack"] = True
115
+
116
+ @wrapt.synchronized(lock)
117
+ def reply(self, id, packet):
118
+ """We got a packet back from the sent message."""
119
+ if id in self.data:
120
+ self.data[id]["reply"] = packet
121
+
122
+
123
+ # HTTPBasicAuth doesn't work on a class method.
124
+ # This has to be out here. Rely on the APRSDFlask
125
+ # class to initialize the users from the config
126
+ @auth.verify_password
127
+ def verify_password(username, password):
128
+ global users
129
+
130
+ if username in users and check_password_hash(users[username], password):
131
+ return username
132
+
133
+
134
+ def calculate_initial_compass_bearing(point_a, point_b):
135
+ """
136
+ Calculates the bearing between two points.
137
+ The formulae used is the following:
138
+ θ = atan2(sin(Δlong).cos(lat2),
139
+ cos(lat1).sin(lat2) − sin(lat1).cos(lat2).cos(Δlong))
140
+ :Parameters:
141
+ - `pointA: The tuple representing the latitude/longitude for the
142
+ first point. Latitude and longitude must be in decimal degrees
143
+ - `pointB: The tuple representing the latitude/longitude for the
144
+ second point. Latitude and longitude must be in decimal degrees
145
+ :Returns:
146
+ The bearing in degrees
147
+ :Returns Type:
148
+ float
149
+ """
150
+ if (type(point_a) is not tuple) or (type(point_b) is not tuple):
151
+ raise TypeError("Only tuples are supported as arguments")
152
+
153
+ lat1 = math.radians(point_a[0])
154
+ lat2 = math.radians(point_b[0])
155
+
156
+ diff_long = math.radians(point_b[1] - point_a[1])
157
+
158
+ x = math.sin(diff_long) * math.cos(lat2)
159
+ y = math.cos(lat1) * math.sin(lat2) - (
160
+ math.sin(lat1)
161
+ * math.cos(lat2) * math.cos(diff_long)
162
+ )
163
+
164
+ initial_bearing = math.atan2(x, y)
165
+
166
+ # Now we have the initial bearing but math.atan2 return values
167
+ # from -180° to + 180° which is not what we want for a compass bearing
168
+ # The solution is to normalize the initial bearing as shown below
169
+ initial_bearing = math.degrees(initial_bearing)
170
+ compass_bearing = (initial_bearing + 360) % 360
171
+
172
+ return compass_bearing
173
+
174
+
175
+ def _build_location_from_repeat(message):
176
+ # This is a location message Format is
177
+ # ^ld^callsign:latitude,longitude,altitude,course,speed,timestamp
178
+ a = message.split(":")
179
+ LOG.warning(a)
180
+ if len(a) == 2:
181
+ callsign = a[0].replace("^ld^", "")
182
+ b = a[1].split(",")
183
+ LOG.warning(b)
184
+ if len(b) == 6:
185
+ lat = float(b[0])
186
+ lon = float(b[1])
187
+ alt = float(b[2])
188
+ course = float(b[3])
189
+ speed = float(b[4])
190
+ time = int(b[5])
191
+ data = {
192
+ "callsign": callsign,
193
+ "lat": lat,
194
+ "lon": lon,
195
+ "altitude": alt,
196
+ "course": course,
197
+ "speed": speed,
198
+ "lasttime": time,
199
+ }
200
+ LOG.warning(f"Location data from REPEAT {data}")
201
+ return data
202
+
203
+
204
+ def _calculate_location_data(location_data):
205
+ """Calculate all of the location data from data from aprs.fi or REPEAT."""
206
+ lat = location_data["lat"]
207
+ lon = location_data["lon"]
208
+ alt = location_data["altitude"]
209
+ speed = location_data["speed"]
210
+ lasttime = location_data["lasttime"]
211
+ # now calculate distance from our own location
212
+ distance = 0
213
+ if CONF.webchat.latitude and CONF.webchat.longitude:
214
+ our_lat = float(CONF.webchat.latitude)
215
+ our_lon = float(CONF.webchat.longitude)
216
+ distance = geodesic((our_lat, our_lon), (lat, lon)).kilometers
217
+ bearing = calculate_initial_compass_bearing(
218
+ (our_lat, our_lon),
219
+ (lat, lon),
220
+ )
221
+ return {
222
+ "callsign": location_data["callsign"],
223
+ "lat": lat,
224
+ "lon": lon,
225
+ "altitude": alt,
226
+ "course": f"{bearing:0.1f}",
227
+ "speed": speed,
228
+ "lasttime": lasttime,
229
+ "distance": f"{distance:0.3f}",
230
+ }
231
+
232
+
233
+ def send_location_data_to_browser(location_data):
234
+ global socketio
235
+ callsign = location_data["callsign"]
236
+ LOG.info(f"Got location for {callsign} {callsign_locations[callsign]}")
237
+ socketio.emit(
238
+ "callsign_location", callsign_locations[callsign],
239
+ namespace="/sendmsg",
240
+ )
241
+
242
+
243
+ def populate_callsign_location(callsign, data=None):
244
+ """Populate the location for the callsign.
245
+
246
+ if data is passed in, then we have the location already from
247
+ an APRS packet. If data is None, then we need to fetch the
248
+ location from aprs.fi or REPEAT.
249
+ """
250
+ global socketio
251
+ """Fetch the location for the callsign."""
252
+ LOG.debug(f"populate_callsign_location {callsign}")
253
+ if data:
254
+ location_data = _calculate_location_data(data)
255
+ callsign_locations[callsign] = location_data
256
+ send_location_data_to_browser(location_data)
257
+ return
258
+
259
+ # First we are going to try to get the location from aprs.fi
260
+ # if there is no internets, then this will fail and we will
261
+ # fallback to calling REPEAT for the location for the callsign.
262
+ fallback = False
263
+ if not CONF.aprs_fi.apiKey:
264
+ LOG.warning(
265
+ "Config aprs_fi.apiKey is not set. Can't get location from aprs.fi "
266
+ " falling back to sending REPEAT to get location.",
267
+ )
268
+ fallback = True
269
+ else:
270
+ try:
271
+ aprs_data = plugin_utils.get_aprs_fi(CONF.aprs_fi.apiKey, callsign)
272
+ if not len(aprs_data["entries"]):
273
+ LOG.error("Didn't get any entries from aprs.fi")
274
+ return
275
+ lat = float(aprs_data["entries"][0]["lat"])
276
+ lon = float(aprs_data["entries"][0]["lng"])
277
+ try: # altitude not always provided
278
+ alt = float(aprs_data["entries"][0]["altitude"])
279
+ except Exception:
280
+ alt = 0
281
+ location_data = {
282
+ "callsign": callsign,
283
+ "lat": lat,
284
+ "lon": lon,
285
+ "altitude": alt,
286
+ "lasttime": int(aprs_data["entries"][0]["lasttime"]),
287
+ "course": float(aprs_data["entries"][0].get("course", 0)),
288
+ "speed": float(aprs_data["entries"][0].get("speed", 0)),
289
+ }
290
+ location_data = _calculate_location_data(location_data)
291
+ callsign_locations[callsign] = location_data
292
+ send_location_data_to_browser(location_data)
293
+ return
294
+ except Exception as ex:
295
+ LOG.error(f"Failed to fetch aprs.fi '{ex}'")
296
+ LOG.error(ex)
297
+ fallback = True
298
+
299
+ if fallback:
300
+ # We don't have the location data
301
+ # and we can't get it from aprs.fi
302
+ # Send a special message to REPEAT to get the location data
303
+ LOG.info(f"Sending REPEAT to get location for callsign {callsign}.")
304
+ tx.send(
305
+ packets.MessagePacket(
306
+ from_call=CONF.callsign,
307
+ to_call="REPEAT",
308
+ message_text=f"ld {callsign}",
309
+ ),
310
+ )
311
+
312
+
313
+ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
314
+ """Class that handles packets being sent to us."""
315
+
316
+ def __init__(self, packet_queue, socketio):
317
+ self.socketio = socketio
318
+ self.connected = False
319
+ super().__init__(packet_queue)
320
+
321
+ def process_ack_packet(self, packet: packets.AckPacket):
322
+ super().process_ack_packet(packet)
323
+ ack_num = packet.get("msgNo")
324
+ SentMessages().ack(ack_num)
325
+ msg = SentMessages().get(ack_num)
326
+ if msg:
327
+ self.socketio.emit(
328
+ "ack", msg,
329
+ namespace="/sendmsg",
330
+ )
331
+ self.got_ack = True
332
+
333
+ def process_our_message_packet(self, packet: packets.MessagePacket):
334
+ global callsign_locations
335
+ # ok lets see if we have the location for the
336
+ # person we just sent a message to.
337
+ from_call = packet.get("from_call").upper()
338
+ if from_call == "REPEAT":
339
+ # We got a message from REPEAT. Is this a location message?
340
+ message = packet.get("message_text")
341
+ if message.startswith("^ld^"):
342
+ location_data = _build_location_from_repeat(message)
343
+ callsign = location_data["callsign"]
344
+ location_data = _calculate_location_data(location_data)
345
+ callsign_locations[callsign] = location_data
346
+ send_location_data_to_browser(location_data)
347
+ return
348
+ elif (
349
+ from_call not in callsign_locations
350
+ and from_call not in callsign_no_track
351
+ ):
352
+ # We have to ask aprs for the location for the callsign
353
+ # We send a message packet to wb4bor-11 asking for location.
354
+ populate_callsign_location(from_call)
355
+ # Send the packet to the browser.
356
+ self.socketio.emit(
357
+ "new", packet.__dict__,
358
+ namespace="/sendmsg",
359
+ )
360
+
361
+
362
+ class LocationProcessingThread(aprsd_threads.APRSDThread):
363
+ """Class to handle the location processing."""
364
+ def __init__(self):
365
+ super().__init__("LocationProcessingThread")
366
+
367
+ def loop(self):
368
+ pass
369
+
370
+
371
+ def set_config():
372
+ global users
373
+
374
+
375
+ def _get_transport(stats):
376
+ if CONF.aprs_network.enabled:
377
+ transport = "aprs-is"
378
+ aprs_connection = (
379
+ "APRS-IS Server: <a href='http://status.aprs2.net' >"
380
+ "{}</a>".format(stats["APRSClientStats"]["server_string"])
381
+ )
382
+ elif kiss.KISSClient.is_enabled():
383
+ transport = kiss.KISSClient.transport()
384
+ if transport == client.TRANSPORT_TCPKISS:
385
+ aprs_connection = (
386
+ "TCPKISS://{}:{}".format(
387
+ CONF.kiss_tcp.host,
388
+ CONF.kiss_tcp.port,
389
+ )
390
+ )
391
+ elif transport == client.TRANSPORT_SERIALKISS:
392
+ # for pep8 violation
393
+ aprs_connection = (
394
+ "SerialKISS://{}@{} baud".format(
395
+ CONF.kiss_serial.device,
396
+ CONF.kiss_serial.baudrate,
397
+ ),
398
+ )
399
+ elif CONF.fake_client.enabled:
400
+ transport = client.TRANSPORT_FAKE
401
+ aprs_connection = "Fake Client"
402
+
403
+ return transport, aprs_connection
404
+
405
+
406
+ @flask_app.route("/location/<callsign>", methods=["POST"])
407
+ def location(callsign):
408
+ LOG.debug(f"Fetch location for callsign {callsign}")
409
+ populate_callsign_location(callsign)
410
+
411
+
412
+ @auth.login_required
413
+ @flask_app.route("/")
414
+ def index():
415
+ stats = _stats()
416
+
417
+ # For development
418
+ html_template = "index.html"
419
+ LOG.debug(f"Template {html_template}")
420
+
421
+ transport, aprs_connection = _get_transport(stats["stats"])
422
+ LOG.debug(f"transport {transport} aprs_connection {aprs_connection}")
423
+
424
+ stats["transport"] = transport
425
+ stats["aprs_connection"] = aprs_connection
426
+ LOG.debug(f"initial stats = {stats}")
427
+ latitude = CONF.webchat.latitude
428
+ if latitude:
429
+ latitude = float(CONF.webchat.latitude)
430
+
431
+ longitude = CONF.webchat.longitude
432
+ if longitude:
433
+ longitude = float(longitude)
434
+
435
+ return flask.render_template(
436
+ html_template,
437
+ initial_stats=stats,
438
+ aprs_connection=aprs_connection,
439
+ callsign=CONF.callsign,
440
+ version=aprsd.__version__,
441
+ latitude=latitude,
442
+ longitude=longitude,
443
+ )
444
+
445
+
446
+ @auth.login_required
447
+ @flask_app.route("/send-message-status")
448
+ def send_message_status():
449
+ LOG.debug(request)
450
+ msgs = SentMessages()
451
+ info = msgs.get_all()
452
+ return json.dumps(info)
453
+
454
+
455
+ def _stats():
456
+ now = datetime.datetime.now()
457
+
458
+ time_format = "%m-%d-%Y %H:%M:%S"
459
+ stats_dict = stats.stats_collector.collect(serializable=True)
460
+ # Webchat doesnt need these
461
+ if "WatchList" in stats_dict:
462
+ del stats_dict["WatchList"]
463
+ if "SeenList" in stats_dict:
464
+ del stats_dict["SeenList"]
465
+ if "APRSDThreadList" in stats_dict:
466
+ del stats_dict["APRSDThreadList"]
467
+ if "PacketList" in stats_dict:
468
+ del stats_dict["PacketList"]
469
+ if "EmailStats" in stats_dict:
470
+ del stats_dict["EmailStats"]
471
+ if "PluginManager" in stats_dict:
472
+ del stats_dict["PluginManager"]
473
+
474
+ result = {
475
+ "time": now.strftime(time_format),
476
+ "stats": stats_dict,
477
+ }
478
+ return result
479
+
480
+
481
+ @flask_app.route("/stats")
482
+ def get_stats():
483
+ return json.dumps(_stats())
484
+
485
+
486
+ class SendMessageNamespace(Namespace):
487
+ """Class to handle the socketio interactions."""
488
+ got_ack = False
489
+ reply_sent = False
490
+ msg = None
491
+ request = None
492
+
493
+ def __init__(self, namespace=None, config=None):
494
+ super().__init__(namespace)
495
+
496
+ def on_connect(self):
497
+ global socketio
498
+ LOG.debug("Web socket connected")
499
+ socketio.emit(
500
+ "connected", {"data": "/sendmsg Connected"},
501
+ namespace="/sendmsg",
502
+ )
503
+
504
+ def on_disconnect(self):
505
+ LOG.debug("WS Disconnected")
506
+
507
+ def on_send(self, data):
508
+ global socketio
509
+ LOG.debug(f"WS: on_send {data}")
510
+ self.request = data
511
+ data["from"] = CONF.callsign
512
+ path = data.get("path", None)
513
+ if not path:
514
+ path = []
515
+ elif "," in path:
516
+ path_opts = path.split(",")
517
+ path = [x.strip() for x in path_opts]
518
+ else:
519
+ path = [path]
520
+
521
+ pkt = packets.MessagePacket(
522
+ from_call=data["from"],
523
+ to_call=data["to"].upper(),
524
+ message_text=data["message"],
525
+ path=path,
526
+ )
527
+ pkt.prepare()
528
+ self.msg = pkt
529
+ msgs = SentMessages()
530
+ msgs.add(pkt)
531
+ tx.send(pkt)
532
+ msgs.set_status(pkt.msgNo, "Sending")
533
+ obj = msgs.get(pkt.msgNo)
534
+ socketio.emit(
535
+ "sent", obj,
536
+ namespace="/sendmsg",
537
+ )
538
+
539
+ def on_gps(self, data):
540
+ LOG.debug(f"WS on_GPS: {data}")
541
+ lat = data["latitude"]
542
+ long = data["longitude"]
543
+ LOG.debug(f"Lat {lat}")
544
+ LOG.debug(f"Long {long}")
545
+ path = data.get("path", None)
546
+ if not path:
547
+ path = []
548
+ elif "," in path:
549
+ path_opts = path.split(",")
550
+ path = [x.strip() for x in path_opts]
551
+ else:
552
+ path = [path]
553
+
554
+ tx.send(
555
+ packets.BeaconPacket(
556
+ from_call=CONF.callsign,
557
+ to_call="APDW16",
558
+ latitude=lat,
559
+ longitude=long,
560
+ comment="APRSD WebChat Beacon",
561
+ path=path,
562
+ ),
563
+ direct=True,
564
+ )
565
+
566
+ def handle_message(self, data):
567
+ LOG.debug(f"WS Data {data}")
568
+
569
+ def handle_json(self, data):
570
+ LOG.debug(f"WS json {data}")
571
+
572
+ def on_get_callsign_location(self, data):
573
+ LOG.debug(f"on_callsign_location {data}")
574
+ populate_callsign_location(data["callsign"])
575
+
576
+
577
+ @trace.trace
578
+ def init_flask(loglevel, quiet):
579
+ global socketio, flask_app
580
+
581
+ socketio = SocketIO(
582
+ flask_app, logger=False, engineio_logger=False,
583
+ async_mode="threading",
584
+ )
585
+
586
+ socketio.on_namespace(
587
+ SendMessageNamespace(
588
+ "/sendmsg",
589
+ ),
590
+ )
591
+ return socketio
592
+
593
+
594
+ # main() ###
595
+ @cli.command()
596
+ @cli_helper.add_options(cli_helper.common_options)
597
+ @click.option(
598
+ "-f",
599
+ "--flush",
600
+ "flush",
601
+ is_flag=True,
602
+ show_default=True,
603
+ default=False,
604
+ help="Flush out all old aged messages on disk.",
605
+ )
606
+ @click.option(
607
+ "-p",
608
+ "--port",
609
+ "port",
610
+ show_default=True,
611
+ default=None,
612
+ help="Port to listen to web requests. This overrides the config.webchat.web_port setting.",
613
+ )
614
+ @click.pass_context
615
+ @cli_helper.process_standard_options
616
+ def webchat(ctx, flush, port):
617
+ """Web based HAM Radio chat program!"""
618
+ loglevel = ctx.obj["loglevel"]
619
+ quiet = ctx.obj["quiet"]
620
+
621
+ signal.signal(signal.SIGINT, signal_handler)
622
+ signal.signal(signal.SIGTERM, signal_handler)
623
+
624
+ level, msg = utils._check_version()
625
+ if level:
626
+ LOG.warning(msg)
627
+ else:
628
+ LOG.info(msg)
629
+ LOG.info(f"APRSD Started version: {aprsd.__version__}")
630
+
631
+ CONF.log_opt_values(logging.getLogger(), logging.DEBUG)
632
+ user = CONF.admin.user
633
+ users[user] = generate_password_hash(CONF.admin.password)
634
+ if not port:
635
+ port = CONF.webchat.web_port
636
+
637
+ # Initialize the client factory and create
638
+ # The correct client object ready for use
639
+ # Make sure we have 1 client transport enabled
640
+ if not client_factory.is_client_enabled():
641
+ LOG.error("No Clients are enabled in config.")
642
+ sys.exit(-1)
643
+
644
+ if not client_factory.is_client_configured():
645
+ LOG.error("APRS client is not properly configured in config file.")
646
+ sys.exit(-1)
647
+
648
+ keepalive = keep_alive.KeepAliveThread()
649
+ LOG.info("Start KeepAliveThread")
650
+ keepalive.start()
651
+
652
+ socketio = init_flask(loglevel, quiet)
653
+ rx_thread = rx.APRSDPluginRXThread(
654
+ packet_queue=threads.packet_queue,
655
+ )
656
+ rx_thread.start()
657
+ process_thread = WebChatProcessPacketThread(
658
+ packet_queue=threads.packet_queue,
659
+ socketio=socketio,
660
+ )
661
+ process_thread.start()
662
+
663
+ LOG.info("Start socketio.run()")
664
+ socketio.run(
665
+ flask_app,
666
+ # This is broken for now after removing cryptography
667
+ # and pyopenssl
668
+ # ssl_context="adhoc",
669
+ host=CONF.webchat.web_ip,
670
+ port=port,
671
+ allow_unsafe_werkzeug=True,
672
+ )
673
+
674
+ LOG.info("WebChat exiting!!!! Bye.")