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/plugin.py
CHANGED
@@ -1,149 +1,190 @@
|
|
1
|
-
|
1
|
+
from __future__ import annotations
|
2
|
+
|
2
3
|
import abc
|
3
|
-
import
|
4
|
-
import imp
|
4
|
+
import importlib
|
5
5
|
import inspect
|
6
|
-
import json
|
7
6
|
import logging
|
8
|
-
import os
|
9
7
|
import re
|
10
|
-
import
|
11
|
-
import
|
8
|
+
import textwrap
|
9
|
+
import threading
|
12
10
|
|
11
|
+
from oslo_config import cfg
|
13
12
|
import pluggy
|
14
|
-
import requests
|
15
|
-
import six
|
16
13
|
|
17
|
-
|
14
|
+
import aprsd
|
15
|
+
from aprsd import client, packets, threads
|
16
|
+
from aprsd.packets import watch_list
|
17
|
+
|
18
18
|
|
19
19
|
# setup the global logger
|
20
|
+
CONF = cfg.CONF
|
20
21
|
LOG = logging.getLogger("APRSD")
|
21
22
|
|
23
|
+
CORE_MESSAGE_PLUGINS = [
|
24
|
+
"aprsd.plugins.email.EmailPlugin",
|
25
|
+
"aprsd.plugins.fortune.FortunePlugin",
|
26
|
+
"aprsd.plugins.location.LocationPlugin",
|
27
|
+
"aprsd.plugins.ping.PingPlugin",
|
28
|
+
"aprsd.plugins.query.QueryPlugin",
|
29
|
+
"aprsd.plugins.time.TimePlugin",
|
30
|
+
"aprsd.plugins.weather.USWeatherPlugin",
|
31
|
+
"aprsd.plugins.version.VersionPlugin",
|
32
|
+
]
|
33
|
+
|
34
|
+
CORE_NOTIFY_PLUGINS = [
|
35
|
+
"aprsd.plugins.notify.NotifySeenPlugin",
|
36
|
+
]
|
37
|
+
|
22
38
|
hookspec = pluggy.HookspecMarker("aprsd")
|
23
39
|
hookimpl = pluggy.HookimplMarker("aprsd")
|
24
40
|
|
25
|
-
CORE_PLUGINS = [
|
26
|
-
"FortunePlugin",
|
27
|
-
"LocationPlugin",
|
28
|
-
"PingPlugin",
|
29
|
-
"TimePlugin",
|
30
|
-
"WeatherPlugin",
|
31
|
-
]
|
32
41
|
|
42
|
+
class APRSDPluginSpec:
|
43
|
+
"""A hook specification namespace."""
|
33
44
|
|
34
|
-
|
35
|
-
|
45
|
+
@hookspec
|
46
|
+
def filter(self, packet: type[packets.Packet]):
|
47
|
+
"""My special little hook that you can customize."""
|
36
48
|
|
37
|
-
LOG.info("Loading Core APRSD Command Plugins")
|
38
|
-
enabled_plugins = config["aprsd"].get("enabled_plugins", None)
|
39
|
-
pm = pluggy.PluginManager("aprsd")
|
40
|
-
pm.add_hookspecs(APRSDCommandSpec)
|
41
|
-
for p_name in CORE_PLUGINS:
|
42
|
-
plugin_obj = None
|
43
|
-
if enabled_plugins:
|
44
|
-
if p_name in enabled_plugins:
|
45
|
-
plugin_obj = globals()[p_name](config)
|
46
|
-
else:
|
47
|
-
# Enabled plugins isn't set, so we default to loading all of
|
48
|
-
# the core plugins.
|
49
|
-
plugin_obj = globals()[p_name](config)
|
50
49
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
50
|
+
class APRSDPluginBase(metaclass=abc.ABCMeta):
|
51
|
+
"""The base class for all APRSD Plugins."""
|
52
|
+
|
53
|
+
config = None
|
54
|
+
rx_count = 0
|
55
|
+
tx_count = 0
|
56
|
+
version = aprsd.__version__
|
57
|
+
|
58
|
+
# Holds the list of APRSDThreads that the plugin creates
|
59
|
+
threads = []
|
60
|
+
# Set this in setup()
|
61
|
+
enabled = False
|
62
|
+
|
63
|
+
def __init__(self):
|
64
|
+
self.message_counter = 0
|
65
|
+
self.setup()
|
66
|
+
self.threads = self.create_threads() or []
|
67
|
+
self.start_threads()
|
68
|
+
|
69
|
+
def start_threads(self) -> None:
|
70
|
+
if self.enabled and self.threads:
|
71
|
+
if not isinstance(self.threads, list):
|
72
|
+
self.threads = [self.threads]
|
73
|
+
|
74
|
+
try:
|
75
|
+
for thread in self.threads:
|
76
|
+
if isinstance(thread, threads.APRSDThread):
|
77
|
+
thread.start()
|
78
|
+
else:
|
79
|
+
LOG.error(
|
80
|
+
"Can't start thread {}:{}, Must be a child "
|
81
|
+
"of aprsd.threads.APRSDThread".format(
|
82
|
+
self,
|
83
|
+
thread,
|
84
|
+
),
|
74
85
|
)
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
if plugin_obj:
|
81
|
-
LOG.info(
|
82
|
-
"Registering Command plugin '{}'({}) '{}'".format(
|
83
|
-
o["name"], o["obj"].version, o["obj"].command_regex
|
84
|
-
)
|
86
|
+
except Exception:
|
87
|
+
LOG.error(
|
88
|
+
"Failed to start threads for plugin {}".format(
|
89
|
+
self,
|
90
|
+
),
|
85
91
|
)
|
86
|
-
pm.register(o["obj"])
|
87
92
|
|
88
|
-
|
89
|
-
|
93
|
+
@property
|
94
|
+
def message_count(self) -> int:
|
95
|
+
return self.message_counter
|
90
96
|
|
91
|
-
|
92
|
-
|
97
|
+
def help(self) -> str:
|
98
|
+
return "Help!"
|
93
99
|
|
100
|
+
@abc.abstractmethod
|
101
|
+
def setup(self):
|
102
|
+
"""Do any plugin setup here."""
|
103
|
+
self.enabled = True
|
94
104
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
self.config = config
|
105
|
+
def create_threads(self):
|
106
|
+
"""Gives the plugin writer the ability start a background thread."""
|
107
|
+
return []
|
99
108
|
|
100
|
-
def
|
101
|
-
|
102
|
-
pattern = "*.py"
|
109
|
+
def rx_inc(self):
|
110
|
+
self.rx_count += 1
|
103
111
|
|
104
|
-
|
112
|
+
def tx_inc(self):
|
113
|
+
self.tx_count += 1
|
105
114
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
name, found_module[0], found_module[1], found_module[2]
|
112
|
-
)
|
113
|
-
for mem_name, obj in inspect.getmembers(module):
|
114
|
-
if (
|
115
|
-
inspect.isclass(obj)
|
116
|
-
and inspect.getmodule(obj) is module
|
117
|
-
and self.is_plugin(obj)
|
118
|
-
):
|
119
|
-
self.obj_list.append(
|
120
|
-
{"name": mem_name, "obj": obj(self.config)}
|
121
|
-
)
|
122
|
-
|
123
|
-
return self.obj_list
|
115
|
+
def stop_threads(self):
|
116
|
+
"""Stop any threads this plugin might have created."""
|
117
|
+
for thread in self.threads:
|
118
|
+
if isinstance(thread, threads.APRSDThread):
|
119
|
+
thread.stop()
|
124
120
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
return True
|
121
|
+
@abc.abstractmethod
|
122
|
+
def filter(self, packet: type[packets.Packet]) -> str | packets.MessagePacket:
|
123
|
+
pass
|
129
124
|
|
130
|
-
|
125
|
+
@abc.abstractmethod
|
126
|
+
def process(self, packet: type[packets.Packet]):
|
127
|
+
"""This is called when the filter passes."""
|
128
|
+
|
129
|
+
|
130
|
+
class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
131
|
+
"""Base plugin class for all notification APRSD plugins.
|
132
|
+
|
133
|
+
All these plugins will get every packet seen by APRSD's
|
134
|
+
registered list of HAM callsigns in the config file's
|
135
|
+
watch_list.
|
136
|
+
|
137
|
+
When you want to 'notify' something when a packet is seen
|
138
|
+
by a particular HAM callsign, write a plugin based off of
|
139
|
+
this class.
|
140
|
+
"""
|
141
|
+
|
142
|
+
def setup(self):
|
143
|
+
# if we have a watch list enabled, we need to add filtering
|
144
|
+
# to enable seeing packets from the watch list.
|
145
|
+
if CONF.watch_list.enabled:
|
146
|
+
# watch list is enabled
|
147
|
+
self.enabled = True
|
148
|
+
watch_list = CONF.watch_list.callsigns
|
149
|
+
# make sure the timeout is set or this doesn't work
|
150
|
+
if watch_list:
|
151
|
+
aprs_client = client.client_factory.create().client
|
152
|
+
filter_str = "b/{}".format("/".join(watch_list))
|
153
|
+
aprs_client.set_filter(filter_str)
|
154
|
+
else:
|
155
|
+
LOG.warning("Watch list enabled, but no callsigns set.")
|
131
156
|
|
157
|
+
@hookimpl
|
158
|
+
def filter(self, packet: type[packets.Packet]) -> str | packets.MessagePacket:
|
159
|
+
result = packets.NULL_MESSAGE
|
160
|
+
if self.enabled:
|
161
|
+
wl = watch_list.WatchList()
|
162
|
+
if wl.callsign_in_watchlist(packet.from_call):
|
163
|
+
# packet is from a callsign in the watch list
|
164
|
+
self.rx_inc()
|
165
|
+
try:
|
166
|
+
result = self.process(packet)
|
167
|
+
except Exception as ex:
|
168
|
+
LOG.error(
|
169
|
+
"Plugin {} failed to process packet {}".format(
|
170
|
+
self.__class__, ex,
|
171
|
+
),
|
172
|
+
)
|
173
|
+
if result:
|
174
|
+
self.tx_inc()
|
175
|
+
else:
|
176
|
+
LOG.warning(f"{self.__class__} plugin is not enabled")
|
132
177
|
|
133
|
-
|
134
|
-
"""A hook specification namespace."""
|
178
|
+
return result
|
135
179
|
|
136
|
-
@hookspec
|
137
|
-
def run(self, fromcall, message, ack):
|
138
|
-
"""My special little hook that you can customize."""
|
139
|
-
pass
|
140
180
|
|
181
|
+
class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
182
|
+
"""Base Message plugin class.
|
141
183
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
self.config = config
|
184
|
+
When you want to search for a particular command in an
|
185
|
+
APRSD message and send a direct reply, write a plugin
|
186
|
+
based off of this class.
|
187
|
+
"""
|
147
188
|
|
148
189
|
@property
|
149
190
|
def command_name(self):
|
@@ -155,196 +196,346 @@ class APRSDPluginBase(object):
|
|
155
196
|
"""The regex to match from the caller"""
|
156
197
|
raise NotImplementedError
|
157
198
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
@hookimpl
|
164
|
-
def run(self, fromcall, message, ack):
|
165
|
-
if re.search(self.command_regex, message):
|
166
|
-
return self.command(fromcall, message, ack)
|
199
|
+
def help(self):
|
200
|
+
return "{}: {}".format(
|
201
|
+
self.command_name.lower(),
|
202
|
+
self.command_regex,
|
203
|
+
)
|
167
204
|
|
168
|
-
|
169
|
-
|
170
|
-
|
205
|
+
def setup(self):
|
206
|
+
"""Do any plugin setup here."""
|
207
|
+
self.enabled = True
|
171
208
|
|
172
|
-
|
173
|
-
|
174
|
-
""
|
175
|
-
|
209
|
+
@hookimpl
|
210
|
+
def filter(self, packet: packets.MessagePacket) -> str | packets.MessagePacket:
|
211
|
+
LOG.debug(f"{self.__class__.__name__} called")
|
212
|
+
if not self.enabled:
|
213
|
+
result = f"{self.__class__.__name__} isn't enabled"
|
214
|
+
LOG.warning(result)
|
215
|
+
return result
|
216
|
+
|
217
|
+
if not isinstance(packet, packets.MessagePacket):
|
218
|
+
LOG.warning(f"{self.__class__.__name__} Got a {packet.__class__.__name__} ignoring")
|
219
|
+
return packets.NULL_MESSAGE
|
220
|
+
|
221
|
+
result = None
|
222
|
+
|
223
|
+
message = packet.message_text
|
224
|
+
tocall = packet.to_call
|
225
|
+
|
226
|
+
# Only process messages destined for us
|
227
|
+
# and is an APRS message format and has a message.
|
228
|
+
if (
|
229
|
+
tocall == CONF.callsign
|
230
|
+
and isinstance(packet, packets.MessagePacket)
|
231
|
+
and message
|
232
|
+
):
|
233
|
+
if re.search(self.command_regex, message, re.IGNORECASE):
|
234
|
+
self.rx_inc()
|
235
|
+
try:
|
236
|
+
result = self.process(packet)
|
237
|
+
except Exception as ex:
|
238
|
+
LOG.error(
|
239
|
+
"Plugin {} failed to process packet {}".format(
|
240
|
+
self.__class__, ex,
|
241
|
+
),
|
242
|
+
)
|
243
|
+
LOG.exception(ex)
|
244
|
+
if result:
|
245
|
+
self.tx_inc()
|
176
246
|
|
247
|
+
return result
|
177
248
|
|
178
|
-
class FortunePlugin(APRSDPluginBase):
|
179
|
-
"""Fortune."""
|
180
249
|
|
181
|
-
|
182
|
-
|
183
|
-
command_name = "fortune"
|
250
|
+
class APRSFIKEYMixin:
|
251
|
+
"""Mixin class to enable checking the existence of the aprs.fi apiKey."""
|
184
252
|
|
185
|
-
def
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
253
|
+
def ensure_aprs_fi_key(self):
|
254
|
+
if not CONF.aprs_fi.apiKey:
|
255
|
+
LOG.error("Config aprs_fi.apiKey is not set")
|
256
|
+
self.enabled = False
|
257
|
+
else:
|
258
|
+
self.enabled = True
|
259
|
+
|
260
|
+
|
261
|
+
class HelpPlugin(APRSDRegexCommandPluginBase):
|
262
|
+
"""Help Plugin that is always enabled.
|
263
|
+
|
264
|
+
This plugin is in this file to prevent a circular import.
|
265
|
+
"""
|
266
|
+
|
267
|
+
command_regex = "^[hH]"
|
268
|
+
command_name = "help"
|
269
|
+
|
270
|
+
def help(self):
|
271
|
+
return "Help: send APRS help or help <plugin>"
|
272
|
+
|
273
|
+
def process(self, packet: packets.MessagePacket):
|
274
|
+
LOG.info("HelpPlugin")
|
275
|
+
# fromcall = packet.get("from")
|
276
|
+
message = packet.message_text
|
277
|
+
# ack = packet.get("msgNo", "0")
|
278
|
+
a = re.search(r"^.*\s+(.*)", message)
|
279
|
+
command_name = None
|
280
|
+
if a is not None:
|
281
|
+
command_name = a.group(1).lower()
|
282
|
+
|
283
|
+
pm = PluginManager()
|
284
|
+
|
285
|
+
if command_name and "?" not in command_name:
|
286
|
+
# user wants help for a specific plugin
|
287
|
+
reply = None
|
288
|
+
for p in pm.get_plugins():
|
289
|
+
if (
|
290
|
+
p.enabled and isinstance(p, APRSDRegexCommandPluginBase)
|
291
|
+
and p.command_name.lower() == command_name
|
292
|
+
):
|
293
|
+
reply = p.help()
|
294
|
+
|
295
|
+
if reply:
|
296
|
+
return reply
|
297
|
+
|
298
|
+
list = []
|
299
|
+
for p in pm.get_plugins():
|
300
|
+
LOG.debug(p)
|
301
|
+
if p.enabled and isinstance(p, APRSDRegexCommandPluginBase):
|
302
|
+
name = p.command_name.lower()
|
303
|
+
if name not in list and "help" not in name:
|
304
|
+
list.append(name)
|
305
|
+
|
306
|
+
list.sort()
|
307
|
+
reply = " ".join(list)
|
308
|
+
lines = textwrap.wrap(reply, 60)
|
309
|
+
replies = ["Send APRS MSG of 'help' or 'help <plugin>'"]
|
310
|
+
for line in lines:
|
311
|
+
replies.append(f"plugins: {line}")
|
312
|
+
|
313
|
+
for entry in replies:
|
314
|
+
LOG.debug(f"{len(entry)} {entry}")
|
315
|
+
|
316
|
+
LOG.debug(f"{replies}")
|
317
|
+
return replies
|
318
|
+
|
319
|
+
|
320
|
+
class PluginManager:
|
321
|
+
# The singleton instance object for this class
|
322
|
+
_instance = None
|
323
|
+
|
324
|
+
# the pluggy PluginManager for all Message plugins
|
325
|
+
_pluggy_pm = None
|
326
|
+
# the pluggy PluginManager for all WatchList plugins
|
327
|
+
_watchlist_pm = None
|
328
|
+
|
329
|
+
lock = None
|
330
|
+
|
331
|
+
def __new__(cls, *args, **kwargs):
|
332
|
+
"""This magic turns this into a singleton."""
|
333
|
+
if cls._instance is None:
|
334
|
+
cls._instance = super().__new__(cls)
|
335
|
+
# Put any initialization here.
|
336
|
+
cls._instance.lock = threading.Lock()
|
337
|
+
cls._instance._init()
|
338
|
+
return cls._instance
|
339
|
+
|
340
|
+
def _init(self):
|
341
|
+
self._pluggy_pm = pluggy.PluginManager("aprsd")
|
342
|
+
self._pluggy_pm.add_hookspecs(APRSDPluginSpec)
|
343
|
+
# For the watchlist plugins
|
344
|
+
self._watchlist_pm = pluggy.PluginManager("aprsd")
|
345
|
+
self._watchlist_pm.add_hookspecs(APRSDPluginSpec)
|
346
|
+
|
347
|
+
def stats(self, serializable=False) -> dict:
|
348
|
+
"""Collect and return stats for all plugins."""
|
349
|
+
def full_name_with_qualname(obj):
|
350
|
+
return "{}.{}".format(
|
351
|
+
obj.__class__.__module__,
|
352
|
+
obj.__class__.__qualname__,
|
191
353
|
)
|
192
|
-
reply = process.communicate()[0]
|
193
|
-
# send_message(fromcall, reply.rstrip())
|
194
|
-
reply = reply.decode(errors="ignore").rstrip()
|
195
|
-
except Exception as ex:
|
196
|
-
reply = "Fortune command failed '{}'".format(ex)
|
197
|
-
LOG.error(reply)
|
198
354
|
|
199
|
-
|
355
|
+
plugin_stats = {}
|
356
|
+
plugins = self.get_plugins()
|
357
|
+
if plugins:
|
200
358
|
|
359
|
+
for p in plugins:
|
360
|
+
plugin_stats[full_name_with_qualname(p)] = {
|
361
|
+
"enabled": p.enabled,
|
362
|
+
"rx": p.rx_count,
|
363
|
+
"tx": p.tx_count,
|
364
|
+
"version": p.version,
|
365
|
+
}
|
201
366
|
|
202
|
-
|
203
|
-
"""Location!"""
|
367
|
+
return plugin_stats
|
204
368
|
|
205
|
-
|
206
|
-
|
207
|
-
|
369
|
+
def is_plugin(self, obj):
|
370
|
+
for c in inspect.getmro(obj):
|
371
|
+
if issubclass(c, APRSDPluginBase):
|
372
|
+
return True
|
373
|
+
|
374
|
+
return False
|
208
375
|
|
209
|
-
def
|
210
|
-
|
211
|
-
|
376
|
+
def _create_class(
|
377
|
+
self,
|
378
|
+
module_class_string,
|
379
|
+
super_cls: type = None,
|
380
|
+
**kwargs,
|
381
|
+
):
|
382
|
+
"""
|
383
|
+
Method to create a class from a fqn python string.
|
384
|
+
:param module_class_string: full name of the class to create an object
|
385
|
+
:param super_cls: expected super class for validity, None if bypass
|
386
|
+
:param kwargs: parameters to pass
|
387
|
+
:return:
|
388
|
+
"""
|
389
|
+
module_name = None
|
390
|
+
class_name = None
|
212
391
|
try:
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
392
|
+
module_name, class_name = module_class_string.rsplit(".", 1)
|
393
|
+
module = importlib.import_module(module_name)
|
394
|
+
# Commented out because the email thread starts in a different context
|
395
|
+
# and hence gives a different singleton for the EmailStats
|
396
|
+
# module = importlib.reload(module)
|
397
|
+
except Exception as ex:
|
398
|
+
if not module_name:
|
399
|
+
LOG.error(f"Failed to load Plugin {module_class_string}")
|
219
400
|
else:
|
220
|
-
|
221
|
-
|
222
|
-
)
|
223
|
-
url = (
|
224
|
-
"http://api.aprs.fi/api/get?name="
|
225
|
-
+ searchcall
|
226
|
-
+ "&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json"
|
227
|
-
)
|
228
|
-
response = requests.get(url)
|
229
|
-
# aprs_data = json.loads(response.read())
|
230
|
-
aprs_data = json.loads(response.text)
|
231
|
-
lat = aprs_data["entries"][0]["lat"]
|
232
|
-
lon = aprs_data["entries"][0]["lng"]
|
233
|
-
try: # altitude not always provided
|
234
|
-
alt = aprs_data["entries"][0]["altitude"]
|
235
|
-
except Exception:
|
236
|
-
alt = 0
|
237
|
-
altfeet = int(alt * 3.28084)
|
238
|
-
aprs_lasttime_seconds = aprs_data["entries"][0]["lasttime"]
|
239
|
-
aprs_lasttime_seconds = aprs_lasttime_seconds.encode(
|
240
|
-
"ascii", errors="ignore"
|
241
|
-
) # unicode to ascii
|
242
|
-
delta_seconds = time.time() - int(aprs_lasttime_seconds)
|
243
|
-
delta_hours = delta_seconds / 60 / 60
|
244
|
-
url2 = (
|
245
|
-
"https://forecast.weather.gov/MapClick.php?lat="
|
246
|
-
+ str(lat)
|
247
|
-
+ "&lon="
|
248
|
-
+ str(lon)
|
249
|
-
+ "&FcstType=json"
|
250
|
-
)
|
251
|
-
response2 = requests.get(url2)
|
252
|
-
wx_data = json.loads(response2.text)
|
253
|
-
|
254
|
-
reply = "{}: {} {}' {},{} {}h ago".format(
|
255
|
-
searchcall,
|
256
|
-
wx_data["location"]["areaDescription"],
|
257
|
-
str(altfeet),
|
258
|
-
str(alt),
|
259
|
-
str(lon),
|
260
|
-
str("%.1f" % round(delta_hours, 1)),
|
261
|
-
).rstrip()
|
262
|
-
except Exception as e:
|
263
|
-
LOG.debug("Locate failed with: " + "%s" % str(e))
|
264
|
-
reply = "Unable to find station " + searchcall + ". Sending beacons?"
|
265
|
-
|
266
|
-
return reply
|
267
|
-
|
268
|
-
|
269
|
-
class PingPlugin(APRSDPluginBase):
|
270
|
-
"""Ping."""
|
271
|
-
|
272
|
-
version = "1.0"
|
273
|
-
command_regex = "^[pP]"
|
274
|
-
command_name = "ping"
|
275
|
-
|
276
|
-
def command(self, fromcall, message, ack):
|
277
|
-
LOG.info("PINGPlugin")
|
278
|
-
stm = time.localtime()
|
279
|
-
h = stm.tm_hour
|
280
|
-
m = stm.tm_min
|
281
|
-
s = stm.tm_sec
|
282
|
-
reply = (
|
283
|
-
"Pong! " + str(h).zfill(2) + ":" + str(m).zfill(2) + ":" + str(s).zfill(2)
|
284
|
-
)
|
285
|
-
return reply.rstrip()
|
401
|
+
LOG.error(f"Failed to load Plugin '{module_name}' : '{ex}'")
|
402
|
+
return
|
286
403
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
version = "1.0"
|
292
|
-
command_regex = "^[tT]"
|
293
|
-
command_name = "time"
|
294
|
-
|
295
|
-
def command(self, fromcall, message, ack):
|
296
|
-
LOG.info("TIME COMMAND")
|
297
|
-
stm = time.localtime()
|
298
|
-
h = stm.tm_hour
|
299
|
-
m = stm.tm_min
|
300
|
-
cur_time = fuzzy(h, m, 1)
|
301
|
-
reply = "{} ({}:{} PDT) ({})".format(
|
302
|
-
cur_time, str(h), str(m).rjust(2, "0"), message.rstrip()
|
404
|
+
assert hasattr(module, class_name), "class {} is not in {}".format(
|
405
|
+
class_name,
|
406
|
+
module_name,
|
303
407
|
)
|
304
|
-
|
408
|
+
# click.echo('reading class {} from module {}'.format(
|
409
|
+
# class_name, module_name))
|
410
|
+
cls = getattr(module, class_name)
|
411
|
+
if super_cls is not None:
|
412
|
+
assert issubclass(cls, super_cls), "class {} should inherit from {}".format(
|
413
|
+
class_name,
|
414
|
+
super_cls.__name__,
|
415
|
+
)
|
416
|
+
# click.echo('initialising {} with params {}'.format(class_name, kwargs))
|
417
|
+
obj = cls(**kwargs)
|
418
|
+
return obj
|
305
419
|
|
420
|
+
def _load_plugin(self, plugin_name):
|
421
|
+
"""
|
422
|
+
Given a python fully qualified class path.name,
|
423
|
+
Try importing the path, then creating the object,
|
424
|
+
then registering it as a aprsd Command Plugin
|
425
|
+
"""
|
426
|
+
plugin_obj = None
|
427
|
+
try:
|
428
|
+
plugin_obj = self._create_class(
|
429
|
+
plugin_name,
|
430
|
+
APRSDPluginBase,
|
431
|
+
)
|
432
|
+
if plugin_obj:
|
433
|
+
if isinstance(plugin_obj, APRSDWatchListPluginBase):
|
434
|
+
if plugin_obj.enabled:
|
435
|
+
LOG.info(
|
436
|
+
"Registering WatchList plugin '{}'({})".format(
|
437
|
+
plugin_name,
|
438
|
+
plugin_obj.version,
|
439
|
+
),
|
440
|
+
)
|
441
|
+
self._watchlist_pm.register(plugin_obj)
|
442
|
+
else:
|
443
|
+
LOG.warning(f"Plugin {plugin_obj.__class__.__name__} is disabled")
|
444
|
+
elif isinstance(plugin_obj, APRSDRegexCommandPluginBase):
|
445
|
+
if plugin_obj.enabled:
|
446
|
+
LOG.info(
|
447
|
+
"Registering Regex plugin '{}'({}) -- {}".format(
|
448
|
+
plugin_name,
|
449
|
+
plugin_obj.version,
|
450
|
+
plugin_obj.command_regex,
|
451
|
+
),
|
452
|
+
)
|
453
|
+
self._pluggy_pm.register(plugin_obj)
|
454
|
+
else:
|
455
|
+
LOG.warning(f"Plugin {plugin_obj.__class__.__name__} is disabled")
|
456
|
+
elif isinstance(plugin_obj, APRSDPluginBase):
|
457
|
+
if plugin_obj.enabled:
|
458
|
+
LOG.info(
|
459
|
+
"Registering Base plugin '{}'({})".format(
|
460
|
+
plugin_name,
|
461
|
+
plugin_obj.version,
|
462
|
+
),
|
463
|
+
)
|
464
|
+
self._pluggy_pm.register(plugin_obj)
|
465
|
+
else:
|
466
|
+
LOG.warning(f"Plugin {plugin_obj.__class__.__name__} is disabled")
|
467
|
+
except Exception as ex:
|
468
|
+
LOG.error(f"Couldn't load plugin '{plugin_name}'")
|
469
|
+
LOG.exception(ex)
|
306
470
|
|
307
|
-
|
308
|
-
|
471
|
+
def reload_plugins(self):
|
472
|
+
with self.lock:
|
473
|
+
del self._pluggy_pm
|
474
|
+
self.setup_plugins()
|
309
475
|
|
310
|
-
|
311
|
-
|
312
|
-
command_name = "weather"
|
476
|
+
def setup_plugins(self, load_help_plugin=True):
|
477
|
+
"""Create the plugin manager and register plugins."""
|
313
478
|
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
"&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json"
|
320
|
-
"&name=%s" % fromcall
|
321
|
-
)
|
322
|
-
response = requests.get(url)
|
323
|
-
# aprs_data = json.loads(response.read())
|
324
|
-
aprs_data = json.loads(response.text)
|
325
|
-
lat = aprs_data["entries"][0]["lat"]
|
326
|
-
lon = aprs_data["entries"][0]["lng"]
|
327
|
-
url2 = (
|
328
|
-
"https://forecast.weather.gov/MapClick.php?lat=%s"
|
329
|
-
"&lon=%s&FcstType=json" % (lat, lon)
|
330
|
-
)
|
331
|
-
response2 = requests.get(url2)
|
332
|
-
# wx_data = json.loads(response2.read())
|
333
|
-
wx_data = json.loads(response2.text)
|
334
|
-
reply = (
|
335
|
-
"%sF(%sF/%sF) %s. %s, %s."
|
336
|
-
% (
|
337
|
-
wx_data["currentobservation"]["Temp"],
|
338
|
-
wx_data["data"]["temperature"][0],
|
339
|
-
wx_data["data"]["temperature"][1],
|
340
|
-
wx_data["data"]["weather"][0],
|
341
|
-
wx_data["time"]["startPeriodName"][1],
|
342
|
-
wx_data["data"]["weather"][1],
|
343
|
-
).rstrip()
|
344
|
-
)
|
345
|
-
LOG.debug("reply: '{}' ".format(reply))
|
346
|
-
except Exception as e:
|
347
|
-
LOG.debug("Weather failed with: " + "%s" % str(e))
|
348
|
-
reply = "Unable to find you (send beacon?)"
|
479
|
+
LOG.info("Loading APRSD Plugins")
|
480
|
+
# Help plugin is always enabled.
|
481
|
+
if load_help_plugin:
|
482
|
+
_help = HelpPlugin()
|
483
|
+
self._pluggy_pm.register(_help)
|
349
484
|
|
350
|
-
|
485
|
+
enabled_plugins = CONF.enabled_plugins
|
486
|
+
if enabled_plugins:
|
487
|
+
for p_name in enabled_plugins:
|
488
|
+
self._load_plugin(p_name)
|
489
|
+
else:
|
490
|
+
# Enabled plugins isn't set, so we default to loading all of
|
491
|
+
# the core plugins.
|
492
|
+
for p_name in CORE_MESSAGE_PLUGINS:
|
493
|
+
self._load_plugin(p_name)
|
494
|
+
|
495
|
+
LOG.info("Completed Plugin Loading.")
|
496
|
+
|
497
|
+
def run(self, packet: packets.MessagePacket):
|
498
|
+
"""Execute all the plugins run method."""
|
499
|
+
with self.lock:
|
500
|
+
return self._pluggy_pm.hook.filter(packet=packet)
|
501
|
+
|
502
|
+
def run_watchlist(self, packet: packets.Packet):
|
503
|
+
with self.lock:
|
504
|
+
return self._watchlist_pm.hook.filter(packet=packet)
|
505
|
+
|
506
|
+
def stop(self):
|
507
|
+
"""Stop all threads created by all plugins."""
|
508
|
+
with self.lock:
|
509
|
+
for p in self.get_plugins():
|
510
|
+
if hasattr(p, "stop_threads"):
|
511
|
+
p.stop_threads()
|
512
|
+
|
513
|
+
def register_msg(self, obj):
|
514
|
+
"""Register the plugin."""
|
515
|
+
with self.lock:
|
516
|
+
self._pluggy_pm.register(obj)
|
517
|
+
|
518
|
+
def get_plugins(self):
|
519
|
+
plugin_list = []
|
520
|
+
if self._pluggy_pm:
|
521
|
+
for plug in self._pluggy_pm.get_plugins():
|
522
|
+
plugin_list.append(plug)
|
523
|
+
if self._watchlist_pm:
|
524
|
+
for plug in self._watchlist_pm.get_plugins():
|
525
|
+
plugin_list.append(plug)
|
526
|
+
|
527
|
+
return plugin_list
|
528
|
+
|
529
|
+
def get_watchlist_plugins(self):
|
530
|
+
pl = []
|
531
|
+
if self._watchlist_pm:
|
532
|
+
for plug in self._watchlist_pm.get_plugins():
|
533
|
+
pl.append(plug)
|
534
|
+
return pl
|
535
|
+
|
536
|
+
def get_message_plugins(self):
|
537
|
+
pl = []
|
538
|
+
if self._pluggy_pm:
|
539
|
+
for plug in self._pluggy_pm.get_plugins():
|
540
|
+
pl.append(plug)
|
541
|
+
return pl
|