Skip to content

Commit 70a61f4

Browse files
feat: new core plugins (#79)
* feat: new core plugins This adds new core plugins for Help, Webserver, and Health and supporting code to make it possible to load them. I had to overload some of the Err backend methods and monkey patch some methods in the PluginManager. Eventually, I'd like to upstream these changes and this will no longer be needed * fix: skip loading plugs if backend = True This allows setting a new flag in the Core section of the .plug file to indicate a plugin is a backend. These backend plugins are then skipped when loaded by _load_plugins_generic This code could eventually be upstreamed into Errbot to allow it to handle multiple plugins per module easier
1 parent 458697d commit 70a61f4

13 files changed

+463
-21
lines changed

aprs_backend/APRSHealth.plug

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[Core]
2+
Name = APRSHealth
3+
Module = aprs_core_plugins
4+
Core = True
5+
6+
[Documentation]
7+
Description = APRS Friendly Health Plugin
8+
9+
[Python]
10+
Version = 3
11+
12+
[Errbot]
13+
Min=6.2.0

aprs_backend/APRSHelp.plug

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[Core]
2+
Name = APRSHelp
3+
Module = aprs_core_plugins
4+
Core = True
5+
6+
[Documentation]
7+
Description = APRS Friendly Help Plugin
8+
9+
[Python]
10+
Version = 3
11+
12+
[Errbot]
13+
Min=6.2.0

aprs_backend/APRSWebserver.plug

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[Core]
2+
Name = APRSWebserver
3+
Module = aprs_core_plugins
4+
Core = True
5+
6+
[Documentation]
7+
Description = APRS Friendly Web Plugin
8+
9+
[Python]
10+
Version = 3
11+
12+
[Errbot]
13+
Min=6.2.0

aprs_backend/aprs.plug

+7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
[Core]
22
Name = APRS
33
Module = aprs
4+
Backend = True
45

56
[Documentation]
67
Description = Backend for APRS
8+
9+
[Python]
10+
Version = 3
11+
12+
[Errbot]
13+
Min=6.2.0

aprs_backend/aprs.py

+29-16
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from aprs_backend.version import __version__ as ERR_APRS_VERSION
88
from errbot.backends.base import Message
99
from errbot.backends.base import ONLINE
10+
from errbot.plugin_manager import BotPluginManager
1011
from errbot.core import ErrBot
1112
from aprs_backend.exceptions import ProcessorError, PacketParseError, APRSISConnnectError
1213
from aprs_backend.packets.parser import parse, hash_packet
@@ -16,12 +17,16 @@
1617
from aprs_backend.utils.counter import MessageCounter
1718
from random import randint
1819
from datetime import datetime
20+
1921
from better_profanity import profanity
2022
from aprs_backend.clients.aprs_registry import APRSRegistryClient, RegistryAppConfig
2123
import logging
2224
import asyncio
2325
from errbot.version import VERSION as ERR_VERSION
2426
from aprs_backend.clients.beacon import BeaconConfig, BeaconClient
27+
from aprs_backend.utils.plugins import _load_plugins_generic, activate_non_started_plugins
28+
from types import MethodType
29+
2530

2631
log = logging.getLogger(__name__)
2732

@@ -33,9 +38,9 @@
3338

3439
class APRSBackend(ErrBot):
3540
def __init__(self, config):
36-
log.debug("Initied")
41+
super().__init__(config)
42+
log.debug("Init called")
3743

38-
self._errbot_config = config
3944
self._multiline = False
4045

4146
aprs_config = {"host": "rotate.aprs.net", "port": 14580}
@@ -64,9 +69,6 @@ def __init__(self, config):
6469
self._send_queue: asyncio.Queue[MessagePacket] = asyncio.Queue(
6570
maxsize=int(self._get_from_config("APRS_SEND_MAX_QUEUE", "2048"))
6671
)
67-
self.help_text = self._get_from_config(
68-
"APRS_HELP_TEXT", f"Errbot {ERR_VERSION} & err-aprs-backend {ERR_APRS_VERSION} by {aprs_config['callsign']}"
69-
)
7072

7173
self._message_counter = MessageCounter(initial_value=randint(1, 20)) # nosec not used cryptographically
7274
self._max_dropped_packets = int(self._get_from_config("APRS_MAX_DROPPED_PACKETS", "25"))
@@ -125,10 +127,23 @@ def __init__(self, config):
125127
)
126128
else:
127129
self.beacon_client = None
128-
super().__init__(config)
130+
131+
def attach_plugin_manager(self, plugin_manager: BotPluginManager | None) -> None:
132+
"""Modified attach_plugin_manager that patches the plugin manager
133+
134+
_log_plugins_generic is modified to remove a check on multiple plugin classes in
135+
a single module
136+
"""
137+
log.debug("In aprs-backend attach_plugin_manager")
138+
if plugin_manager is not None:
139+
log.debug("Patching plugin manager with custom _load_plugins_generic")
140+
funcType = MethodType
141+
plugin_manager._load_plugins_generic = funcType(_load_plugins_generic, plugin_manager)
142+
plugin_manager.activate_non_started_plugins = funcType(activate_non_started_plugins, plugin_manager)
143+
self.plugin_manager = plugin_manager
129144

