From 9c08b3160318cd5cb4dd01db204aad1ed8e86686 Mon Sep 17 00:00:00 2001 From: SebastianObi Date: Mon, 5 Jun 2023 14:36:42 +0200 Subject: [PATCH 1/2] Bug fixing and improvements --- lxmf_bridge_matrix/README.md | 211 ++ lxmf_bridge_matrix/lxmf_bridge_matrix.py | 1811 +++++++++++++++++ lxmf_bridge_mqtt/README.md | 13 +- lxmf_bridge_mqtt/lxmf_bridge_mqtt.py | 98 +- lxmf_chatbot/lxmf_chatbot.py | 66 +- lxmf_cmd/lxmf_cmd.py | 67 +- .../config.cfg.owr | 10 +- .../config.cfg.owr | 12 +- lxmf_distribution_group/README.md | 7 +- .../lxmf_distribution_group.py | 215 +- lxmf_distribution_group_minimal/README.md | 3 + .../lxmf_distribution_group_minimal.py | 95 +- lxmf_echo/lxmf_echo.py | 82 +- lxmf_ping/lxmf_ping.py | 22 +- lxmf_provisioning/lxmf_provisioning.py | 103 +- lxmf_terminal/lxmf_terminal.py | 69 +- 16 files changed, 2710 insertions(+), 174 deletions(-) diff --git a/lxmf_bridge_matrix/README.md b/lxmf_bridge_matrix/README.md index a643b47..f5d7f2a 100644 --- a/lxmf_bridge_matrix/README.md +++ b/lxmf_bridge_matrix/README.md @@ -1,2 +1,213 @@ # lxmf_bridge_matrix +This program provides an interface between LXMF and Matrix. It serves as a single message endpoint and not to transfer the LXMF/Reticlum traffic 1:1 to Matrix. It serves the purpose of providing an endpoint in the Reticulum network for matrix rooms. Through this all LXMF capable applications can communicate with it via messages. + +The main functionality is the connection of a matrix room with a lxmf distribution group. Therefore, this program is optimized for the distribution group. But it could also act as a normal message endpoint. + For more information, see the configuration options (at the end of the program files). Everything else is briefly documented there. After the first start this configuration will be created as default config in the corresponding file. + + +### Features +- Optimized for compatibility with the distributon group +- Compatible with all LXMF applications (NomadNet, Sideband, ...) +- Compatible with all Matrix rooms +- Configurable routing table to connect more than one room/group + + +## Examples of use + +### + +### General info how the messages are transported +All messages between client<->server are transported as single 1:1 messages in the LXMF/Reticulum network. +Accordingly, encryption takes place between these end points. +If a direct delivery of the message does not work, it is sent to a propagation node. There it is stored temporarily and can be retrieved by the client later. + +As these are normal LXMF messages, any LXMF capable application can be used to communicate with it. + + +## Current Status +It should currently be considered beta software and still work in progress. + +All core features are implemented and functioning, but additions will probably occur as real-world use is explored. + +There may be errors or the compatibility after an update is no longer guaranteed. + +The full documentation is not yet available. Due to lack of time I can also not say when this will be further processed. + + +## Screenshots / Usage examples + + + +## Installation manual + +### Install: +- Install all required prerequisites. (Default Reticulum installation. Only necessary if reticulum is not yet installed.) + ```bash + apt update + apt upgrade + + apt install python3-pip + + pip install pip --upgrade + reboot + + pip3 install rns + pip3 install pyserial netifaces + + pip3 install lxmf + ``` +- Install all required prerequisites. + ```bash + apt-get install libolm-dev + pip3 install matrix-nio[e2e] + ``` +- Change the Reticulum configuration to suit your needs and use-case. + ```bash + nano /.reticulum/config + ``` +- Download the [file](lxmf_bridge_matrix.py) from this repository. + ```bash + wget https://raw.githubusercontent.com/SebastianObi/LXMF-Tools/main/lxmf_bridge_matrix/lxmf_bridge_matrix.py + ``` +- Make it executable with the following command + ```bash + chmod +x lxmf_bridge_matrix.py + ``` + +### Start: +- Start it + ```bash + ./lxmf_bridge_matrix.py + ``` +- After the first start edit the configuration file to suit your needs and use-case. The file location is displayed. +- Example minimal configuration (override of the default config `config.cfg`). These are the most relevant settings that need to be adjusted. All other settings are in `config.cfg` + ```bash + nano /root/.lxmf_bridge_matrix/config.cfg.owr + ``` + ```bash + ``` +- Start it again. Finished! + ```bash + ./lxmf_bridge_matrix.py + ``` + + +### Run as a system service/deamon: +- Create a service file. + ```bash + nano /etc/systemd/system/lxmf_bridge_matrix.service + ``` +- Copy and edit the following content to your own needs. + ```bash + [Unit] + Description=LXMF Bridge Matrix Daemon + After=multi-user.target + [Service] + # ExecStartPre=/bin/sleep 10 + Type=simple + Restart=always + RestartSec=3 + User=root + ExecStart=/root/lxmf_bridge_matrix.py + [Install] + WantedBy=multi-user.target + ``` +- Enable the service. + ```bash + systemctl enable lxmf_bridge_matrix + ``` +- Start the service. + ```bash + systemctl start lxmf_bridge_matrix + ``` + + +### Start/Stop service: + ```bash + systemctl start lxmf_bridge_matrix + systemctl stop lxmf_bridge_matrix + ``` + + +### Enable/Disable service: + ```bash + systemctl enable lxmf_bridge_matrix + systemctl disable lxmf_bridge_matrix + ``` + + +### Run several instances (To copy the same application): +- Run the program with a different configuration path. + ```bash + ./lxmf_bridge_matrix.py -p /root/.lxmf_bridge_matrix_2nd + ./lxmf_bridge_matrix.py -p /root/.lxmf_bridge_matrix_3nd + ``` +- After the first start edit the configuration file to suit your needs and use-case. The file location is displayed. + + +### First usage: +- With a manual start via the console, the own LXMF address is displayed: + ``` + [] ............................................................................... + [] LXMF - Address: <801f48d54bc71cb3e0886944832aaf8d> + [] ...............................................................................` + ``` +- This address is also annouced at startup in the default setting. +- Now the software can be used. + + +### Startup parameters: +```bash +usage: lxmf_bridge_matrix.py [-h] [-p PATH] [-pr PATH_RNS] [-pl PATH_LOG] [-l LOGLEVEL] [-s] [--exampleconfig] + [--exampleconfigoverride] + +LXMF Bridge Matrix + +optional arguments: + -h, --help show this help message and exit + -p PATH, --path PATH Path to alternative config directory + -pr PATH_RNS, --path_rns PATH_RNS + Path to alternative Reticulum config directory + -pl PATH_LOG, --path_log PATH_LOG + Path to alternative log directory + -l LOGLEVEL, --loglevel LOGLEVEL + -s, --service Running as a service and should log to file + --exampleconfig Print verbose configuration example to stdout and exit + --exampleconfigoverride + Print verbose configuration example to stdout and exit +``` + + +### Config/data files: +- config.cfg + + This is the default config file. + +- config.cfg.owr + + This is the user configuration file to override the default configuration file. + All settings made here have precedence. + This file can be used to clearly summarize all settings that deviate from the default. + This also has the advantage that all changed settings can be kept when updating the program. + + +## Configuration manual (Examples) +The configurations shown here are only a part of the total configuration. +It only serves to show the configuration that is necessary and adapted for the respective function. +All configurations must be made in the file `config.cfg.owr`. +All possible settings can be seen in the default configuration file `config.cfg`. + + +## Admin manual +This guide applies to all admins. Here are briefly explained the administative possibilities. + + +## User manual +This guide applies to users or admins. Here are briefly explained the normal possibilities of the software. + + +## FAQ + +### How do I start with the software? +You should read the `Installation manual` section. There everything is explained briefly. Just work through everything from top to bottom :) \ No newline at end of file diff --git a/lxmf_bridge_matrix/lxmf_bridge_matrix.py b/lxmf_bridge_matrix/lxmf_bridge_matrix.py index e69de29..22be40d 100755 --- a/lxmf_bridge_matrix/lxmf_bridge_matrix.py +++ b/lxmf_bridge_matrix/lxmf_bridge_matrix.py @@ -0,0 +1,1811 @@ +#!/usr/bin/env python3 +############################################################################################################## +# +# Copyright (c) 2023 Sebastian Obele / obele.eu +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# This software uses the following software-parts: +# Reticulum, LXMF, NomadNet / Copyright (c) 2016-2022 Mark Qvist / unsigned.io / MIT License +# +############################################################################################################## + + +############################################################################################################## +# Include + + +#### System #### +import sys +import os +import time +import datetime +import argparse + +#### Config #### +import configparser + +#### JSON #### +import json +import pickle + +#### String #### +import string + +#### Regex #### +import re + +#### Process #### +import signal +import threading + +#### Reticulum, LXMF #### +# Install: pip3 install rns lxmf +# Source: https://markqvist.github.io +import RNS +import LXMF +import RNS.vendor.umsgpack as umsgpack + +#### Matrix #### +# Install: apt-get install libolm-dev +# Install: pip3 install matrix-nio[e2e] +# Source: https://github.com/poljar/matrix-nio +import asyncio +from nio import AsyncClient, LoginResponse, UploadResponse, SyncResponse, MatrixRoom, RoomMessage, RoomMessageText + + +############################################################################################################## +# Globals + + +#### Global Variables - Configuration #### +NAME = "LXMF Bridge Matrix" +DESCRIPTION = "" +VERSION = "0.0.1 (2023-05-05)" +COPYRIGHT = "(c) 2023 Sebastian Obele / obele.eu" +PATH = os.path.expanduser("~") + "/." + os.path.splitext(os.path.basename(__file__))[0] +PATH_RNS = None + + + + +#### Global Variables - System (Not changeable) #### +CONFIG = None +DATA = {} +ROUTING_TABLE = {} +RNS_CONNECTION = None +LXMF_CONNECTION = None +MATRIX_CONNECTION = None + + +############################################################################################################## +# LXMF Class + + +class lxmf_connection: + message_received_callback = None + message_notification_callback = None + message_notification_success_callback = None + message_notification_failed_callback = None + config_set_callback = None + + + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, announce_hidden=False, send_delay=0, desired_method="direct", propagation_node=None, propagation_node_auto=False, propagation_node_active=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): + self.storage_path = storage_path + + self.identity_file = identity_file + + self.identity = identity + + self.destination_name = destination_name + self.destination_type = destination_type + self.aspect_filter = self.destination_name + "." + self.destination_type + + self.display_name = display_name + self.announce_data = announce_data + self.announce_hidden = announce_hidden + + self.send_delay = int(send_delay) + + if desired_method == "propagated" or desired_method == "PROPAGATED": + self.desired_method_direct = False + else: + self.desired_method_direct = True + self.propagation_node = propagation_node + self.propagation_node_auto = propagation_node_auto + self.propagation_node_active = propagation_node_active + self.try_propagation_on_fail = try_propagation_on_fail + + self.announce_startup = announce_startup + self.announce_startup_delay = int(announce_startup_delay) + + self.announce_periodic = announce_periodic + self.announce_periodic_interval = int(announce_periodic_interval) + + self.sync_startup = sync_startup + self.sync_startup_delay = int(sync_startup_delay) + self.sync_limit = int(sync_limit) + self.sync_periodic = sync_periodic + self.sync_periodic_interval = int(sync_periodic_interval) + + if not self.storage_path: + log("LXMF - No storage_path parameter", LOG_ERROR) + return + + if not os.path.isdir(self.storage_path): + os.makedirs(self.storage_path) + log("LXMF - Storage path was created", LOG_NOTICE) + log("LXMF - Storage path: " + self.storage_path, LOG_INFO) + + if self.identity: + log("LXMF - Using existing Primary Identity %s" % (str(self.identity))) + else: + if not self.identity_file: + self.identity_file = "identity" + self.identity_path = self.storage_path + "/" + self.identity_file + if os.path.isfile(self.identity_path): + try: + self.identity = RNS.Identity.from_file(self.identity_path) + if self.identity != None: + log("LXMF - Loaded Primary Identity %s from %s" % (str(self.identity), self.identity_path)) + else: + log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + except Exception as e: + log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) + else: + try: + log("LXMF - No Primary Identity file found, creating new...") + self.identity = RNS.Identity() + self.identity.to_file(self.identity_path) + log("LXMF - Created new Primary Identity %s" % (str(self.identity))) + except Exception as e: + log("LXMF - Could not create and save a new Primary Identity", LOG_ERROR) + log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) + + self.message_router = LXMF.LXMRouter(identity=self.identity, storagepath=self.storage_path) + + if self.destination_name == "lxmf" and self.destination_type == "delivery": + self.destination = self.message_router.register_delivery_identity(self.identity, display_name=self.display_name) + self.message_router.register_delivery_callback(self.process_lxmf_message_propagated) + else: + self.destination = RNS.Destination(self.identity, RNS.Destination.IN, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + + if self.display_name == "": + self.display_name = RNS.prettyhexrep(self.destination_hash()) + + self.destination.set_default_app_data(self.display_name.encode("utf-8")) + + self.destination.set_proof_strategy(RNS.Destination.PROVE_ALL) + + RNS.Identity.remember(packet_hash=None, destination_hash=self.destination.hash, public_key=self.identity.get_public_key(), app_data=None) + + log("LXMF - Identity: " + str(self.identity), LOG_INFO) + log("LXMF - Destination: " + str(self.destination), LOG_INFO) + log("LXMF - Hash: " + RNS.prettyhexrep(self.destination_hash()), LOG_INFO) + + self.destination.set_link_established_callback(self.client_connected) + + if self.propagation_node_auto: + self.propagation_callback = lxmf_connection_propagation(self, "lxmf.propagation") + RNS.Transport.register_announce_handler(self.propagation_callback) + if self.propagation_node_active: + self.propagation_node_set(self.propagation_node_active) + elif self.propagation_node: + self.propagation_node_set(self.propagation_node) + else: + self.propagation_node_set(self.propagation_node) + + if self.announce_startup or self.announce_periodic: + self.announce(initial=True) + + if self.sync_startup or self.sync_periodic: + self.sync(True) + + + def register_announce_callback(self, handler_function): + self.announce_callback = handler_function(self.aspect_filter) + RNS.Transport.register_announce_handler(self.announce_callback) + + + def register_message_received_callback(self, handler_function): + self.message_received_callback = handler_function + + + def register_message_notification_callback(self, handler_function): + self.message_notification_callback = handler_function + + + def register_message_notification_success_callback(self, handler_function): + self.message_notification_success_callback = handler_function + + + def register_message_notification_failed_callback(self, handler_function): + self.message_notification_failed_callback = handler_function + + + def register_config_set_callback(self, handler_function): + self.config_set_callback = handler_function + + + def destination_hash(self): + return self.destination.hash + + + def destination_hash_str(self): + return RNS.hexrep(self.destination.hash, False) + + + def destination_check(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Destination length is invalid", LOG_ERROR) + return False + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return False + + return True + + + def destination_correct(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + return "" + + try: + destination_bytes = bytes.fromhex(destination) + return destination + except Exception as e: + return "" + + return "" + + + def send(self, destination, content="", title="", fields=None, timestamp=None, app_data="", destination_name=None, destination_type=None): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Destination length is invalid", LOG_ERROR) + return None + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return None + + if destination_name == None: + destination_name = self.destination_name + if destination_type == None: + destination_type = self.destination_type + + destination_identity = RNS.Identity.recall(destination) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type) + return self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + + + def send_message(self, destination, source, content="", title="", fields=None, timestamp=None, app_data=""): + if self.desired_method_direct: + desired_method = LXMF.LXMessage.DIRECT + else: + desired_method = LXMF.LXMessage.PROPAGATED + + message = LXMF.LXMessage(destination, source, content, title=title, desired_method=desired_method) + + if fields is not None: + message.fields = fields + + if timestamp is not None: + message.timestamp = timestamp + + message.app_data = app_data + + self.message_method(message) + self.log_message(message, "LXMF - Message send") + + message.register_delivery_callback(self.message_notification) + message.register_failed_callback(self.message_notification) + + if self.message_router.get_outbound_propagation_node() != None: + message.try_propagation_on_fail = self.try_propagation_on_fail + + try: + self.message_router.handle_outbound(message) + time.sleep(self.send_delay) + return message.hash + except Exception as e: + log("LXMF - Could not send message " + str(message), LOG_ERROR) + log("LXMF - The contained exception was: " + str(e), LOG_ERROR) + return None + + + def message_notification(self, message): + self.message_method(message) + + if self.message_notification_callback is not None: + self.message_notification_callback(message) + + if message.state == LXMF.LXMessage.FAILED and hasattr(message, "try_propagation_on_fail") and message.try_propagation_on_fail: + self.log_message(message, "LXMF - Delivery receipt (failed) Retrying as propagated message") + message.try_propagation_on_fail = None + message.delivery_attempts = 0 + del message.next_delivery_attempt + message.packed = None + message.desired_method = LXMF.LXMessage.PROPAGATED + self.message_router.handle_outbound(message) + elif message.state == LXMF.LXMessage.FAILED: + self.log_message(message, "LXMF - Delivery receipt (failed)") + if self.message_notification_failed_callback is not None: + self.message_notification_failed_callback(message) + else: + self.log_message(message, "LXMF - Delivery receipt (success)") + if self.message_notification_success_callback is not None: + self.message_notification_success_callback(message) + + + def message_method(self, message): + if message.desired_method == LXMF.LXMessage.DIRECT: + message.desired_method_str = "direct" + elif message.desired_method == LXMF.LXMessage.PROPAGATED: + message.desired_method_str = "propagated" + + + def announce(self, app_data=None, attached_interface=None, initial=False): + announce_timer = None + + if self.announce_periodic and self.announce_periodic_interval > 0: + announce_timer = threading.Timer(self.announce_periodic_interval*60, self.announce) + announce_timer.daemon = True + announce_timer.start() + + if initial: + if self.announce_startup: + if self.announce_startup_delay > 0: + if announce_timer is not None: + announce_timer.cancel() + announce_timer = threading.Timer(self.announce_startup_delay, self.announce) + announce_timer.daemon = True + announce_timer.start() + else: + self.announce_now(app_data=app_data, attached_interface=attached_interface) + return + + self.announce_now(app_data=app_data, attached_interface=attached_interface) + + + def announce_now(self, app_data=None, attached_interface=None): + if self.announce_hidden: + self.destination.announce("".encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +" (Hidden)", LOG_DEBUG) + elif app_data != None: + if isinstance(app_data, str): + self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG) + else: + self.destination.announce(app_data, attached_interface=attached_interface) + log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) + elif self.announce_data: + if isinstance(self.announce_data, str): + self.destination.announce(self.announce_data.encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG) + else: + self.destination.announce(self.announce_data, attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) + else: + self.destination.announce() + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ": " + self.display_name, LOG_DEBUG) + + + def sync(self, initial=False): + sync_timer = None + + if self.sync_periodic and self.sync_periodic_interval > 0: + sync_timer = threading.Timer(self.sync_periodic_interval*60, self.sync) + sync_timer.daemon = True + sync_timer.start() + + if initial: + if self.sync_startup: + if self.sync_startup_delay > 0: + if sync_timer is not None: + sync_timer.cancel() + sync_timer = threading.Timer(self.sync_startup_delay, self.sync) + sync_timer.daemon = True + sync_timer.start() + else: + self.sync_now(self.sync_limit) + return + + self.sync_now(self.sync_limit) + + + def sync_now(self, limit=None): + if self.message_router.get_outbound_propagation_node() is not None: + if self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_IDLE or self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_COMPLETE: + log("LXMF - Message sync requested from propagation node " + RNS.prettyhexrep(self.message_router.get_outbound_propagation_node()) + " for " + str(self.identity)) + self.message_router.request_messages_from_propagation_node(self.identity, max_messages = limit) + return True + else: + return False + else: + return False + + + def propagation_node_set(self, dest_str): + if not dest_str: + return False + + if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + return False + + try: + dest_hash = bytes.fromhex(dest_str) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return False + + node_identity = RNS.Identity.recall(dest_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(dest_hash), LOG_INFO) + dest_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(dest_hash) + self.propagation_node_active = dest_str + return True + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + return False + + + def propagation_node_update(self, dest_str): + if self.propagation_node_hash_str() != dest_str: + if self.propagation_node_set(dest_str) and self.config_set_callback is not None: + self.config_set_callback("propagation_node_active", dest_str) + + + def propagation_node_hash(self): + try: + return bytes.fromhex(self.propagation_node_active) + except: + return None + + + def propagation_node_hash_str(self): + if self.propagation_node_active: + return self.propagation_node_active + else: + return "" + + + def client_connected(self, link): + log("LXMF - Client connected " + str(link), LOG_EXTREME) + link.set_resource_strategy(RNS.Link.ACCEPT_ALL) + link.set_resource_concluded_callback(self.resource_concluded) + link.set_packet_callback(self.packet_received) + + + def packet_received(self, lxmf_bytes, packet): + log("LXMF - Single packet delivered " + str(packet), LOG_EXTREME) + self.process_lxmf_message_bytes(lxmf_bytes) + + + def resource_concluded(self, resource): + log("LXMF - Resource data transfer (multi packet) delivered " + str(resource.file), LOG_EXTREME) + if resource.status == RNS.Resource.COMPLETE: + lxmf_bytes = resource.data.read() + self.process_lxmf_message_bytes(lxmf_bytes) + else: + log("LXMF - Received resource message is not complete", LOG_EXTREME) + + + def process_lxmf_message_bytes(self, lxmf_bytes): + try: + message = LXMF.LXMessage.unpack_from_bytes(lxmf_bytes) + except Exception as e: + log("LXMF - Could not assemble LXMF message from received data", LOG_ERROR) + log("LXMF - The contained exception was: " + str(e), LOG_ERROR) + return + + message.desired_method = LXMF.LXMessage.DIRECT + + self.message_method(message) + self.log_message(message, "LXMF - Message received") + + if self.message_received_callback is not None: + log("LXMF - Call to registered message received callback", LOG_DEBUG) + self.message_received_callback(message) + else: + log("LXMF - No message received callback registered", LOG_DEBUG) + + + def process_lxmf_message_propagated(self, message): + message.desired_method = LXMF.LXMessage.PROPAGATED + + self.message_method(message) + self.log_message(message, "LXMF - Message received") + + if self.message_received_callback is not None: + log("LXMF - Call to registered message received callback", LOG_DEBUG) + self.message_received_callback(message) + else: + log("LXMF - No message received callback registered", LOG_DEBUG) + + + def log_message(self, message, message_tag="LXMF - Message log"): + if message.signature_validated: + signature_string = "Validated" + else: + if message.unverified_reason == LXMF.LXMessage.SIGNATURE_INVALID: + signature_string = "Invalid signature" + elif message.unverified_reason == LXMF.LXMessage.SOURCE_UNKNOWN: + signature_string = "Cannot verify, source is unknown" + else: + signature_string = "Signature is invalid, reason undetermined" + title = message.title.decode('utf-8') + content = message.content.decode('utf-8') + fields = message.fields + log(message_tag + ":", LOG_DEBUG) + log("- Date/Time: " + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.timestamp)), LOG_DEBUG) + log("- Title: " + title, LOG_DEBUG) + log("- Content: " + content, LOG_DEBUG) + log("- Fields: " + str(fields), LOG_DEBUG) + log("- Size: " + str(len(title) + len(content) + len(title) + len(pickle.dumps(fields))) + " bytes", LOG_DEBUG) + log("- Source: " + RNS.prettyhexrep(message.source_hash), LOG_DEBUG) + log("- Destination: " + RNS.prettyhexrep(message.destination_hash), LOG_DEBUG) + log("- Signature: " + signature_string, LOG_DEBUG) + log("- Attempts: " + str(message.delivery_attempts), LOG_DEBUG) + if hasattr(message, "desired_method_str"): + log("- Method: " + message.desired_method_str + " (" + str(message.desired_method) + ")", LOG_DEBUG) + else: + log("- Method: " + str(message.desired_method), LOG_DEBUG) + if hasattr(message, "app_data"): + log("- App Data: " + message.app_data, LOG_DEBUG) + + + + +class lxmf_connection_propagation(): + def __init__(self, owner, aspect_filter=None): + self.owner = owner + self.aspect_filter = aspect_filter + + EMITTED_DELTA_GRACE = 300 + EMITTED_DELTA_IGNORE = 10 + + def received_announce(self, destination_hash, announced_identity, app_data): + if app_data == None: + return + + if len(app_data) == 0: + return + + try: + unpacked = umsgpack.unpackb(app_data) + node_active = unpacked[0] + emitted = unpacked[1] + hop_count = RNS.Transport.hops_to(destination_hash) + age = time.time() - emitted + if age < 0: + if age < -1*PropDetector.EMITTED_DELTA_GRACE: + return + log("LXMF - Received an propagation node announce from "+RNS.prettyhexrep(destination_hash)+": "+str(age)+" seconds ago, "+str(hop_count)+" hops away", LOG_INFO) + if self.owner.propagation_node_active == None: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + else: + prev_hop_count = RNS.Transport.hops_to(self.owner.propagation_node_hash()) + if hop_count <= prev_hop_count: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + except: + return + + +############################################################################################################## +# Matrix Class + + +class matrix_connection: + message_received_callback = None + message_send_callback = None + + + def __init__(self, storage_path, address, username, password): + self.storage_path = storage_path + + self.address = address + self.username = username + self.password = password + + if not self.storage_path: + log("Matrix - No storage_path parameter", LOG_ERROR) + return + + if not os.path.isdir(self.storage_path): + os.makedirs(self.storage_path) + log("Matrix - Storage path was created", LOG_NOTICE) + log("Matrix - Storage path: " + self.storage_path, LOG_INFO) + + self.sync_path = self.storage_path + "/matrix_sync" + + self.client = None + + + def loop_forever(self): + self.thread = asyncio.get_event_loop() + self.thread.run_until_complete(self.main()) + + + def register_message_received_callback(self, handler_function): + self.message_received_callback = handler_function + + + def register_message_send_callback(self, handler_function): + self.message_send_callback = handler_function + + + async def message_callback(self, room: MatrixRoom, event: RoomMessage) -> None: + if event.sender == self.username: + return + + self.log_message(source_address=event.sender, source_name= room.user_name(event.sender), destination_address=room.room_id, destination_name=room.display_name, content=event.source.get("content"), message_tag="Matrix - Message received") + + if self.message_received_callback is not None: + log("Matrix - Call to registered message received callback", LOG_DEBUG) + self.message_received_callback(room, event) + else: + log("Matrix - No message received callback registered", LOG_DEBUG) + + + async def sync_cb(self, response): + log("Matrix - Synced token: " + response.next_batch, LOG_DEBUG) + try: + with open(self.sync_path, "w") as fh: + fh.write(response.next_batch) + except: + pass + + + def send(self, room_id, content, hash): + asyncio.run_coroutine_threadsafe(self.send_message(room_id, content, hash), self.thread) + + + async def send_message(self, room_id, content, hash): + global DATA + + self.log_message(source_address=self.address, destination_address=room_id, content=content, message_tag="Matrix - Message send") + + resp = await self.client.room_send( + room_id=room_id, + message_type="m.room.message", + content=content, + ) + + if self.message_send_callback is not None: + self.message_send_callback(resp.event_id, hash) + + + def send_file(self, room_id, name, size, data): + # TODO + asyncio.run_coroutine_threadsafe(self.send_message_file(room_id, name, size, data), self.thread) + + + async def send_message_file(self, room_id, name, size, data): + # TODO + print("#1") + self.log_message(source_address=self.address, destination_address=room_id, content=name+" ("+str(size)+")", message_tag="Matrix - File send") + + mime_type = "image/jpe" + + print("#2") + resp, maybe_keys = await self.client.upload( + data, + content_type=mime_type, + filename=name, + filesize=size) + + print("#3") + if isinstance(resp, UploadResponse): + log("Matrix - Upload ok: "+str(resp), LOG_DEBUG) + else: + log("Matrix - Upload error: "+str(resp), LOG_ERROR) + + content = { + "body": name, + "info": { + "size": size, + "mimetype": mime_type, + }, + "msgtype": "m.file", + "url": resp.content_uri, + } + + print("#4") + resp = await self.client.room_send( + room_id=room_id, + message_type="m.room.message", + content=content, + ) + print("#5") + + + async def main(self) -> None: + while True: + try: + self.client = AsyncClient(self.address, self.username) + try: + with open(self.sync_path, "r") as fh: + self.client.next_batch = fh.read() + except: + pass + + self.client.add_event_callback(self.message_callback, RoomMessage) + self.client.add_response_callback(self.sync_cb, SyncResponse) + + resp = await self.client.login(self.password) + if isinstance(resp, LoginResponse): + log("Matrix - Login ok: "+str(resp), LOG_DEBUG) + else: + log("Matrix - Login error: "+str(resp), LOG_ERROR) + + log("Matrix - Connected", LOG_DEBUG) + await self.client.sync_forever(timeout=30000) # Milliseconds + log("Matrix - Connection timeout", LOG_ERROR) + log("Matrix - Reconnect in 10 seconds", LOG_ERROR) + except Exception as e: + log("Matrix - Connection error: "+str(e), LOG_ERROR) + log("Matrix - Reconnect in 10 seconds", LOG_ERROR) + time.sleep(10) + + + def log_message(self, source_address="", source_name="", destination_address="", destination_name="", content="", message_tag="Matrix - Message log"): + log(message_tag + ":", LOG_DEBUG) + log("- Content: " + str(content), LOG_DEBUG) + log("- Size: " + str(len(content)) + " bytes", LOG_DEBUG) + log("- Source addr.: " + str(source_address), LOG_DEBUG) + log("- Source name: " + str(source_name), LOG_DEBUG) + log("- Destination addr.: " + str(destination_address), LOG_DEBUG) + log("- Destination name : " + str(destination_name), LOG_DEBUG) + + +############################################################################################################## +# LXMF Functions + + +#### LXMF - Announce #### +class lxmf_announce_callback: + def __init__(self, aspect_filter=None): + self.aspect_filter = aspect_filter + + + @staticmethod + def received_announce(destination_hash, announced_identity, app_data): + if app_data == None: + return + + if len(app_data) == 0: + return + + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + + + + +#### LXMF - Message #### +def lxmf_message_received_callback(message): + if not CONFIG["router"].getboolean("lxmf_to_matrix"): + log("LXMF - Routing disabled", LOG_DEBUG) + return + + if CONFIG["lxmf"].getboolean("signature_validated") and not message.signature_validated: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " have no valid signature", LOG_DEBUG) + return + + if CONFIG.has_option("allowed", "any") or CONFIG.has_option("allowed", "all") or CONFIG.has_option("allowed", "anybody") or CONFIG.has_option("allowed", RNS.hexrep(message.source_hash, False)) or CONFIG.has_option("allowed", RNS.prettyhexrep(message.source_hash)): + + destination_address = RNS.hexrep(message.source_hash, False) + + if destination_address not in ROUTING_TABLE: + log("LXMF - Routing table not found for '"+destination_address+"'", LOG_DEBUG) + return + + title = message.title.decode('utf-8').strip() + denys = config_get(CONFIG, "message", "lxmf_to_matrix_deny_title") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in title: + return + + content = message.content.decode('utf-8').strip() + denys = config_get(CONFIG, "message", "lxmf_to_matrix_deny_content") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in title: + return + + if message.fields: + denys = config_get(CONFIG, "message", "lxmf_to_matrix_deny_fields") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in message.fields: + return + + length = config_getint(CONFIG, "message", "lxmf_to_matrix_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "lxmf_to_matrix_length_max", 0) + if length > 0: + if len(content) > length: + return + + source_address = RNS.hexrep(message.source_hash, False) + source_name = "" + + if message.fields: + if "src" in message.fields: + source_address = RNS.hexrep(message.fields["src"]["h"], False) + source_name = message.fields["src"]["n"] + + routing_destination = ROUTING_TABLE[destination_address][0] + routing_table = ROUTING_TABLE[destination_address][1] + + content_prefix = config_get(CONFIG, "message", "lxmf_to_matrix_prefix") + content_prefix = replace(content_prefix, source_address=source_address, source_name=source_name, destination_address=destination_address, destination_name="", routing_table=routing_table) + content_suffix = config_get(CONFIG, "message", "lxmf_to_matrix_suffix") + content_suffix = replace(content_suffix, source_address=source_address, source_name=source_name, destination_address=destination_address, destination_name="", routing_table=routing_table) + + search = config_get(CONFIG, "message", "lxmf_to_matrix_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "lxmf_to_matrix_replace")) + + search = config_get(CONFIG, "message", "lxmf_to_matrix_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "lxmf_to_matrix_regex_replace"), content) + + content = content_prefix + content + content_suffix + + if message.fields and "hash" in message.fields: + hash = message.fields["hash"] + else: + hash = message.hash + + if message.fields and "edit" in message.fields and "hash" in message.fields and message.fields["hash"] in DATA: + content = {"msgtype": "m.text", "body": "", "m.new_content": {"body": content, "msgtype": "m.text"}, "m.relates_to": {"event_id": DATA[message.fields["hash"]][0], "rel_type": "m.replace"}} + elif message.fields and "delete" in message.fields and "hash" in message.fields and message.fields["hash"] in DATA: + content = {"msgtype": "m.text", "body": "", "m.new_content": {"body": "-", "msgtype": "m.text"}, "m.relates_to": {"event_id": DATA[message.fields["hash"]][0], "rel_type": "m.replace"}} + elif message.fields and "answer" in message.fields and message.fields["answer"] in DATA: + content = {"msgtype": "m.text", "body": content, "m.relates_to": {"m.in_reply_to": {"event_id": DATA[message.fields["answer"]][0]}}} + else: + content = {"msgtype": "m.text", "body": content} + + MATRIX_CONNECTION.send(routing_destination, content, hash) + + # TODO + #if message.fields and "attachment" in message.fields: + # for attachment in message.fields["attachment"]: + # MATRIX_CONNECTION.send_file(routing_destination, attachment["name"], attachment["size"], attachment["data"]) + else: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not allowed", LOG_DEBUG) + return + + +############################################################################################################## +# Matrix Functions + + +#### Matrix - Message #### +def matrix_message_received_callback(room: MatrixRoom, event: RoomMessage): + global DATA + + if not CONFIG["router"].getboolean("matrix_to_lxmf"): + log("Matrix - Routing disabled", LOG_DEBUG) + return + + if room.room_id not in ROUTING_TABLE: + log("Matrix - Routing table not found for '"+room.room_id+"'", LOG_DEBUG) + return + + fields = {} + + user_name = room.user_name(event.sender) + if not user_name: + user_name = re.sub(r'@(.*):.*', r'\1', event.sender) + if user_name == event.sender: + user_name = "" + + content_dict = event.source.get("content") + + try: + if "m.relates_to" in content_dict and "rel_type" in content_dict["m.relates_to"] and content_dict["m.relates_to"]["rel_type"] == "m.replace": + content = content_dict["m.new_content"]["body"] + event_id = content_dict["m.relates_to"]["event_id"] + if event_id in DATA: + fields["hash"] = DATA[event_id][0] + if DATA[event_id][1]: + fields["answer"] = DATA[event_id][1] + fields["edit"] = time.time() + elif "m.relates_to" in content_dict and "m.in_reply_to" in content_dict["m.relates_to"]: + content = content_dict["formatted_body"] + content = re.sub(r'.*<\/mx-reply>', '', content) + event_id = content_dict["m.relates_to"]["m.in_reply_to"]["event_id"] + if event_id in DATA: + fields["answer"] = DATA[event_id][0] + else: + content = event.body + except: + content = event.body + + content = content.strip() + + length = config_getint(CONFIG, "message", "matrix_to_lxmf_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "matrix_to_lxmf_length_max", 0) + if length > 0: + if len(content) > length: + return + + routing_destination = ROUTING_TABLE[room.room_id][0] + routing_table = ROUTING_TABLE[room.room_id][1] + + content_prefix = config_get(CONFIG, "message", "matrix_to_lxmf_prefix") + content_prefix = replace(content_prefix, source_address=event.sender, source_name=user_name, destination_address=room.room_id, destination_name=room.display_name, routing_table=routing_table) + content_suffix = config_get(CONFIG, "message", "matrix_to_lxmf_suffix") + content_suffix = replace(content_suffix, source_address=event.sender, source_name=user_name, destination_address=room.room_id, destination_name=room.display_name, routing_table=routing_table) + + search = config_get(CONFIG, "message", "matrix_to_lxmf_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "matrix_to_lxmf_replace")) + + search = config_get(CONFIG, "message", "matrix_to_lxmf_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "matrix_to_lxmf_regex_replace"), content) + + content = content_prefix + content + content_suffix + + fields["src"] = {} + fields["src"]["h"] = b'' + fields["src"]["n"] = replace(config_get(CONFIG, "message", "matrix_to_lxmf"), source_address=event.sender, source_name=user_name, destination_address=room.room_id, destination_name=room.display_name, routing_table=routing_table) + + result = LXMF_CONNECTION.send(routing_destination, content, "", fields=fields) + + if result: + if "answer" in fields: + answer = fields["answer"] + else: + answer = None + DATA[result] = [event.event_id, answer] + DATA[event.event_id] = [result, answer] + DATA["unsaved"] = True + + if CONFIG["main"].getboolean("auto_save_data"): + del(DATA["unsaved"]) + if not data_save(PATH + "/data.data"): + DATA["unsaved"] = True + + +#### Matrix - Message #### +def matrix_message_send_callback(hash_0, hash_1): + global DATA + + DATA[hash_0] = [hash_1, None] + DATA[hash_1] = [hash_0, None] + DATA["unsaved"] = True + + if CONFIG["main"].getboolean("auto_save_data"): + del(DATA["unsaved"]) + if not data_save(PATH + "/data.data"): + DATA["unsaved"] = True + + +############################################################################################################## +# Functions + + +#### Replace ##### +def replace(text, source_address="", source_name="", destination_address="", destination_name="", routing_table=""): + text = text.replace("!source_address!", source_address) + text = text.replace("!source_name!", source_name) + text = text.replace("!destination_address!", destination_address) + text = text.replace("!destination_name!", destination_name) + text = text.replace("!routing_table!", routing_table) + + text = text.replace("!n!", "\n") + + return text + + +############################################################################################################## +# Config + + +#### Config - Get ##### +def config_get(config, section, key, default="", lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config[section][key+lng_key] + elif config.has_option(section, key): + return config[section][key] + return default + + +def config_getint(config, section, key, default=0, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config.getint(section, key+lng_key) + elif config.has_option(section, key): + return config.getint(section, key) + return default + + +def config_getboolean(config, section, key, default=False, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config[section].getboolean(key+lng_key) + elif config.has_option(section, key): + return config[section].getboolean(key) + return default + + +def config_getsection(config, section, default="", lng_key=""): + if not config or section == "": return default + if not config.has_section(section): return default + if config.has_section(section+lng_key): + return key+lng_key + elif config.has_section(section): + return key + return default + + +def config_getoption(config, section, key, default=False, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return key+lng_key + elif config.has_option(section, key): + return key + return default + + + + +#### Config - Set ##### +def config_set(key=None, value=""): + global PATH + + try: + file = PATH + "/config.cfg.owr" + if os.path.isfile(file): + fh = open(file,'r') + data = fh.read() + fh.close() + data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE) + fh = open(file,'w') + fh.write(data) + fh.close() + + file = PATH + "/config.cfg" + if os.path.isfile(file): + fh = open(file,'r') + data = fh.read() + fh.close() + data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE) + fh = open(file,'w') + fh.write(data) + fh.close() + except: + pass + + + + +#### Config - Read ##### +def config_read(file=None, file_override=None): + global CONFIG + + if file is None: + return False + else: + CONFIG = configparser.ConfigParser(allow_no_value=True, inline_comment_prefixes="#") + CONFIG.sections() + if os.path.isfile(file): + try: + if file_override is None: + CONFIG.read(file, encoding='utf-8') + elif os.path.isfile(file_override): + CONFIG.read([file, file_override], encoding='utf-8') + else: + CONFIG.read(file, encoding='utf-8') + except Exception as e: + return False + else: + if not config_default(file=file, file_override=file_override): + return False + return True + + + + +#### Config - Save ##### +def config_save(file=None): + global CONFIG + + if file is None: + return False + else: + if os.path.isfile(file): + try: + with open(file,"w") as file: + CONFIG.write(file) + except Exception as e: + return False + else: + return False + return True + + + + +#### Config - Default ##### +def config_default(file=None, file_override=None): + global CONFIG + + if file is None: + return False + elif DEFAULT_CONFIG != "": + if file_override and DEFAULT_CONFIG_OVERRIDE != "": + if not os.path.isdir(os.path.dirname(file_override)): + try: + os.makedirs(os.path.dirname(file_override)) + except Exception: + return False + if not os.path.exists(file_override): + try: + config_file = open(file_override, "w") + config_file.write(DEFAULT_CONFIG_OVERRIDE) + config_file.close() + except: + return False + + if not os.path.isdir(os.path.dirname(file)): + try: + os.makedirs(os.path.dirname(file)) + except Exception: + return False + try: + config_file = open(file, "w") + config_file.write(DEFAULT_CONFIG) + config_file.close() + if not config_read(file=file, file_override=file_override): + return False + except: + return False + else: + return False + + if not CONFIG.has_section("main"): CONFIG.add_section("main") + CONFIG["main"]["default_config"] = "True" + return True + + +############################################################################################################## +# Data + + +#### Data - Read ##### +def data_read(file=None): + global DATA + + if file is None: + DATA = {} + return False + else: + if os.path.isfile(file): + try: + fh = open(file , "rb") + DATA = umsgpack.unpackb(fh.read()) + fh.close() + except Exception as e: + DATA = {} + return False + else: + DATA = {} + return True + + + + +#### Data - Save ##### +def data_save(file=None): + global DATA + + if file is None: + return False + else: + try: + fh = open(file, "wb") + fh.write(umsgpack.packb(DATA)) + fh.close() + except Exception as e: + return False + return True + + + + +#### Data - Save ##### +def data_save_periodic(initial=False): + data_timer = threading.Timer(CONFIG.getint("main", "periodic_save_data_interval"), data_save_periodic) + data_timer.daemon = True + data_timer.start() + + if initial: + return + + global DATA + if "unsaved" in DATA: + del(DATA["unsaved"]) + if not data_save(PATH + "/data.data"): + DATA["unsaved"] = True + + +############################################################################################################## +# Value convert + + +def val_to_bool(val, fallback_true=True, fallback_false=False): + if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up": + return True + elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down": + return False + elif val != "": + return fallback_true + else: + return fallback_false + + +def val_to_val(val): + if val.isdigit(): + return int(val) + elif val.isnumeric(): + return float(val) + elif val.lower() == "true": + return True + elif val.lower() == "false": + return False + else: + return val + + +############################################################################################################## +# Log + + +LOG_FORCE = -1 +LOG_CRITICAL = 0 +LOG_ERROR = 1 +LOG_WARNING = 2 +LOG_NOTICE = 3 +LOG_INFO = 4 +LOG_VERBOSE = 5 +LOG_DEBUG = 6 +LOG_EXTREME = 7 + +LOG_LEVEL = LOG_NOTICE +LOG_LEVEL_SERVICE = LOG_NOTICE +LOG_TIMEFMT = "%Y-%m-%d %H:%M:%S" +LOG_MAXSIZE = 5*1024*1024 +LOG_PREFIX = "" +LOG_SUFFIX = "" +LOG_FILE = "" + + + + +def log(text, level=3, file=None): + if not LOG_LEVEL: + return + + if LOG_LEVEL >= level: + name = "Unknown" + if (level == LOG_FORCE): + name = "" + if (level == LOG_CRITICAL): + name = "Critical" + if (level == LOG_ERROR): + name = "Error" + if (level == LOG_WARNING): + name = "Warning" + if (level == LOG_NOTICE): + name = "Notice" + if (level == LOG_INFO): + name = "Info" + if (level == LOG_VERBOSE): + name = "Verbose" + if (level == LOG_DEBUG): + name = "Debug" + if (level == LOG_EXTREME): + name = "Extra" + + if not isinstance(text, str): + text = str(text) + + text = "[" + time.strftime(LOG_TIMEFMT, time.localtime(time.time())) +"] [" + name + "] " + LOG_PREFIX + text + LOG_SUFFIX + + if file == None and LOG_FILE != "": + file = LOG_FILE + + if file == None: + print(text) + else: + try: + file_handle = open(file, "a") + file_handle.write(text + "\n") + file_handle.close() + + if os.path.getsize(file) > LOG_MAXSIZE: + file_prev = file + ".1" + if os.path.isfile(file_prev): + os.unlink(file_prev) + os.rename(file, file_prev) + except: + return + + +############################################################################################################## +# System + + +#### Panic ##### +def panic(): + sys.exit(255) + + +#### Exit ##### +def exit(): + sys.exit(0) + + +############################################################################################################## +# Setup/Start + + +#### Setup ##### +def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False): + global PATH + global PATH_RNS + global LOG_LEVEL + global LOG_FILE + global ROUTING_TABLE + global RNS_CONNECTION + global LXMF_CONNECTION + global MATRIX_CONNECTION + + if path is not None: + if path.endswith("/"): + path = path[:-1] + PATH = path + + if path_rns is not None: + if path_rns.endswith("/"): + path_rns = path_rns[:-1] + PATH_RNS = path_rns + + if loglevel is not None: + LOG_LEVEL = loglevel + rns_loglevel = loglevel + else: + rns_loglevel = None + + if service: + LOG_LEVEL = LOG_LEVEL_SERVICE + if path_log is not None: + if path_log.endswith("/"): + path_log = path_log[:-1] + LOG_FILE = path_log + else: + LOG_FILE = PATH + LOG_FILE = LOG_FILE + "/" + NAME + ".log" + rns_loglevel = None + + if not config_read(PATH + "/config.cfg", PATH + "/config.cfg.owr"): + print("Config - Error reading config file " + PATH + "/config.cfg") + panic() + + if not data_read(PATH + "/data.data"): + print("Data - Error reading data file " + PATH + "/data.data") + panic() + + if CONFIG["main"].getboolean("default_config"): + print("Exit!") + print("First start with the default config!") + print("You should probably edit the config file \"" + PATH + "/config.cfg\" to suit your needs and use-case!") + print("You should make all your changes at the user configuration file \"" + PATH + "/config.cfg.owr\" to override the default configuration file!") + print("Then restart this program again!") + exit() + + if not CONFIG["main"].getboolean("enabled"): + print("Disabled in config file. Exit!") + exit() + + ROUTING_TABLE = {} + if CONFIG.has_section("routing_table"): + for (key, val) in CONFIG.items("routing_table"): + try: + value, name = val.split(" = ", 1) + except: + value = val + name = "" + ROUTING_TABLE[key] = [value, name] + ROUTING_TABLE[value] = [key, name] + + RNS_CONNECTION = RNS.Reticulum(configdir=PATH_RNS, loglevel=rns_loglevel) + + log("...............................................................................", LOG_INFO) + log(" Name: " + CONFIG["main"]["name"], LOG_INFO) + log("Program File: " + __file__, LOG_INFO) + log(" Config File: " + PATH + "/config", LOG_INFO) + log(" Version: " + VERSION, LOG_INFO) + log(" Copyright: " + COPYRIGHT, LOG_INFO) + log("...............................................................................", LOG_INFO) + + if CONFIG["main"].getboolean("periodic_save_data"): + data_save_periodic(True) + + log("LXMF - Connecting ...", LOG_DEBUG) + + if CONFIG.has_option("lxmf", "propagation_node"): + config_propagation_node = CONFIG["lxmf"]["propagation_node"] + else: + config_propagation_node = None + + if CONFIG.has_option("lxmf", "propagation_node_active"): + config_propagation_node_active = CONFIG["lxmf"]["propagation_node_active"] + else: + config_propagation_node_active = None + + if path is None: + path = PATH + + LXMF_CONNECTION = lxmf_connection( + storage_path=path, + destination_name=CONFIG["lxmf"]["destination_name"], + destination_type=CONFIG["lxmf"]["destination_type"], + display_name=CONFIG["lxmf"]["display_name"], + announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"), + send_delay=CONFIG["lxmf"]["send_delay"], + desired_method=CONFIG["lxmf"]["desired_method"], + propagation_node=config_propagation_node, + propagation_node_auto=CONFIG["lxmf"].getboolean("propagation_node_auto"), + propagation_node_active=config_propagation_node_active, + try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"), + announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), + announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], + announce_periodic=CONFIG["lxmf"].getboolean("announce_periodic"), + announce_periodic_interval=CONFIG["lxmf"]["announce_periodic_interval"], + sync_startup=CONFIG["lxmf"].getboolean("sync_startup"), + sync_startup_delay=CONFIG["lxmf"]["sync_startup_delay"], + sync_limit=CONFIG["lxmf"]["sync_limit"], + sync_periodic=CONFIG["lxmf"].getboolean("sync_periodic"), + sync_periodic_interval=CONFIG["lxmf"]["sync_periodic_interval"]) + + LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback) + LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) + LXMF_CONNECTION.register_config_set_callback(config_set) + + log("LXMF - Connected", LOG_DEBUG) + + log("...............................................................................", LOG_FORCE) + log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE) + log("...............................................................................", LOG_FORCE) + + log("Matrix - Connecting ...", LOG_DEBUG) + MATRIX_CONNECTION = matrix_connection( + storage_path=path, + address=CONFIG["matrix"]["address"], + username=CONFIG["matrix"]["username"], + password=CONFIG["matrix"]["password"] + ) + MATRIX_CONNECTION.register_message_received_callback(matrix_message_received_callback) + MATRIX_CONNECTION.register_message_send_callback(matrix_message_send_callback) + MATRIX_CONNECTION.loop_forever() + + while True: + time.sleep(1) + + + + +#### Start #### +def main(): + try: + description = NAME + " - " + DESCRIPTION + parser = argparse.ArgumentParser(description=description) + + parser.add_argument("-p", "--path", action="store", type=str, default=None, help="Path to alternative config directory") + parser.add_argument("-pr", "--path_rns", action="store", type=str, default=None, help="Path to alternative Reticulum config directory") + parser.add_argument("-pl", "--path_log", action="store", type=str, default=None, help="Path to alternative log directory") + parser.add_argument("-l", "--loglevel", action="store", type=int, default=LOG_LEVEL) + parser.add_argument("-s", "--service", action="store_true", default=False, help="Running as a service and should log to file") + parser.add_argument("--exampleconfig", action="store_true", default=False, help="Print verbose configuration example to stdout and exit") + parser.add_argument("--exampleconfigoverride", action="store_true", default=False, help="Print verbose configuration example to stdout and exit") + + params = parser.parse_args() + + if params.exampleconfig: + print("Config File: " + PATH + "/config.cfg") + print("Content:") + print(DEFAULT_CONFIG) + exit() + + if params.exampleconfigoverride: + print("Config Override File: " + PATH + "/config.cfg.owr") + print("Content:") + print(DEFAULT_CONFIG_OVERRIDE) + exit() + + setup(path=params.path, path_rns=params.path_rns, path_log=params.path_log, loglevel=params.loglevel, service=params.service) + + except KeyboardInterrupt: + print("Terminated by CTRL-C") + exit() + + +############################################################################################################## +# Files + + +#### Default configuration override file #### +DEFAULT_CONFIG_OVERRIDE = '''# This is the user configuration file to override the default configuration file. +# All settings made here have precedence. +# This file can be used to clearly summarize all settings that deviate from the default. +# This also has the advantage that all changed settings can be kept when updating the program. +''' + + +#### Default configuration file #### +DEFAULT_CONFIG = '''# This is the default config file. +# You should probably edit it to suit your needs and use-case. + + + + +#### Main program settings #### +[main] + +enabled = True + +# Name of the program. Only for display in the log or program startup. +name = + + +# Auto save changes. +# If there are changes in the data, they can be saved directly in the files. +# Attention: This can lead to very high write cycles. +# If you want to prevent frequent writing, please set this to 'False' and use the peridodic save function. +auto_save_data = False + +# Periodic actions - Save changes periodically. +periodic_save_data = True +periodic_save_data_interval = 1 #Minutes + + + + +#### LXMF connection settings #### +[lxmf] + +# Destination name & type need to fits the LXMF protocoll +# to be compatibel with other LXMF programs. +destination_name = lxmf +destination_type = delivery + +# The name will be visible to other peers +# on the network, and included in announces. +display_name = + +# Default send method. +desired_method = direct #direct/propagated + +# Propagation node address/hash. +propagation_node = + +# Set propagation node automatically. +propagation_node_auto = True + +# Current propagation node (Automatically set by the software). +propagation_node_active = + +# Try to deliver a message via the LXMF propagation network, +# if a direct delivery to the recipient is not possible. +try_propagation_on_fail = Yes + +# The peer is announced at startup +# to let other peers reach it immediately. +announce_startup = No +announce_startup_delay = 0 #Seconds + +# The peer is announced periodically +# to let other peers reach it. +announce_periodic = No +announce_periodic_interval = 360 #Minutes + +# The announce is hidden for client applications +# but is still used for the routing tables. +announce_hidden = No + +# Some waiting time after message send +# for LXMF/Reticulum processing. +send_delay = 0 #Seconds + +# Sync LXMF messages at startup. +sync_startup = No +sync_startup_delay = 0 #Seconds + +# Sync LXMF messages periodically. +sync_periodic = No + +# The sync interval in minutes. +sync_periodic_interval = 360 #Minutes + +# Automatic LXMF syncs will only +# download x messages at a time. You can change +# this number, or set the option to 0 to disable +# the limit, and download everything every time. +sync_limit = 0 + +# Allow only messages with valid signature. +signature_validated = Yes + + + + +#### Matrix connection settings #### +[matrix] + +address = + +username = + +password = + + + + +#### Message router settings #### +[router] + +# Transmit LXMF messages to Matrix +lxmf_to_matrix = True + +# Transmit Matrix messages to LXMF +matrix_to_lxmf = True + + + + +#### Message routing table #### +# Definition of the assignment of lxmf addresses to matrix room ids (bidirectional routing). +# Format: = = +# Example: 2858b7a096899116cd529559cc679ffe = !ADeAldKEzhgebazEzG:matrix.org = Test-Room +[routing_table] + + + + +#### Message settings #### +[message] + +# Deny message if the title/content/fields contains the following content. +# Comma-separated list with text or field keys. +# *=any +lxmf_to_matrix_deny_title = +lxmf_to_matrix_deny_content = +lxmf_to_matrix_deny_fields = + +# Source name +lxmf_to_matrix = + +# Text is added. +lxmf_to_matrix_prefix = !source_name! !n! +lxmf_to_matrix_suffix = + +# Text is replaced. +lxmf_to_matrix_search = +lxmf_to_matrix_replace = + +# Text is replaced by regular expression. +lxmf_to_matrix_regex_search = +lxmf_to_matrix_regex_replace = + +# Length limitation. +lxmf_to_matrix_length_min = 0 #0=any length +lxmf_to_matrix_length_max = 0 #0=any length + + +# Deny message if the title/content/fields contains the following content. +# Comma-separated list with text or field keys. +# *=any +matrix_to_lxmf_deny_title = +matrix_to_lxmf_deny_content = +matrix_to_lxmf_deny_fields = + +# Source name +matrix_to_lxmf = !source_name! (!routing_table!) + +# Text is added. +matrix_to_lxmf_prefix = +matrix_to_lxmf_suffix = + +# Text is replaced. +matrix_to_lxmf_search = +matrix_to_lxmf_replace = + +# Text is replaced by regular expression. +matrix_to_lxmf_regex_search = +matrix_to_lxmf_regex_replace = + +# Length limitation. +matrix_to_lxmf_length_min = 0 #0=any length +matrix_to_lxmf_length_max = 0 #0=any length + + + + +#### Right settings #### +# Allow only specific source addresses/hashs or any. +[allowed] + +any +#2858b7a096899116cd529559cc679ffe +''' + + +############################################################################################################## +# Init + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/lxmf_bridge_mqtt/README.md b/lxmf_bridge_mqtt/README.md index 32b38ce..b817fbc 100644 --- a/lxmf_bridge_mqtt/README.md +++ b/lxmf_bridge_mqtt/README.md @@ -18,7 +18,7 @@ All messages between client<->server are transported as single 1:1 messages in t Accordingly, encryption takes place between these end points. If a direct delivery of the message does not work, it is sent to a propagation node. There it is stored temporarily and can be retrieved by the client later. -As these are normal LXMF messages, any LXMF capable application can be used to communicate with the group. +As these are normal LXMF messages, any LXMF capable application can be used to communicate with it. ## Current Status @@ -53,6 +53,10 @@ The full documentation is not yet available. Due to lack of time I can also not pip3 install lxmf ``` +- Install all required prerequisites. + ```bash + pip3 install paho-mqtt + ``` - Change the Reticulum configuration to suit your needs and use-case. ```bash nano /.reticulum/config @@ -150,10 +154,10 @@ The full documentation is not yet available. Due to lack of time I can also not ### Startup parameters: ```bash -usage: lxmf_distribution_group_minimal.py [-h] [-p PATH] [-pr PATH_RNS] [-pl PATH_LOG] [-l LOGLEVEL] [-s] [--exampleconfig] - [--exampleconfigoverride] [--exampledata] +usage: lxmf_bridge_mqtt.py [-h] [-p PATH] [-pr PATH_RNS] [-pl PATH_LOG] [-l LOGLEVEL] [-s] [--exampleconfig] + [--exampleconfigoverride] -LXMF Distribution Group - Server-Side group functions for LXMF based apps +LXMF Bridge MQTT optional arguments: -h, --help show this help message and exit @@ -167,7 +171,6 @@ optional arguments: --exampleconfig Print verbose configuration example to stdout and exit --exampleconfigoverride Print verbose configuration example to stdout and exit - --exampledata Print verbose configuration example to stdout and exit ``` diff --git a/lxmf_bridge_mqtt/lxmf_bridge_mqtt.py b/lxmf_bridge_mqtt/lxmf_bridge_mqtt.py index 5660893..f942620 100755 --- a/lxmf_bridge_mqtt/lxmf_bridge_mqtt.py +++ b/lxmf_bridge_mqtt/lxmf_bridge_mqtt.py @@ -291,13 +291,13 @@ class lxmf_connection: if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): log("LXMF - Destination length is invalid", LOG_ERROR) - return + return None try: destination = bytes.fromhex(destination) except Exception as e: log("LXMF - Destination is invalid", LOG_ERROR) - return + return None if destination_name == None: destination_name = self.destination_name @@ -306,7 +306,7 @@ class lxmf_connection: destination_identity = RNS.Identity.recall(destination) destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type) - self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + return self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) def send_message(self, destination, source, content="", title="", fields=None, timestamp=None, app_data=""): @@ -337,10 +337,11 @@ class lxmf_connection: try: self.message_router.handle_outbound(message) time.sleep(self.send_delay) + return message.hash except Exception as e: log("LXMF - Could not send message " + str(message), LOG_ERROR) log("LXMF - The contained exception was: " + str(e), LOG_ERROR) - return + return None def message_notification(self, message): @@ -635,23 +636,28 @@ class lxmf_announce_callback: @staticmethod def received_announce(destination_hash, announced_identity, app_data): - if app_data != None: - log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + if app_data == None: + return - if not CONFIG["main"].getboolean("power") or not CONFIG["router"].getboolean("lxmf_announce_to_mqtt"): - log("LXMF - Routing disabled", LOG_DEBUG) - return + if len(app_data) == 0: + return - if CONFIG.has_option("allowed", "any") or CONFIG.has_option("allowed", "all") or CONFIG.has_option("allowed", "anybody") or CONFIG.has_option("allowed", RNS.hexrep(destination_hash, False)) or CONFIG.has_option("allowed", RNS.prettyhexrep(destination_hash)): - message_out = json.dumps({ - "source": RNS.hexrep(destination_hash, False), - "data": app_data.decode("utf-8") - }) + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) - MQTT_CONNECTION.publish(CONFIG["mqtt"]["topic_announce"], message_out) - else: - log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not allowed", LOG_DEBUG) - return + if not CONFIG["main"].getboolean("power") or not CONFIG["router"].getboolean("lxmf_announce_to_mqtt"): + log("LXMF - Routing disabled", LOG_DEBUG) + return + + if CONFIG.has_option("allowed", "any") or CONFIG.has_option("allowed", "all") or CONFIG.has_option("allowed", "anybody") or CONFIG.has_option("allowed", RNS.hexrep(destination_hash, False)) or CONFIG.has_option("allowed", RNS.prettyhexrep(destination_hash)): + message_out = json.dumps({ + "source": RNS.hexrep(destination_hash, False), + "data": app_data.decode("utf-8") + }) + + MQTT_CONNECTION.publish(CONFIG["mqtt"]["topic_announce"], message_out) + else: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not allowed", LOG_DEBUG) + return @@ -667,7 +673,36 @@ def lxmf_message_received_callback(message): return if CONFIG.has_option("allowed", "any") or CONFIG.has_option("allowed", "all") or CONFIG.has_option("allowed", "anybody") or CONFIG.has_option("allowed", RNS.hexrep(message.source_hash, False)) or CONFIG.has_option("allowed", RNS.prettyhexrep(message.source_hash)): + + title = message.title.decode('utf-8').strip() + denys = config_get(CONFIG, "message", "lxmf_to_mqtt_deny_title") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in title: + return + content = message.content.decode('utf-8').strip() + denys = config_get(CONFIG, "message", "lxmf_to_mqtt_deny_content") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in title: + return + + if message.fields: + denys = config_get(CONFIG, "message", "lxmf_to_mqtt_deny_fields") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in message.fields: + return length = config_getint(CONFIG, "message", "lxmf_to_mqtt_length_min", 0) if length> 0: @@ -1074,6 +1109,19 @@ def val_to_bool(val, fallback_true=True, fallback_false=False): return fallback_false +def val_to_val(val): + if val.isdigit(): + return int(val) + elif val.isnumeric(): + return float(val) + elif val.lower() == "true": + return True + elif val.lower() == "false": + return False + else: + return val + + ############################################################################################################## # Log @@ -1514,6 +1562,13 @@ state_to_mqtt = True #### Message settings #### [message] +# Deny message if the title/content/fields contains the following content. +# Comma-separated list with text or field keys. +# *=any +lxmf_to_mqtt_deny_title = +lxmf_to_mqtt_deny_content = +lxmf_to_mqtt_deny_fields = + # Text is added. lxmf_to_mqtt_prefix = lxmf_to_mqtt_suffix = @@ -1531,6 +1586,13 @@ lxmf_to_mqtt_length_min = 0 #0=any length lxmf_to_mqtt_length_max = 0 #0=any length +# Deny message if the title/content/fields contains the following content. +# Comma-separated list with text or field keys. +# *=any +mqtt_to_lxmf_deny_title = +mqtt_to_lxmf_deny_content = +mqtt_to_lxmf_deny_fields = + # Text is added. mqtt_to_lxmf_prefix = mqtt_to_lxmf_suffix = diff --git a/lxmf_chatbot/lxmf_chatbot.py b/lxmf_chatbot/lxmf_chatbot.py index 22d47d1..4090fea 100755 --- a/lxmf_chatbot/lxmf_chatbot.py +++ b/lxmf_chatbot/lxmf_chatbot.py @@ -289,13 +289,13 @@ class lxmf_connection: if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): log("LXMF - Destination length is invalid", LOG_ERROR) - return + return None try: destination = bytes.fromhex(destination) except Exception as e: log("LXMF - Destination is invalid", LOG_ERROR) - return + return None if destination_name == None: destination_name = self.destination_name @@ -304,7 +304,7 @@ class lxmf_connection: destination_identity = RNS.Identity.recall(destination) destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type) - self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + return self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) def send_message(self, destination, source, content="", title="", fields=None, timestamp=None, app_data=""): @@ -335,10 +335,11 @@ class lxmf_connection: try: self.message_router.handle_outbound(message) time.sleep(self.send_delay) + return message.hash except Exception as e: log("LXMF - Could not send message " + str(message), LOG_ERROR) log("LXMF - The contained exception was: " + str(e), LOG_ERROR) - return + return None def message_notification(self, message): @@ -633,8 +634,13 @@ class lxmf_announce_callback: @staticmethod def received_announce(destination_hash, announced_identity, app_data): - if app_data != None: - log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + if app_data == None: + return + + if len(app_data) == 0: + return + + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) @@ -647,7 +653,35 @@ def lxmf_message_received_callback(message): if CONFIG.has_option("allowed", "any") or CONFIG.has_option("allowed", "all") or CONFIG.has_option("allowed", "anybody") or CONFIG.has_option("allowed", RNS.hexrep(message.source_hash, False)) or CONFIG.has_option("allowed", RNS.prettyhexrep(message.source_hash)): + title = message.title.decode('utf-8').strip() + denys = config_get(CONFIG, "message", "deny_title") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in title: + return + content = message.content.decode('utf-8').strip() + denys = config_get(CONFIG, "message", "deny_content") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in title: + return + + if message.fields: + denys = config_get(CONFIG, "message", "deny_fields") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in message.fields: + return length = config_getint(CONFIG, "message", "receive_length_min", 0) if length> 0: @@ -894,6 +928,19 @@ def val_to_bool(val, fallback_true=True, fallback_false=False): return fallback_false +def val_to_val(val): + if val.isdigit(): + return int(val) + elif val.isnumeric(): + return float(val) + elif val.lower() == "true": + return True + elif val.lower() == "false": + return False + else: + return val + + ############################################################################################################## # Log @@ -1249,6 +1296,13 @@ signature_validated = Yes #### Message settings #### [message] +# Deny message if the title/content/fields contains the following content. +# Comma-separated list with text or field keys. +# *=any +deny_title = +deny_content = +deny_fields = + # Text is added. receive_prefix = receive_suffix = diff --git a/lxmf_cmd/lxmf_cmd.py b/lxmf_cmd/lxmf_cmd.py index 232a033..bb33f07 100755 --- a/lxmf_cmd/lxmf_cmd.py +++ b/lxmf_cmd/lxmf_cmd.py @@ -287,13 +287,13 @@ class lxmf_connection: if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): log("LXMF - Destination length is invalid", LOG_ERROR) - return + return None try: destination = bytes.fromhex(destination) except Exception as e: log("LXMF - Destination is invalid", LOG_ERROR) - return + return None if destination_name == None: destination_name = self.destination_name @@ -302,7 +302,7 @@ class lxmf_connection: destination_identity = RNS.Identity.recall(destination) destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type) - self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + return self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) def send_message(self, destination, source, content="", title="", fields=None, timestamp=None, app_data=""): @@ -333,10 +333,11 @@ class lxmf_connection: try: self.message_router.handle_outbound(message) time.sleep(self.send_delay) + return message.hash except Exception as e: log("LXMF - Could not send message " + str(message), LOG_ERROR) log("LXMF - The contained exception was: " + str(e), LOG_ERROR) - return + return None def message_notification(self, message): @@ -631,8 +632,13 @@ class lxmf_announce_callback: @staticmethod def received_announce(destination_hash, announced_identity, app_data): - if app_data != None: - log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + if app_data == None: + return + + if len(app_data) == 0: + return + + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) @@ -644,7 +650,36 @@ def lxmf_message_received_callback(message): return if CONFIG.has_option("allowed", "any") or CONFIG.has_option("allowed", "all") or CONFIG.has_option("allowed", "anybody") or CONFIG.has_option("allowed", RNS.hexrep(message.source_hash, False)) or CONFIG.has_option("allowed", RNS.prettyhexrep(message.source_hash)): + + title = message.title.decode('utf-8').strip() + denys = config_get(CONFIG, "message", "deny_title") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in title: + return + content = message.content.decode('utf-8').strip() + denys = config_get(CONFIG, "message", "deny_content") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in title: + return + + if message.fields: + denys = config_get(CONFIG, "message", "deny_fields") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in message.fields: + return length = config_getint(CONFIG, "message", "receive_length_min", 0) if length> 0: @@ -929,6 +964,19 @@ def val_to_bool(val, fallback_true=True, fallback_false=False): return fallback_false +def val_to_val(val): + if val.isdigit(): + return int(val) + elif val.isnumeric(): + return float(val) + elif val.lower() == "true": + return True + elif val.lower() == "false": + return False + else: + return val + + ############################################################################################################## # Log @@ -1273,6 +1321,13 @@ signature_validated = Yes #### Message settings #### [message] +# Deny message if the title/content/fields contains the following content. +# Comma-separated list with text or field keys. +# *=any +deny_title = +deny_content = +deny_fields = + # Text is added. receive_prefix = receive_suffix = diff --git a/lxmf_distribution_group/Examples/Channel_for_Communicator_Software/config.cfg.owr b/lxmf_distribution_group/Examples/Channel_for_Communicator_Software/config.cfg.owr index 43d930b..90b5259 100644 --- a/lxmf_distribution_group/Examples/Channel_for_Communicator_Software/config.cfg.owr +++ b/lxmf_distribution_group/Examples/Channel_for_Communicator_Software/config.cfg.owr @@ -43,6 +43,8 @@ enabled = False #### Message settings #### [message] +deny_fields = ts + send_title_prefix = send_prefix = @@ -63,8 +65,8 @@ enabled = True #### User rights assignment #### [rights] -admin = receive_local,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,send_local,anonymous,join -mod = receive_local,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,send_local,anonymous,join +admin = receive_local,send_local,anonymous,join +mod = receive_local,send_local,anonymous,join user = receive_local,join guest = receive_local,join wait = join @@ -72,8 +74,8 @@ wait = join #### User cmd assignment #### [cmds] -admin = update,leave,invite,kick,block,unblock,allow,deny -mod = update,leave,invite,kick,block,unblock,allow,deny +admin = update,update_all,leave,invite,kick,block,unblock +mod = update,update_all,leave,invite,kick,block,unblock user = update,leave guest = update,leave wait = update,leave diff --git a/lxmf_distribution_group/Examples/Group_for_Communicator_Software/config.cfg.owr b/lxmf_distribution_group/Examples/Group_for_Communicator_Software/config.cfg.owr index 0577d35..f0cfce9 100644 --- a/lxmf_distribution_group/Examples/Group_for_Communicator_Software/config.cfg.owr +++ b/lxmf_distribution_group/Examples/Group_for_Communicator_Software/config.cfg.owr @@ -43,6 +43,8 @@ enabled = False #### Message settings #### [message] +deny_fields = ts + send_title_prefix = !source_name! send_prefix = @@ -63,17 +65,17 @@ enabled = True #### User rights assignment #### [rights] -admin = receive_local,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,send_local,join -mod = receive_local,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,send_local,join -user = receive_local,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,send_local,join +admin = receive_local,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,send_local,join +mod = receive_local,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,send_local,join +user = receive_local,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,send_local,join guest = receive_local,join wait = join,leave #### User cmd assignment #### [cmds] -admin = update,leave,invite,kick,block,unblock,allow,deny -mod = update,leave,invite,kick,block,unblock,allow,deny +admin = update,update_all,leave,invite,kick,block,unblock +mod = update,update_all,leave,invite,kick,block,unblock user = update,leave guest = update,leave wait = update,leave diff --git a/lxmf_distribution_group/README.md b/lxmf_distribution_group/README.md index 9908547..90a54a8 100644 --- a/lxmf_distribution_group/README.md +++ b/lxmf_distribution_group/README.md @@ -64,8 +64,6 @@ The full documentation is not yet available. Due to lack of time I can also not ## Development Roadmap - Planned, but not yet scheduled - Propagation Node fallback - - Propagation Node auto discover - - Propagation Node auto select - Parameters for backup/restore configuration and data - Parameters for backup/restore identity - Cluster bridges/repeater @@ -149,6 +147,9 @@ The full documentation is not yet available. Due to lack of time I can also not # Propagation node address/hash. propagation_node = ca2762fe5283873719aececfb9e18835 + # Set propagation node automatically. + propagation_node_auto = True + # Try to deliver a message via the LXMF propagation network, # if a direct delivery to the recipient is not possible. try_propagation_on_fail = Yes @@ -452,6 +453,7 @@ Not yet implemented [lxmf] desired_method = direct #direct/propagated propagation_node = ca2762fe5283873719aececfb9e18835 + propagation_node_auto = True try_propagation_on_fail = Yes ``` @@ -461,6 +463,7 @@ Not yet implemented ``` [lxmf] propagation_node = ca2762fe5283873719aececfb9e18835 + propagation_node_auto = True sync_startup = Yes sync_startup_delay = 30 #Seconds sync_periodic = Yes diff --git a/lxmf_distribution_group/lxmf_distribution_group.py b/lxmf_distribution_group/lxmf_distribution_group.py index 0fda855..86533fa 100755 --- a/lxmf_distribution_group/lxmf_distribution_group.py +++ b/lxmf_distribution_group/lxmf_distribution_group.py @@ -298,13 +298,13 @@ class lxmf_connection: if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): log("LXMF - Destination length is invalid", LOG_ERROR) - return + return None try: destination = bytes.fromhex(destination) except Exception as e: log("LXMF - Destination is invalid", LOG_ERROR) - return + return None if destination_name == None: destination_name = self.destination_name @@ -313,7 +313,7 @@ class lxmf_connection: destination_identity = RNS.Identity.recall(destination) destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type) - self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + return self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) def send_message(self, destination, source, content="", title="", fields=None, timestamp=None, app_data=""): @@ -344,10 +344,11 @@ class lxmf_connection: try: self.message_router.handle_outbound(message) time.sleep(self.send_delay) + return message.hash except Exception as e: log("LXMF - Could not send message " + str(message), LOG_ERROR) log("LXMF - The contained exception was: " + str(e), LOG_ERROR) - return + return None def message_notification(self, message): @@ -801,52 +802,64 @@ class lxmf_announce_callback: @staticmethod def received_announce(destination_hash, announced_identity, app_data): - if app_data != None: - log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + if app_data == None: + return - global DATA + if len(app_data) == 0: + return - lng_key = "-" + CONFIG["main"]["lng"] + try: + app_data_dict = umsgpack.unpackb(app_data) + if isinstance(app_data_dict, dict) and "c" in app_data_dict: + app_data = app_data_dict["c"] + except: + pass - sections = [] - for (key, val) in CONFIG.items("rights"): - if DATA.has_section(key): - sections.append(key) + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) - if CONFIG["main"].getboolean("auto_name_def") or CONFIG["main"].getboolean("auto_name_change"): - source_hash = RNS.hexrep(destination_hash, False) - for section in DATA.sections(): - for (key, val) in DATA.items(section): - if key == source_hash: - if (val == "" and CONFIG["main"].getboolean("auto_name_def")) or (val != "" and CONFIG["main"].getboolean("auto_name_change")): - value = app_data.decode("utf-8").strip() - if value != DATA[section][key]: - if DATA[section][key] == "": - content_type = "name_def" - content_add = " " + value - else: - content_type = "name_change" - content_add = " " + DATA[section][key] + " -> " + value + global DATA - DATA[section][key] = value + lng_key = "-" + CONFIG["main"]["lng"] - content_group = config_get(CONFIG, "interface_messages", "member_"+content_type, "", lng_key) - if content_group != "": - fields = fields_generate(lng_key, h=destination_hash ,n=value, tpl=content_type) - content_group = replace(content_group, source_hash, value, "", lng_key) - content_group = content_group + content_add - for section in sections: - if "receive_auto_"+content_type in config_get(CONFIG, "rights", section).split(","): - for (key, val) in DATA.items(section): - if key != source_hash: - LXMF_CONNECTION.send(key, content_group, "", fields, None, "interface_send") + sections = [] + for (key, val) in CONFIG.items("rights"): + if DATA.has_section(key): + sections.append(key) - if CONFIG["main"].getboolean("auto_save_data"): - DATA.remove_option("main", "unsaved") - if not data_save(PATH + "/data.cfg"): - DATA["main"]["unsaved"] = "True" - else: + if CONFIG["main"].getboolean("auto_name_def") or CONFIG["main"].getboolean("auto_name_change"): + source_hash = RNS.hexrep(destination_hash, False) + for section in DATA.sections(): + for (key, val) in DATA.items(section): + if key == source_hash: + if (val == "" and CONFIG["main"].getboolean("auto_name_def")) or (val != "" and CONFIG["main"].getboolean("auto_name_change")): + value = app_data.decode("utf-8").strip() + if value != DATA[section][key]: + if DATA[section][key] == "": + content_type = "name_def" + content_add = " " + value + else: + content_type = "name_change" + content_add = " " + DATA[section][key] + " -> " + value + + DATA[section][key] = value + + content_group = config_get(CONFIG, "interface_messages", "member_"+content_type, "", lng_key) + if content_group != "": + fields = fields_generate(lng_key, h=destination_hash ,n=value, tpl=content_type) + content_group = replace(content_group, source_hash, value, "", lng_key) + content_group = content_group + content_add + for section in sections: + if "receive_auto_"+content_type in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content_group, "", fields, None, "interface_send") + + if CONFIG["main"].getboolean("auto_save_data"): + DATA.remove_option("main", "unsaved") + if not data_save(PATH + "/data.cfg"): DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["unsaved"] = "True" @@ -857,19 +870,44 @@ def lxmf_message_received_callback(message): log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " have no valid signature", LOG_DEBUG) return - content = message.content.decode('utf-8') - content = content.strip() + title = message.title.decode('utf-8').strip() + denys = config_get(CONFIG, "message", "deny_title") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in title: + return + + content = message.content.decode('utf-8').strip() + denys = config_get(CONFIG, "message", "deny_content") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in title: + return + + if message.fields: + denys = config_get(CONFIG, "message", "deny_fields") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in message.fields: + return + + if not CONFIG["message"].getboolean("title"): + title = "" + if CONFIG["message"].getboolean("fields") and message.fields: pass elif content == "": return - if CONFIG["message"].getboolean("title"): - title = message.title.decode('utf-8') - title = title.strip() - else: - title = "" - fields = message.fields lng_key = "-" + CONFIG["main"]["lng"] @@ -1007,7 +1045,13 @@ def lxmf_message_received_callback(message): if DATA.has_section(source_right) and source_right != "main": if CONFIG["main"].getboolean("auto_name_add"): app_data = RNS.Identity.recall_app_data(message.source_hash) - if app_data != None: + if app_data != None and len(app_data) > 0: + try: + app_data_dict = umsgpack.unpackb(app_data) + if isinstance(app_data_dict, dict) and "c" in app_data_dict: + app_data = app_data_dict["c"] + except: + pass source_name = app_data.decode('utf-8') DATA[source_right][source_hash] = source_name DATA.remove_option("main", "unsaved") @@ -1220,8 +1264,9 @@ def lxmf_message_received_callback(message): else: fields = {} if CONFIG["main"].getboolean("fields_message"): - fields["hash"] = message.hash - if not "anonymous" in source_rights: + if not "hash" in fields: + fields["hash"] = message.hash + if not "anonymous" in source_rights and "src" not in fields: fields["src"] = {} fields["src"]["h"] = message.source_hash fields["src"]["n"] = source_name @@ -1270,8 +1315,9 @@ def lxmf_message_received_callback(message): if CONFIG["main"].getboolean("fields_message"): if CONFIG["lxmf"]["destination_type_conv"] != "": fields["type"] = CONFIG["lxmf"].getint("destination_type_conv") - fields["hash"] = message.hash - if not "anonymous" in source_rights: + if not "hash" in fields: + fields["hash"] = message.hash + if not "anonymous" in source_rights and "src" not in fields: fields["src"] = {} fields["src"]["h"] = message.source_hash fields["src"]["n"] = source_name @@ -1344,8 +1390,9 @@ def lxmf_message_received_callback(message): if CONFIG["main"].getboolean("fields_message"): if CONFIG["lxmf"]["destination_type_conv"] != "": fields["type"] = CONFIG["lxmf"].getint("destination_type_conv") - fields["hash"] = message.hash - if not "anonymous" in source_rights: + if not "hash" in fields: + fields["hash"] = message.hash + if not "anonymous" in source_rights and "src" not in fields: fields["src"] = {} fields["src"]["h"] = message.source_hash fields["src"]["n"] = source_name @@ -1519,6 +1566,18 @@ def interface(cmd, source_hash, source_name, source_right, source_rights, lng_ke content = config_get(CONFIG, "interface_menu", "update_error", "", lng_key) + # "/update_all" command. + elif (cmd == "update_all") and "update_all" in source_rights: + try: + content = config_get(CONFIG, "interface_menu", "update_all_ok", "", lng_key) + for section in sections: + for (key, val) in DATA.items(section): + LXMF_CONNECTION.send(key, content, "", fields_generate(lng_key, m=True, d=True, r=True, cmd=section, config=section, tpl="update"), None, "interface_send") + content = "" + except: + content = config_get(CONFIG, "interface_menu", "update_all_error", "", lng_key) + + # "/join" command. elif (cmd == "join" or cmd == "subscribe") and "join" in source_rights: try: @@ -2922,7 +2981,7 @@ def fields_generate(lng_key, fields=None, h=None, n=None, m=False, d=False, r=Fa for config in configs: if config != "": key, value = config.split("=", 1) - fields["data"]["config"][key] = val_to_bool(value, fallback_true=value, fallback_false=value) + fields["data"]["config"][key] = val_to_val(value) if tpl: fields["tpl"] = tpl @@ -3548,6 +3607,19 @@ def val_to_bool(val, fallback_true=True, fallback_false=False): return fallback_false +def val_to_val(val): + if val.isdigit(): + return int(val) + elif val.isnumeric(): + return float(val) + elif val.lower() == "true": + return True + elif val.lower() == "false": + return False + else: + return val + + ############################################################################################################## # Log @@ -4001,7 +4073,7 @@ periodic_save_statistic_interval = 30 #Minutes # As an alternative to defining the nickname manually, it can be used automatically from the announce. auto_name_add = True auto_name_def = True -auto_name_change = False +auto_name_change = True # Transport extended data in the announce and fields variable. # This is needed for the integration of advanced client apps. @@ -4018,7 +4090,7 @@ fields_message = False # to be compatibel with other LXMF programs. destination_name = lxmf destination_type = delivery -destination_type_conv = #4=Group, 6=Channel +destination_type_conv = #4=Group, 6=Channel (Only for use with Communicator-Software.) # The name will be visible to other peers # on the network, and included in announces. @@ -4073,7 +4145,7 @@ sync_periodic_interval = 360 #Minutes # download x messages at a time. You can change # this number, or set the option to 0 to disable # the limit, and download everything every time. -sync_limit = 8 +sync_limit = 0 # Allow only messages with valid signature. signature_validated = No @@ -4177,6 +4249,13 @@ heartbeat_timeout = 15 #Minutes ## Each message received (message and command) ## +# Deny message if the title/content/fields contains the following content. +# Comma-separated list with text or field keys. +# *=any +deny_title = +deny_content = +deny_fields = + # Text is added. receive_title_prefix = receive_prefix = @@ -4304,8 +4383,8 @@ user = True # Delimiter for different rights: , [rights] -admin = interface,receive_local,receive_cluster,receive_cluster_pin_add,receive_cluster_loop,receive_cluster_join,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,receive_deny,receive_description,receive_rules,receive_pin_add,receive_pin_remove,receive_name_def,receive_name_change,receive_auto_name_def,receive_auto_name_change,reply_signature,reply_cluster_enabled,reply_cluster_right,reply_interface_enabled,reply_interface_right,reply_local_enabled,reply_local_right,reply_block,reply_length_min,reply_length_max,send_local,send_cluster,help,update,join,leave,name,address,info,pin,pin_add,pin_remove,cluster_pin_add,description,rules,readme,time,version,groups,members,admins,moderators,users,guests,search,activitys,statistic,statistic_min,statistic_full,statistic_cluster,statistic_router,statistic_local,statistic_interface,statistic_self,statistic_user,status,delivery,enable_local,enable_cluster,auto_add_user,auto_add_user_type,auto_add_cluster,auto_add_router,invite_user,invite_user_type,allow_user,allow_user_type,deny_user,deny_user_type,description_set,rules_set,announce,sync,show_run,show,add,del,move,rename,invite,kick,block,unblock,allow,deny,load,save,reload,reset,unsaved -mod = interface,receive_local,receive_cluster,receive_cluster_pin_add,receive_cluster_loop,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,receive_deny,receive_description,receive_rules,receive_pin_add,reply_signature,reply_cluster_enabled,reply_cluster_right,reply_interface_enabled,reply_interface_right,reply_local_enabled,reply_local_right,reply_block,reply_length_min,reply_length_max,send_local,send_cluster,help,update,join,leave,name,address,info,pin,pin_add,pin_remove,cluster_pin_add,description,rules,readme,time,version,groups,members,admins,moderators,users,guests,search,activitys,statistic,statistic_min,statistic_cluster,statistic_router,statistic_local,statistic_self,delivery,show,add,del,move,rename,invite,kick,block,unblock,allow,deny +admin = interface,receive_local,receive_cluster,receive_cluster_pin_add,receive_cluster_loop,receive_cluster_join,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,receive_deny,receive_description,receive_rules,receive_pin_add,receive_pin_remove,receive_name_def,receive_name_change,receive_auto_name_def,receive_auto_name_change,reply_signature,reply_cluster_enabled,reply_cluster_right,reply_interface_enabled,reply_interface_right,reply_local_enabled,reply_local_right,reply_block,reply_length_min,reply_length_max,send_local,send_cluster,help,update,update_all,join,leave,name,address,info,pin,pin_add,pin_remove,cluster_pin_add,description,rules,readme,time,version,groups,members,admins,moderators,users,guests,search,activitys,statistic,statistic_min,statistic_full,statistic_cluster,statistic_router,statistic_local,statistic_interface,statistic_self,statistic_user,status,delivery,enable_local,enable_cluster,auto_add_user,auto_add_user_type,auto_add_cluster,auto_add_router,invite_user,invite_user_type,allow_user,allow_user_type,deny_user,deny_user_type,description_set,rules_set,announce,sync,show_run,show,add,del,move,rename,invite,kick,block,unblock,allow,deny,load,save,reload,reset,unsaved +mod = interface,receive_local,receive_cluster,receive_cluster_pin_add,receive_cluster_loop,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,receive_deny,receive_description,receive_rules,receive_pin_add,reply_signature,reply_cluster_enabled,reply_cluster_right,reply_interface_enabled,reply_interface_right,reply_local_enabled,reply_local_right,reply_block,reply_length_min,reply_length_max,send_local,send_cluster,help,update,update_all,join,leave,name,address,info,pin,pin_add,pin_remove,cluster_pin_add,description,rules,readme,time,version,groups,members,admins,moderators,users,guests,search,activitys,statistic,statistic_min,statistic_cluster,statistic_router,statistic_local,statistic_self,delivery,show,add,del,move,rename,invite,kick,block,unblock,allow,deny user = interface,receive_local,receive_cluster,receive_cluster_pin_add,receive_cluster_loop,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,receive_description,receive_rules,receive_pin_add,reply_signature,reply_cluster_enabled,reply_cluster_right,reply_interface_enabled,reply_interface_right,reply_local_enabled,reply_local_right,reply_block,reply_length_min,reply_length_max,send_local,send_cluster,help,update,join,leave,name,address,info,pin,pin_add,pin_remove,cluster_pin_add,description,rules,readme,time,version,groups,members,admins,moderators,users,guests,search,activitys,statistic,statistic_min,statistic_cluster,statistic_router,statistic_local,statistic_self,delivery,invite guest = interface,receive_local,receive_cluster,receive_cluster_loop,update,join,leave wait = interface,update,join,leave @@ -4319,8 +4398,8 @@ wait = interface,update,join,leave # Delimiter for different cmds: , [cmds] -admin = leave,invite,kick,block,unblock,allow,deny -mod = leave,invite,kick,block,unblock,allow,deny +admin = update,update_all,leave,invite,kick,block,unblock,allow,deny +mod = update,update_all,leave,invite,kick,block,unblock,allow,deny user = leave,invite guest = leave wait = leave @@ -4652,6 +4731,12 @@ update_ok-de = OK: Daten aktualisiert. update_error = ERROR: Updating data. update_error-de = FEHLER: Daten aktualisieren. +# "/update_all" command. +update_all_ok = OK: Data updated. +update_all_ok-de = OK: Daten aktualisiert. +update_all_error = ERROR: Updating data. +update_all_error-de = FEHLER: Daten aktualisieren. + # "/join" command. join_error = ERROR: While joining group. join_error-de = FEHLER: Beim Beitritt in die Gruppe. diff --git a/lxmf_distribution_group_minimal/README.md b/lxmf_distribution_group_minimal/README.md index 82facba..6fe5de4 100644 --- a/lxmf_distribution_group_minimal/README.md +++ b/lxmf_distribution_group_minimal/README.md @@ -99,6 +99,9 @@ The full documentation is not yet available. Due to lack of time I can also not # Propagation node address/hash. propagation_node = ca2762fe5283873719aececfb9e18835 + # Set propagation node automatically. + propagation_node_auto = True + # Try to deliver a message via the LXMF propagation network, # if a direct delivery to the recipient is not possible. try_propagation_on_fail = Yes diff --git a/lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py b/lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py index 7b0efcc..5f9f291 100755 --- a/lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py +++ b/lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py @@ -285,13 +285,13 @@ class lxmf_connection: if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): log("LXMF - Destination length is invalid", LOG_ERROR) - return + return None try: destination = bytes.fromhex(destination) except Exception as e: log("LXMF - Destination is invalid", LOG_ERROR) - return + return None if destination_name == None: destination_name = self.destination_name @@ -300,7 +300,7 @@ class lxmf_connection: destination_identity = RNS.Identity.recall(destination) destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type) - self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + return self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) def send_message(self, destination, source, content="", title="", fields=None, timestamp=None, app_data=""): @@ -331,10 +331,11 @@ class lxmf_connection: try: self.message_router.handle_outbound(message) time.sleep(self.send_delay) + return message.hash except Exception as e: log("LXMF - Could not send message " + str(message), LOG_ERROR) log("LXMF - The contained exception was: " + str(e), LOG_ERROR) - return + return None def message_notification(self, message): @@ -629,8 +630,20 @@ class lxmf_announce_callback: @staticmethod def received_announce(destination_hash, announced_identity, app_data): - if app_data != None: - log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + if app_data == None: + return + + if len(app_data) == 0: + return + + try: + app_data_dict = umsgpack.unpackb(app_data) + if isinstance(app_data_dict, dict) and "c" in app_data_dict: + app_data = app_data_dict["c"] + except: + pass + + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) @@ -641,21 +654,45 @@ def lxmf_message_received_callback(message): log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " have no valid signature", LOG_DEBUG) return - content = message.content.decode('utf-8') - content = content.strip() - if content == "": - return + title = message.title.decode('utf-8').strip() + denys = config_get(CONFIG, "message", "deny_title") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in title: + return - if CONFIG["message"].getboolean("title"): - title = message.title.decode('utf-8') - title = title.strip() - else: + content = message.content.decode('utf-8').strip() + denys = config_get(CONFIG, "message", "deny_content") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in title: + return + + if message.fields: + denys = config_get(CONFIG, "message", "deny_fields") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in message.fields: + return + + if not CONFIG["message"].getboolean("title"): title = "" - if CONFIG["message"].getboolean("fields"): - fields = message.fields - else: - fields = None + if CONFIG["message"].getboolean("fields") and message.fields: + pass + elif content == "": + return + + fields = message.fields source_hash = RNS.hexrep(message.source_hash, False) source_name = "" @@ -1022,6 +1059,19 @@ def val_to_bool(val, fallback_true=True, fallback_false=False): return fallback_false +def val_to_val(val): + if val.isdigit(): + return int(val) + elif val.isnumeric(): + return float(val) + elif val.lower() == "true": + return True + elif val.lower() == "false": + return False + else: + return val + + ############################################################################################################## # Log @@ -1382,7 +1432,7 @@ sync_periodic_interval = 360 #Minutes # download x messages at a time. You can change # this number, or set the option to 0 to disable # the limit, and download everything every time. -sync_limit = 8 +sync_limit = 0 # Allow only messages with valid signature. signature_validated = No @@ -1394,6 +1444,13 @@ signature_validated = No [message] ## Each message received (message and command) ## +# Deny message if the title/content/fields contains the following content. +# Comma-separated list with text or field keys. +# *=any +deny_title = +deny_content = +deny_fields = + # Text is added. receive_prefix = receive_suffix = diff --git a/lxmf_echo/lxmf_echo.py b/lxmf_echo/lxmf_echo.py index 4b349b6..1c0f1c2 100755 --- a/lxmf_echo/lxmf_echo.py +++ b/lxmf_echo/lxmf_echo.py @@ -283,13 +283,13 @@ class lxmf_connection: if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): log("LXMF - Destination length is invalid", LOG_ERROR) - return + return None try: destination = bytes.fromhex(destination) except Exception as e: log("LXMF - Destination is invalid", LOG_ERROR) - return + return None if destination_name == None: destination_name = self.destination_name @@ -298,7 +298,7 @@ class lxmf_connection: destination_identity = RNS.Identity.recall(destination) destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type) - self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + return self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) def send_message(self, destination, source, content="", title="", fields=None, timestamp=None, app_data=""): @@ -329,10 +329,11 @@ class lxmf_connection: try: self.message_router.handle_outbound(message) time.sleep(self.send_delay) + return message.hash except Exception as e: log("LXMF - Could not send message " + str(message), LOG_ERROR) log("LXMF - The contained exception was: " + str(e), LOG_ERROR) - return + return None def message_notification(self, message): @@ -627,8 +628,13 @@ class lxmf_announce_callback: @staticmethod def received_announce(destination_hash, announced_identity, app_data): - if app_data != None: - log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + if app_data == None: + return + + if len(app_data) == 0: + return + + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) @@ -641,7 +647,35 @@ def lxmf_message_received_callback(message): if CONFIG.has_option("allowed", "any") or CONFIG.has_option("allowed", "all") or CONFIG.has_option("allowed", "anybody") or CONFIG.has_option("allowed", RNS.hexrep(message.source_hash, False)) or CONFIG.has_option("allowed", RNS.prettyhexrep(message.source_hash)): + title = message.title.decode('utf-8').strip() + denys = config_get(CONFIG, "message", "deny_title") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in title: + return + content = message.content.decode('utf-8').strip() + denys = config_get(CONFIG, "message", "deny_content") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in title: + return + + if message.fields: + denys = config_get(CONFIG, "message", "deny_fields") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in message.fields: + return length = config_getint(CONFIG, "message", "receive_length_min", 0) if length> 0: @@ -689,14 +723,19 @@ def lxmf_message_received_callback(message): content = content_prefix + content + content_suffix - if CONFIG["message"].getboolean("title"): - title = message.title.decode('utf-8') - title = title.strip() - else: + if not CONFIG["message"].getboolean("title"): title = "" if CONFIG["message"].getboolean("fields"): fields = message.fields + if fields: + search = config_get(CONFIG, "message", "fields_remove").split(",") + delete = [] + for field in fields: + if field in search: + delete.append(field) + for field in delete: + del fields[field] else: fields = None @@ -897,6 +936,19 @@ def val_to_bool(val, fallback_true=True, fallback_false=False): return fallback_false +def val_to_val(val): + if val.isdigit(): + return int(val) + elif val.isnumeric(): + return float(val) + elif val.lower() == "true": + return True + elif val.lower() == "false": + return False + else: + return val + + ############################################################################################################## # Log @@ -1241,6 +1293,13 @@ signature_validated = Yes #### Message settings #### [message] +# Deny message if the title/content/fields contains the following content. +# Comma-separated list with text or field keys. +# *=any +deny_title = +deny_content = +deny_fields = + # Text is added. receive_prefix = receive_suffix = @@ -1278,6 +1337,9 @@ send_length_max = 0 #0=any length title = Yes fields = Yes +# Comma-separated list with fields which will be removed. +fields_remove = + diff --git a/lxmf_ping/lxmf_ping.py b/lxmf_ping/lxmf_ping.py index 08cda06..56e9e60 100755 --- a/lxmf_ping/lxmf_ping.py +++ b/lxmf_ping/lxmf_ping.py @@ -284,13 +284,13 @@ class lxmf_connection: if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): log("LXMF - Destination length is invalid", LOG_ERROR) - return + return None try: destination = bytes.fromhex(destination) except Exception as e: log("LXMF - Destination is invalid", LOG_ERROR) - return + return None if destination_name == None: destination_name = self.destination_name @@ -299,7 +299,7 @@ class lxmf_connection: destination_identity = RNS.Identity.recall(destination) destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type) - self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + return self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) def send_message(self, destination, source, content="", title="", fields=None, timestamp=None, app_data=""): @@ -330,10 +330,11 @@ class lxmf_connection: try: self.message_router.handle_outbound(message) time.sleep(self.send_delay) + return message.hash except Exception as e: log("LXMF - Could not send message " + str(message), LOG_ERROR) log("LXMF - The contained exception was: " + str(e), LOG_ERROR) - return + return None def message_notification(self, message): @@ -680,6 +681,19 @@ def val_to_bool(val, fallback_true=True, fallback_false=False): return fallback_false +def val_to_val(val): + if val.isdigit(): + return int(val) + elif val.isnumeric(): + return float(val) + elif val.lower() == "true": + return True + elif val.lower() == "false": + return False + else: + return val + + ############################################################################################################## # Log diff --git a/lxmf_provisioning/lxmf_provisioning.py b/lxmf_provisioning/lxmf_provisioning.py index 8ca78cd..a6365de 100755 --- a/lxmf_provisioning/lxmf_provisioning.py +++ b/lxmf_provisioning/lxmf_provisioning.py @@ -35,6 +35,7 @@ import sys import os import time +from datetime import datetime, timezone import argparse #### Config #### @@ -300,13 +301,13 @@ class lxmf_connection: if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): log("LXMF - Destination length is invalid", LOG_ERROR) - return + return None try: destination = bytes.fromhex(destination) except Exception as e: log("LXMF - Destination is invalid", LOG_ERROR) - return + return None if destination_name == None: destination_name = self.destination_name @@ -315,7 +316,7 @@ class lxmf_connection: destination_identity = RNS.Identity.recall(destination) destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type) - self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + return self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) def send_message(self, destination, source, content="", title="", fields=None, timestamp=None, app_data=""): @@ -346,10 +347,11 @@ class lxmf_connection: try: self.message_router.handle_outbound(message) time.sleep(self.send_delay) + return message.hash except Exception as e: log("LXMF - Could not send message " + str(message), LOG_ERROR) log("LXMF - The contained exception was: " + str(e), LOG_ERROR) - return + return None def message_notification(self, message): @@ -644,8 +646,13 @@ class lxmf_announce_callback: @staticmethod def received_announce(destination_hash, announced_identity, app_data): - if app_data != None: - log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + if app_data == None: + return + + if len(app_data) == 0: + return + + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) @@ -721,7 +728,7 @@ def jobs_in(): CACHE_DEL = [] db = None try: - db = psycopg2.connect(user=CONFIG["database"]["user"], password=CONFIG["database"]["password"], host=CONFIG["database"]["host"], port=CONFIG["database"]["port"], database=CONFIG["database"]["database"]) + db = psycopg2.connect(user=CONFIG["database"]["user"], password=CONFIG["database"]["password"], host=CONFIG["database"]["host"], port=CONFIG["database"]["port"], database=CONFIG["database"]["database"], client_encoding=CONFIG["database"]["encoding"]) dbc = db.cursor() for key in CACHE["in"]: @@ -737,7 +744,7 @@ def jobs_in(): result = dbc.fetchall() if len(result) == 0: user_id = str(uuid.uuid4()) - dbc.execute("INSERT INTO members (member_user_id, member_email, member_password, member_dob, member_sex, member_introduction, member_country, member_state, member_city, member_occupation, member_skills, member_tasks, member_wallet_address, member_accept_rules, member_language, member_locale, member_status) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, '0')", ( + dbc.execute("INSERT INTO members (member_user_id, member_email, member_password, member_dob, member_sex, member_introduction, member_country, member_state, member_city, member_occupation, member_skills, member_tasks, member_wallet_address, member_accept_rules, member_language, member_locale, member_ts_add, member_status) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, '0')", ( user_id, data["email"], data["password"], @@ -753,7 +760,8 @@ def jobs_in(): data["wallet_address"], data["accept_rules"], data["language"], - data["language"] + data["language"], + datetime.now(timezone.utc) ) ) if CONFIG["features"].getboolean("account_add_auth"): @@ -790,7 +798,7 @@ def jobs_in(): result = dbc.fetchall() if len(result) == 0: user_id = str(uuid.uuid4()) - dbc.execute("INSERT INTO members (member_user_id, member_email, member_password, member_dob, member_sex, member_introduction, member_country, member_state, member_city, member_occupation, member_skills, member_tasks, member_wallet_address, member_accept_rules, member_language, member_locale, member_status) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, '0')", ( + dbc.execute("INSERT INTO members (member_user_id, member_email, member_password, member_dob, member_sex, member_introduction, member_country, member_state, member_city, member_occupation, member_skills, member_tasks, member_wallet_address, member_accept_rules, member_language, member_locale, member_ts_add, member_status) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, '0')", ( user_id, data["email"], data["password"], @@ -806,7 +814,8 @@ def jobs_in(): data["wallet_address"], data["accept_rules"], data["language"], - data["language"] + data["language"], + datetime.now(timezone.utc) ) ) if CONFIG["features"].getboolean("account_add_auth"): @@ -820,6 +829,35 @@ def jobs_in(): CACHE_CHANGE = True elif len(result) == 1: user_id = result[0][0] + dbc.execute("UPDATE members SET member_email = %s, member_password = %s, member_dob = %s, member_sex = %s, member_introduction = %s, member_country = %s, member_state = %s, member_city = %s, member_occupation = %s, member_skills = %s, member_tasks = %s, member_wallet_address = %s, member_accept_rules = %s, member_language = %s, member_locale = %s, member_ts_edit = %s WHERE member_user_id = %s", ( + data["email"], + data["password"], + data["dob"], + data["sex"], + data["introduction"], + data["country"], + data["state"], + data["city"], + data["occupation"], + data["skills"], + data["tasks"], + data["wallet_address"], + data["accept_rules"], + data["language"], + data["language"], + datetime.now(timezone.utc), + user_id + ) + ) + if CONFIG["features"].getboolean("account_edit_auth"): + fields = {} + if CONFIG["lxmf"]["destination_type_conv"] != "": + fields["type"] = CONFIG["lxmf"].getint("destination_type_conv") + fields["prov"] = {} + fields["prov"]["auth_state"] = CONFIG["features"].getint("account_edit_auth_state") + fields["prov"]["auth_role"] = CONFIG["features"].getint("account_edit_auth_role") + CACHE["out"][str(uuid.uuid4())] = {"hash_destination": data["hash_destination"], "content": "", "title": "", "fields": fields} + CACHE_CHANGE = True else: continue @@ -1188,6 +1226,19 @@ def val_to_bool(val, fallback_true=True, fallback_false=False): return fallback_false +def val_to_val(val): + if val.isdigit(): + return int(val) + elif val.isnumeric(): + return float(val) + elif val.lower() == "true": + return True + elif val.lower() == "false": + return False + else: + return val + + ############################################################################################################## # Log @@ -1363,11 +1414,19 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) path = PATH announce_data = {} - if CONFIG["features"].getboolean("announce_versions"): + if CONFIG["features"].getboolean("announce_data"): section = "data" if CONFIG.has_section(section): for (key, val) in CONFIG.items(section): - announce_data[key] = val + if "=" in val or ";" in val: + announce_data[key] = {} + keys = val.split(";") + for val in keys: + val = val.split("=") + if len(val) == 2: + announce_data[key][val[0]] = val_to_val(val[1]) + else: + announce_data[key] = val LXMF_CONNECTION = lxmf_connection( storage_path=path, @@ -1469,7 +1528,7 @@ announce_periodic = Yes announce_periodic_interval = 15 #Minutes [features] -announce_versions = True +announce_data = True account_add = True account_edit = True account_del = True @@ -1482,12 +1541,11 @@ interval_out = 60 #Seconds [data] v_s = 0.0.0 #Version software -v_c = 2022-01-01 00:00 #Version config -v_d = 2022-01-01 00:00 #Version data -v_a = 2022-01-01 00:00 #Version auth +v_c = 0.0.0 #Version config u_s = #URL Software i_s = #Info Software cmd = #CMD +config = #Config ''' @@ -1516,7 +1574,7 @@ name = LXMF Provisioning Server # to be compatibel with other LXMF programs. destination_name = lxmf destination_type = provisioning -destination_type_conv = 11 +destination_type_conv = 174 # The name will be visible to other peers # on the network, and included in announces. @@ -1586,14 +1644,14 @@ port = 5432 user = postgres password = password database = database - +encoding = utf8 #### Features enabled/disabled #### [features] -announce_versions = True +announce_data = True account_add = True account_add_auth = False @@ -1629,12 +1687,11 @@ interval_out = 60 #Seconds [data] v_s = 0.0.0 #Version software -v_c = 2022-01-01 00:00 #Version config -v_d = 2022-01-01 00:00 #Version data -v_a = 2022-01-01 00:00 #Version auth +v_c = 0.0.0 #Version config u_s = #URL Software i_s = #Info Software cmd = #CMD +config = #Config ''' diff --git a/lxmf_terminal/lxmf_terminal.py b/lxmf_terminal/lxmf_terminal.py index 6ae7eef..9660887 100755 --- a/lxmf_terminal/lxmf_terminal.py +++ b/lxmf_terminal/lxmf_terminal.py @@ -384,13 +384,13 @@ class lxmf_connection: if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): log("LXMF - Destination length is invalid", LOG_ERROR) - return + return None try: destination = bytes.fromhex(destination) except Exception as e: log("LXMF - Destination is invalid", LOG_ERROR) - return + return None if destination_name == None: destination_name = self.destination_name @@ -399,7 +399,7 @@ class lxmf_connection: destination_identity = RNS.Identity.recall(destination) destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type) - self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + return self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) def send_message(self, destination, source, content="", title="", fields=None, timestamp=None, app_data=""): @@ -430,10 +430,11 @@ class lxmf_connection: try: self.message_router.handle_outbound(message) time.sleep(self.send_delay) + return message.hash except Exception as e: log("LXMF - Could not send message " + str(message), LOG_ERROR) log("LXMF - The contained exception was: " + str(e), LOG_ERROR) - return + return None def message_notification(self, message): @@ -728,8 +729,13 @@ class lxmf_announce_callback: @staticmethod def received_announce(destination_hash, announced_identity, app_data): - if app_data != None: - log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + if app_data == None: + return + + if len(app_data) == 0: + return + + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) @@ -746,7 +752,36 @@ def lxmf_message_received_callback(message): if TERMINAL: SESSION["source"] = message.source_hash - content = message.content.decode('utf-8') + + title = message.title.decode('utf-8').strip() + denys = config_get(CONFIG, "message", "deny_title") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in title: + return + + content = message.content.decode('utf-8').strip() + denys = config_get(CONFIG, "message", "deny_content") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in title: + return + + if message.fields: + denys = config_get(CONFIG, "message", "deny_fields") + if denys != "": + denys = denys.split(",") + if "*" in denys: + return + for deny in denys: + if deny in message.fields: + return length = config_getint(CONFIG, "message", "receive_length_min", 0) if length> 0: @@ -1064,6 +1099,19 @@ def val_to_bool(val, fallback_true=True, fallback_false=False): return fallback_false +def val_to_val(val): + if val.isdigit(): + return int(val) + elif val.isnumeric(): + return float(val) + elif val.lower() == "true": + return True + elif val.lower() == "false": + return False + else: + return val + + ############################################################################################################## # Log @@ -1429,6 +1477,13 @@ interval = 5 #Seconds #### Message settings #### [message] +# Deny message if the title/content/fields contains the following content. +# Comma-separated list with text or field keys. +# *=any +deny_title = +deny_content = +deny_fields = + # Text is added. receive_prefix = receive_suffix = From 7f167666ed04c99a5eab548ea9cc4e82471ad129 Mon Sep 17 00:00:00 2001 From: SebastianObi Date: Tue, 6 Jun 2023 13:23:31 +0200 Subject: [PATCH 2/2] Improved isocalendar() function compatibility. --- README.md | 1 + lxmf_bridge_matrix/README.md | 1 + lxmf_bridge_matrix/lxmf_bridge_matrix.py | 7 ++++++- lxmf_bridge_mqtt/README.md | 1 + lxmf_bridge_mqtt/lxmf_bridge_mqtt.py | 16 ++++++++++++++-- lxmf_chatbot/README.md | 1 + lxmf_chatbot/lxmf_chatbot.py | 7 ++++++- lxmf_cmd/README.md | 1 + lxmf_cmd/lxmf_cmd.py | 7 ++++++- lxmf_distribution_group/README.md | 1 + .../lxmf_distribution_group.py | 15 ++++++++++----- lxmf_distribution_group_minimal/README.md | 1 + .../lxmf_distribution_group_minimal.py | 7 ++++++- lxmf_echo/README.md | 1 + lxmf_echo/lxmf_echo.py | 7 ++++++- lxmf_provisioning/README.md | 1 + lxmf_provisioning/lxmf_provisioning.py | 7 ++++++- lxmf_terminal/README.md | 1 + lxmf_terminal/lxmf_terminal.py | 7 ++++++- 19 files changed, 76 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index a059e1d..4f4ca6e 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ The full documentation is not yet available. Due to lack of time I can also not Restart=always RestartSec=3 User=root + Group=root ExecStart=/root/lxmf_distribution_group.py [Install] WantedBy=multi-user.target diff --git a/lxmf_bridge_matrix/README.md b/lxmf_bridge_matrix/README.md index f5d7f2a..3abbb9d 100644 --- a/lxmf_bridge_matrix/README.md +++ b/lxmf_bridge_matrix/README.md @@ -109,6 +109,7 @@ The full documentation is not yet available. Due to lack of time I can also not Restart=always RestartSec=3 User=root + Group=root ExecStart=/root/lxmf_bridge_matrix.py [Install] WantedBy=multi-user.target diff --git a/lxmf_bridge_matrix/lxmf_bridge_matrix.py b/lxmf_bridge_matrix/lxmf_bridge_matrix.py index 22be40d..c7c5a77 100755 --- a/lxmf_bridge_matrix/lxmf_bridge_matrix.py +++ b/lxmf_bridge_matrix/lxmf_bridge_matrix.py @@ -812,7 +812,12 @@ class lxmf_announce_callback: if len(app_data) == 0: return - log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + try: + app_data = app_data.decode("utf-8").strip() + except: + return + + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data, LOG_INFO) diff --git a/lxmf_bridge_mqtt/README.md b/lxmf_bridge_mqtt/README.md index b817fbc..a6981b5 100644 --- a/lxmf_bridge_mqtt/README.md +++ b/lxmf_bridge_mqtt/README.md @@ -104,6 +104,7 @@ The full documentation is not yet available. Due to lack of time I can also not Restart=always RestartSec=3 User=root + Group=root ExecStart=/root/lxmf_bridge_mqtt.py [Install] WantedBy=multi-user.target diff --git a/lxmf_bridge_mqtt/lxmf_bridge_mqtt.py b/lxmf_bridge_mqtt/lxmf_bridge_mqtt.py index f942620..9e0f113 100755 --- a/lxmf_bridge_mqtt/lxmf_bridge_mqtt.py +++ b/lxmf_bridge_mqtt/lxmf_bridge_mqtt.py @@ -642,7 +642,19 @@ class lxmf_announce_callback: if len(app_data) == 0: return - log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + try: + app_data_dict = umsgpack.unpackb(app_data) + if isinstance(app_data_dict, dict) and "c" in app_data_dict: + app_data = app_data_dict["c"] + except: + pass + + try: + app_data = app_data.decode("utf-8").strip() + except: + return + + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data, LOG_INFO) if not CONFIG["main"].getboolean("power") or not CONFIG["router"].getboolean("lxmf_announce_to_mqtt"): log("LXMF - Routing disabled", LOG_DEBUG) @@ -651,7 +663,7 @@ class lxmf_announce_callback: if CONFIG.has_option("allowed", "any") or CONFIG.has_option("allowed", "all") or CONFIG.has_option("allowed", "anybody") or CONFIG.has_option("allowed", RNS.hexrep(destination_hash, False)) or CONFIG.has_option("allowed", RNS.prettyhexrep(destination_hash)): message_out = json.dumps({ "source": RNS.hexrep(destination_hash, False), - "data": app_data.decode("utf-8") + "data": app_data }) MQTT_CONNECTION.publish(CONFIG["mqtt"]["topic_announce"], message_out) diff --git a/lxmf_chatbot/README.md b/lxmf_chatbot/README.md index 0167c4e..8ea3607 100644 --- a/lxmf_chatbot/README.md +++ b/lxmf_chatbot/README.md @@ -99,6 +99,7 @@ The full documentation is not yet available. Due to lack of time I can also not Restart=always RestartSec=3 User=root + Group=root ExecStart=/root/lxmf_chatbot.py [Install] WantedBy=multi-user.target diff --git a/lxmf_chatbot/lxmf_chatbot.py b/lxmf_chatbot/lxmf_chatbot.py index 4090fea..dbd7391 100755 --- a/lxmf_chatbot/lxmf_chatbot.py +++ b/lxmf_chatbot/lxmf_chatbot.py @@ -640,7 +640,12 @@ class lxmf_announce_callback: if len(app_data) == 0: return - log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + try: + app_data = app_data.decode("utf-8").strip() + except: + return + + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data, LOG_INFO) diff --git a/lxmf_cmd/README.md b/lxmf_cmd/README.md index 12db9ea..14e2cb2 100644 --- a/lxmf_cmd/README.md +++ b/lxmf_cmd/README.md @@ -98,6 +98,7 @@ The full documentation is not yet available. Due to lack of time I can also not Restart=always RestartSec=3 User=root + Group=root ExecStart=/root/lxmf_cmd.py [Install] WantedBy=multi-user.target diff --git a/lxmf_cmd/lxmf_cmd.py b/lxmf_cmd/lxmf_cmd.py index bb33f07..3607d97 100755 --- a/lxmf_cmd/lxmf_cmd.py +++ b/lxmf_cmd/lxmf_cmd.py @@ -638,7 +638,12 @@ class lxmf_announce_callback: if len(app_data) == 0: return - log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + try: + app_data = app_data.decode("utf-8").strip() + except: + return + + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data, LOG_INFO) diff --git a/lxmf_distribution_group/README.md b/lxmf_distribution_group/README.md index 90a54a8..16d750c 100644 --- a/lxmf_distribution_group/README.md +++ b/lxmf_distribution_group/README.md @@ -227,6 +227,7 @@ The full documentation is not yet available. Due to lack of time I can also not Restart=always RestartSec=3 User=root + Group=root ExecStart=/root/lxmf_distribution_group.py [Install] WantedBy=multi-user.target diff --git a/lxmf_distribution_group/lxmf_distribution_group.py b/lxmf_distribution_group/lxmf_distribution_group.py index 86533fa..5d2b9ef 100755 --- a/lxmf_distribution_group/lxmf_distribution_group.py +++ b/lxmf_distribution_group/lxmf_distribution_group.py @@ -815,7 +815,12 @@ class lxmf_announce_callback: except: pass - log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + try: + app_data = app_data.decode("utf-8").strip() + except: + return + + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data, LOG_INFO) global DATA @@ -832,7 +837,7 @@ class lxmf_announce_callback: for (key, val) in DATA.items(section): if key == source_hash: if (val == "" and CONFIG["main"].getboolean("auto_name_def")) or (val != "" and CONFIG["main"].getboolean("auto_name_change")): - value = app_data.decode("utf-8").strip() + value = app_data if value != DATA[section][key]: if DATA[section][key] == "": content_type = "name_def" @@ -3348,7 +3353,7 @@ def statistic_add(section="global", value=1): day = date.timetuple().tm_yday month = date.timetuple().tm_mon year = date.timetuple().tm_year - week = date.isocalendar().week + week = date.isocalendar()[1] #day if STATISTIC[section]["day_index"] == str(day): @@ -3389,7 +3394,7 @@ def statistic_recalculate(section="global"): day = date.timetuple().tm_yday month = date.timetuple().tm_mon year = date.timetuple().tm_year - week = date.isocalendar().week + week = date.isocalendar()[1] #day if STATISTIC[section]["day_index"] != str(day): @@ -3568,7 +3573,7 @@ def statistic_default(section="global"): day = date.timetuple().tm_yday month = date.timetuple().tm_mon year = date.timetuple().tm_year - week = date.isocalendar().week + week = date.isocalendar()[1] STATISTIC.add_section(section) STATISTIC[section]["day_value"] = "0" diff --git a/lxmf_distribution_group_minimal/README.md b/lxmf_distribution_group_minimal/README.md index 6fe5de4..0041f02 100644 --- a/lxmf_distribution_group_minimal/README.md +++ b/lxmf_distribution_group_minimal/README.md @@ -128,6 +128,7 @@ The full documentation is not yet available. Due to lack of time I can also not Restart=always RestartSec=3 User=root + Group=root ExecStart=/root/lxmf_distribution_group_minimal.py [Install] WantedBy=multi-user.target diff --git a/lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py b/lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py index 5f9f291..bd89b65 100755 --- a/lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py +++ b/lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py @@ -643,7 +643,12 @@ class lxmf_announce_callback: except: pass - log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + try: + app_data = app_data.decode("utf-8").strip() + except: + return + + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data, LOG_INFO) diff --git a/lxmf_echo/README.md b/lxmf_echo/README.md index 2195171..a527039 100644 --- a/lxmf_echo/README.md +++ b/lxmf_echo/README.md @@ -99,6 +99,7 @@ The full documentation is not yet available. Due to lack of time I can also not Restart=always RestartSec=3 User=root + Group=root ExecStart=/root/lxmf_echo.py [Install] WantedBy=multi-user.target diff --git a/lxmf_echo/lxmf_echo.py b/lxmf_echo/lxmf_echo.py index 1c0f1c2..1e3f7d5 100755 --- a/lxmf_echo/lxmf_echo.py +++ b/lxmf_echo/lxmf_echo.py @@ -634,7 +634,12 @@ class lxmf_announce_callback: if len(app_data) == 0: return - log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + try: + app_data = app_data.decode("utf-8").strip() + except: + return + + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data, LOG_INFO) diff --git a/lxmf_provisioning/README.md b/lxmf_provisioning/README.md index 6079a93..f6f1f6e 100644 --- a/lxmf_provisioning/README.md +++ b/lxmf_provisioning/README.md @@ -121,6 +121,7 @@ The full documentation is not yet available. Due to lack of time I can also not Restart=always RestartSec=3 User=root + Group=root ExecStart=/root/lxmf_provisioning.py [Install] WantedBy=multi-user.target diff --git a/lxmf_provisioning/lxmf_provisioning.py b/lxmf_provisioning/lxmf_provisioning.py index a6365de..58aec8b 100755 --- a/lxmf_provisioning/lxmf_provisioning.py +++ b/lxmf_provisioning/lxmf_provisioning.py @@ -652,7 +652,12 @@ class lxmf_announce_callback: if len(app_data) == 0: return - log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + try: + app_data = app_data.decode("utf-8").strip() + except: + return + + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data, LOG_INFO) diff --git a/lxmf_terminal/README.md b/lxmf_terminal/README.md index 7d6760b..f5fec08 100644 --- a/lxmf_terminal/README.md +++ b/lxmf_terminal/README.md @@ -99,6 +99,7 @@ The full documentation is not yet available. Due to lack of time I can also not Restart=always RestartSec=3 User=root + Group=root ExecStart=/root/lxmf_terminal.py [Install] WantedBy=multi-user.target diff --git a/lxmf_terminal/lxmf_terminal.py b/lxmf_terminal/lxmf_terminal.py index 9660887..69af219 100755 --- a/lxmf_terminal/lxmf_terminal.py +++ b/lxmf_terminal/lxmf_terminal.py @@ -735,7 +735,12 @@ class lxmf_announce_callback: if len(app_data) == 0: return - log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + try: + app_data = app_data.decode("utf-8").strip() + except: + return + + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data, LOG_INFO)