From 6e5082c4498dde60366f1e8fffd9378bf458f4f3 Mon Sep 17 00:00:00 2001 From: geoffwhittington Date: Sat, 27 May 2023 14:02:52 -0400 Subject: [PATCH] Allow config for map plugin (#46) * Allow config for map plugin --- plugin_loader.py | 23 ++++++-- plugins/base_plugin.py | 13 +++++ plugins/debug_plugin.py | 17 ++++++ plugins/drop_plugin.py | 108 +++++++++++++++++++++++++++++++++++ plugins/map_plugin.py | 37 ++++++++---- plugins/mesh_relay_plugin.py | 12 ---- 6 files changed, 182 insertions(+), 28 deletions(-) create mode 100644 plugins/debug_plugin.py create mode 100644 plugins/drop_plugin.py diff --git a/plugin_loader.py b/plugin_loader.py index b2c247c..6f32fbf 100644 --- a/plugin_loader.py +++ b/plugin_loader.py @@ -2,7 +2,7 @@ logger = get_logger(name="Plugins") -active_plugins = [] +sorted_active_plugins = [] def load_plugins(): @@ -14,10 +14,12 @@ def load_plugins(): from plugins.weather_plugin import Plugin as WeatherPlugin from plugins.help_plugin import Plugin as HelpPlugin from plugins.nodes_plugin import Plugin as NodesPlugin + from plugins.drop_plugin import Plugin as DropPlugin + from plugins.debug_plugin import Plugin as DebugPlugin - global plugins - if active_plugins: - return active_plugins + global sorted_active_plugins + if sorted_active_plugins: + return sorted_active_plugins plugins = [ HealthPlugin(), @@ -28,11 +30,20 @@ def load_plugins(): WeatherPlugin(), HelpPlugin(), NodesPlugin(), + DropPlugin(), + DebugPlugin(), ] + active_plugins = [] for plugin in plugins: if plugin.config["active"]: - logger.info(f"Loaded {plugin.plugin_name}") + plugin.priority = ( + plugin.config["priority"] + if "priority" in plugin.config + else plugin.priority + ) + logger.info(f"Loaded {plugin.plugin_name} ({plugin.priority})") active_plugins.append(plugin) - return active_plugins + sorted_active_plugins = sorted(active_plugins, key=lambda plugin: plugin.priority) + return sorted_active_plugins diff --git a/plugins/base_plugin.py b/plugins/base_plugin.py index 021ba79..e5c0e70 100644 --- a/plugins/base_plugin.py +++ b/plugins/base_plugin.py @@ -13,6 +13,7 @@ class BasePlugin(ABC): plugin_name = None max_data_rows_per_node = 100 + priority = 10 @property def description(self): @@ -25,6 +26,18 @@ def __init__(self) -> None: if "plugins" in relay_config and self.plugin_name in relay_config["plugins"]: self.config = relay_config["plugins"][self.plugin_name] + def strip_raw(self, data): + if type(data) is not dict: + return data + + if "raw" in data: + del data["raw"] + + for k, v in data.items(): + data[k] = self.strip_raw(v) + + return data + def get_matrix_commands(self): return [self.plugin_name] diff --git a/plugins/debug_plugin.py b/plugins/debug_plugin.py new file mode 100644 index 0000000..c192687 --- /dev/null +++ b/plugins/debug_plugin.py @@ -0,0 +1,17 @@ +from plugins.base_plugin import BasePlugin + + +class Plugin(BasePlugin): + plugin_name = "debug" + priority = 1 + + async def handle_meshtastic_message( + self, packet, formatted_message, longname, meshnet_name + ): + packet = self.strip_raw(packet) + + self.logger.debug(f"Packet received: {packet}") + return False + + async def handle_room_message(self, room, event, full_message): + return False diff --git a/plugins/drop_plugin.py b/plugins/drop_plugin.py new file mode 100644 index 0000000..ac72a78 --- /dev/null +++ b/plugins/drop_plugin.py @@ -0,0 +1,108 @@ +import re +from haversine import haversine +from plugins.base_plugin import BasePlugin +from meshtastic_utils import connect_meshtastic +from meshtastic import mesh_pb2 + + +class Plugin(BasePlugin): + plugin_name = "drop" + special_node = "!NODE_MSGS!" + + def get_position(self, meshtastic_client, node_id): + for node, info in meshtastic_client.nodes.items(): + if info["user"]["id"] == node_id: + return info["position"] + return None + + async def handle_meshtastic_message( + self, packet, formatted_message, longname, meshnet_name + ): + meshtastic_client = connect_meshtastic() + nodeInfo = meshtastic_client.getMyNodeInfo() + + # Attempt message drop to packet originator if not relay + if "fromId" in packet and packet["fromId"] != nodeInfo["user"]["id"]: + position = self.get_position(meshtastic_client, packet["fromId"]) + if position and "latitude" in position and "longitude" in position: + packet_location = ( + position["latitude"], + position["longitude"], + ) + + self.logger.debug(f"Packet originates from: {packet_location}") + messages = self.get_node_data(self.special_node) + unsent_messages = [] + for message in messages: + # You cannot pickup what you dropped + if ( + "originator" in message + and message["originator"] == packet["fromId"] + ): + unsent_messages.append(message) + continue + + try: + distance_km = haversine( + (packet_location[0], packet_location[1]), + message["location"], + ) + except: + distance_km = 1000 + radius_km = ( + self.config["radius_km"] if "radius_km" in self.config else 5 + ) + if distance_km <= radius_km: + target_node = packet["fromId"] + self.logger.debug(f"Sending dropped message to {target_node}") + meshtastic_client.sendText( + text=message["text"], destinationId=target_node + ) + else: + unsent_messages.append(message) + self.set_node_data(self.special_node, unsent_messages) + total_unsent_messages = len(unsent_messages) + if total_unsent_messages > 0: + self.logger.debug(f"{total_unsent_messages} message(s) remaining") + + # Attempt to drop a message + if ( + "decoded" in packet + and "portnum" in packet["decoded"] + and packet["decoded"]["portnum"] == "TEXT_MESSAGE_APP" + ): + text = packet["decoded"]["text"] if "text" in packet["decoded"] else None + if f"!{self.plugin_name}" not in text: + return False + + match = re.search(r"!drop\s+(.+)$", text) + if not match: + return False + + drop_message = match.group(1) + + position = {} + for node, info in meshtastic_client.nodes.items(): + if info["user"]["id"] == packet["fromId"]: + position = info["position"] + + if "latitude" not in position or "longitude" not in position: + self.logger.debug( + "Position of dropping node is not known. Skipping ..." + ) + return True + + self.store_node_data( + self.special_node, + { + "location": (position["latitude"], position["longitude"]), + "text": drop_message, + "originator": packet["fromId"], + }, + ) + self.logger.debug(f"Dropped a message: {drop_message}") + return True + + async def handle_room_message(self, room, event, full_message): + if self.matches(full_message): + return True diff --git a/plugins/map_plugin.py b/plugins/map_plugin.py index 5ac064d..2e8982f 100644 --- a/plugins/map_plugin.py +++ b/plugins/map_plugin.py @@ -22,7 +22,7 @@ def anonymize_location(lat, lon, radius=1000): return new_lat, new_lon -def get_map(locations, zoom=None, image_size=None, radius=10000): +def get_map(locations, zoom=None, image_size=None, anonymize=True, radius=10000): """ Anonymize a location to 10km by default """ @@ -31,12 +31,17 @@ def get_map(locations, zoom=None, image_size=None, radius=10000): context.set_zoom(zoom) for location in locations: - new_location = anonymize_location( - lat=float(location["lat"]), - lon=float(location["lon"]), - radius=radius, - ) - radio = staticmaps.create_latlng(new_location[0], new_location[1]) + if anonymize: + new_location = anonymize_location( + lat=float(location["lat"]), + lon=float(location["lon"]), + radius=radius, + ) + radio = staticmaps.create_latlng(new_location[0], new_location[1]) + else: + radio = staticmaps.create_latlng( + float(location["lat"]), float(location["lon"]) + ) context.add_object(staticmaps.Marker(radio, size=10)) # render non-anti-aliased png @@ -120,7 +125,7 @@ async def handle_room_message(self, room, event, full_message): try: zoom = int(zoom) except: - zoom = 8 + zoom = self.config["zoom"] if "zoom" in self.config else 8 if zoom < 0 or zoom > 30: zoom = 8 @@ -128,7 +133,10 @@ async def handle_room_message(self, room, event, full_message): try: image_size = (int(image_size[0]), int(image_size[1])) except: - image_size = (1000, 1000) + image_size = ( + self.config["image_width"] if "image_width" in self.config else 1000, + self.config["image_height"] if "image_height" in self.config else 1000, + ) if image_size[0] > 1000 or image_size[1] > 1000: image_size = (1000, 1000) @@ -143,7 +151,16 @@ async def handle_room_message(self, room, event, full_message): } ) - pillow_image = get_map(locations=locations, zoom=zoom, image_size=image_size) + anonymize = self.config["anonymize"] if "anonymize" in self.config else True + radius = self.config["radius"] if "radius" in self.config else 1000 + + pillow_image = get_map( + locations=locations, + zoom=zoom, + image_size=image_size, + anonymize=anonymize, + radius=radius, + ) await send_image(matrix_client, room.room_id, pillow_image) diff --git a/plugins/mesh_relay_plugin.py b/plugins/mesh_relay_plugin.py index fc1ede9..468d7d0 100644 --- a/plugins/mesh_relay_plugin.py +++ b/plugins/mesh_relay_plugin.py @@ -16,18 +16,6 @@ class Plugin(BasePlugin): plugin_name = "mesh_relay" max_data_rows_per_node = 50 - def strip_raw(self, data): - if type(data) is not dict: - return data - - if "raw" in data: - del data["raw"] - - for k, v in data.items(): - data[k] = self.strip_raw(v) - - return data - def normalize(self, dict_obj): """ Packets are either a dict, string dict or string