130145
def _get_from_config(self, key: str, default: any = None) -> any:
131-
return getattr(self._errbot_config, key, default)
146+
return getattr(self.bot_config, key, default)
132147

133148
def _get_beacon_config(self) -> BeaconConfig | None:
134149
if self._get_from_config("APRS_BEACON_ENABLE", "false") == "true":
@@ -317,6 +332,13 @@ async def receive_worker(self) -> bool:
317332
return False
318333

319334
async def async_serve_once(self) -> bool:
335+
"""The async portion of serve once
336+
337+
Starts the bot tasks for receiving aprs messages, sending messages, and retrying
338+
"""
339+
log.debug(
340+
"Bot plugins: %s", [plugin.__class__.__name__ for plugin in self.plugin_manager.get_all_active_plugins()]
341+
)
320342
receive_task = asyncio.create_task(self.receive_worker())
321343

322344
worker_tasks = [asyncio.create_task(self.send_worker()), asyncio.create_task(self.retry_worker())]
@@ -426,13 +448,6 @@ async def __drop_message_from_waiting(self, message_hash: str) -> None:
426448
else:
427449
log.debug("Dropped Packet from waiting_ack: %s", packet)
428450

429-
def handle_help(self, msg: APRSMessage) -> None:
430-
"""Returns simplified help text for the APRS backend"""
431-
help_msg = APRSMessage(body=self.help_text, extras=msg.extras)
432-
help_msg.to = msg.frm
433-
help_msg.frm = self.bot_identifier
434-
self.send_message(help_msg)
435-
436451
async def _process_message(self, packet: MessagePacket) -> None:
437452
"""
438453
Check if this message is a dupe of one the bot is already processing
@@ -449,8 +464,6 @@ async def _process_message(self, packet: MessagePacket) -> None:
449464
self._packet_cache[this_packet_hash] = packet
450465
msg = APRSMessage.from_message_packet(packet)
451466
msg.body = msg.body.strip("\n").strip("\r")
452-
if msg.body.lower().strip(" ") == "help":
453-
return self.handle_help(msg)
454467
return self.callback_message(msg)
455468

456469
async def _ack_message(self, packet: MessagePacket) -> None:

aprs_backend/aprs_core_plugins.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from aprs_backend.plugins import APRSHelp
2+
from aprs_backend.plugins import APRSWebserver
3+
from aprs_backend.plugins import APRSHealth
4+
5+
__all__ = ["APRSHelp", "APRSWebserver", "APRSHealth"]

aprs_backend/plugins/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from aprs_backend.plugins.help import APRSHelp
2+
from aprs_backend.plugins.web import APRSWebserver
3+
from aprs_backend.plugins.health import APRSHealth
4+
5+
__all__ = ["APRSHelp", "APRSWebserver", "APRSHealth"]

aprs_backend/plugins/health.py

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import gc
2+
from datetime import datetime
3+
4+
from errbot import BotPlugin, webhook
5+
from errbot.utils import format_timedelta
6+
7+
8+
class APRSHealth(BotPlugin):
9+
"""Customized health plugin that shifts most of the outputs to webhooks and removes the botcmds"""
10+
11+
@webhook
12+
def status(self, _):
13+
"""If I am alive I should be able to respond to this one"""
14+
pm = self._bot.plugin_manager
15+
all_blacklisted = pm.get_blacklisted_plugin()
16+
all_loaded = pm.get_all_active_plugin_names()
17+
all_attempted = sorted(pm.plugin_infos.keys())
18+
plugins_statuses = []
19+
for name in all_attempted:
20+
if name in all_blacklisted:
21+
if name in all_loaded:
22+
plugins_statuses.append(("BA", name))
23+
else:
24+
plugins_statuses.append(("BD", name))
25+
elif name in all_loaded:
26+
plugins_statuses.append(("A", name))
27+
elif (
28+
pm.get_plugin_obj_by_name(name) is not None
29+
and pm.get_plugin_obj_by_name(name).get_configuration_template() is not None
30+
and pm.get_plugin_configuration(name) is None
31+
):
32+
plugins_statuses.append(("C", name))
33+
else:
34+
plugins_statuses.append(("D", name))
35+
loads = self.status_load("")
36+
gc = self.status_gc("")
37+
38+
return {
39+
"plugins_statuses": plugins_statuses,
40+
"loads": loads["loads"],
41+
"gc": gc["gc"],
42+
}
43+
44+
@webhook
45+
def status_load(self, _):
46+
"""shows the load status"""
47+
try:
48+
from posix import getloadavg
49+
50+
loads = getloadavg()
51+
except Exception:
52+
loads = None
53+
54+
return {"loads": loads}
55+
56+
@webhook
57+
def status_gc(self, _):
58+
"""shows the garbage collection details"""
59+
return {"gc": gc.get_count()}
60+
61+
@webhook
62+
def uptime(self, _):
63+
"""Return the uptime of the bot"""
64+
return {"up": format_timedelta(datetime.now() - self._bot.startup_time), "since": self._bot.startup_time}

aprs_backend/plugins/help.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from errbot import BotPlugin, botcmd
2+
3+
4+
class APRSHelp(BotPlugin):
5+
"""An alternative help plugin.
6+
7+
For now, it simply replies with preconfigured help text.
8+
9+
In the future, it would be great to use the internal webserver to serve
10+
help text or generate it as a static file that could be served via
11+
static site serving
12+
"""
13+
14+
def __init__(self, bot, name: str = "Help") -> None:
15+
"""
16+
Calls super init and adds a few plugin variables of our own. This makes PEP8 happy
17+
"""
18+
super().__init__(bot, name)
19+
self.help_text = getattr(self._bot.bot_config, "APRS_HELP_TEXT")
20+
21+
@botcmd
22+
def help(self, _, __):
23+
return self.help_text

aprs_backend/plugins/web.py

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import logging
2+
from threading import Thread
3+
4+
from webtest import TestApp
5+
from werkzeug.serving import ThreadedWSGIServer
6+
7+
from errbot import BotPlugin, webhook
8+
from errbot.core_plugins import flask_app
9+
10+
11+
class APRSWebserver(BotPlugin):
12+
def __init__(self, *args, **kwargs):
13+
self.server = None
14+
self.server_thread = None
15+
self.ssl_context = None
16+
self.test_app = TestApp(flask_app)
17+
# TODO: Make this configurable in the APRS bot config, since there's no plugin config anymore
18+
self.web_config = {"HOST": "0.0.0.0", "PORT": 3141} # nosec
19+
super().__init__(*args, **kwargs)
20+
21+
def activate(self):
22+
if self.server_thread and self.server_thread.is_alive():
23+
raise Exception("Invalid state, you should not have a webserver already running.")
24+
self.server_thread = Thread(target=self.run_server, name="Webserver Thread")
25+
self.server_thread.start()
26+
self.log.debug("Webserver started.")
27+
28+
super().activate()
29+
30+
def deactivate(self):
31+
if self.server is not None:
32+
self.log.info("Shutting down the internal webserver.")
33+
self.server.shutdown()
34+
self.log.info("Waiting for the webserver thread to quit.")
35+
self.server_thread.join()
36+
self.log.info("Webserver shut down correctly.")
37+
super().deactivate()
38+
39+
def run_server(self):
40+
host = self.web_config["HOST"]
41+
port = self.web_config["PORT"]
42+
self.log.info("Starting the webserver on %s:%i", host, port)
43+
try:
44+
self.server = ThreadedWSGIServer(
45+
host,
46+
port,
47+
flask_app,
48+
)
49+
wsgi_log = logging.getLogger("werkzeug")
50+
wsgi_log.setLevel(self.bot_config.BOT_LOG_LEVEL)
51+
self.server.serve_forever()
52+
except KeyboardInterrupt:
53+
self.log.info("Keyboard interrupt, request a global shutdown.")
54+
self.server.shutdown()
55+
except Exception as exc:
56+
self.log.exception("Exception with webserver: %s", exc)
57+
self.log.debug("Webserver stopped")
58+
59+
@webhook
60+
def echo(self, incoming_request):
61+
"""
62+
A simple test webhook
63+
"""
64+
self.log.debug("Your incoming request is: %s", incoming_request)
65+
return str(incoming_request)

0 commit comments

Comments
 (0)