commit b4bc35ee311f125352491f6cccb99cf47e16baa5 Author: SebastianObi Date: Fri Oct 21 17:58:11 2022 +0200 Removed history, due to sensitive data diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..161e4d5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022 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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd95a79 --- /dev/null +++ b/README.md @@ -0,0 +1,189 @@ +# LXMF-Tools +Various small programs and tools which use the message protocol LXMF from https://github.com/markqvist/LXMF + + +## lxmf_bridge_matrix +For more information, see the detailed [README.md](lxmf_bridge_matrix). + + +## lxmf_bridge_meshtastic +For more information, see the detailed [README.md](lxmf_bridge_meshtastic). + + +## lxmf_bridge_mqtt +This program provides an interface between LXMF and MQTT. It serves as a single message endpoint and not to transfer the LXMF/Reticlum traffic 1:1 to MQTT. It serves the purpose of providing an endpoint in the Reticulum network for third party applications that can communicate via MQTT. Through this all LXMF capable applications can communicate with it via messages. This can be used for example to communicate via text messages with a smarthome system (FHEM, openHAB, ioBroker, Node-RED or similar). The transmission format used by MQTT is JSON with freely definable topics. The target system can then respond to these JSON messages. + +For more information, see the detailed [README.md](lxmf_bridge_mqtt). + + +## lxmf_bridge_telegram +For more information, see the detailed [README.md](lxmf_bridge_telegram). + + +## lxmf_chatbot +This program provides a simple chatbot (RiveScript) which can communicate via LXMF. + +For more information, see the detailed [README.md](lxmf_chatbot). + + +## lxmf_cmd +This program executes any text received by message as a system command and returns the output of the command as a message. Only single commands can be executed directly. No interactive terminal is created. + +For more information, see the detailed [README.md](lxmf_cmd). + + +## lxmf_distribution_group +This program provides an email like distribution group. It will distribute incoming LXMF messages to multiple recipients. Since this program acts as a normal LXMF endpoint, all compatible chat applications can be used. In addition to simple messaging, there is a simple command-based user interface. Where all relevant actions for daily administration can be performed. The basic configuration is done in the configuration files. There are various options to adapt the entire behavior of the group to personal needs. This distribution group is much more than a standard email distribution group. It emulates advanced group functions with automatic notifications etc. Different user permissions can be defined. For each user type, the range of functions can be defined individually. The normal users have only small rights. While a moderator or admin can perform everything necessary by simple commands. Once the basic configuration is done, everything else can be done by LXMF messages as commands. + +For more information, see the detailed [README.md](lxmf_distribution_group). + + +## lxmf_distribution_group_minimal +This program is a minimalist version of the normal distribution group. The functionality is reduced to a minimum. Only sender and receiver users can be defined. Messages are then sent to the other users accordingly. There is no user interface or other notifications. Only the messages are distributed 1:1. The administration is done completely by the respective configuration files which are to be edited accordingly. + +For more information, see the detailed [README.md](lxmf_distribution_group_minimal). + + +## lxmf_echo +This program is a simple echo server. All received messages are sent back 1:1 as an answer. This can be used as a simple counterpart to test the chat functionality of applications. + +For more information, see the detailed [README.md](lxmf_echo). + + +## lxmf_ping +This program sends an adjustable number of LXMF messages to a destination. Then a simple statistic is created to check the success or failure of a single message. This tool can be useful to load the LXMF/Reticulum network with a defined load of messages. This can be used to simulate a certain amount of users. + +For more information, see the detailed [README.md](lxmf_ping). + + +## lxmf_terminal +This program provides a complete terminal session on the server. Any commands can be executed on the target device. The communication is done by single LXMF messages. This offers the advantage that simple terminal commands can be used by any LXMF capable application. + +For more information, see the detailed [README.md](lxmf_terminal). + + +## General Information for all tools/programs + + +### Current Status: +It should currently be considered beta software. All core features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. +The full documentation is not yet available. Due to lack of time I can also not say when this will be further processed. + + +### 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 + ``` + +- Change the Reticulum configuration to suit your needs and use-case. + ```bash + nano /.reticulum/config + ``` + +- Download the [file](lxmf_distribution_group.py) from this repository. + ```bash + wget https://raw.githubusercontent.com/SebastianObi/LXMF-Tools/main/lxmf_distribution_group/lxmf_distribution_group.py + ``` + +- Make it executable with the following command + ```bash + chmod +x lxmf_distribution_group.py + ``` + +### Start: +- Start it + ```bash + ./lxmf_distribution_group.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_distribution_group/config.cfg.owr + ``` +- Start it again. Finished! + ```bash + ./lxmf_distribution_group.py + ``` + + +### Run as a system service/deamon: +- Create a service file. + ```bash + nano /etc/systemd/system/lxmf_distribution_group.service + ``` +- Copy and edit the following content to your own needs. + ```bash + [Unit] + Description=lxmf_distribution_group.py Daemon + After=multi-user.target + [Service] + # ExecStartPre=/bin/sleep 10 + Type=simple + Restart=always + RestartSec=3 + User=root + ExecStart=/root/lxmf_distribution_group.py + [Install] + WantedBy=multi-user.target + ``` +- Enable the service. + ```bash + systemctl enable lxmf_distribution_group + ``` +- Start the service. + ```bash + systemctl start lxmf_distribution_group + ``` + + +### Start/Stop service: + ```bash + systemctl start lxmf_distribution_group + systemctl stop lxmf_distribution_group + ``` + + +### Enable/Disable service: + ```bash + systemctl enable lxmf_distribution_group + systemctl disable lxmf_distribution_group + ``` + + +### Run several instances (To copy the same application): +- Run the program with a different configuration path. + ```bash + ./lxmf_distribution_group.py -p /root/.lxmf_distribution_group_2nd + ./lxmf_distribution_group.py -p /root/.lxmf_distribution_group_3nd + ``` +- After the first start edit the configuration file to suit your needs and use-case. The file location is displayed. + + +## Support / Donations +You can help support the continued development by donating via one of the following channels: + +- PayPal: https://paypal.me/SebastianObi +- Liberapay: https://liberapay.com/SebastianObi/donate + + +## Support in another way? +You are welcome to participate in the development. Just create a pull request. Or just contact me for further clarifications. + + +## Do you need a special function or customization? +Then feel free to contact me. Customizations or tools developed specifically for you can be realized. + + +## FAQ diff --git a/build_git.sh b/build_git.sh new file mode 100755 index 0000000..0410369 --- /dev/null +++ b/build_git.sh @@ -0,0 +1,132 @@ +#!/bin/bash + + +############################################################################################################## +# Configuration + + +BRANCH="main" +ORIGIN="git@github.com:SebastianObi/LXMF-Tools.git" +FILES_ADD=("*") +FILES_REMOVE=(".git/*") +COMMENT_COMMIT="$(date +%Y-%m-%d_%H:%M:%S)" +COMMENT_CLEAR="Removed history, due to sensitive data" +COMMENT_INIT="Initial commit" + + +############################################################################################################## +# Functions + + +_prompt() { + echo -e "" + echo -e "Select an option:" + options=("Commit/Push" "Clear History" "Init" "Exit") + select opt in "${options[@]}"; do + case $opt in + "Commit/Push"*) + _commit + break;; + "Clear History"*) + _clear + break;; + "Init"*) + _init + break;; + "Exit"*) + echo -e "" + echo -e "Exit" + break;; + *) + echo -e "Invalid choice!" + break;; + esac + done +} + + +_define_files() { + for file in ${FILES_ADD[@]}; do + git add "${file}" + done + + for file in ${FILES_REMOVE[@]}; do + git reset -- "${file}" + done +} + + +_commit() { + _define_files + + git diff --numstat + + echo -e "" + echo -e "Commit/Push to Git" + echo -e "Comment:" + + read VAR + if [ -z "${VAR}" ]; then + VAR="${COMMENT_COMMIT}" + fi + + git commit -a -m "${VAR}" + git push +} + + +_clear() { + echo -e "" + echo -e "Clear History" + echo -e "Comment:" + + read VAR + if [ -z "${VAR}" ]; then + VAR="${COMMENT_CLEAR}" + fi + + rm -rf .git + + git init + + _define_files + + git commit -m "${VAR}" + git branch -M "${BRANCH}" + git remote add origin "${ORIGIN}" + git push -f -u origin "${BRANCH}" +} + + +_init() { + echo -e "" + echo -e "Init" + echo -e "Comment:" + + read VAR + if [ -z "${VAR}" ]; then + VAR="${COMMENT_INIT}" + fi + + rm -rf .git + + git init + + _define_files + + git branch -M "${BRANCH}" + git remote add origin "${ORIGIN}" + + git pull origin "${BRANCH}" + + git commit -m "${VAR}" + + git push -f -u origin "${BRANCH}" +} + + +############################################################################################################## +# Setup/Start + + +_prompt \ No newline at end of file diff --git a/docs/screenshots/lxmf_bridge_mqtt_01.png b/docs/screenshots/lxmf_bridge_mqtt_01.png new file mode 100644 index 0000000..73d2a5e Binary files /dev/null and b/docs/screenshots/lxmf_bridge_mqtt_01.png differ diff --git a/docs/screenshots/lxmf_chatbot_01.png b/docs/screenshots/lxmf_chatbot_01.png new file mode 100644 index 0000000..1c0b600 Binary files /dev/null and b/docs/screenshots/lxmf_chatbot_01.png differ diff --git a/docs/screenshots/lxmf_cmd_01.png b/docs/screenshots/lxmf_cmd_01.png new file mode 100644 index 0000000..56048fa Binary files /dev/null and b/docs/screenshots/lxmf_cmd_01.png differ diff --git a/docs/screenshots/lxmf_distribution_group_01.png b/docs/screenshots/lxmf_distribution_group_01.png new file mode 100644 index 0000000..589243d Binary files /dev/null and b/docs/screenshots/lxmf_distribution_group_01.png differ diff --git a/docs/screenshots/lxmf_distribution_group_02.png b/docs/screenshots/lxmf_distribution_group_02.png new file mode 100644 index 0000000..9969664 Binary files /dev/null and b/docs/screenshots/lxmf_distribution_group_02.png differ diff --git a/docs/screenshots/lxmf_distribution_group_03.png b/docs/screenshots/lxmf_distribution_group_03.png new file mode 100644 index 0000000..e67ba64 Binary files /dev/null and b/docs/screenshots/lxmf_distribution_group_03.png differ diff --git a/docs/screenshots/lxmf_distribution_group_04.png b/docs/screenshots/lxmf_distribution_group_04.png new file mode 100644 index 0000000..422348f Binary files /dev/null and b/docs/screenshots/lxmf_distribution_group_04.png differ diff --git a/docs/screenshots/lxmf_distribution_group_05.png b/docs/screenshots/lxmf_distribution_group_05.png new file mode 100644 index 0000000..345c4b2 Binary files /dev/null and b/docs/screenshots/lxmf_distribution_group_05.png differ diff --git a/docs/screenshots/lxmf_distribution_group_06.png b/docs/screenshots/lxmf_distribution_group_06.png new file mode 100644 index 0000000..ba03250 Binary files /dev/null and b/docs/screenshots/lxmf_distribution_group_06.png differ diff --git a/docs/screenshots/lxmf_distribution_group_07.png b/docs/screenshots/lxmf_distribution_group_07.png new file mode 100644 index 0000000..6e0e3ab Binary files /dev/null and b/docs/screenshots/lxmf_distribution_group_07.png differ diff --git a/docs/screenshots/lxmf_distribution_group_08.png b/docs/screenshots/lxmf_distribution_group_08.png new file mode 100644 index 0000000..36d4a91 Binary files /dev/null and b/docs/screenshots/lxmf_distribution_group_08.png differ diff --git a/docs/screenshots/lxmf_distribution_group_minimal_01.png b/docs/screenshots/lxmf_distribution_group_minimal_01.png new file mode 100644 index 0000000..eb6aa93 Binary files /dev/null and b/docs/screenshots/lxmf_distribution_group_minimal_01.png differ diff --git a/docs/screenshots/lxmf_distribution_group_minimal_02.png b/docs/screenshots/lxmf_distribution_group_minimal_02.png new file mode 100644 index 0000000..e2e7a49 Binary files /dev/null and b/docs/screenshots/lxmf_distribution_group_minimal_02.png differ diff --git a/docs/screenshots/lxmf_distribution_group_minimal_03.png b/docs/screenshots/lxmf_distribution_group_minimal_03.png new file mode 100644 index 0000000..4cd7bcf Binary files /dev/null and b/docs/screenshots/lxmf_distribution_group_minimal_03.png differ diff --git a/docs/screenshots/lxmf_echo_01.png b/docs/screenshots/lxmf_echo_01.png new file mode 100644 index 0000000..a7d4337 Binary files /dev/null and b/docs/screenshots/lxmf_echo_01.png differ diff --git a/docs/screenshots/lxmf_ping_01.png b/docs/screenshots/lxmf_ping_01.png new file mode 100644 index 0000000..816c2ac Binary files /dev/null and b/docs/screenshots/lxmf_ping_01.png differ diff --git a/docs/screenshots/lxmf_terminal_01.png b/docs/screenshots/lxmf_terminal_01.png new file mode 100644 index 0000000..2cb3ae8 Binary files /dev/null and b/docs/screenshots/lxmf_terminal_01.png differ diff --git a/lxmf_bridge_matrix/CHANGELOG.md b/lxmf_bridge_matrix/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/lxmf_bridge_matrix/README.md b/lxmf_bridge_matrix/README.md new file mode 100644 index 0000000..6b2d10a --- /dev/null +++ b/lxmf_bridge_matrix/README.md @@ -0,0 +1,6 @@ +# lxmf_bridge_matrix + + +## Overview + +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. diff --git a/lxmf_bridge_matrix/lxmf_bridge_matrix.py b/lxmf_bridge_matrix/lxmf_bridge_matrix.py new file mode 100755 index 0000000..e69de29 diff --git a/lxmf_bridge_meshtastic/CHANGELOG.md b/lxmf_bridge_meshtastic/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/lxmf_bridge_meshtastic/README.md b/lxmf_bridge_meshtastic/README.md new file mode 100644 index 0000000..90cb3b9 --- /dev/null +++ b/lxmf_bridge_meshtastic/README.md @@ -0,0 +1,6 @@ +# lxmf_bridge_meshtastic + + +## Overview + +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. diff --git a/lxmf_bridge_meshtastic/lxmf_bridge_meshtastic.py b/lxmf_bridge_meshtastic/lxmf_bridge_meshtastic.py new file mode 100755 index 0000000..e69de29 diff --git a/lxmf_bridge_mqtt/CHANGELOG.md b/lxmf_bridge_mqtt/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/lxmf_bridge_mqtt/README.md b/lxmf_bridge_mqtt/README.md new file mode 100644 index 0000000..bda6e9a --- /dev/null +++ b/lxmf_bridge_mqtt/README.md @@ -0,0 +1,200 @@ +# lxmf_bridge_mqtt +This program provides an interface between LXMF and MQTT. It serves as a single message endpoint and not to transfer the LXMF/Reticlum traffic 1:1 to MQTT. It serves the purpose of providing an endpoint in the Reticulum network for third party applications that can communicate via MQTT. Through this all LXMF capable applications can communicate with it via messages. This can be used for example to communicate via text messages with a smarthome system (FHEM, openHAB, ioBroker, Node-RED or similar). The transmission format used by MQTT is JSON with freely definable topics. The target system can then respond to these JSON messages. + +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 +- Compatible with all LXMF applications (NomadNet, Sideband, ...) +- Compatible with all MQTT servers + + +## 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 the group. + + +## Current Status +It should currently be considered beta software. All core features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. +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 + ``` +- Change the Reticulum configuration to suit your needs and use-case. + ```bash + nano /.reticulum/config + ``` +- Download the [file](lxmf_bridge_mqtt.py) from this repository. + ```bash + wget https://raw.githubusercontent.com/SebastianObi/LXMF-Tools/main/lxmf_bridge_mqtt/lxmf_bridge_mqtt.py + ``` +- Make it executable with the following command + ```bash + chmod +x lxmf_bridge_mqtt.py + ``` + +### Start: +- Start it + ```bash + ./lxmf_bridge_mqtt.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_mqtt/config.cfg.owr + ``` + ```bash + ``` +- Start it again. Finished! + ```bash + ./lxmf_bridge_mqtt.py + ``` + + +### Run as a system service/deamon: +- Create a service file. + ```bash + nano /etc/systemd/system/lxmf_bridge_mqtt.service + ``` +- Copy and edit the following content to your own needs. + ```bash + [Unit] + Description=LXMF Bridge MQTT Daemon + After=multi-user.target + [Service] + # ExecStartPre=/bin/sleep 10 + Type=simple + Restart=always + RestartSec=3 + User=root + ExecStart=/root/lxmf_bridge_mqtt.py + [Install] + WantedBy=multi-user.target + ``` +- Enable the service. + ```bash + systemctl enable lxmf_bridge_mqtt + ``` +- Start the service. + ```bash + systemctl start lxmf_bridge_mqtt + ``` + + +### Start/Stop service: + ```bash + systemctl start lxmf_bridge_mqtt + systemctl stop lxmf_bridge_mqtt + ``` + + +### Enable/Disable service: + ```bash + systemctl enable lxmf_bridge_mqtt + systemctl disable lxmf_bridge_mqtt + ``` + + +### Run several instances (To copy the same application): +- Run the program with a different configuration path. + ```bash + ./lxmf_bridge_mqtt.py -p /root/.lxmf_bridge_mqtt_2nd + ./lxmf_bridge_mqtt.py -p /root/.lxmf_bridge_mqtt_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_distribution_group_minimal.py [-h] [-p PATH] [-pr PATH_RNS] [-pl PATH_LOG] [-l LOGLEVEL] [-s] [--exampleconfig] + [--exampleconfigoverride] [--exampledata] + +LXMF Distribution Group - Server-Side group functions for LXMF based apps + +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 + --exampledata 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_mqtt/lxmf_bridge_mqtt.py b/lxmf_bridge_mqtt/lxmf_bridge_mqtt.py new file mode 100755 index 0000000..cadfeb3 --- /dev/null +++ b/lxmf_bridge_mqtt/lxmf_bridge_mqtt.py @@ -0,0 +1,1407 @@ +#!/usr/bin/env python3 +############################################################################################################## +# +# Copyright (c) 2022 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 + +#### 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 + +#### MQTT #### +# Install: pip3 install paho-mqtt +# Source: https://pypi.org/project/paho-mqtt/ +import paho.mqtt.client as mqtt +import paho.mqtt.publish as publish + + +############################################################################################################## +# Globals + + +#### Global Variables - Configuration #### +NAME = "LXMF Bridge MQTT" +DESCRIPTION = "" +VERSION = "0.0.1 (2022-10-21)" +COPYRIGHT = "(c) 2022 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 +RNS_CONNECTION = None +LXMF_CONNECTION = None +MQTT_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 + + + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", send_delay=0, desired_method="direct", propagation_node=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.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.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 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) + + 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) + + 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) + + self.autoselect_propagation_node() + + if self.announce_startup or self.announce_periodic: + self.announce(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 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=None, fields=None, timestamp=None, app_data=""): + 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 + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return + + destination_identity = RNS.Identity.recall(destination) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + + + def send_message(self, destination, source, content="", title=None, 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) + 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 + + + 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, 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() + return + + self.announce_now() + + + def announce_now(self, app_data=None): + if app_data: + self.destination.announce(app_data.encode("utf-8")) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ":" + app_data, 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 autoselect_propagation_node(self): + if self.propagation_node is not None: + if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + else: + try: + propagation_hash = bytes.fromhex(self.propagation_node) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return + + node_identity = RNS.Identity.recall(propagation_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) + propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(propagation_hash) + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + + + 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) + + +############################################################################################################## +# 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: + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + + 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 + + + + +#### LXMF - Message #### +def lxmf_message_received_callback(message): + if not CONFIG["main"].getboolean("power") or not CONFIG["router"].getboolean("lxmf_to_mqtt"): + 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)): + content = message.content.decode('utf-8').strip() + + length = config_getint(CONFIG, "message", "lxmf_to_mqtt_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "lxmf_to_mqtt_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "lxmf_to_mqtt_prefix") + content_suffix = config_get(CONFIG, "message", "lxmf_to_mqtt_suffix") + + search = config_get(CONFIG, "message", "lxmf_to_mqtt_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "lxmf_to_mqtt_replace")) + + search = config_get(CONFIG, "message", "lxmf_to_mqtt_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "lxmf_to_mqtt_regex_replace"), content) + + content = content_prefix + content + content_suffix + + if message.signature_validated: + signature_string = "Validated" + signature_valid = 1 + else: + signature_valid = 0 + 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" + + message_out = json.dumps({ + "source": RNS.hexrep(message.source_hash, False), + "destination": RNS.hexrep(message.destination_hash, False), + "title": message.title.decode('utf-8').strip(), + "content": content, + "fields": str(message.fields), + "date_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.timestamp)), + "timestamp": message.timestamp, + "signature_valid": signature_valid, + "signature_string": signature_string + }) + + MQTT_CONNECTION.publish(CONFIG["mqtt"]["topic_receive"], message_out) + else: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not allowed", LOG_DEBUG) + return + + +############################################################################################################## +# MQTT Functions + + +#### MQTT - Log #### +def mqtt_log_message(message, message_tag="MQTT - Message log"): + log(message_tag + ":", LOG_DEBUG) + log("- Topic: " + str(message.topic), LOG_DEBUG) + log("- Payload: " + str(message.payload.decode('utf-8')), LOG_DEBUG) + log("- QOS: " + str(message.qos), LOG_DEBUG) + + + + +#### MQTT - State #### +def mqtt_state(): + t = threading.Timer(int(CONFIG["mqtt"]["state_interval"])*60, mqtt_state_now) + t.daemon = True + t.start() + + + + +#### MQTT - State #### +def mqtt_state_now(): + if not CONFIG["router"].getboolean("state_to_mqtt"): + log("MQTT - Routing disabled", LOG_DEBUG) + return + + if CONFIG["main"].getboolean("power"): + power = "on" + else: + power = "off" + + MQTT_CONNECTION.publish(CONFIG["mqtt"]["topic_rm_power"], power) + + interfaces = None + try: + interfaces = RNS_CONNECTION.get_interface_stats() + except Exception: + pass + + message_out = json.dumps({ + "power": power, + "identity": str(LXMF_CONNECTION.identity), + "destination": str(LXMF_CONNECTION.destination), + "hash": RNS.hexrep(LXMF_CONNECTION.destination_hash(), False), + #"interfaces": interfaces, + "file_info": __file__, + "path_info": PATH, + "version_info": VERSION, + "host_info": os.uname(), + }) + + MQTT_CONNECTION.publish(CONFIG["mqtt"]["topic_rm_state"], message_out) + + + + +#### MQTT - Connected #### +def mqtt_connected_callback(client, userdata, flags, rc): + MQTT_CONNECTION.subscribe(CONFIG["mqtt"]["topic_power"]) + MQTT_CONNECTION.subscribe(CONFIG["mqtt"]["topic_state"]) + MQTT_CONNECTION.subscribe(CONFIG["mqtt"]["topic_send"]) + + if CONFIG["mqtt"].getboolean("state_startup"): + mqtt_state_now() + + if CONFIG["mqtt"].getboolean("state_periodic"): + mqtt_state() + + log("MQTT - Connected", LOG_DEBUG) + + + + +#### MQTT - Message #### +def mqtt_message_received_callback_power(client, userdata, message): + global CONFIG + + mqtt_log_message(message) + + if message.payload.decode('utf-8') == "on" or message.payload.decode('utf-8') == "1": + CONFIG["main"]["power"] = True + MQTT_CONNECTION.publish(CONFIG["mqtt"]["topic_rm_power"], "on") + else: + CONFIG["main"]["power"] = False + MQTT_CONNECTION.publish(CONFIG["mqtt"]["topic_rm_power"], "off") + + + + +#### MQTT - Message #### +def mqtt_message_received_callback_state(client, userdata, message): + global CONFIG + + mqtt_log_message(message) + + mqtt_state_now() + + + + +#### MQTT - Message #### +def mqtt_message_received_callback_send(client, userdata, message): + mqtt_log_message(message) + + if not CONFIG["main"].getboolean("power") or not CONFIG["router"].getboolean("mqtt_to_lxmf"): + log("MQTT - Routing disabled", LOG_DEBUG) + return + + message_data = json.loads(message.payload.decode('utf-8')) + + if "destination" not in message_data or "content" not in message_data: + return + + content = message_data["content"].strip() + + length = config_getint(CONFIG, "message", "mqtt_to_lxmf_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "mqtt_to_lxmf_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "mqtt_to_lxmf_prefix") + content_suffix = config_get(CONFIG, "message", "mqtt_to_lxmf_suffix") + + search = config_get(CONFIG, "message", "mqtt_to_lxmf_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "mqtt_to_lxmf_replace")) + + search = config_get(CONFIG, "message", "mqtt_to_lxmf_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "mqtt_to_lxmf_regex_replace"), content) + + content = content_prefix + content + content_suffix + + if "title" not in message_data: + message_data["title"] = "" + + timestamp = None + if "timestamp" in message_data and timestamp is None: + message_data["timestamp"] = message_data["timestamp"].strip() + if message_data["timestamp"] != "": + timestamp = float(message_data["timestamp"]) + + if "date_time" in message_data and timestamp is None: + message_data["date_time"] = message_data["date_time"].strip() + if message_data["date_time"] != "": + timestamp = time.mktime(datetime.datetime.strptime(message_data["date_time"], '%Y-%m-%d %H:%M:%S').timetuple()) + + LXMF_CONNECTION.send(message_data["destination"].strip(), content, message_data["title"].strip(), timestamp) + + +############################################################################################################## +# 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 - 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 + + +############################################################################################################## +# Value convert + + +def val_to_bool(val): + 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 True + else: + return False + + +############################################################################################################## +# 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 RNS_CONNECTION + global LXMF_CONNECTION + global MQTT_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 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() + + 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) + + 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 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"], + send_delay=CONFIG["lxmf"]["send_delay"], + desired_method=CONFIG["lxmf"]["desired_method"], + propagation_node=config_propagation_node, + 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) + + log("LXMF - Connected", LOG_DEBUG) + + log("...............................................................................", LOG_FORCE) + log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE) + log("...............................................................................", LOG_FORCE) + + log("MQTT - Connecting ...", LOG_DEBUG) + MQTT_CONNECTION = mqtt.Client(CONFIG["mqtt"]["client_id"], False, userdata=None, transport=CONFIG["mqtt"]["transport"]) + MQTT_CONNECTION.on_connect = mqtt_connected_callback + MQTT_CONNECTION.message_callback_add(CONFIG["mqtt"]["topic_power"], mqtt_message_received_callback_power) + MQTT_CONNECTION.message_callback_add(CONFIG["mqtt"]["topic_state"], mqtt_message_received_callback_state) + MQTT_CONNECTION.message_callback_add(CONFIG["mqtt"]["topic_send"], mqtt_message_received_callback_send) + if CONFIG.has_option("mqtt", "username") and CONFIG.has_option("mqtt", "password"): + if CONFIG["mqtt"]["username"] != "" and CONFIG["mqtt"]["password"] != "": + MQTT_CONNECTION.username_pw_set(CONFIG["mqtt"]["username"], CONFIG["mqtt"]["password"]) + MQTT_CONNECTION.connect(CONFIG["mqtt"]["host"], int(CONFIG["mqtt"]["port"]), 60) + MQTT_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 = + +# Enable/Disable the message routing. +# This is controllable with a MQTT message. +power = Yes + + + + +#### 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 = ca2762fe5283873719aececfb9e18835 + +# 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 + +# 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 = 8 + +# Allow only messages with valid signature. +signature_validated = Yes + + + + +#### MQTT connection settings #### +[mqtt] + +host = 192.168.10.201 + +port = 1883 + +transport = tcp + +client_id = lxmf_mqtt_bridge + +#username = + +#password = + +# Topic for power switch. +# Direction: From MQTT server. +# Data: on/off +topic_power = message/lxmf/power + +# Topic for power switch state. +# Direction: To MQTT server. +# Data: on/off +topic_rm_power = message/lxmf/rm_power + +# Topic for state request. +# Direction: From MQTT server. +# Data: any +topic_state = message/lxmf/state + +# Topic for state (diagnose data). +# Direction: To MQTT server. +# Data: {"power": "on", "identity": "<36a82374456b9c31a37f>", "destination": "", "hash": "36a82374456b9c31a37f", "interfaces": [{"clients": 0, "name": "Shared Instance[37428]", "rxb": 0, "txb": 0, "status": true}, {"clients": null, "name": "AutoInterface[Default Interface]", "rxb": 1111, "txb": 1176, "status": true}], "file_info": "/root/./lxmf_mqtt_bridge.py", "path_info": "/root/.lxmf_mqtt_bridge", "version_info": "0.0.1", "host_info": ["Linux", "test", "5.13.19-6", "#1 5.13.19-15 (Tue, 29 Mar 2022 15:59:50 +0200)", "x86_64"]} +topic_rm_state = message/lxmf/rm_state + +# Topic for announce. +# Direction: To MQTT server. +# Data: {"source": "36a82374456b9c31a37f", "data": "Sideband"} +topic_announce = message/lxmf/announce + +# Topic for receive messages (LXMF->MQTT). +# Direction: To MQTT server. +# Data: {"source": "36a82374456b9c31a37f", "destination": "36a82374456b9c31a37f", "title": "", "content": "Frage", "fields": "None", "date_time": "2022-05-07 14:19:10", "timestamp": 1651933150.803059, "signature_valid": 1, "signature_string": "Validated"} +topic_receive = message/lxmf/receive + +# Topic for send messages (MQTT->LXMF). +# Direction: From MQTT server. +# Data: {"destination":"36a82374456b9c31a37f", "content":"Antwort"} +# Data: {"destination":"36a82374456b9c31a37f", "content":"Antwort", "title":"Example", "date_time":"2022-10-01 11:45:00", "timestamp":""} +topic_send = message/lxmf/send + +# Send state at startup. +state_startup = Yes + +# Send state periodically. +state_periodic = Yes +state_interval = 30 #Minutes + + + + +#### Message router settings #### +[router] + +# Transmit LXMF messages to MQTT +lxmf_to_mqtt = True + +# Transmit MQTT messages to LXMF +mqtt_to_lxmf = True + +# Transmit LXMF announces to MQTT +lxmf_announce_to_mqtt = False + +# Transmit state to MQTT +state_to_mqtt = True + + + + +#### Message settings #### +[message] + +# Text is added. +lxmf_to_mqtt_prefix = +lxmf_to_mqtt_suffix = + +# Text is replaced. +lxmf_to_mqtt_search = +lxmf_to_mqtt_replace = + +# Text is replaced by regular expression. +lxmf_to_mqtt_regex_search = +lxmf_to_mqtt_regex_replace = + +# Length limitation. +lxmf_to_mqtt_length_min = 0 #0=any length +lxmf_to_mqtt_length_max = 0 #0=any length + + +# Text is added. +mqtt_to_lxmf_prefix = +mqtt_to_lxmf_suffix = + +# Text is replaced. +mqtt_to_lxmf_search = +mqtt_to_lxmf_replace = + +# Text is replaced by regular expression. +mqtt_to_lxmf_regex_search = +mqtt_to_lxmf_regex_replace = + +# Length limitation. +mqtt_to_lxmf_length_min = 0 #0=any length +mqtt_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_telegram/CHANGELOG.md b/lxmf_bridge_telegram/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/lxmf_bridge_telegram/README.md b/lxmf_bridge_telegram/README.md new file mode 100644 index 0000000..be907b3 --- /dev/null +++ b/lxmf_bridge_telegram/README.md @@ -0,0 +1,6 @@ +# lxmf_bridge_telegram + + +## Overview + +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. diff --git a/lxmf_bridge_telegram/lxmf_bridge_telegram.py b/lxmf_bridge_telegram/lxmf_bridge_telegram.py new file mode 100755 index 0000000..e69de29 diff --git a/lxmf_chatbot/CHANGELOG.md b/lxmf_chatbot/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/lxmf_chatbot/README.md b/lxmf_chatbot/README.md new file mode 100644 index 0000000..a981d64 --- /dev/null +++ b/lxmf_chatbot/README.md @@ -0,0 +1,197 @@ +# lxmf_chatbot +This program provides a simple chatbot (RiveScript) which can communicate via LXMF. + +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 +- Compatible with all LXMF applications (NomadNet, Sideband, ...) + + +## 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 the group. + + +## Current Status +It should currently be considered beta software. All core features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. +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 + ``` +- Change the Reticulum configuration to suit your needs and use-case. + ```bash + nano /.reticulum/config + ``` +- Download the [file](lxmf_chatbot.py) from this repository. + ```bash + wget https://raw.githubusercontent.com/SebastianObi/LXMF-Tools/main/lxmf_chatbot/lxmf_chatbot.py + ``` +- Make it executable with the following command + ```bash + chmod +x lxmf_chatbot.py + ``` + +### Start: +- Start it + ```bash + ./lxmf_chatbot.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_chatbot/config.cfg.owr + ``` + ```bash + ``` +- Start it again. Finished! + ```bash + ./lxmf_chatbot.py + ``` + + +### Run as a system service/deamon: +- Create a service file. + ```bash + nano /etc/systemd/system/lxmf_chatbot.service + ``` +- Copy and edit the following content to your own needs. + ```bash + [Unit] + Description=lxmf_chatbot.py Daemon + After=multi-user.target + [Service] + # ExecStartPre=/bin/sleep 10 + Type=simple + Restart=always + RestartSec=3 + User=root + ExecStart=/root/lxmf_chatbot.py + [Install] + WantedBy=multi-user.target + ``` +- Enable the service. + ```bash + systemctl enable lxmf_chatbot + ``` +- Start the service. + ```bash + systemctl start lxmf_chatbot + ``` + + +### Start/Stop service: + ```bash + systemctl start lxmf_chatbot + systemctl stop lxmf_chatbot + ``` + + +### Enable/Disable service: + ```bash + systemctl enable lxmf_chatbot + systemctl disable lxmf_chatbot + ``` + + +### Run several instances (To copy the same application): +- Run the program with a different configuration path. + ```bash + ./lxmf_chatbot.py -p /root/.lxmf_chatbot_2nd + ./lxmf_chatbot.py -p /root/.lxmf_chatbot_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_chatbot.py [-h] [-p PATH] [-pr PATH_RNS] [-pl PATH_LOG] [-l LOGLEVEL] [-s] [--exampleconfig] [--exampleconfigoverride] + +LXMF Chatbot - Simple chatbot (RiveScript) for LXMF messages + +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_chatbot/lxmf_chatbot.py b/lxmf_chatbot/lxmf_chatbot.py new file mode 100755 index 0000000..bdff852 --- /dev/null +++ b/lxmf_chatbot/lxmf_chatbot.py @@ -0,0 +1,1145 @@ +#!/usr/bin/env python3 +############################################################################################################## +# +# Copyright (c) 2022 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 argparse + +#### Config #### +import configparser + +#### JSON #### +import json +import pickle + +#### String #### +import string + +#### 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 + +#### RiveScript #### +# Install: pip3 install rivescript +# Source: https://github.com/aichaos/rivescript-python +from rivescript import RiveScript + + +############################################################################################################## +# Globals + + +#### Global Variables - Configuration #### +NAME = "LXMF Chatbot" +DESCRIPTION = "Simple chatbot (RiveScript) for LXMF messages" +VERSION = "0.0.1 (2022-10-21)" +COPYRIGHT = "(c) 2022 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 +RNS_CONNECTION = None +LXMF_CONNECTION = None +RIVESCRIPT_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 + + + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", send_delay=0, desired_method="direct", propagation_node=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.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.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 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) + + 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) + + 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) + + self.autoselect_propagation_node() + + if self.announce_startup or self.announce_periodic: + self.announce(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 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=None, fields=None, timestamp=None, app_data=""): + 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 + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return + + destination_identity = RNS.Identity.recall(destination) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + + + def send_message(self, destination, source, content="", title=None, 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) + 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 + + + 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, 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() + return + + self.announce_now() + + + def announce_now(self, app_data=None): + if app_data: + self.destination.announce(app_data.encode("utf-8")) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ":" + app_data, 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 autoselect_propagation_node(self): + if self.propagation_node is not None: + if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + else: + try: + propagation_hash = bytes.fromhex(self.propagation_node) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return + + node_identity = RNS.Identity.recall(propagation_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) + propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(propagation_hash) + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + + + 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) + + +############################################################################################################## +# 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: + 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 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)): + + content = message.content.decode('utf-8').strip() + + length = config_getint(CONFIG, "message", "receive_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "receive_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "receive_prefix") + content_suffix = config_get(CONFIG, "message", "receive_suffix") + + search = config_get(CONFIG, "message", "receive_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "receive_replace")) + + search = config_get(CONFIG, "message", "receive_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "receive_regex_replace"), content) + + content = content_prefix + content + content_suffix + + content = RIVESCRIPT_CONNECTION.reply(RNS.hexrep(message.source_hash, False), content) + + length = config_getint(CONFIG, "message", "send_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "send_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "send_prefix") + content_suffix = config_get(CONFIG, "message", "send_suffix") + + search = config_get(CONFIG, "message", "send_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "send_replace")) + + search = config_get(CONFIG, "message", "send_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "send_regex_replace"), content) + + content = content_prefix + content + content_suffix + + LXMF_CONNECTION.send(message.source_hash, content, message.title.decode('utf-8').strip()) + else: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not allowed", LOG_DEBUG) + return + + +############################################################################################################## +# 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 - 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 + + +############################################################################################################## +# Value convert + + +def val_to_bool(val): + 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 True + else: + return False + + +############################################################################################################## +# 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 RNS_CONNECTION + global LXMF_CONNECTION + global RIVESCRIPT_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 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() + + 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) + + 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 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"], + send_delay=CONFIG["lxmf"]["send_delay"], + desired_method=CONFIG["lxmf"]["desired_method"], + propagation_node=config_propagation_node, + 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) + + log("LXMF - Connected", LOG_DEBUG) + + log("...............................................................................", LOG_FORCE) + log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE) + log("...............................................................................", LOG_FORCE) + + log("RiveScript - Connecting ...", LOG_DEBUG) + RIVESCRIPT_CONNECTION = RiveScript(utf8=True) + if not os.path.isdir(PATH + "/rivescript"): + os.makedirs(PATH + "/rivescript") + log("RiveScript - Path was created. You need to put the RiveScript configuration into the path befor you can use it!", LOG_ERROR) + else: + RIVESCRIPT_CONNECTION.load_directory(PATH + "/rivescript") + RIVESCRIPT_CONNECTION.sort_replies() + log("RiveScript - Connected", LOG_DEBUG) + + 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 = Echo Test + + + + +#### 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 = Chatbot + +# Default send method. +desired_method = direct #direct/propagated + +# Propagation node address/hash. +#propagation_node = + +# Try to deliver a message via the LXMF propagation network, +# if a direct delivery to the recipient is not possible. +try_propagation_on_fail = No + +# 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 + +# 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 = 8 + +# Allow only messages with valid signature. +signature_validated = Yes + + + + +#### Message settings #### +[message] + +# Text is added. +receive_prefix = +receive_suffix = + +# Text is replaced. +receive_search = +receive_replace = + +# Text is replaced by regular expression. +receive_regex_search = +receive_regex_replace = + +# Length limitation. +receive_length_min = 0 #0=any length +receive_length_max = 0 #0=any length + + +# Text is added. +send_prefix = +send_suffix = + +# Text is replaced. +send_search = +send_replace = + +# Text is replaced by regular expression. +send_regex_search = +send_regex_replace = + +# Length limitation. +send_length_min = 0 #0=any length +send_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_cmd/CHANGELOG.md b/lxmf_cmd/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/lxmf_cmd/README.md b/lxmf_cmd/README.md new file mode 100644 index 0000000..73dc465 --- /dev/null +++ b/lxmf_cmd/README.md @@ -0,0 +1,196 @@ +# lxmf_cmd +This program executes any text received by message as a system command and returns the output of the command as a message. Only single commands can be executed directly. No interactive terminal is created. + +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 +- Compatible with all LXMF applications (NomadNet, Sideband, ...) + + +## 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 the group. + + +## Current Status +It should currently be considered beta software. All core features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. +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 + ``` +- Change the Reticulum configuration to suit your needs and use-case. + ```bash + nano /.reticulum/config + ``` +- Download the [file](lxmf_cmd.py) from this repository. + ```bash + wget https://raw.githubusercontent.com/SebastianObi/LXMF-Tools/main/lxmf_cmd/lxmf_cmd.py + ``` +- Make it executable with the following command + ```bash + chmod +x lxmf_cmd.py + ``` + +### Start: +- Start it + ```bash + ./lxmf_cmd.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_cmd/config.cfg.owr + ``` + ```bash + ``` +- Start it again. Finished! + ```bash + ./lxmf_cmd.py + ``` + + +### Run as a system service/deamon: +- Create a service file. + ```bash + nano /etc/systemd/system/lxmf_cmd.service + ``` +- Copy and edit the following content to your own needs. + ```bash + [Unit] + Description=lxmf_cmd.py Daemon + After=multi-user.target + [Service] + # ExecStartPre=/bin/sleep 10 + Type=simple + Restart=always + RestartSec=3 + User=root + ExecStart=/root/lxmf_cmd.py + [Install] + WantedBy=multi-user.target + ``` +- Enable the service. + ```bash + systemctl enable lxmf_cmd + ``` +- Start the service. + ```bash + systemctl start lxmf_cmd + ``` + + +### Start/Stop service: + ```bash + systemctl start lxmf_cmd + systemctl stop lxmf_cmd + ``` + + +### Enable/Disable service: + ```bash + systemctl enable lxmf_cmd + systemctl disable lxmf_cmd + ``` + + +### Run several instances (To copy the same application): +- Run the program with a different configuration path. + ```bash + ./lxmf_cmd.py -p /root/.lxmf_cmd_2nd + ./lxmf_cmd.py -p /root/.lxmf_cmd_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_cmd.py [-h] [-p PATH] [-pr PATH_RNS] [-pl PATH_LOG] [-l LOGLEVEL] [-s] [--exampleconfig] [--exampleconfigoverride] + +LXMF CMD - + +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_cmd/lxmf_cmd.py b/lxmf_cmd/lxmf_cmd.py new file mode 100755 index 0000000..e4c77f2 --- /dev/null +++ b/lxmf_cmd/lxmf_cmd.py @@ -0,0 +1,1169 @@ +#!/usr/bin/env python3 +############################################################################################################## +# +# Copyright (c) 2022 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 argparse + +#### Config #### +import configparser + +#### JSON #### +import json +import pickle + +#### String #### +import string + +#### Process #### +import signal +import threading + +#### External process #### +import subprocess +import socket + +#### Reticulum, LXMF #### +# Install: pip3 install rns lxmf +# Source: https://markqvist.github.io +import RNS +import LXMF +import RNS.vendor.umsgpack as umsgpack + + +############################################################################################################## +# Globals + + +#### Global Variables - Configuration #### +NAME = "LXMF CMD" +DESCRIPTION = "" +VERSION = "0.0.1 (2022-10-21)" +COPYRIGHT = "(c) 2022 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 +RNS_CONNECTION = None +LXMF_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 + + + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", send_delay=0, desired_method="direct", propagation_node=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.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.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 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) + + 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) + + 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) + + self.autoselect_propagation_node() + + if self.announce_startup or self.announce_periodic: + self.announce(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 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=None, fields=None, timestamp=None, app_data=""): + 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 + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return + + destination_identity = RNS.Identity.recall(destination) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + + + def send_message(self, destination, source, content="", title=None, 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) + 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 + + + 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, 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() + return + + self.announce_now() + + + def announce_now(self, app_data=None): + if app_data: + self.destination.announce(app_data.encode("utf-8")) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ":" + app_data, 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 autoselect_propagation_node(self): + if self.propagation_node is not None: + if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + else: + try: + propagation_hash = bytes.fromhex(self.propagation_node) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return + + node_identity = RNS.Identity.recall(propagation_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) + propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(propagation_hash) + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + + + 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) + + +############################################################################################################## +# 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: + 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 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)): + content = message.content.decode('utf-8').strip() + + length = config_getint(CONFIG, "message", "receive_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "receive_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "receive_prefix") + content_suffix = config_get(CONFIG, "message", "receive_suffix") + + search = config_get(CONFIG, "message", "receive_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "receive_replace")) + + search = config_get(CONFIG, "message", "receive_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "receive_regex_replace"), content) + + content = content_prefix + content + content_suffix + + content = cmd(content) + + length = config_getint(CONFIG, "message", "send_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "send_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "send_prefix") + content_suffix = config_get(CONFIG, "message", "send_suffix") + + content_prefix = replace(content_prefix) + content_suffix = replace(content_suffix) + + search = config_get(CONFIG, "message", "send_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "send_replace")) + + search = config_get(CONFIG, "message", "send_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "send_regex_replace"), content) + + LXMF_CONNECTION.send(message.source_hash, content_prefix + content + content_suffix, "") + else: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not allowed", LOG_DEBUG) + return + + +############################################################################################################## +# Functions + + +def replace(text): + text = text.replace("!user!", os.getlogin()) + text = text.replace("!hostname!", socket.gethostname()) + text = text.replace("!path!", os.getcwd()) + text = text.replace("!n!", "\n") + return text + + + + +def cmd(cmd): + content = "" + + params = cmd.split(' ') + + if params[0] == "cd": + try: + os.chdir(params[1]) + except: + content = "ERROR: No such directory: '" + params[1] + "'" + else: + try: + #process = subprocess.Popen('/bin/bash', stdin=subprocess.PIPE, stdout=subprocess.PIPE) + #out, err = process.communicate(cmd.encode('utf-8')) + #content = out.decode('utf-8') + result = subprocess.run(params, capture_output=True, text=True) + content = result.stdout + result.stderr + except: + content = "ERROR: Command '" + cmd + "' not found" + + return content + + +############################################################################################################## +# 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 - 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 + + +############################################################################################################## +# Value convert + + +def val_to_bool(val): + 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 True + else: + return False + + +############################################################################################################## +# 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 RNS_CONNECTION + global LXMF_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 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() + + 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) + + 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 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"], + send_delay=CONFIG["lxmf"]["send_delay"], + desired_method=CONFIG["lxmf"]["desired_method"], + propagation_node=config_propagation_node, + 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) + + log("LXMF - Connected", LOG_DEBUG) + + log("...............................................................................", LOG_FORCE) + log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE) + log("...............................................................................", LOG_FORCE) + + 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 = CMD + + + + +#### 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 = CMD + +# Default send method. +desired_method = direct #direct/propagated + +# Propagation node address/hash. +#propagation_node = + +# Try to deliver a message via the LXMF propagation network, +# if a direct delivery to the recipient is not possible. +try_propagation_on_fail = No + +# 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 + +# 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 = 8 + +# Allow only messages with valid signature. +signature_validated = Yes + + + + +#### Message settings #### +[message] + +# Text is added. +receive_prefix = +receive_suffix = + +# Text is replaced. +receive_search = +receive_replace = + +# Text is replaced by regular expression +receive_regex_search = +receive_regex_replace = + +# Length limitation. +receive_length_min = 0 #0=any length +receive_length_max = 0 #0=any length + + +# Text is added. +send_prefix = !user!@!hostname!:!path!#!n! +send_suffix = + +# Text is replaced. +send_search = +send_replace = + +# Text is replaced by regular expression +send_regex_search = +send_regex_replace = + +# Length limitation. +send_length_min = 0 #0=any length +send_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_distribution_group/CHANGELOG.md b/lxmf_distribution_group/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/lxmf_distribution_group/README.md b/lxmf_distribution_group/README.md new file mode 100644 index 0000000..abe8f27 --- /dev/null +++ b/lxmf_distribution_group/README.md @@ -0,0 +1,685 @@ +# lxmf_distribution_group +This program provides an email like distribution group. It will distribute incoming LXMF messages to multiple recipients. Since this program acts as a normal LXMF endpoint, all compatible chat applications can be used. In addition to simple messaging, there is a simple command-based user interface. Where all relevant actions for daily administration can be performed. The basic configuration is done in the configuration files. There are various options to adapt the entire behavior of the group to personal needs. This distribution group is much more than a standard email distribution group. It emulates advanced group functions with automatic notifications etc. Different user permissions can be defined. For each user type, the range of functions can be defined individually. The normal users have only small rights. While a moderator or admin can perform everything necessary by simple commands. Once the basic configuration is done, everything else can be done by LXMF messages as commands. + +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 +- Compatible with all LXMF applications (NomadNet, Sideband, ...) +- Server/Node based message routing and processing +- Direct or propagated message delivery (receive/send) +- Simple group functions (As in other messenger apps) +- User authorization and permissions +- Different user types with different permissions +- Automatic or manual group joining +- Text based interface to display advanced functions or infos +- Cluster of several groups (communication between groups with different levels) +- Automatic negotiation of the clusters +- Statistics at cluster, group and user level +- Easy configuration within readable config files +- Various admin commands for the daily tasks to be controlled via LXMF messages +- Group description, rules and pinned messages +- Optional enableable waiting room for new members before joining the group +- Multiple language support (English & German are predifined) + + +## Examples of use + +### Local self-sufficient group +In a small group of people, this group software can be hosted on a centrally located node. This then allows users to communicate with each other via this group. + +### Multiple local self-sufficient group +On the same node/server several groups can be operated independently of each other. How this works is described below in the installation instructions. + +### Networking groups as a cluster +It is possible to connect several locally independent groups to a cluster. This makes it possible to send messages from one group to another. + +### Hierarchical cluster groups over widely spread areas +A group cluster can be built in several levels. A group can be labeled with several names according to the naming of the levels. +This makes it possible, for example to send a messages to several groups at the same time. So you could define the cluster names as follows. `Country/Region/City` +With this it is possible to contact all groups of a certain country or region. + +### General info how the messages are transported +All messages between client<->group-server and group-server<->group-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 the group. + +When a message is sent to a multi-level (hierarchical) cluster. A 1:1 connection is always established from the source to each target group in this cluster level. + +There is no central server for communication between the individual groups. This offers the advantage that all groups work autonomously. A failure of a group only affects this one local group. + + +## Current Status +It should currently be considered beta software. All core features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. +The full documentation is not yet available. Due to lack of time I can also not say when this will be further processed. + + +## Development Roadmap +- Planned, but not yet scheduled + - Parameters for backup/restore configuration and data + - Parameters for backup/restore identity + - Cluster bridges/repeater + - Different message priorities + - Fallback solution: Master/Slave + - Centralized user/group authorization + - Internal queue with prioritization + - More intelligent messages sending + - Command to display the send status of the last message + - Automatic send confirmation + - Complete documentation + + +## 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 + ``` +- Change the Reticulum configuration to suit your needs and use-case. + ```bash + nano /.reticulum/config + ``` +- Download the [file](lxmf_distribution_group.py) from this repository. + ```bash + wget https://raw.githubusercontent.com/SebastianObi/LXMF-Tools/main/lxmf_distribution_group/lxmf_distribution_group.py + ``` +- Make it executable with the following command + ```bash + chmod +x lxmf_distribution_group.py + ``` + +### Start: +- Start it + ```bash + ./lxmf_distribution_group.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_distribution_group/config.cfg.owr + ``` + ```bash + # 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. + + + #### Main program settings #### + [main] + + # Default language. + lng = en # en/de + + + #### LXMF connection settings #### + [lxmf] + + # The name will be visible to other peers + # on the network, and included in announces. + # It is also used in the group description/info. + display_name = Distribution Group + + # Propagation node address/hash. + propagation_node = ca2762fe5283873719aececfb9e18835 + + # 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 + + + #### Cluster settings #### + [cluster] + + # Enable/Disable this functionality. + enabled = True + + # To use several completely separate clusters/groups, + # an individual name and type can be assigned here. + name = grp + type = cluster + + # Slash-separated list with the names of this cluster. + # No spaces are allowed in the name. + # All send messages that match the name will be received. + # The last name is the main name of this group + # and is used as source for send messages. + display_name = County/Region/City + + + #### Statistic/Counter settings #### + [statistic] + + # Enable/Disable this functionality. + enabled = True + ``` +- Start it again. Finished! + ```bash + ./lxmf_distribution_group.py + ``` + + +### Run as a system service/deamon: +- Create a service file. + ```bash + nano /etc/systemd/system/lxmf_distribution_group.service + ``` +- Copy and edit the following content to your own needs. + ```bash + [Unit] + Description=lxmf_distribution_group.py Daemon + After=multi-user.target + [Service] + # ExecStartPre=/bin/sleep 10 + Type=simple + Restart=always + RestartSec=3 + User=root + ExecStart=/root/lxmf_distribution_group.py + [Install] + WantedBy=multi-user.target + ``` +- Enable the service. + ```bash + systemctl enable lxmf_distribution_group + ``` +- Start the service. + ```bash + systemctl start lxmf_distribution_group + ``` + + +### Start/Stop service: + ```bash + systemctl start lxmf_distribution_group + systemctl stop lxmf_distribution_group + ``` + + +### Enable/Disable service: + ```bash + systemctl enable lxmf_distribution_group + systemctl disable lxmf_distribution_group + ``` + + +### Run several instances (To copy the same application): +- Run the program with a different configuration path. + ```bash + ./lxmf_distribution_group.py -p /root/.lxmf_distribution_group_2nd + ./lxmf_distribution_group.py -p /root/.lxmf_distribution_group_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 group LXMF address is displayed: + ``` + [] ............................................................................... + [] LXMF - Address: <801f48d54bc71cb3e0886944832aaf8d> + [] ...............................................................................` + ``` +- This address is also annouced at startup in the default setting. +- If auto add user is active (default) you can simply send a first message via Sideband/NomadNet to this address. After that you are a member of the group and can use the functions. +- Alternatively, the users can also be entered manually in the `data.cfg` file. It is necessary to add an admin user here to use all commands via LXMF messages! +- Now the group can be used. + + +### Startup parameters: +```bash +usage: lxmf_distribution_group.py [-h] [-p PATH] [-pr PATH_RNS] [-pl PATH_LOG] [-l LOGLEVEL] [-s] [--exampleconfig] [--exampleconfigoverride] [--exampledata] + +LXMF Distribution Group - Server-Side group functions for LXMF based apps + +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 + --exampledata 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. + +- data.cfg + + This is the data file. It is automatically created and saved/overwritten. + It contains data managed by the software itself. + If manual adjustments are made here, the program must be shut down first! + + +## 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`. + + +### Cluster: +This example shows the configuration for a cluster with 2 groups. This allows communication between both groups. +It is possible to write directly to each group or to the higher level which then includes both groups. + +- Group #1 `config.cfg.owr` + ``` + [lxmf] + display_name = Group Test 1 + [cluster] + enabled = True + name = test + type = cluster + display_name = Germany/NRW/Düsseldorf + ``` + +- Group #1 `data.cfg` + ``` + [main] + enabled_cluster = True + auto_add_cluster = True + ``` + +- Group #2 `config.cfg.owr` + ``` + [lxmf] + display_name = Group Test 2 + [cluster] + enabled = True + name = test + type = cluster + display_name = Germany/Bayern/München + ``` + +- Group #2 `data.cfg` + ``` + [main] + enabled_cluster = True + auto_add_cluster = True + ``` + + +### 2 independent cluster: +This example shows the configuration for 2 separate clusters. +This makes it possible to operate several clusters in parallel via the same communication network. +It is important to configure the `name` and `type` differently. + +- Cluster #1 - Group #1 `config.cfg.owr` + ``` + [lxmf] + display_name = Group Test 1 + [cluster] + enabled = True + name = test1 + type = cluster + display_name = Germany/NRW/Düsseldorf + ``` + +- Cluster #1 - Group #1 `data.cfg` + ``` + [main] + enabled_cluster = True + auto_add_cluster = True + ``` + +- Cluster #2 - Group #1 `config.cfg.owr` + ``` + [lxmf] + display_name = Group Test 1 + [cluster] + enabled = True + name = test2 + type = cluster + display_name = Germany/NRW/Düsseldorf + ``` + +- Cluster #2 - Group #1 `data.cfg` + ``` + [main] + enabled_cluster = True + auto_add_cluster = True + ``` + + +### Members/Clusters: +Normally all data here (`data.cfg`) is created automatically by the software. Based on automatic creation of new users/clusters or executed commands for administration. +Here are a few examples of how the content can look. Of course, the file can also be edited manually. This is necessary if an auto add is disabled. +Please do not forget to close the program first! + +- Group #1 `data.cfg` + ``` + [user] + 04652a820cc69d47940ce39050c455a6 = Test User 1 + + [cluster] + d1b551e1b89fff5a4a6f2aaff2464971 = Germany/Bayern/München + ``` + +- Group #2 `data.cfg` + ``` + [user] + 18201a931dd69d47940ce39050c487c9 = Test User 1 + + [cluster] + 801f48d54bc71cb3e0886944832aaf8d = Germany/NRW/Düsseldorf + ``` + + +### Waiting room for new members: +This example shows the configuration for a waiting room for new members. +When an unknown user joins the group by the first message to the group, he is added to the "wait" type. +There he will be in a kind of waiting room where no messages can be written and received. +An admin or moderator can then allow or disallow this user. + +The configuration shows only the minimum necessary part for this functionality. Of course, further rights can be assigned to the users. + +- `config.cfg.owr` + ``` + [rights] + admin = interface,receive_join,allow,deny + mod = interface,receive_join,allow,deny + wait = + + [interface_messages] + auto_add_wait = Welcome to the group "!display_name!"!!n!!n!You still need to be allowed to join. You will be notified automatically. + auto_add_wait-de = Willkommen in der Gruppe "!display_name!"!!n!!n!Der Beitritt muss ihnen noch erlaubt werden. Sie werden darüber automatisch benachrichtigt. + + allow_user = You have been allowed to join the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Please assign a nickname with the command /name + allow_user-de = Sie wurden erlaubt der Gruppe "!display_name!" beizutreten!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name + + deny_user = You have been denied to join the group "!display_name!"! + deny_user-de = Ihnen wurde der Beitritt in die Gruppe "!display_name!" abgelehnt! + + member_join = !source_name! joins the waiting room and must be allowed to join the group. + member_join-de = !source_name! betritt den Warteraum und muss zur Gruppe zugelassen werden. + ``` + +- `data.cfg` + ``` + [main] + auto_add_user = True + auto_add_user_type = wait + allow_user = True + allow_user_type = user + deny_user = True + deny_user_type = block_wait + ``` + + +## Admin manual +This guide applies to all admins. Here are briefly explained the administative possibilities. + +An administartor has correspondingly higher permissions and more commands are available. In general, the permissions can be freely defined. All users/admins etc. can also generally have the same permissions. + + +### Activate/deactivate functions: +The following functions can be adjusted accordingly by command. + +`/enable_local ` = Local message routing + +`/enable_cluster ` = Cluster message routing + +`/auto_add_user ` = Add unknown user functionality + +`/auto_add_cluster ` = Add unknown cluster functionality + + +### Change values: +`/description ` = Change description + +`/rules ` = Change rules + + +### Send a manual announce of the group and cluster: +`/announce` + + +### Manage users (display of existing users): +`/show or /list` + +`/show or /list ` + +`/search ` + + +### Manage users (invite): +Additional users can be invited, this can be done with the command `/invite `. +Then the user gets a welcome message and enters the group. + + +### Manage users (allow/deny): +If the waiting room is activated, the users can be administered with the following 2 commands. + +`/allow ` + +`/deny ` + + +### Manage users (add/delete/move): +The following commands can be used to administrate the users. +Only in case of an invite a welcome message will be sent to the user. Users added here will not get a notification and have to start the first conversation with the group themselves. Or get a message sent directly. + +`/add ` + +`/del or /rm ` + +`/del or /rm ` + +`/move ` + + +### Manage users (kick/block/unblock): +The following commands can be used to remove/enable the users. + +`/kick ` + +`/block ` + +`/unblock ` + + +### Save data: +If an automatic save is set in the config nothing has to be done here. If not or additionally the data can be saved with the following command. + +`/save` + + +### Help: +To display the help and all available commands the following commands can be used. `/help` or `/?` + + +### Examples of possible commands: +``` +/help or /? = Shows this help +/leave or /part = Leave group +/name = Show current nickname +/nick = Show current nickname +/name = Change/Define nickname +/nick = Change/Define nickname +/address = Dislay address info +/info = Show group info +/description = Show current description +/rules = Show current rules +/version = Show version info +/groups or /cluster = Show all groups/clusters +/groups = Searches for a group/cluster by name +/members or /names or /who = Show all group members +/admins = Show group admins +/moderators or /mods = Show group moderators +/users = Show group users +/guests = Show group guests +/search = Searches for a user by nickname or address +/whois = Searches for a user by nickname or address +/activitys = Show user activitys +/statistic or /stat = Show group statistic +/status = Show status +/delivery or /message = Show delivery status of last message +/enable_local = Local message routing +/enable_cluster = Cluster message routing +/auto_add_user = Add unknown user functionality +/auto_add_user_type +/auto_add_cluster = Add unknown cluster functionality +/invite_user = Invite functionality +/invite_user_type +/description = Change description +/rules = Change rules +/announce = Send announce +/sync = Synchronize messages with propagation node +/show run = Show current configuration +/show or /list +/show or /list +/add +/del or /rm +/del or /rm +/move +/invite = Invites user to group +/kick = Kicks user out of group +/block = Block user +/ban = Block user +/unblock = Unblock user +/unban = Unblock user +/load or /read = Read the configuration/data +/save or /wr = Saves the current configuration/data +``` + + +## User manual +This guide applies to users or admins. Here are briefly explained the normal possibilities of the software. + + +### Start/Join the group: +Just send a first message to the group address with Sideband/NomadNet. +However, this is only possible if automatic joining of the group is activated. + + +### Send local group message: +Any normal text without `/` or `@` at the beginning will be interpreted as a normal message and sent to all local members accordingly. There is nothing else to consider here. + + +### Send cluster message: +It is possible to send messages to other groups which are part of the cluster. To do this you must first enter the command `@` followed by the target name of the group and then the normal message text. + +For example `@Berlin Hello this is a test :)`. So this example would send this message to the Berlin group. + +A group in a cluster can be arranged hierarchically in different levels. If the higher level is defined as the target, all groups below it receive this message. + +For example, there are the following 3 groups `Germany/Berlin` and `Germany/Hamburg` and `Germany/Munich`. Accordingly, these can be written to directly or a higher level. + +With the command `@Germany ` all 3 groups are now accessible. With the command `@Munich ` only this one group is accessible. + + +### Pin message (local group): +It is possible to pin local group messages permanently. This will then sent to all members. Additionally, all pinned messages can be displayed later. + +This feature is useful to give new members access to important news from the past. + +`/pin` = Display all pinned messages + +`/pin ` = Pin a new message + +`/unpin <#id>` = Remove a pinned message + + +### Pin message (cluster group): +It is possible to pin cluster group messages permanently. This will then sent to all members. Additionally, all pinned messages can be displayed later. + +This feature is useful to give new members access to important news from the past. + +`@Group /pin ` = Pin a new message + + +### Interface/Commands: +A simple text-message based user interface is integrated. Like you might know it from other chat programs. Every command must start with the delimiter `/`. Then followed by the command and any data. For example `/name My new nick name`. + +If there is no `/` at the beginning this is a normal message and will be sent to the other members. + + +### Help: +To display the help and all available commands the following commands can be used. `/help` or `/?` + + +### Leave the group: +The `/leave` command is used to leave the group. Afterwards, the group can be re-entered (if it is allowed). + + +### Invite users: +If the admin has allowed additional users to be invited, this can be done with the command `/invite `. +Then the user gets a welcome message and enters the group. + + +### Change nickname: +The own nickname is either assigned automatically via received announce (after joining the group) or can be changed via the following command. + +`/name ` For example `/name Max Walker`. + + +### Examples of possible commands: +``` +/help or /? = Shows this help +/leave or /part = Leave group +/name = Show current nickname +/nick = Show current nickname +/name = Change/Define nickname +/nick = Change/Define nickname +/address = Dislay address info +/info = Show group info +/description = Show current description +/rules = Show current rules +/version = Show version info +/groups or /cluster = Show all groups/clusters +/groups = Searches for a group/cluster by name +/members or /names or /who = Show all group members +/admins = Show group admins +/moderators or /mods = Show group moderators +/users = Show group users +/guests = Show group guests +/search = Searches for a user by nickname or address +/whois = Searches for a user by nickname or address +/activitys = Show user activitys +/statistic or /stat = Show group statistic +/delivery or /message = Show delivery status of last message +/invite = Invites user to group +``` + + +## FAQ + +### Why this server based group function and no direct groups in the client software? +At the time of the development of these group functions there is no other possibility to use groups via Sideband/Nomadnet. Therefore this software was developed as a workaround. +This software also offers other functions than a normal group broadcast. + +### 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_distribution_group/lxmf_distribution_group.py b/lxmf_distribution_group/lxmf_distribution_group.py new file mode 100755 index 0000000..ca11350 --- /dev/null +++ b/lxmf_distribution_group/lxmf_distribution_group.py @@ -0,0 +1,4676 @@ +#!/usr/bin/env python3 +############################################################################################################## +# +# Copyright (c) 2022 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 + +#### Variables #### +from collections import defaultdict + +#### JSON #### +import json +import pickle + +#### String #### +import string + +#### Search #### +import fnmatch + +#### 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 + + +############################################################################################################## +# Globals + + +#### Global Variables - Configuration #### +NAME = "LXMF Distribution Group" +DESCRIPTION = "Server-Side group functions for LXMF based apps" +VERSION = "0.0.1 (2022-10-21)" +COPYRIGHT = "(c) 2022 Sebastian Obele / obele.eu" +PATH = os.path.expanduser("~") + "/." + os.path.splitext(os.path.basename(__file__))[0] +PATH_RNS = None + + + + +#### Global Variables - System (Not changeable) #### +DATA = None +CONFIG = None +STATISTIC = None +RNS_MAIN_CONNECTION = None +LXMF_CONNECTION = None +RNS_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 + + + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", send_delay=0, desired_method="direct", propagation_node=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.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.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 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) + + 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) + + 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) + + self.autoselect_propagation_node() + + if self.announce_startup or self.announce_periodic: + self.announce(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 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=None, fields=None, timestamp=None, app_data=""): + 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 + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return + + destination_identity = RNS.Identity.recall(destination) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + + + def send_message(self, destination, source, content="", title=None, 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) + 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 + + + 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, 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() + return + + self.announce_now() + + + def announce_now(self, app_data=None): + if app_data: + self.destination.announce(app_data.encode("utf-8")) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ":" + app_data, 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 autoselect_propagation_node(self): + if self.propagation_node is not None: + if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + else: + try: + propagation_hash = bytes.fromhex(self.propagation_node) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return + + node_identity = RNS.Identity.recall(propagation_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) + propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(propagation_hash) + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + + + 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) + + +############################################################################################################## +# RNS Class + + +class rns_connection: + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="rns", destination_type="connect", announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, announce_data=""): + 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.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.announce_data = announce_data + + if not os.path.isdir(self.storage_path): + os.makedirs(self.storage_path) + log("RNS - Storage path was created", LOG_NOTICE) + log("RNS - Storage path: " + self.storage_path, LOG_INFO) + + if self.identity: + log("RNS - 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("RNS - Loaded Primary Identity %s from %s" % (str(self.identity), self.identity_path)) + else: + log("RNS - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + except Exception as e: + log("RNS - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + log("RNS - The contained exception was: %s" % (str(e)), LOG_ERROR) + else: + try: + log("RNS - No Primary Identity file found, creating new...") + self.identity = RNS.Identity() + self.identity.to_file(self.identity_path) + log("RNS - Created new Primary Identity %s" % (str(self.identity))) + except Exception as e: + log("RNS - Could not create and save a new Primary Identity", LOG_ERROR) + log("RNS - The contained exception was: %s" % (str(e)), LOG_ERROR) + + self.destination = RNS.Destination(self.identity, RNS.Destination.IN, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + + self.destination.set_proof_strategy(RNS.Destination.PROVE_ALL) + + if self.announce_startup or self.announce_periodic: + self.announce(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 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("RNS - Destination length is invalid", LOG_ERROR) + return False + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("RNS - 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 announce(self, initial=False): + 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: + announce_timer.cancel() + announce_timer = threading.Timer(self.announce_startup_delay, self.announce) + announce_timer.daemon = True + announce_timer.start() + else: + self.announce_now() + return + + self.announce_now() + + + def announce_now(self, app_data=None): + if app_data: + self.destination.announce(app_data.encode("utf-8")) + log("RNS - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ":" + app_data, LOG_DEBUG) + else: + self.destination.announce(self.announce_data.encode("utf-8")) + log("RNS - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, 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: + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + + global DATA + + lng_key = "-" + CONFIG["main"]["lng"] + + sections = [] + for (key, val) in CONFIG.items("rights"): + if DATA.has_section(key): + sections.append(key) + + 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 != "": + 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, "", None, 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" + + + + +#### LXMF - Message #### +def lxmf_message_received_callback(message): + 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 + + content = message.content.decode('utf-8') + content = content.strip() + if content == "": + return + + title = message.title.decode('utf-8') + title = title.strip() + + fields = message.fields + + lng_key = "-" + CONFIG["main"]["lng"] + + sections = [] + for (key, val) in CONFIG.items("rights"): + if DATA.has_section(key): + sections.append(key) + + destination_hash = RNS.hexrep(message.destination_hash, False) + source_hash = RNS.hexrep(message.source_hash, False) + source_name = "" + source_right = "" + + for section in DATA.sections(): + if section.startswith("block"): + if DATA.has_option(section, source_hash): + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " blocked", LOG_DEBUG) + source_right = section.replace("block_", "") + source_rights = config_get(CONFIG, "rights", source_right) + source_rights = source_rights.split(",") + if "reply_block" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_block", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + for section in DATA.sections(): + for (key, val) in DATA.items(section): + if key == source_hash: + source_name = val + source_right = section + + if fields: + if "c_n" in fields and "c_t" in fields and "m_t" in fields: + if fields["c_n"] == CONFIG["cluster"]["name"] and fields["c_t"] == CONFIG["cluster"]["type"] and source_right == "cluster" and config_getboolean(CONFIG, "cluster", "enabled"): + content_prefix = config_get(CONFIG, "message", "cluster_receive_prefix", "", lng_key) + content_suffix = config_get(CONFIG, "message", "cluster_receive_suffix", "", lng_key) + + content_prefix = replace(content_prefix, source_hash, source_name, source_right, lng_key) + content_suffix = replace(content_suffix, source_hash, source_name, source_right, lng_key) + + source = source_name.rsplit('/', 1)[-1] + destination = config_get(CONFIG, "cluster", "display_name", "", lng_key).rsplit('/', 1)[-1] + content_prefix = content_prefix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_source"+CONFIG["interface"]["delimiter_output"], source) + content_prefix = content_prefix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_destination"+CONFIG["interface"]["delimiter_output"], destination) + content_suffix = content_suffix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_source"+CONFIG["interface"]["delimiter_output"], source) + content_suffix = content_suffix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_destination"+CONFIG["interface"]["delimiter_output"], destination) + + search = config_get(CONFIG, "message", "cluster_receive_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "cluster_receive_replace")) + + search = config_get(CONFIG, "message", "cluster_receive_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "cluster_receive_regex_replace"), content) + + content = content_prefix + content + content_suffix + + if config_get(CONFIG, "message", "timestamp", "", lng_key) == "client": + timestamp = message.timestamp + else: + timestamp = time.time() + + if CONFIG["statistic"].getboolean("enabled") and CONFIG["statistic"].getboolean("cluster"): + statistic("add", "cluster_in_" + message.desired_method_str) + + if fields["m_t"] == "message": + for section in sections: + if "receive_cluster" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content, title, None, timestamp, "cluster_send") + elif fields["m_t"] == "pin": + delimiter = CONFIG["interface"]["delimiter_output"] + + value_new = config_get(CONFIG, "interface_menu", "cluster_pin", "", lng_key) + value_new = replace(value_new, source_hash, source_name, source_right, lng_key) + value_new = value_new.replace(delimiter+"value"+delimiter, content) + + key = time.strftime(config_get(CONFIG, "message", "pin_id", "%y%m%d-%H%M%S", lng_key), time.localtime(time.time())) + if DATA.has_option("pin", key): + key = key + "-" + key_int = 0 + while DATA.has_option("pin", key+str(key_int)): + key_int += 1 + key = key+str(key_int) + + DATA["pin"][key] = value_new + + content_group = config_get(CONFIG, "interface_messages", "cluster_pin_add", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group.replace(delimiter+"key"+delimiter, key) + content_group = content_group.replace(delimiter+"value"+delimiter, value_new) + if content_group != "": + for section in sections: + if "receive_cluster_pin_add" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content_group, "", None, None, "cluster_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" + + return + + if source_right == "" and DATA["main"].getboolean("auto_add_user"): + if CONFIG["lxmf"].getboolean("signature_validated_new") and not message.signature_validated: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " have no valid signature 'new'", LOG_DEBUG) + return + + key = DATA["main"]["auto_add_user_type"] + if DATA.has_section(key) and key != "main": + DATA[key][source_hash] = "" + DATA.remove_option("main", "unsaved") + content = config_get(CONFIG, "interface_messages", "auto_add_"+key, "", lng_key) + content_group = config_get(CONFIG, "interface_messages", "member_join", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + if content_group != "": + for section in sections: + if "receive_join" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content_group, title, None, 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" + content = replace(content, source_hash, source_name, source_right, lng_key) + if content != "": + LXMF_CONNECTION.send(source_hash, content, title, None, None, "interface_send") + return + elif source_right == "": + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not exist (auto add disabled)", LOG_DEBUG) + return + + + source_rights = config_get(CONFIG, "rights", source_right) + if source_rights == "": + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " have no right", LOG_DEBUG) + return + source_rights = source_rights.split(",") + + + if CONFIG["lxmf"].getboolean("signature_validated_known") and not message.signature_validated: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " have no valid signature 'known'", LOG_DEBUG) + if "reply_signature" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_signature", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + + length = config_getint(CONFIG, "message", "receive_length_min", 0, lng_key) + if length> 0: + if len(content) < length: + if "reply_length_min" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_length_min", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + + length = config_getint(CONFIG, "message", "receive_length_max", 0, lng_key) + if length > 0: + if len(content) > length: + if "reply_length_max" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_length_max", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + + content_prefix = config_get(CONFIG, "message", "receive_prefix", "", lng_key) + content_suffix = config_get(CONFIG, "message", "receive_suffix", "", lng_key) + + search = config_get(CONFIG, "message", "receive_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "receive_replace")) + + search = config_get(CONFIG, "message", "receive_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "receive_regex_replace"), content) + + content = content_prefix + content + content_suffix + + + # Interface + if content.startswith(CONFIG["interface"]["delimiter_input"]): + if not config_getboolean(CONFIG, "interface", "enabled"): + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " 'interface' disabled", LOG_DEBUG) + if "reply_interface_enabled" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_interface_enabled", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + if "interface" not in source_rights: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " 'interface' not allowed", LOG_DEBUG) + if "reply_interface_right" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_interface_right", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + content = interface(content[len(CONFIG["interface"]["delimiter_input"]):], source_hash, source_name, source_right, source_rights, lng_key) + if content == "": + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " 'interface' not allowed (empty response)", LOG_DEBUG) + return + + if CONFIG["statistic"].getboolean("enabled"): + if CONFIG["statistic"].getboolean("interface"): + statistic("add", "interface_received_" + message.desired_method_str) + if CONFIG["statistic"].getboolean("user"): + statistic("value_set", source_hash, "activity", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))) + statistic("value_set", source_hash, "activity_receive", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))) + + LXMF_CONNECTION.send(source_hash, content, "", None, None, "interface_send") + return + + + # Message - Cluster + if content.startswith(CONFIG["cluster"]["delimiter_input"]): + if not config_getboolean(CONFIG, "cluster", "enabled") or not DATA["main"].getboolean("enabled_cluster"): + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " 'cluster' disabled", LOG_DEBUG) + if "reply_cluster_enabled" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_cluster_enabled", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + if "send_cluster" not in source_rights: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " 'cluster' not allowed", LOG_DEBUG) + if "reply_cluster_right" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_cluster_right", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + try: + content = content[len(CONFIG["cluster"]["delimiter_input"]):] + destination, content = content.split(" ", 1) + except: + LXMF_CONNECTION.send(source_hash, config_get(CONFIG, "interface_menu", "cluster_format_error", "", lng_key) , "", None, None, "interface_send") + return + + destinations = [] + for (key, val) in DATA.items("cluster"): + if key != destination_hash and destination in val.split("/"): + destinations.append(key) + + if len(destinations) == 0: + LXMF_CONNECTION.send(source_hash, config_get(CONFIG, "interface_menu", "cluster_found_error", "", lng_key) , "", None, None, "interface_send") + return + + length = config_getint(CONFIG, "message", "cluster_send_length_min", 0, lng_key) + if length> 0: + if len(content) < length: + if "reply_length_min" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_length_min", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + length = config_getint(CONFIG, "message", "cluster_send_length_max", 0, lng_key) + if length > 0: + if len(content) > length: + if "reply_length_max" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_length_max", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + content_prefix = config_get(CONFIG, "message", "cluster_send_prefix", "", lng_key) + content_suffix = config_get(CONFIG, "message", "cluster_send_suffix", "", lng_key) + + content_prefix = replace(content_prefix, source_hash, source_name, source_right, lng_key) + content_suffix = replace(content_suffix, source_hash, source_name, source_right, lng_key) + + source = config_get(CONFIG, "cluster", "display_name", "", lng_key).rsplit('/', 1)[-1] + content_prefix = content_prefix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_source"+CONFIG["interface"]["delimiter_output"], source) + content_prefix = content_prefix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_destination"+CONFIG["interface"]["delimiter_output"], destination) + content_suffix = content_suffix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_source"+CONFIG["interface"]["delimiter_output"], source) + content_suffix = content_suffix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_destination"+CONFIG["interface"]["delimiter_output"], destination) + + search = config_get(CONFIG, "message", "cluster_send_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "cluster_send_replace")) + + search = config_get(CONFIG, "message", "cluster_send_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "cluster_send_regex_replace"), content) + + fields = defaultdict(dict) + fields["c_n"] = CONFIG["cluster"]["name"] + fields["c_t"] = CONFIG["cluster"]["type"] + + delimiter_input = CONFIG["interface"]["delimiter_input"] + if (content.startswith(delimiter_input+"pin ") or content.startswith(delimiter_input+"pins ")) and "cluster_pin_add" in source_rights: + content = content.lstrip(delimiter_input+"pin ") + content = content.lstrip(delimiter_input+"pins ") + fields["m_t"] = "pin" + else: + fields["m_t"] = "message" + + content = content_prefix + content + content_suffix + + if config_get(CONFIG, "message", "timestamp", "", lng_key) == "client": + timestamp = message.timestamp + else: + timestamp = time.time() + + if CONFIG["statistic"].getboolean("enabled"): + if CONFIG["statistic"].getboolean("cluster"): + statistic("add", "cluster_received_" + message.desired_method_str) + if CONFIG["statistic"].getboolean("user"): + statistic("add", source_hash) + statistic("value_set", source_hash, "activity", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))) + statistic("value_set", source_hash, "activity_receive", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))) + + for val in destinations: + LXMF_CONNECTION.send(key, content, title, fields, timestamp, "cluster_out") + + cluster_loop = False + if destination in config_get(CONFIG, "cluster", "display_name", "", lng_key).split("/"): + cluster_loop = True + + for section in sections: + if "receive_cluster_send" in config_get(CONFIG, "rights", section).split(",") or (cluster_loop and "receive_cluster_loop" in config_get(CONFIG, "rights", section).split(",")): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content, title, None, timestamp, "local_send") + + return + + + # Message - Local + if DATA["main"].getboolean("enabled_local"): + if "send_local" in source_rights: + + length = config_getint(CONFIG, "message", "send_length_min", 0, lng_key) + if length> 0: + if len(content) < length: + if "reply_length_min" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_length_min", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + length = config_getint(CONFIG, "message", "send_length_max", 0, lng_key) + if length > 0: + if len(content) > length: + if "reply_length_max" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_length_max", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + content_prefix = config_get(CONFIG, "message", "send_prefix", "", lng_key) + content_suffix = config_get(CONFIG, "message", "send_suffix", "", lng_key) + + content_prefix = replace(content_prefix, source_hash, source_name, source_right, lng_key) + content_suffix = replace(content_suffix, source_hash, source_name, source_right, lng_key) + + search = config_get(CONFIG, "message", "send_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "send_replace")) + + search = config_get(CONFIG, "message", "send_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "send_regex_replace"), content) + + content = content_prefix + content + content_suffix + + if config_get(CONFIG, "message", "timestamp", "", lng_key) == "client": + timestamp = message.timestamp + else: + timestamp = time.time() + + if CONFIG["statistic"].getboolean("enabled"): + if CONFIG["statistic"].getboolean("local"): + statistic("add", "local_received_" + message.desired_method_str) + if CONFIG["statistic"].getboolean("user"): + statistic("add", source_hash) + statistic("value_set", source_hash, "activity", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))) + statistic("value_set", source_hash, "activity_receive", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))) + + for section in sections: + if "receive_local" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content, title, None, timestamp, "local_send") + return + else: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " 'send' not allowed", LOG_DEBUG) + if "reply_local_right" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_local_right", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + else: + if "reply_local_enabled" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_local_enabled", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + + + return + + + + +#### LXMF - Notification #### +def lxmf_message_notification_success_callback(message): + if CONFIG["statistic"].getboolean("enabled"): + if message.app_data.startswith("cluster") and CONFIG["statistic"].getboolean("cluster"): + statistic("add", message.app_data + "_" + message.desired_method_str + "_success") + elif message.app_data.startswith("local") and CONFIG["statistic"].getboolean("local"): + statistic("add", message.app_data + "_" + message.desired_method_str + "_success") + elif message.app_data.startswith("interface") and CONFIG["statistic"].getboolean("interface"): + statistic("add", message.app_data + "_" + message.desired_method_str + "_success") + + if CONFIG["statistic"].getboolean("user"): + if message.desired_method_str == "direct": + destination_hash = RNS.hexrep(message.destination_hash, False) + statistic("value_set", destination_hash, "activity", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))) + statistic("value_set", destination_hash, "activity_send", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))) + return + + + + +#### LXMF - Notification #### +def lxmf_message_notification_failed_callback(message): + if CONFIG["statistic"].getboolean("enabled"): + if message.app_data.startswith("cluster") and CONFIG["statistic"].getboolean("cluster"): + statistic("add", message.app_data + "_" + message.desired_method_str + "_failed") + elif message.app_data.startswith("local") and CONFIG["statistic"].getboolean("local"): + statistic("add", message.app_data + "_" + message.desired_method_str + "_failed") + elif message.app_data.startswith("interface") and CONFIG["statistic"].getboolean("interface"): + statistic("add", message.app_data + "_" + message.desired_method_str + "_failed") + return + + +############################################################################################################## +# RNS Functions + + +class rns_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: + log("Cluster - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + + global DATA + + lng_key = "-" + CONFIG["main"]["lng"] + + sections = [] + for (key, val) in CONFIG.items("rights"): + if DATA.has_section(key): + sections.append(key) + + receive = app_data.decode("utf-8") + if receive != "": + receive = json.loads(receive) + + if "c" in receive and "c_h" in receive and "c_d" in receive and CONFIG["cluster"].getboolean("enabled") and DATA["main"].getboolean("auto_add_cluster"): + if receive["c"] == "1": + if not DATA.has_option("cluster", receive["c_h"]): + content_group = config_get(CONFIG, "interface_messages", "cluster_join", "", lng_key) + content_group = replace(content_group, receive["c_h"], receive["c_d"], "", lng_key) + if content_group != "": + for section in sections: + if "receive_cluster_join" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + DATA["cluster"][receive["c_h"]] = receive["c_d"] + + 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" + + +############################################################################################################## +# Interface + + +#### Interface ##### +def interface(cmd, source_hash, source_name, source_right, source_rights, lng_key): + cmd = cmd.strip() + + content = "" + + delimiter = CONFIG["interface"]["delimiter_output"] + + sections = [] + for (key, val) in CONFIG.items("rights"): + if DATA.has_section(key): + sections.append(key) + + # "/help" command. + if (cmd == "help" or cmd == "?") and "help" in source_rights: + content = config_get(CONFIG, "interface_menu", "help_"+source_right, "", lng_key) + interface_help = "" + interface_help_command = "" + for value in source_rights: + interface_help = interface_help + config_get(CONFIG, "interface_help", value, "", lng_key) + interface_help_command = interface_help_command + config_get(CONFIG, "interface_help_command", value, "", lng_key) + content = content.replace(delimiter+"interface_help"+delimiter, interface_help) + content = content.replace(delimiter+"interface_help_command"+delimiter, interface_help_command) + content = replace(content, source_hash, source_name, source_right, lng_key) + + + # "/leave" command. + elif (cmd == "leave" or cmd == "part") and "leave" in source_rights: + try: + for section in sections: + for (key, val) in DATA.items(section): + if key == source_hash: + DATA.remove_option(section, key) + + if CONFIG["statistic"].getboolean("enabled"): + statistic("del", key) + + content_group = config_get(CONFIG, "interface_messages", "member_leave", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + if content_group != "": + for section in sections: + if "receive_leave" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "leave_ok", "", lng_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: + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "leave_error", "", lng_key) + + + # "/name" command. + elif (cmd == "name" or cmd == "nick") and "name" in source_rights: + content = config_get(CONFIG, "interface_menu", "name", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + + elif (cmd.startswith("name ") or cmd.startswith("nick ") or cmd.startswith("setname ")) and "name" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + for section in sections: + for (key, val) in DATA.items(section): + if key == source_hash: + DATA[section][key] = value + + if source_name == "": + content_type = "name_def" + content_add = " " + value + else: + content_type = "name_change" + content_add = " " + source_name + " -> " + value + + content_group = config_get(CONFIG, "interface_messages", "member_"+content_type, "", lng_key) + if content_group != "": + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group + content_add + for section in sections: + if "receive_"+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, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "name_ok", "", lng_key) + " " + value + + 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" + except: + content = config_get(CONFIG, "interface_menu", "name_error", "", lng_key) + + + # "/address" command. + elif cmd == "address" and "address" in source_rights: + content = config_get(CONFIG, "interface_menu", "address_"+source_right, "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + + + # "/info" command. + elif cmd == "info" and "info" in source_rights: + content = config_get(CONFIG, "interface_menu", "info_"+source_right, "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + + + # "/pin" command. + elif (cmd == "pin" or cmd == "pins") and "pin" in source_rights: + count = 0 + content = config_get(CONFIG, "interface_menu", "pin_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + for (key, val) in DATA.items("pin"): + count += 1 + content = content + "#" + key + "\n" + val + "\n\n" + content = content.replace(delimiter+"count"+delimiter, str(count)) + + elif (cmd.startswith("pin ") or cmd.startswith("pins ")) and "pin_add" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + value_new = config_get(CONFIG, "interface_menu", "pin", "", lng_key) + value_new = replace(value_new, source_hash, source_name, source_right, lng_key) + value_new = value_new.replace(delimiter+"value"+delimiter, value) + + key = time.strftime(config_get(CONFIG, "message", "pin_id", "%y%m%d-%H%M%S", lng_key), time.localtime(time.time())) + if DATA.has_option("pin", key): + key = key + "-" + key_int = 0 + while DATA.has_option("pin", key+str(key_int)): + key_int += 1 + key = key+str(key_int) + + DATA["pin"][key] = value_new + + content_group = config_get(CONFIG, "interface_messages", "pin_add", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group.replace(delimiter+"key"+delimiter, key) + content_group = content_group.replace(delimiter+"value"+delimiter, value_new) + if content_group != "": + for section in sections: + if "receive_pin_add" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "pin_add_ok", "", lng_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: + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + elif (cmd.startswith("unpin ") or cmd.startswith("unpins ")) and "pin_remove" in source_rights: + try: + cmd, key = cmd.split(" ", 1) + if key.startswith("#"): + key = key[1:] + if DATA.has_option("pin", key): + value = DATA["pin"][key] + DATA.remove_option("pin", key) + + content_group = config_get(CONFIG, "interface_messages", "pin_remove", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group.replace(delimiter+"key"+delimiter, key) + content_group = content_group.replace(delimiter+"value"+delimiter, value) + if content_group != "": + for section in sections: + if "receive_pin_add" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "pin_remove_ok", "", lng_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: + DATA["main"]["unsaved"] = "True" + else: + content = config_get(CONFIG, "interface_menu", "pin_found_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/version" command. + elif cmd == "version" and "version" in source_rights: + content = config_get(CONFIG, "interface_menu", "version_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + content = content + NAME + "\n" + DESCRIPTION + "\nV" + VERSION + + + # "/groups" command. + elif (cmd == "groups" or cmd == "group" or cmd == "cluster") and "groups" in source_rights: + count = 0 + content = config_get(CONFIG, "interface_menu", "groups_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + content_member = config_get(CONFIG, "interface_menu", "groups_member", "", lng_key) + data_dict = defaultdict(dict) + section = "cluster" + for (key, val) in DATA.items(section): + data_dict[val] = key + for key in sorted(data_dict): + count += 1 + content = content + replace(content_member, data_dict[key], key, section, lng_key) + content = content.replace(delimiter+"count"+delimiter, str(count)) + + elif (cmd.startswith("groups ") or cmd.startswith("group ") or cmd.startswith("cluster ")) and "groups" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + executed = False + count = 0 + content = config_get(CONFIG, "interface_menu", "groups_search_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + content_member = config_get(CONFIG, "interface_menu", "groups_search_member", "", lng_key) + data_dict = defaultdict(dict) + section = "cluster" + for (key, val) in DATA.items(section): + if value in val: + executed = True + data_dict[val] = key + for key in sorted(data_dict): + count += 1 + content = content + replace(content_member, data_dict[key], key, section, lng_key) + content = content.replace(delimiter+"count"+delimiter, str(count)) + if not executed: + content = config_get(CONFIG, "interface_menu", "groups_search_found_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/members" command. + elif (cmd == "members" or cmd == "member" or cmd == "names" or cmd == "who") and "members" in source_rights: + count = 0 + content = config_get(CONFIG, "interface_menu", "members_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + content_member = config_get(CONFIG, "interface_menu", "members_member", "", lng_key) + for section in sections: + for (key, val) in DATA.items(section): + count += 1 + content = content + replace(content_member, key, val, section, lng_key) + content = content.replace(delimiter+"count"+delimiter, str(count)) + + + # "/search" command. + elif (cmd.startswith("search ") or cmd.startswith("whois ") or cmd.startswith("w ")) and "search" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + executed = False + count = 0 + content = config_get(CONFIG, "interface_menu", "search_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + content_member = config_get(CONFIG, "interface_menu", "search_member", "", lng_key) + for section in sections: + for (key, val) in DATA.items(section): + if fnmatch.fnmatch(key, value) or fnmatch.fnmatch(val, value): + executed = True + count += 1 + content = content + replace(content_member, key, val, section, lng_key).replace(delimiter+"activity_receive"+delimiter, statistic_value_get(key, "activity_receive")).replace(delimiter+"activity_send"+delimiter, statistic_value_get(key, "activity_send")) + content = content.replace(delimiter+"count"+delimiter, str(count)) + if not executed: + content = config_get(CONFIG, "interface_menu", "search_found_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/activitys" command. + elif (cmd == "activitys" or cmd == "activity") and "activitys" in source_rights: + count = 0 + content = config_get(CONFIG, "interface_menu", "activitys_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + content_member = config_get(CONFIG, "interface_menu", "activitys_member", "", lng_key) + for section in sections: + for (key, val) in DATA.items(section): + count += 1 + content = content + replace(content_member, key, val, section, lng_key).replace(delimiter+"activity_receive"+delimiter, statistic_value_get(key, "activity_receive")).replace(delimiter+"activity_send"+delimiter, statistic_value_get(key, "activity_send")) + content = content.replace(delimiter+"count"+delimiter, str(count)) + + + # "/statistic" command. + elif (cmd == "statistic" or cmd == "stat" or cmd == "stats" or cmd.startswith("statistic ") or cmd.startswith("stat ") or cmd.startswith("stats ")) and "statistic" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + except: + value = "day" + values = ["day", "last_day", "week", "last_week", "month", "last_month", "year", "last_year", "all", "max"] + if value in values: + if CONFIG["statistic"].getboolean("enabled") and CONFIG["statistic"].getboolean("cluster") and "statistic_cluster" in source_rights and ("statistic_min" in source_rights or "statistic_full" in source_rights): + content = content + replace(config_get(CONFIG, "interface_menu", "statistic_header_cluster", "", lng_key), source_hash, source_name, source_right, lng_key).replace(delimiter+"value"+delimiter, value) + if "statistic_min" in source_rights: + statistic_recalculate("cluster_received_direct") + statistic_recalculate("cluster_received_propagated") + statistic_recalculate("cluster_send_direct_success") + statistic_recalculate("cluster_send_propagated_success") + statistic_recalculate("cluster_send_direct_failed") + statistic_recalculate("cluster_send_propagated_failed") + statistic_recalculate("cluster_in_direct") + statistic_recalculate("cluster_in_propagated") + statistic_recalculate("cluster_out_direct_success") + statistic_recalculate("cluster_out_propagated_success") + statistic_recalculate("cluster_out_direct_failed") + statistic_recalculate("cluster_out_propagated_failed") + content = content + "#Received: " + statistic_value_get("cluster_received_direct", value+"_value", "0") + "d/" + statistic_value_get("cluster_received_propagated", value+"_value", "0") + "p\n" + content = content + "#Send OK: " + statistic_value_get("cluster_send_direct_success", value+"_value", "0") + "d/" + statistic_value_get("cluster_send_propagated_success", value+"_value", "0") + "p\n" + content = content + "#Send Failed: " + statistic_value_get("cluster_send_direct_failed", value+"_value", "0") + "d/" + statistic_value_get("cluster_send_propagated_failed", value+"_value", "0") + "p\n" + content = content + "#In: " + statistic_value_get("cluster_in_direct", value+"_value", "0") + "d/" + statistic_value_get("cluster_in_propagated", value+"_value", "0") + "p\n" + content = content + "#Out OK: " + statistic_value_get("cluster_out_direct_success", value+"_value", "0") + "d/" + statistic_value_get("cluster_out_propagated_success", value+"_value", "0") + "p\n" + content = content + "#Out Failed: " + statistic_value_get("cluster_out_direct_failed", value+"_value", "0") + "d/" + statistic_value_get("cluster_out_propagated_failed", value+"_value", "0") + "p\n\n" + if "statistic_full" in source_rights: + content = content + "#Received - Direct:\n" + statistic_get("cluster_received_direct") + "\n\n" + content = content + "#Received - Propagated:\n" + statistic_get("cluster_received_propagated") + "\n\n" + content = content + "#Send - Direct - Success:\n" + statistic_get("cluster_send_direct_success") + "\n\n" + content = content + "#Send - Propagated - Success:\n" + statistic_get("cluster_send_propagated_success") + "\n\n" + content = content + "#Send - Direct - Failed:\n" + statistic_get("cluster_send_direct_failed") + "\n\n" + content = content + "#Send - Propagated - Failed:\n" + statistic_get("cluster_send_propagated_failed") + "\n\n" + content = content + "#In - Direct:\n" + statistic_get("cluster_in_direct") + "\n\n" + content = content + "#In - Propagated:\n" + statistic_get("cluster_in_propagated") + "\n\n" + content = content + "#Out - Direct - Success:\n" + statistic_get("cluster_out_direct_success") + "\n\n" + content = content + "#Out - Propagated - Success:\n" + statistic_get("cluster_out_propagated_success") + "\n\n" + content = content + "#Out - Direct - Failed:\n" + statistic_get("cluster_out_direct_failed") + "\n\n" + content = content + "#Out - Propagated - Failed:\n" + statistic_get("cluster_out_propagated_failed") + "\n\n" + + if CONFIG["statistic"].getboolean("enabled") and CONFIG["statistic"].getboolean("local") and "statistic_local" in source_rights and ("statistic_min" in source_rights or "statistic_full" in source_rights): + content = content + replace(config_get(CONFIG, "interface_menu", "statistic_header_local", "", lng_key), source_hash, source_name, source_right, lng_key).replace(delimiter+"value"+delimiter, value) + if "statistic_min" in source_rights: + statistic_recalculate("local_received_direct") + statistic_recalculate("local_received_propagated") + statistic_recalculate("local_send_direct_success") + statistic_recalculate("local_send_propagated_success") + statistic_recalculate("local_send_direct_failed") + statistic_recalculate("local_send_propagated_failed") + content = content + "#Received: " + statistic_value_get("local_received_direct", value+"_value", "0") + "d/" + statistic_value_get("local_received_propagated", value+"_value", "0") + "p\n" + content = content + "#Send OK: " + statistic_value_get("local_send_direct_success", value+"_value", "0") + "d/" + statistic_value_get("local_send_propagated_success", value+"_value", "0") + "p\n" + content = content + "#Send Failed: " + statistic_value_get("local_send_direct_failed", value+"_value", "0") + "d/" + statistic_value_get("local_send_propagated_failed", value+"_value", "0") + "p\n\n" + if "statistic_full" in source_rights: + content = content + "#Received - Direct:\n" + statistic_get("local_received_direct") + "\n\n" + content = content + "#Received - Propagated:\n" + statistic_get("local_received_propagated") + "\n\n" + content = content + "#Send - Direct - Success:\n" + statistic_get("local_send_direct_success") + "\n\n" + content = content + "#Send - Propagated - Success:\n" + statistic_get("local_send_propagated_success") + "\n\n" + content = content + "#Send - Direct - Failed:\n" + statistic_get("local_send_direct_failed") + "\n\n" + content = content + "#Send - Propagated - Failed:\n" + statistic_get("local_send_propagated_failed") + "\n\n" + + if CONFIG["statistic"].getboolean("enabled") and CONFIG["statistic"].getboolean("interface") and "statistic_interface" in source_rights and ("statistic_min" in source_rights or "statistic_full" in source_rights): + content = content + replace(config_get(CONFIG, "interface_menu", "statistic_header_interface", "", lng_key), source_hash, source_name, source_right, lng_key).replace(delimiter+"value"+delimiter, value) + if "statistic_min" in source_rights: + statistic_recalculate("interface_received_direct") + statistic_recalculate("interface_received_propagated") + statistic_recalculate("interface_send_direct_success") + statistic_recalculate("interface_send_propagated_success") + statistic_recalculate("interface_send_direct_failed") + statistic_recalculate("interface_send_propagated_failed") + content = content + "#Received: " + statistic_value_get("local_received_direct", value+"_value", "0") + "d/" + statistic_value_get("local_received_propagated", value+"_value", "0") + "p\n" + content = content + "#Send OK: " + statistic_value_get("interface_send_direct_success", value+"_value", "0") + "d/" + statistic_value_get("interface_send_propagated_success", value+"_value", "0") + "p\n" + content = content + "#Send Failed: " + statistic_value_get("interface_send_direct_failed", value+"_value", "0") + "d/" + statistic_value_get("interface_send_propagated_failed", value+"_value", "0") + "p\n\n" + if "statistic_full" in source_rights: + content = content + "#Received - Direct:\n" + statistic_get("interface_received_direct") + "\n\n" + content = content + "#Received - Propagated:\n" + statistic_get("interface_received_propagated") + "\n\n" + content = content + "#Send - Direct - Success:\n" + statistic_get("interface_send_direct_success") + "\n\n" + content = content + "#Send - Propagated - Success:\n" + statistic_get("interface_send_propagated_success") + "\n\n" + content = content + "#Send - Direct - Failed:\n" + statistic_get("interface_send_direct_failed") + "\n\n" + content = content + "#Send - Propagated - Failed:\n" + statistic_get("interface_send_propagated_failed") + "\n\n" + + if CONFIG["statistic"].getboolean("enabled") and CONFIG["statistic"].getboolean("user") and "statistic_self" in source_rights and ("statistic_min" in source_rights or "statistic_full" in source_rights): + content = content + replace(config_get(CONFIG, "interface_menu", "statistic_header_self", "", lng_key), source_hash, source_name, source_right, lng_key).replace(delimiter+"value"+delimiter, value) + if "statistic_min" in source_rights or "statistic_full" in source_rights: + content = content + statistic_get(source_hash) + "\n\n" + + if CONFIG["statistic"].getboolean("enabled") and CONFIG["statistic"].getboolean("user") and "statistic_user" in source_rights and ("statistic_min" in source_rights or "statistic_full" in source_rights): + content = content + replace(config_get(CONFIG, "interface_menu", "statistic_header_user", "", lng_key), source_hash, source_name, source_right, lng_key).replace(delimiter+"value"+delimiter, value) + for section in STATISTIC.sections(): + if section != "main" and not section.startswith("cluster") and not section.startswith("local") and not section.startswith("interface"): + if "statistic_min" in source_rights: + statistic_recalculate(section) + content = "<" + section + ">: " + statistic_value_get(section, value+"_value") + "\n" + if "statistic_full" in source_rights: + content = "<" + section + ">:\n" + statistic_get(section) + "\n\n" + else: + content = config_get(CONFIG, "interface_menu", "statistic_found_error", "", lng_key) + + + # "/status" command. + elif cmd == "status" and "status" in source_rights: + content = config_get(CONFIG, "interface_menu", "status_"+source_right, "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + content = content.replace(delimiter+"enabled_local"+delimiter, DATA["main"]["enabled_local"]) + content = content.replace(delimiter+"enabled_cluster"+delimiter, DATA["main"]["enabled_cluster"]) + + + # "/delivery" command. + #elif cmd == "delivery" and "delivery" in source_rights: + #todo + + + # "/enable_local" command. + elif cmd == "enable_local" and "enable_local" in source_rights: + if DATA["main"].getboolean("enabled_local"): + content = config_get(CONFIG, "interface_menu", "enable_local_true", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "enable_local_false", "", lng_key) + + elif cmd.startswith("enable_local ") and "enable_local" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + if val_to_bool(value): + DATA["main"]["enabled_local"] = "True" + content = config_get(CONFIG, "interface_menu", "enable_local_true", "", lng_key) + DATA["main"]["unsaved_local"] = "True" + else: + DATA["main"]["enabled_local"] = "False" + content = config_get(CONFIG, "interface_menu", "enable_local_false", "", lng_key) + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "enable_local_error", "", lng_key) + + + # "/enable_cluster" command. + elif cmd == "enable_cluster" and "enable_cluster" in source_rights: + if DATA["main"].getboolean("enabled_cluster"): + content = config_get(CONFIG, "interface_menu", "enable_cluster_true", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "enable_cluster_false", "", lng_key) + + elif cmd.startswith("enable_cluster ") and "enable_cluster" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + if val_to_bool(value): + DATA["main"]["enabled_cluster"] = "True" + content = config_get(CONFIG, "interface_menu", "enable_cluster_true", "", lng_key) + DATA["main"]["unsaved_cluster"] = "True" + else: + DATA["main"]["enabled_cluster"] = "False" + content = config_get(CONFIG, "interface_menu", "enable_cluster_false", "", lng_key) + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "enable_cluster_error", "", lng_key) + + + # "/auto_add_user" command. + elif cmd == "auto_add_user" and "auto_add_user" in source_rights: + if DATA["main"].getboolean("auto_add_user"): + content = config_get(CONFIG, "interface_menu", "auto_add_user_true", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "auto_add_user_false", "", lng_key) + + elif cmd.startswith("auto_add_user ") and "auto_add_user" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + if val_to_bool(value): + DATA["main"]["auto_add_user"] = "True" + content = config_get(CONFIG, "interface_menu", "auto_add_user_true", "", lng_key) + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["auto_add_user"] = "False" + content = config_get(CONFIG, "interface_menu", "auto_add_user_false", "", lng_key) + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "auto_add_user_error", "", lng_key) + + + # "/auto_add_user_type" command. + elif cmd == "auto_add_user_type" and "auto_add_user_type" in source_rights: + content = config_get(DATA, "main", "auto_add_user_type", "", lng_key) + + elif cmd.startswith("auto_add_user_type ") and "auto_add_user_type" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + DATA["main"]["auto_add_user_type"] = value + content = config_get(CONFIG, "interface_menu", "auto_add_user_type", "", lng_key) + " " + value + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "auto_add_user_type_error", "", lng_key) + + + # "/auto_add_cluster" command. + elif cmd == "auto_add_cluster" and "auto_add_cluster" in source_rights: + if DATA["main"].getboolean("auto_add_cluster"): + content = config_get(CONFIG, "interface_menu", "auto_add_cluster_true", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "auto_add_cluster_false", "", lng_key) + + elif cmd.startswith("auto_add_cluster ") and "auto_add_cluster" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + if val_to_bool(value): + DATA["main"]["auto_add_cluster"] = "True" + content = config_get(CONFIG, "interface_menu", "auto_add_cluster_true", "", lng_key) + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["auto_add_cluster"] = "False" + content = config_get(CONFIG, "interface_menu", "auto_add_cluster_false", "", lng_key) + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "auto_add_cluster_error", "", lng_key) + + + # "/invite_user" command. + elif cmd == "invite_user" and "invite_user" in source_rights: + if DATA["main"].getboolean("invite_user"): + content = config_get(CONFIG, "interface_menu", "invite_user_true", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "invite_user_false", "", lng_key) + + elif cmd.startswith("invite_user ") and "invite_user" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + if val_to_bool(value): + DATA["main"]["invite_user"] = "True" + content = config_get(CONFIG, "interface_menu", "invite_user_true", "", lng_key) + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["invite_user"] = "False" + content = config_get(CONFIG, "interface_menu", "invite_user_false", "", lng_key) + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "invite_user_error", "", lng_key) + + + # "/invite_user_type" command. + elif cmd == "invite_user_type" and "invite_user_type" in source_rights: + content = config_get(DATA, "main", "invite_user_type", "", lng_key) + + elif cmd.startswith("invite_user_type ") and "invite_user_type" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + DATA["main"]["invite_user_type"] = value + content = config_get(CONFIG, "interface_menu", "invite_user_type", "", lng_key) + " " + value + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "invite_user_type_error", "", lng_key) + + + # "/allow_user" command. + elif cmd == "allow_user" and "allow_user" in source_rights: + if DATA["main"].getboolean("allow_user"): + content = config_get(CONFIG, "interface_menu", "allow_user_true", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "allow_user_false", "", lng_key) + + elif cmd.startswith("allow_user ") and "allow_user" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + if val_to_bool(value): + DATA["main"]["allow_user"] = "True" + content = config_get(CONFIG, "interface_menu", "allow_user_true", "", lng_key) + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["allow_user"] = "False" + content = config_get(CONFIG, "interface_menu", "allow_user_false", "", lng_key) + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "allow_user_error", "", lng_key) + + + # "/allow_user_type" command. + elif cmd == "allow_user_type" and "allow_user_type" in source_rights: + content = config_get(DATA, "main", "allow_user_type", "", lng_key) + + elif cmd.startswith("allow_user_type ") and "allow_user_type" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + DATA["main"]["allow_user_type"] = value + content = config_get(CONFIG, "interface_menu", "allow_user_type", "", lng_key) + " " + value + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "allow_user_type_error", "", lng_key) + + + # "/deny_user" command. + elif cmd == "deny_user" and "deny_user" in source_rights: + if DATA["main"].getboolean("deny_user"): + content = config_get(CONFIG, "interface_menu", "deny_user_true", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "deny_user_false", "", lng_key) + + elif cmd.startswith("deny_user ") and "deny_user" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + if val_to_bool(value): + DATA["main"]["deny_user"] = "True" + content = config_get(CONFIG, "interface_menu", "deny_user_true", "", lng_key) + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["deny_user"] = "False" + content = config_get(CONFIG, "interface_menu", "deny_user_false", "", lng_key) + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "deny_user_error", "", lng_key) + + + # "/deny_user_type" command. + elif cmd == "deny_user_type" and "deny_user_type" in source_rights: + content = config_get(DATA, "main", "deny_user_type", "", lng_key) + + elif cmd.startswith("deny_user_type ") and "deny_user_type" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + DATA["main"]["deny_user_type"] = value + content = config_get(CONFIG, "interface_menu", "deny_user_type", "", lng_key) + " " + value + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "deny_user_type_error", "", lng_key) + + + # "/description" command. + elif cmd == "description" and "description" in source_rights: + content = config_get(DATA, "main", "description", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + + elif cmd.startswith("description ") and "description_set" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + DATA["main"]["description"+lng_key] = value + + content_group = config_get(CONFIG, "interface_messages", "description", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + if content_group != "": + for section in sections: + if "receive_description" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "description", "", lng_key) + " " + value + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "description_error", "", lng_key) + + + # "/rules" command. + elif cmd == "rules" and "rules" in source_rights: + content = config_get(DATA, "main", "rules", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + + elif cmd.startswith("rules ") and "rules_set" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + DATA["main"]["rules"+lng_key] = value + + content_group = config_get(CONFIG, "interface_messages", "rules", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + if content_group != "": + for section in sections: + if "receive_rules" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "rules", "", lng_key) + " " + value + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "rules_error", "", lng_key) + + + # "/readme" command. + elif cmd == "readme" and "readme" in source_rights: + content = config_get(CONFIG, "interface_menu", "readme", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + + + # "/time" command. + elif cmd == "time" and "time" in source_rights: + content = config_get(CONFIG, "interface_menu", "time", "", lng_key) + content = time.strftime(content, time.localtime(time.time())) + content = replace(content, source_hash, source_name, source_right, lng_key) + + + # "/announce" command. + elif cmd == "announce" and "announce" in source_rights: + content = config_get(CONFIG, "interface_menu", "announce", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + LXMF_CONNECTION.announce_now() + if CONFIG["cluster"].getboolean("enabled"): + RNS_CONNECTION.announce_now() + + + # "/sync" command. + elif cmd == "sync" and "sync" in source_rights: + content = config_get(CONFIG, "interface_menu", "sync", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + LXMF_CONNECTION.sync_now() + + + # "/show run" command. + elif (cmd == "show run" or cmd == "sh run") and "show_run" in source_rights: + content = config_get(CONFIG, "interface_menu", "show_run_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + for (key, val) in DATA.items("main"): + content = content + key + " = " + val + "\n" + + + # "/show" command. + elif (cmd.startswith("show") or cmd.startswith("list") or cmd.startswith("sh")) and "show" in source_rights: + try: + cmd, key = cmd.split(" ", 1) + if DATA.has_section(key) and key != "main": + content = config_get(CONFIG, "interface_menu", "show_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + content = content + "[" + key + "]\n" + for (section_key, section_val) in DATA.items(key): + content = content + section_key + " = " + section_val + "\n" + else: + content = config_get(CONFIG, "interface_menu", "user_type_error", "", lng_key) + " " + key + except: + content = config_get(CONFIG, "interface_menu", "show_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + for section in DATA.sections(): + if section in sections or section.replace("block_", "") in sections: + content = content + "[" + section + "]\n" + for (key, val) in DATA.items(section): + content = content + key + " = " + val + "\n" + content = content + "\n" + + + # "/user" command. + elif cmd.startswith("add ") and "add" in source_rights: + try: + cmd, key, value, name = cmd.split(" ", 3) + if DATA.has_section(key) and key != "main": + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + for section in DATA.sections(): + if section != "main": + for (key, val) in DATA.items(section): + if key == value: + DATA.remove_option(section, key) + DATA[key][value] = name + content = config_get(CONFIG, "interface_menu", "user_add", "", lng_key) + " " + value + " -> " + key + DATA["main"]["unsaved"] = "True" + else: + content = config_get(CONFIG, "interface_menu", "user_format_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "user_type_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/user" command. + elif (cmd.startswith("del ") or cmd.startswith("rm ") or cmd.startswith("delete ")) and "del" in source_rights: + try: + cmd, key, value = cmd.split(" ", 2) + if DATA.has_section(key) and key != "main": + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + if DATA.has_option(key, value): + DATA.remove_option(key, value) + content = config_get(CONFIG, "interface_menu", "user_del", "", lng_key) + " " + value + " -> " + key + DATA["main"]["unsaved"] = "True" + + if CONFIG["statistic"].getboolean("enabled"): + statistic("del", value) + + else: + content = config_get(CONFIG, "interface_menu", "user_error", "", lng_key) + " " + value + " -> " + key + else: + content = config_get(CONFIG, "interface_menu", "user_format_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "user_type_error", "", lng_key) + except: + try: + cmd, value = cmd.split(" ", 1) + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + for section in DATA.sections(): + if section != "main": + for (key, val) in DATA.items(section): + if key == value: + DATA.remove_option(section, key) + if CONFIG["statistic"].getboolean("enabled"): + statistic("del", value) + content = "OK: Removed user '" + value + "' from all types" + DATA["main"]["unsaved"] = "True" + else: + content = config_get(CONFIG, "interface_menu", "user_format_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/user" command. + elif (cmd.startswith("move ") or cmd.startswith("mv ")) and "move" in source_rights: + try: + cmd, key, value = cmd.split(" ", 2) + if DATA.has_section(key) and key != "main": + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + for section in DATA.sections(): + if section != "main": + for (key_old, val_old) in DATA.items(section): + if key_old == value: + DATA.remove_option(section, key_old) + DATA[key][value] = val_old + content = config_get(CONFIG, "interface_menu", "user_move", "", lng_key) + " " + value + " -> " + key + DATA["main"]["unsaved"] = "True" + if content == "": + content = config_get(CONFIG, "interface_menu", "user_format_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "user_format_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "user_type_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/invite" command. + elif cmd.startswith("invite ") and "invite" in source_rights: + if DATA["main"].getboolean("invite_user"): + try: + cmd, value = cmd.split(" ", 1) + key = DATA["main"]["invite_user_type"] + if DATA.has_section(key) and key != "main": + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + DATA[key][value] = "" + + content_user = config_get(CONFIG, "interface_messages", "invite_"+key, "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(value, content_user, "", None, None, "interface_send") + + content_group = config_get(CONFIG, "interface_messages", "member_invite", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group.replace(delimiter+"user_address"+delimiter, value) + if content_group != "": + for section in sections: + if "receive_invite" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "invite_ok", "", lng_key) + " <" + value + ">" + + 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" + else: + content = config_get(CONFIG, "interface_menu", "invite_format_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "invite_type_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/kick" command. + elif cmd.startswith("kick ") and "kick" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + executed = False + for section in sections: + for (key, val) in DATA.items(section): + if key == value: + user_section = section + user_name = val + executed = True + DATA.remove_option(section, key) + if executed: + if CONFIG["statistic"].getboolean("enabled"): + statistic("del", value) + + content_user = config_get(CONFIG, "interface_messages", "kick_"+user_section, "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(value, content_user, "", None, None, "interface_send") + + content_group = config_get(CONFIG, "interface_messages", "member_kick", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group.replace(delimiter+"user_address"+delimiter, value) + content_group = content_group.replace(delimiter+"user_name"+delimiter, user_name) + if content_group != "": + for section in sections: + if "receive_kick" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "kick_ok", "", lng_key) + content = content.replace(delimiter+"user_address"+delimiter, value) + content = content.replace(delimiter+"user_name"+delimiter, user_name) + + 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" + else: + content = config_get(CONFIG, "interface_menu", "kick_found_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "kick_format_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/block" command. + elif (cmd.startswith("block ") or cmd.startswith("ban ")) and "block" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + executed = False + for section in sections: + for (key, val) in DATA.items(section): + if key == value: + user_section = section + user_name = val + executed = True + if not DATA.has_section("block_"+section): + DATA.add_section("block_"+section) + DATA["block_"+section][key] = val + DATA.remove_option(section, key) + if executed: + content_user = config_get(CONFIG, "interface_messages", "block_"+user_section, "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(value, content_user, "", None, None, "interface_send") + + content_group = config_get(CONFIG, "interface_messages", "member_block", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group.replace(delimiter+"user_address"+delimiter, value) + content_group = content_group.replace(delimiter+"user_name"+delimiter, user_name) + if content_group != "": + for section in sections: + if "receive_block" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "block_ok", "", lng_key) + content = content.replace(delimiter+"user_address"+delimiter, value) + content = content.replace(delimiter+"user_name"+delimiter, user_name) + + 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" + else: + content = config_get(CONFIG, "interface_menu", "block_found_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "block_format_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/unblock" command. + elif (cmd.startswith("unblock ") or cmd.startswith("unban ")) and "unblock" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + executed = False + for section in DATA.sections(): + if section.startswith("block"): + for (key, val) in DATA.items(section): + if key == value: + user_section = section.replace("block_", "") + user_name = val + executed = True + if not DATA.has_section(user_section): + DATA.add_section(user_section) + DATA[user_section][key] = val + DATA.remove_option(section, key) + if executed: + content_user = config_get(CONFIG, "interface_messages", "unblock_"+user_section, "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(value, content_user, "", None, None, "interface_send") + + content_group = config_get(CONFIG, "interface_messages", "member_unblock", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group.replace(delimiter+"user_address"+delimiter, value) + content_group = content_group.replace(delimiter+"user_name"+delimiter, user_name) + if content_group != "": + for section in sections: + if "receive_block" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "unblock_ok", "", lng_key) + content = content.replace(delimiter+"user_address"+delimiter, value) + content = content.replace(delimiter+"user_name"+delimiter, user_name) + + 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" + else: + content = config_get(CONFIG, "interface_menu", "unblock_found_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "unblock_format_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/allow" command. + elif cmd.startswith("allow ") and "allow" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + user_section = DATA["main"]["allow_user_type"] + if DATA.has_section(user_section) and user_section != "main": + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + executed = False + section = "wait" + if DATA.has_section(section): + for (key, val) in DATA.items(section): + if key == value: + user_name = val + executed = True + DATA[user_section][key] = val + DATA.remove_option(section, key) + if executed: + content_user = config_get(CONFIG, "interface_messages", "allow_"+user_section, "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(value, content_user, "", None, None, "interface_send") + + content_group = config_get(CONFIG, "interface_messages", "member_allow", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group.replace(delimiter+"user_address"+delimiter, value) + content_group = content_group.replace(delimiter+"user_name"+delimiter, user_name) + if content_group != "": + for section in sections: + if "receive_block" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "allow_ok", "", lng_key) + content = content.replace(delimiter+"user_address"+delimiter, value) + content = content.replace(delimiter+"user_name"+delimiter, user_name) + + 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" + else: + content = config_get(CONFIG, "interface_menu", "allow_found_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "allow_format_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "allow_type_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/deny" command. + elif cmd.startswith("deny ") and "deny" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + user_section = DATA["main"]["deny_user_type"] + if DATA.has_section(user_section) and user_section != "main": + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + executed = False + for section in sections: + for (key, val) in DATA.items(section): + if key == value: + user_name = val + executed = True + DATA[user_section][key] = val + DATA.remove_option(section, key) + if executed: + content_user = config_get(CONFIG, "interface_messages", "deny_"+user_section, "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(value, content_user, "", None, None, "interface_send") + + content_group = config_get(CONFIG, "interface_messages", "member_deny", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group.replace(delimiter+"user_address"+delimiter, value) + content_group = content_group.replace(delimiter+"user_name"+delimiter, user_name) + if content_group != "": + for section in sections: + if "receive_block" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "deny_ok", "", lng_key) + content = content.replace(delimiter+"user_address"+delimiter, value) + content = content.replace(delimiter+"user_name"+delimiter, user_name) + + 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" + else: + content = config_get(CONFIG, "interface_menu", "deny_found_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "deny_format_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "deny_type_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/load" command. + elif (cmd == "load" or cmd == "read") and "load" in source_rights: + if data_read(PATH + "/data.cfg"): + content = config_get(CONFIG, "interface_menu", "load_ok", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "load_error", "", lng_key) + + + # "/save" command. + elif (cmd == "save" or cmd == "wr") and "save" in source_rights: + DATA.remove_option("main", "unsaved") + if data_save(PATH + "/data.cfg"): + content = config_get(CONFIG, "interface_menu", "save_ok", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "save_error", "", lng_key) + DATA["main"]["unsaved"] = "True" + + if CONFIG["statistic"].getboolean("enabled"): + statistic_save(PATH + "/statistic.cfg") + + + # "/reload" command. + elif cmd == "reload" and "reload" in source_rights: + content = config_get(CONFIG, "interface_menu", "reload_error", "", lng_key) + DATA.remove_option("main", "unsaved") + if data_save(PATH + "/data.cfg"): + if data_read(PATH + "/data.cfg"): + content = config_get(CONFIG, "interface_menu", "reload_ok", "", lng_key) + else: + DATA["main"]["unsaved"] = "True" + + + # "/reset" command. + elif cmd.startswith("reset statistic ") and "reset" in source_rights: + try: + cmd, key, value = cmd.split(" ", 2) + + if value == "all": + for section in STATISTIC.sections(): + statistic_reset(section) + if CONFIG["main"].getboolean("auto_save_statistic"): + statistic_save(PATH + "/statistic.cfg") + else: + if not STATISTIC.has_section("main"): + STATISTIC.add_section("main") + STATISTIC["main"]["unsaved"] = "True" + content = config_get(CONFIG, "interface_menu", "reset_statistic_ok", "", lng_key) + + elif value == "cluster": + for section in STATISTIC.sections(): + if section.startswith("cluster"): + statistic_reset(section) + if CONFIG["main"].getboolean("auto_save_statistic"): + statistic_save(PATH + "/statistic.cfg") + else: + if not STATISTIC.has_section("main"): + STATISTIC.add_section("main") + STATISTIC["main"]["unsaved"] = "True" + content = config_get(CONFIG, "interface_menu", "reset_statistic_ok", "", lng_key) + + elif value == "local": + for section in STATISTIC.sections(): + if section.startswith("local"): + statistic_reset(section) + if CONFIG["main"].getboolean("auto_save_statistic"): + statistic_save(PATH + "/statistic.cfg") + else: + if not STATISTIC.has_section("main"): + STATISTIC.add_section("main") + STATISTIC["main"]["unsaved"] = "True" + content = config_get(CONFIG, "interface_menu", "reset_statistic_ok", "", lng_key) + + elif value == "interface": + for section in STATISTIC.sections(): + if section.startswith("interface"): + statistic_reset(section) + if CONFIG["main"].getboolean("auto_save_statistic"): + statistic_save(PATH + "/statistic.cfg") + else: + if not STATISTIC.has_section("main"): + STATISTIC.add_section("main") + STATISTIC["main"]["unsaved"] = "True" + content = config_get(CONFIG, "interface_menu", "reset_statistic_ok", "", lng_key) + + elif value == "user": + for section in STATISTIC.sections(): + if not section.startswith("cluster") and not section.startswith("local") and not section.startswith("interface"): + statistic_reset(section) + if CONFIG["main"].getboolean("auto_save_statistic"): + statistic_save(PATH + "/statistic.cfg") + else: + if not STATISTIC.has_section("main"): + STATISTIC.add_section("main") + STATISTIC["main"]["unsaved"] = "True" + content = config_get(CONFIG, "interface_menu", "reset_statistic_ok", "", lng_key) + + else: + content = config_get(CONFIG, "interface_menu", "reset_statistic_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + else: + # "/admins" command. + # "/moderators" command. + # "/users" command. + # "/guests" command. + executed = False + for section in sections: + if (cmd == section or cmd == section+"s") and section+"s" in source_rights: + count = 0 + content = config_get(CONFIG, "interface_menu", section+"s_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + content_member = config_get(CONFIG, "interface_menu", section+"s_member", "", lng_key) + for (key, val) in DATA.items(section): + count += 1 + content = content + replace(content_member, key, val, section, lng_key) + content = content.replace(delimiter+"count"+delimiter, str(count)) + executed = True + break + + # cmd_unknown + if not executed: + content = config_get(CONFIG, "interface_menu", "cmd_unknown", "", lng_key) + + + # unsaved + if DATA["main"].getboolean("unsaved") and "unsaved" in source_rights: + if CONFIG["main"].getboolean("auto_save_data"): + DATA.remove_option("main", "unsaved") + if data_save(PATH + "/data.cfg"): + content = content + "\n" + config_get(CONFIG, "interface_menu", "save_ok", "", lng_key) + else: + content = content + "\n" + config_get(CONFIG, "interface_menu", "save_error", "", lng_key) + DATA["main"]["unsaved"] = "True" + else: + content = content + "\n" + config_get(CONFIG, "interface_menu", "save_info", "", lng_key) + + + return content + + + + +#### Replace ##### +def replace(text, source_hash, source_name, source_right, lng_key): + delimiter = CONFIG["interface"]["delimiter_output"] + + text = text.replace(delimiter+"source_address"+delimiter, source_hash) + text = text.replace(delimiter+"source_name"+delimiter, source_name) + text = text.replace(delimiter+"source_right"+delimiter, source_right) + + text = text.replace(delimiter+"name"+delimiter, config_get(CONFIG, "main", "name", "", lng_key)) + text = text.replace(delimiter+"display_name"+delimiter, config_get(CONFIG, "lxmf", "display_name", "", lng_key)) + text = text.replace(delimiter+"description"+delimiter, config_get(DATA, "main", "description", "", lng_key)) + text = text.replace(delimiter+"rules"+delimiter, config_get(DATA, "main", "rules", "", lng_key)) + text = text.replace(delimiter+"destination_address"+delimiter, LXMF_CONNECTION.destination_hash_str()) + text = text.replace(delimiter+"propagation_node"+delimiter, config_get(CONFIG, "lxmf", "propagation_node", "", lng_key)) + text = text.replace(delimiter+"cluster_name"+delimiter, config_get(CONFIG, "cluster", "display_name", "", lng_key).rsplit('/', 1)[-1]) + + text = text.replace(delimiter+"n"+delimiter, "\n") + + if delimiter+"count_members"+delimiter in text: + count = 0 + for (section) in CONFIG.items("rights"): + if DATA.has_section(section): + for (key, val) in DATA.items(section): + count += 1 + text = text.replace(delimiter+"count_members"+delimiter, str(count)) + + if delimiter+"count_pin"+delimiter in text: + count = 0 + if DATA.has_section("pin"): + for (key, val) in DATA.items("pin"): + count += 1 + text = text.replace(delimiter+"count_pin"+delimiter, str(count)) + + 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 - 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: + return False + else: + DATA = configparser.ConfigParser(allow_no_value=True, inline_comment_prefixes="#") + DATA.sections() + if os.path.isfile(file): + try: + DATA.read(file) + except Exception as e: + return False + else: + if not data_default(file=file): + return False + return True + + + + +#### Data - Save ##### +def data_save(file=None): + global DATA + + if file is None: + return False + else: + if os.path.isfile(file): + try: + with open(file,"w") as file: + DATA.write(file) + except Exception as e: + return False + else: + return False + return True + + + + +#### Data - Save ##### +def data_save_periodic(initial=False): + data_timer = threading.Timer(CONFIG.getint("main", "periodic_save_data_interval")*60, data_save_periodic) + data_timer.daemon = True + data_timer.start() + + if initial: + return + + global DATA + if DATA.has_section("main"): + if DATA["main"].getboolean("unsaved"): + DATA.remove_option("main", "unsaved") + if not data_save(PATH + "/data.cfg"): + DATA["main"]["unsaved"] = "True" + + + + +#### Data - Default ##### +def data_default(file=None): + global DATA + + if file is None: + return False + elif DEFAULT_DATA != "": + if not os.path.isdir(os.path.dirname(file)): + try: + os.makedirs(os.path.dirname(file)) + except Exception: + return False + try: + data_file = open(file, "w") + data_file.write(DEFAULT_DATA) + data_file.close() + if not data_read(file=file): + return False + except: + return False + else: + return False + return True + + +############################################################################################################## +# Statistic/Counter + + +#### Statistic ##### +def statistic(cmd="add", section="global", key="", value=1): + global STATISTIC + + changed = False + + if cmd == "add": + statistic_add(section, value) + changed = True + elif cmd == "del": + statistic_del(section) + changed = True + elif cmd == "reset": + statistic_reset(section) + changed = True + elif cmd == "get": + return statistic_get(section) + elif cmd == "value_set": + statistic_value_set(section, key, value) + changed = True + elif cmd == "value_get": + return statistic_value_get(section, key) + elif cmd == "read": + return statistic_read(PATH + "/statistic.cfg") + elif cmd == "save": + return statistic_save(PATH + "/statistic.cfg") + + if changed: + if CONFIG["main"].getboolean("auto_save_statistic"): + statistic_save(PATH + "/statistic.cfg") + else: + if not STATISTIC.has_section("main"): + STATISTIC.add_section("main") + STATISTIC["main"]["unsaved"] = "True" + + + + +#### Statistic - Add ##### +def statistic_add(section="global", value=1): + global STATISTIC + + if not STATISTIC.has_section(section): + statistic_default(section) + + statistic_recalculate(section) + + date = datetime.date.today() + day = date.timetuple().tm_yday + month = date.timetuple().tm_mon + year = date.timetuple().tm_year + week = date.isocalendar().week + + #day + if STATISTIC[section]["day_index"] == str(day): + STATISTIC[section]["day_value"] = str(STATISTIC.getint(section, "day_value")+value) + + #week + if STATISTIC[section]["week_index"] == str(week): + STATISTIC[section]["week_value"] = str(STATISTIC.getint(section, "week_value")+value) + + #month + if STATISTIC[section]["month_index"] == str(month): + STATISTIC[section]["month_value"] = str(STATISTIC.getint(section, "month_value")+value) + + #year + if STATISTIC[section]["year_index"] == str(year): + STATISTIC[section]["year_value"] = str(STATISTIC.getint(section, "year_value")+value) + + #all + STATISTIC[section]["all_value"] = str(STATISTIC.getint(section, "all_value")+value) + + #max + if STATISTIC.getint(section, "day_value") > STATISTIC.getint(section, "max_value"): + STATISTIC[section]["max_value"] = STATISTIC[section]["day_value"] + STATISTIC[section]["max_index"] = time.strftime("%Y-%m-%d", time.localtime(time.time())) + return + + + + +#### Statistic - Recalculate ##### +def statistic_recalculate(section="global"): + global STATISTIC + + if not STATISTIC.has_section(section): + return + + date = datetime.date.today() + day = date.timetuple().tm_yday + month = date.timetuple().tm_mon + year = date.timetuple().tm_year + week = date.isocalendar().week + + #day + if STATISTIC[section]["day_index"] != str(day): + if STATISTIC[section]["day_index"] == str(day-1): + STATISTIC[section]["last_day_value"] = STATISTIC[section]["day_value"] + STATISTIC[section]["last_day_index"] = str(day-1) + else: + STATISTIC[section]["last_day_value"] = "0" + STATISTIC[section]["last_day_index"] = str(day-1) + STATISTIC[section]["day_value"] = "0" + STATISTIC[section]["day_index"] = str(day) + + #week + if STATISTIC[section]["week_index"] != str(week): + if STATISTIC[section]["week_index"] == str(week-1): + STATISTIC[section]["last_week_value"] = STATISTIC[section]["week_value"] + STATISTIC[section]["last_week_index"] = str(week-1) + else: + STATISTIC[section]["last_week_value"] = "0" + STATISTIC[section]["last_week_index"] = str(week-1) + STATISTIC[section]["week_value"] = "0" + STATISTIC[section]["week_index"] = str(week) + + #month + if STATISTIC[section]["month_index"] != str(month): + if STATISTIC[section]["month_index"] == str(month-1): + STATISTIC[section]["last_month_value"] = STATISTIC[section]["month_value"] + STATISTIC[section]["last_month_index"] = str(month-1) + else: + STATISTIC[section]["last_month_value"] = "0" + STATISTIC[section]["last_month_index"] = str(month-1) + STATISTIC[section]["month_value"] = "0" + STATISTIC[section]["month_index"] = str(month) + + #year + if STATISTIC[section]["year_index"] != str(year): + if STATISTIC[section]["year_index"] == str(year-1): + STATISTIC[section]["last_year_value"] = STATISTIC[section]["year_value"] + STATISTIC[section]["last_year_index"] = str(year-1) + else: + STATISTIC[section]["last_year_value"] = "0" + STATISTIC[section]["last_year_index"] = str(year-1) + STATISTIC[section]["year_value"] = "0" + STATISTIC[section]["year_index"] = str(year) + + return + + + + +#### Statistic - Del ##### +def statistic_del(section="global"): + global STATISTIC + + if STATISTIC.has_section(section): + STATISTIC.remove_section(section) + + + + +#### Statistic - Reset ##### +def statistic_reset(section="global"): + statistic_del(section) + statistic_add(section, 0) + + + + +#### Statistic - Get ##### +def statistic_get(section="global"): + global STATISTIC + + text = "" + if STATISTIC.has_section(section): + statistic_recalculate(section) + for (key, val) in STATISTIC.items(section): + if key.endswith("_value"): + text = text + key.capitalize() + ": " + val + "\n" + text = text.replace("_value", "") + text = text.replace("_", " ") + text = text.strip() + + return text + + + + +#### Statistic - Value set ##### +def statistic_value_set(section, key, value): + global STATISTIC + + if not STATISTIC.has_section(section): + statistic_default(section) + + STATISTIC[section][key] = value + + + + +#### Statistic - Value get ##### +def statistic_value_get(section, key, default=""): + global STATISTIC + + if STATISTIC.has_section(section): + if STATISTIC.has_option(section, key): + return STATISTIC[section][key] + return default + + + + +#### Statistic - Read ##### +def statistic_read(file=None): + global STATISTIC + + if file is None: + return False + else: + STATISTIC = configparser.ConfigParser(allow_no_value=True, inline_comment_prefixes="#") + STATISTIC.sections() + if os.path.isfile(file): + try: + STATISTIC.read(file) + except Exception as e: + return False + return True + + + + +#### Statistic - Save ##### +def statistic_save(file=None): + global STATISTIC + + if file is None: + return False + else: + try: + with open(file,"w") as file: + if STATISTIC.has_section("main"): + STATISTIC.remove_section("main") + STATISTIC.write(file) + except Exception as e: + return False + return True + + + + +#### Statistic - Save ##### +def statistic_save_periodic(initial=False): + statistic_timer = threading.Timer(CONFIG.getint("main", "periodic_save_statistic_interval")*60, statistic_save_periodic) + statistic_timer.daemon = True + statistic_timer.start() + + if initial: + return + + global STATISTIC + if STATISTIC.has_section("main"): + if STATISTIC["main"].getboolean("unsaved"): + STATISTIC.remove_section("main") + if not statistic_save(PATH + "/statistic.cfg"): + if not STATISTIC.has_section("main"): + STATISTIC.add_section("main") + STATISTIC["main"]["unsaved"] = "True" + + + + +#### Statistic - Default ##### +def statistic_default(section="global"): + global STATISTIC + + date = datetime.date.today() + day = date.timetuple().tm_yday + month = date.timetuple().tm_mon + year = date.timetuple().tm_year + week = date.isocalendar().week + + STATISTIC.add_section(section) + STATISTIC[section]["day_value"] = "0" + STATISTIC[section]["day_index"] = str(day) + STATISTIC[section]["last_day_value"] = "0" + STATISTIC[section]["last_day_index"] = str(day-1) + STATISTIC[section]["week_value"] = "0" + STATISTIC[section]["week_index"] = str(week) + STATISTIC[section]["last_week_value"] = "0" + STATISTIC[section]["last_week_index"] = str(week-1) + STATISTIC[section]["month_value"] = "0" + STATISTIC[section]["month_index"] = str(month) + STATISTIC[section]["last_month_value"] = "0" + STATISTIC[section]["last_month_index"] = str(month-1) + STATISTIC[section]["year_value"] = "0" + STATISTIC[section]["year_index"] = str(year) + STATISTIC[section]["last_year_value"] = "0" + STATISTIC[section]["last_year_index"] = str(year-1) + STATISTIC[section]["all_value"] = "0" + STATISTIC[section]["max_value"] = "0" + STATISTIC[section]["max_index"] = time.strftime("%Y-%m-%d", time.localtime(time.time())) + + +############################################################################################################## +# Value convert + + +def val_to_bool(val): + 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 True + else: + return False + + +############################################################################################################## +# 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 RNS_MAIN_CONNECTION + global LXMF_CONNECTION + global RNS_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.cfg"): + print("Data - Error reading data file " + PATH + "/data.cfg") + 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() + + if CONFIG["statistic"].getboolean("enabled"): + statistic_read(PATH + "/statistic.cfg") + + RNS_MAIN_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(" Data File: " + PATH + "/data.cfg", LOG_INFO) + log(" Version: " + VERSION, LOG_INFO) + log(" Copyright: " + COPYRIGHT, LOG_INFO) + log("...............................................................................", LOG_INFO) + + 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 path is None: + path = PATH + + LXMF_CONNECTION = lxmf_connection( + storage_path=path, + identity_file="identity", + identity=None, + destination_name=CONFIG["lxmf"]["destination_name"], + destination_type=CONFIG["lxmf"]["destination_type"], + display_name=CONFIG["lxmf"]["display_name"], + send_delay=CONFIG["lxmf"]["send_delay"], + desired_method=CONFIG["lxmf"]["desired_method"], + propagation_node=config_propagation_node, + 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) + + if CONFIG["statistic"].getboolean("enabled"): + LXMF_CONNECTION.register_message_notification_success_callback(lxmf_message_notification_success_callback) + LXMF_CONNECTION.register_message_notification_failed_callback(lxmf_message_notification_failed_callback) + + log("LXMF - Connected", LOG_DEBUG) + + log("...............................................................................", LOG_FORCE) + log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE) + log("...............................................................................", LOG_FORCE) + + if CONFIG["cluster"].getboolean("enabled") or CONFIG["high_availability"].getboolean("enabled"): + announce_data = defaultdict(dict) + + if CONFIG["high_availability"].getboolean("enabled"): + announce_data["ha"] = "1" + announce_data["ha_r"] = CONFIG["high_availability"]["role"] + else: + announce_data["ha"] = "0" + + if CONFIG["cluster"].getboolean("enabled"): + announce_data["c"] = "1" + announce_data["c_h"] = LXMF_CONNECTION.destination_hash_str() + announce_data["c_d"] = CONFIG["cluster"]["display_name"].replace(" ", "") + else: + announce_data["c"] = "0" + + + log("RNS - Connecting ...", LOG_DEBUG) + RNS_CONNECTION = rns_connection( + storage_path=path, + identity_file="identity", + identity=LXMF_CONNECTION.identity, + destination_name=CONFIG["cluster"]["name"], + destination_type=CONFIG["cluster"]["type"], + announce_startup=CONFIG["rns"].getboolean("announce_startup"), + announce_startup_delay=CONFIG["rns"]["announce_startup_delay"], + announce_periodic=CONFIG["rns"].getboolean("announce_periodic"), + announce_periodic_interval=CONFIG["rns"]["announce_periodic_interval"], + announce_data = json.dumps(announce_data) + ) + RNS_CONNECTION.register_announce_callback(rns_announce_callback) + log("RNS - Connected", LOG_DEBUG) + + if CONFIG["main"].getboolean("periodic_save_data"): + data_save_periodic(True) + + if CONFIG["main"].getboolean("periodic_save_statistic"): + statistic_save_periodic(True) + + 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") + parser.add_argument("--exampledata", 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() + + if params.exampledata: + print("Data File: " + PATH + "/data.cfg") + print("Content:") + print(DEFAULT_DATA) + 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. + + +#### Main program settings #### +[main] + +# Default language. +lng = en # en/de + + +#### LXMF connection settings #### +[lxmf] + +# The name will be visible to other peers +# on the network, and included in announces. +# It is also used in the group description/info. +display_name = Distribution Group + +# Propagation node address/hash. +propagation_node = ca2762fe5283873719aececfb9e18835 + +# 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 + + +#### Cluster settings #### +[cluster] + +# Enable/Disable this functionality. +enabled = True + +# To use several completely separate clusters/groups, +# an individual name and type can be assigned here. +name = grp +type = cluster + +# Slash-separated list with the names of this cluster. +# No spaces are allowed in the name. +# All send messages that match the name will be received. +# The last name is the main name of this group +# and is used as source for send messages. +display_name = County/Region/City + + +#### High availability settings #### +[high_availability] + +# Enable/Disable this functionality. +enabled = False + +# Role of this node (master/slave) +role = master + +# Peer address +peer = + + +#### Statistic/Counter settings #### +[statistic] + +# Enable/Disable this functionality. +enabled = True +''' + + +#### 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] + +# Enable/Disable this functionality. +enabled = True + +# Name of the program. Only for display in the log or program startup. +name = Distribution Group + +# Default language. +# The following languages are available. Other languages can be defined below in the "interface" settings. +# You have to add the language key to the settings to be used. For example "-de". +# en/de +lng = en + +# Auto save changes. +# If there are changes in the data or statistics, 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 = True +auto_save_statistic = False + +# Periodic actions - Save changes periodically. +periodic_save_data = True +periodic_save_data_interval = 30 #Minutes +periodic_save_statistic = True +periodic_save_statistic_interval = 30 #Minutes + +# Auto apply name from announces. +# As an alternative to defining the nickname manually, it can be used automatically from the announce. +# This works only after the user has joined the group and then an announce is received. +auto_name_def = True +auto_name_change = False + + + + +#### 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. +# It is also used in the group description/info. +display_name = Distribution Group + +# Default send method. +desired_method = direct #direct/propagated + +# Propagation node address/hash. +propagation_node = ca2762fe5283873719aececfb9e18835 + +# 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 = Yes +announce_startup_delay = 0 #Seconds + +# The peer is announced periodically +# to let other peers reach it. +announce_periodic = Yes +announce_periodic_interval = 120 #Minutes + +# 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 = 8 + +# Allow only messages with valid signature. +signature_validated = No +signature_validated_new = No +signature_validated_known = No + + + + +#### RNS connection settings #### +[rns] + +# Destination name & type need to fits the RNS protocoll +# to be compatibel with other RNS programs. +destination_name = grp +destination_type = cluster + +# The peer is announced at startup +# to let other peers reach it immediately. +announce_startup = Yes +announce_startup_delay = 0 #Seconds + +# The peer is announced periodically +# to let other peers reach it. +announce_periodic = Yes +announce_periodic_interval = 120 #Minutes + + + + +#### Cluster settings #### +[cluster] + +# Enable/Disable this functionality. +enabled = True + +# To use several completely separate clusters/groups, +# an individual name and type can be assigned here. +name = grp +type = cluster + +# Slash-separated list with the names of this cluster. +# No spaces are allowed in the name. +# All send messages that match the name will be received. +# The last name is the main name of this group +# and is used as source for send messages. +display_name = County/Region/City + +# Define the delimiters for cluster input. +delimiter_input = @ + + + + +#### High availability settings #### +[high_availability] + +# Enable/Disable this functionality. +enabled = False + +# Role of this node (master/slave) +role = master + +# Peer address +peer = + +# Sync +sync_periodic_interval = 30 #Minutes + +# Sync at startup +sync_startup = No +sync_startup_delay = 0 #Seconds + +# Heartbeat +heartbeat_interval = 1 #Minutes +heartbeat_timeout = 15 #Minutes + + + + +#### Message settings #### +[message] + +## Each message received (message and command) ## + +# Text is added. +receive_prefix = +receive_suffix = + +# Text is replaced. +receive_search = +receive_replace = + +# Text is replaced by regular expression. +receive_regex_search = +receive_regex_replace = + +# Length limitation. +receive_length_min = 0 #0=any length +receive_length_max = 0 #0=any length + + +## Each message send (message) ## + +# Text is added. +send_prefix = !source_name!!n!!n! +send_suffix = + +# Text is replaced. +send_search = +send_replace = + +# Text is replaced by regular expression. +send_regex_search = +send_regex_replace = + +# Length limitation. +send_length_min = 0 #0=any length +send_length_max = 0 #0=any length + + +## Each cluster message received (message and command) ## + +# Text is added. +cluster_receive_prefix = @!cluster_source!-> +cluster_receive_suffix = + +# Text is replaced. +cluster_receive_search = +cluster_receive_replace = + +# Text is replaced by regular expression. +cluster_receive_regex_search = +cluster_receive_regex_replace = + +# Length limitation. +cluster_receive_length_min = 0 #0=any length +cluster_receive_length_max = 0 #0=any length + + +## Each cluster message send (message) ## + +# Text is added. +cluster_send_prefix = @!cluster_destination!!n!!source_name!!n!!n! +cluster_send_suffix = + +# Text is replaced. +cluster_send_search = +cluster_send_replace = + +# Text is replaced by regular expression. +cluster_send_regex_search = +cluster_send_regex_replace = + +# Length limitation. +cluster_send_length_min = 0 #0=any length +cluster_send_length_max = 0 #0=any length + + +## Each pinned message ## + +pin_id = %%y%%m%%d-%%H%%M%%S + + +# Define which message timestamp should be used. +timestamp = client #client/server + + + + +#### Statistic/Counter settings #### +[statistic] + +# Enable/Disable this functionality. +enabled = True + +# Create cluster statistics. +cluster = True + +# Create local/group statistics. +local = True + +# Create interface statistics. +interface = True + +# Create user statistics. +user = True + + + + +#### User rights assignment #### + +# Define the individual rights for the different user types. +# 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,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_local,statistic_interface,statistic_self,statistic_user,status,delivery,enable_local,enable_cluster,auto_add_user,auto_add_user_type,auto_add_cluster,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,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,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_local,statistic_self,delivery,show,add,del,move,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,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_local,statistic_self,delivery,invite +guest = interface,receive_local,receive_cluster,receive_cluster_loop,leave +wait = + + +# The following rights can be assigned: +# interface = General function of the command interface. +# receive_local = Receive local (own group) messages. +# receive_cluster = Receive cluster (foreign group) messages. +# receive_cluster_pin_add = Receive cluster (foreign group) pinned messages. +# receive_cluster_send = Receive a copy of the message sent to another cluster. +# receive_cluster_loop = Receive message which is sent to a higher hierarchy in the cluster and includes the own cluster. +# receive_cluster_join = Receive an info message when a new cluster joins. +# receive_join = Receive an info message when a new user joins. +# receive_leave = Receive an info message when a user leaves. +# receive_invite = Receive an info message when a user is invited. +# receive_kick = Receive an info message when a user is kicked. +# receive_block = Receive an info message when a user is blocked. +# receive_unblock = Receive an info message when a user is unblocked. +# receive_allow = Receive an info message when a user has been allowed. +# receive_deny = Receive an info message when a user has been denied. +# receive_description = Receive an info message when the group description is changed. +# receive_pin_add = Receive an info message when a message is pinned. +# receive_pin_remove = Receive an info message when a pinned message is removed. +# receive_rules = Receive an info message when the group rules are changed. +# receive_name_def = Receive an info message when a user assigns a name. +# receive_name_change = Receive an info message when a user changes his name. +# receive_auto_name_def = Receive an info message when a user assigns a name. +# receive_auto_name_change = Receive an info message when a user changes his name. +# reply_signature = Receive an error message if the signature is invalid. +# reply_cluster_enabled = Receive an error message when the cluster is disabled. +# reply_cluster_right = Receive an error message when you do not have permission to send in the cluster. +# reply_interface_enabled = Receive an error message when the interface is disabled. +# reply_interface_right = Receive an error message if you do not have permission to use the interface. +# reply_local_enabled = Receive an error message when sending a local message is disabled. +# reply_local_right = Receive an error message when you do not have permission to send locally. +# reply_block = Receive an error message when you are blocked. +# reply_length_min = Receive an error message if the message length is too short. +# reply_length_max = Receive an error message if the message length is too long. +# send_local = Allows you to send loacally in your own group. +# send_cluster = Allows sending to another cluster/group. +# help = Use of the "/help" command allowed. +# leave = Use of the "/leave" command allowed. +# name = Use of the "/name" command allowed. +# address = Use of the "/address" command allowed. +# info = Use of the "/info" command allowed. +# pin = Use of the "/pin" command allowed. +# pin_add = Use of the "/pin" command allowed. +# pin_remove = Use of the "/pin" command allowed. +# cluster_pin_add = Use of the "/pin" command allowed. +# version = Use of the "/version" command allowed. +# groups = Use of the "/groups" command allowed. +# members = Use of the "/members" command allowed. +# admins = Use of the "/admins" command allowed. +# moderators = Use of the "/moderators" command allowed. +# users = Use of the "/users" command allowed. +# guests = Use of the "/guests" command allowed. +# search = Use of the "/search" command allowed. +# activitys = Use of the "/activitys" command allowed. +# statistic = Use of the "/statistic" command allowed. +# statistic_min = Minimal statistics output. +# statistic_full = Full/Maximal statistics output. +# statistic_cluster = Displays the cluster statistics on the statistics text. +# statistic_local = Displays the local statistics on the statistics text. +# statistic_interface = Displays the interface statistics on the statistics text. +# statistic_self = Displays the own statistics on the statistics text. +# statistic_user = Displays the user statistics on the statistics text. +# status = Use of the "/status" command allowed. +# delivery = Use of the "/delivery" command allowed. +# enable_local = Use of the "/enable_local" command allowed. +# enable_cluster = Use of the "/enable_cluster" command allowed. +# auto_add_user = Use of the "/auto_add_user" command allowed. +# auto_add_user_type = Use of the "/auto_add_user_type" command allowed. +# auto_add_cluster = Use of the "/auto_add_cluster" command allowed. +# invite_user = Use of the "/invite_user" command allowed. +# invite_user_type = Use of the "/invite_user_type" command allowed. +# allow_user = Use of the "/allow_user" command allowed. +# allow_user_type = Use of the "/allow_user_type" command allowed. +# deny_user = Use of the "/deny_user" command allowed. +# deny_user_type = Use of the "/deny_user_type" command allowed. +# description = Use of the "/description" command allowed. +# description_set = Use of the "/description" command allowed. +# rules = Use of the "/rules" command allowed. +# rules_set = Use of the "/rules" command allowed. +# readme = Use of the "/readme" command allowed. +# time = Use of the "/time" command allowed. +# announce = Use of the "/announce" command allowed. +# sync = Use of the "/sync" command allowed. +# show_run = Use of the "/show_run" command allowed. +# show = Use of the "/show" command allowed. +# add = Use of the "/add" command allowed. +# del = Use of the "/del" command allowed. +# move = Use of the "/move" command allowed. +# invite = Use of the "/invite" command allowed. +# kick = Use of the "/kick" command allowed. +# block = Use of the "/block" command allowed. +# unblock = Use of the "/unblock" command allowed. +# allow = Use of the "/allow" command allowed. +# deny = Use of the "/deny" command allowed. +# load = Use of the "/load" command allowed. +# save = Use of the "/save" command allowed. +# reload = Use of the "/reload" command allowed. +# reset = Use of the "/reset" command allowed. +# unsaved = Displays the status of the data file when using any action/command. + + + + +#### Interface settings - General #### +[interface] + +# Enable/Disable the whole interface/commands. +enabled = True + +# Define the delimiters for command input/output. +delimiter_input = / +delimiter_output = ! + + + + +#### Interface settings - Messages #### + +# Define messages for user or automatic actions. +# These messages are sent automatically when a corresponding action is triggered. +# If a message is to be deactivated simply comment it out. +[interface_messages] + +# Auto user add. (Single message to the user.) +auto_error = ERROR: Joining the group is not possible. +auto_error-de = FEHLER: Beitritt zur Gruppe ist nicht möglich. + +auto_add_admin = Welcome to the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Pinned messages: !count_pin!!n!Use the command /pin to display them.!n!!n!Please assign a nickname with the command /name +auto_add_admin-de = Willkommen in der Gruppe "!display_name!"!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Angepinnte Nachrichten: !count_pin!!n!Verwenden Sie den Befehl /pin um sie anzuzeigen.!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name +auto_add_mod = Welcome to the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Pinned messages: !count_pin!!n!Use the command /pin to display them.!n!!n!Please assign a nickname with the command /name +auto_add_mod-de = Willkommen in der Gruppe "!display_name!"!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Angepinnte Nachrichten: !count_pin!!n!Verwenden Sie den Befehl /pin um sie anzuzeigen.!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name +auto_add_user = Welcome to the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Pinned messages: !count_pin!!n!Use the command /pin to display them.!n!!n!Please assign a nickname with the command /name +auto_add_user-de = Willkommen in der Gruppe "!display_name!"!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Angepinnte Nachrichten: !count_pin!!n!Verwenden Sie den Befehl /pin um sie anzuzeigen.!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name +auto_add_guest = Welcome to the group "!display_name!"!!n!!n!!description!!n!!n!You can only receive messages.!n!!n!Pinned messages: !count_pin!!n!Use the command /pin to display them.!n!!n!To leave the group use the following command: /leave +auto_add_guest-de = Willkommen in der Gruppe "!display_name!"!!n!!n!!description!!n!!n!Sie können nur Nachrichten empfangen.!n!!n!Angepinnte Nachrichten: !count_pin!!n!Verwenden Sie den Befehl /pin um sie anzuzeigen.!n!!n!Um die Gruppe zu verlassen verwenden Sie folgenden Befehl: /leave +auto_add_wait = Welcome to the group "!display_name!"!!n!!n!You still need to be allowed to join. You will be notified automatically. +auto_add_wait-de = Willkommen in der Gruppe "!display_name!"!!n!!n!Der Beitritt muss ihnen noch erlaubt werden. Sie werden darüber automatisch benachrichtigt. + +# Manual/Admin user add. (Single message to the user.) +add_admin = +add_admin-de = +add_mod = +add_mod-de = +add_user = +add_user-de = +add_guest = +add_guest-de = +add_wait = +add_wait-de = + +# Invite user. (Single message to the user.) +invite_admin = You have been invited by !source_name! to the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Please assign a nickname with the command /name +invite_admin-de = Sie wurden von !source_name! in die Gruppe "!display_name!" eingeladen!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name +invite_mod = You have been invited by !source_name! to the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Please assign a nickname with the command /name +invite_mod-de = Sie wurden von !source_name! in die Gruppe "!display_name!" eingeladen!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name +invite_user = You have been invited by !source_name! to the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Please assign a nickname with the command /name +invite_user-de = Sie wurden von !source_name! in die Gruppe "!display_name!" eingeladen!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name +invite_guest = You have been invited by !source_name! to the group "!display_name!"!!n!!n!!description!!n!!n!You can only receive messages.!n!!n!To leave the group use the following command: /leave +invite_guest-de = Sie wurden von !source_name! in die Gruppe "!display_name!" eingeladen!!n!!n!!description!!n!!n!Sie können nur Nachrichten empfangen.!n!!n!Um die Gruppe zu verlassen verwenden Sie folgenden Befehl: /leave +invite_wait = You have been invited by !source_name! to the group "!display_name!"!!n!!n!You still need to be allowed to join. You will be notified automatically. +invite_wait-de = Sie wurden von !source_name! in die Gruppe "!display_name!" eingeladen!!n!!n!Der Beitritt muss ihnen noch erlaubt werden. Sie werden darüber automatisch benachrichtigt. + +# Kick user. (Single message to the user.) +kick_admin = You have been kicked out of the group! +kick_admin-de = Sie wurden aus der Gruppe geworfen! +kick_mod = You have been kicked out of the group! +kick_mod-de = Sie wurden aus der Gruppe geworfen! +kick_user = You have been kicked out of the group! +kick_user-de = Sie wurden aus der Gruppe geworfen! +kick_guest = You have been kicked out of the group! +kick_guest-de = Sie wurden aus der Gruppe geworfen! +kick_wait = You have been kicked out of the group! +kick_wait-de = Sie wurden aus der Gruppe geworfen! + +# Block user. (Single message to the user.) +block_admin = +block_admin-de = +block_mod = +block_mod-de = +block_user = +block_user-de = +block_guest = +block_guest-de = +block_wait = +block_wait-de = + +# Unblock user. (Single message to the user.) +unblock_admin = +unblock_admin-de = +unblock_mod = +unblock_mod-de = +unblock_user = +unblock_user-de = +unblock_guest = +unblock_guest-de = +unblock_wait = +unblock_wait-de = + +# Allow user. (Single message to the user.) +allow_admin = You have been allowed to join the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Please assign a nickname with the command /name +allow_admin-de = Sie wurden erlaubt der Gruppe "!display_name!" beizutreten!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name +allow_mod = You have been allowed to join the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Please assign a nickname with the command /name +allow_mod-de = Sie wurden erlaubt der Gruppe "!display_name!" beizutreten!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name +allow_user = You have been allowed to join the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Please assign a nickname with the command /name +allow_user-de = Sie wurden erlaubt der Gruppe "!display_name!" beizutreten!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name +allow_guest = You have been allowed to join the group "!display_name!"!!n!!n!!description!!n!!n!You can only receive messages.!n!!n!To leave the group use the following command: /leave +allow_guest-de = Sie wurden erlaubt der Gruppe "!display_name!" beizutreten!!n!!n!!description!!n!!n!Sie können nur Nachrichten empfangen.!n!!n!Um die Gruppe zu verlassen verwenden Sie folgenden Befehl: /leave +allow_wait = +allow_wait-de = + +# Deny user. (Single message to the user.) +deny_admin = You have been denied to join the group "!display_name!"! +deny_admin-de = Ihnen wurde der Beitritt in die Gruppe "!display_name!" abgelehnt! +deny_mod = You have been denied to join the group "!display_name!"! +deny_mod-de = Ihnen wurde der Beitritt in die Gruppe "!display_name!" abgelehnt! +deny_user = You have been denied to join the group "!display_name!"! +deny_user-de = Ihnen wurde der Beitritt in die Gruppe "!display_name!" abgelehnt! +deny_guest = You have been denied to join the group "!display_name!"! +deny_guest-de = Ihnen wurde der Beitritt in die Gruppe "!display_name!" abgelehnt! +deny_wait = +deny_wait-de = + +# General user/member messages. (Group message to all group members.) +member_join = !source_name! joins the group. +member_join-de = !source_name! tritt der Gruppe bei. +member_leave = !source_name! leave the group. +member_leave-de = !source_name! verlässt die Gruppe. +member_invite = was invited to the group by !source_name! +member_invite-de = wurde in die Gruppe eingeladen von !source_name! +member_kick = !user_name! was kicked out of the group by !source_name! +member_kick-de = !user_name! wurde aus der Gruppe geworfen von !source_name! +member_block = !user_name! was blocked by !source_name! +member_block-de = !user_name! wurde geblockt von !source_name! +member_unblock = !user_name! was unblocked by !source_name! +member_unblock-de = !user_name! wurde entsperrt von !source_name! +member_allow = !user_name! was allowed by !source_name! +member_allow-de = !user_name! wurde erlaubt von !source_name! +member_deny = !user_name! was denied by !source_name! +member_deny-de = !user_name! wurde abgelehnt von !source_name! +member_name_def = defined the name: +member_name_def-de = hat den Namen definiert: +member_name_change = changed the name: +member_name_change-de = hat den Namen geändert: +description = !source_name! has changed the group description:!n!!n!!description! +description-de = !source_name! hat die Gruppenbeschreibung geändert:!n!!n!!description! +rules = !source_name! has changed the group rules:!n!!n!!rules! +rules-de = !source_name! hat die Gruppenregeln geändert:!n!!n!!rules! +cluster_join = New cluster/group connected: !source_name! +cluster_join-de = Neue Cluster/Gruppe verbunden: !source_name! +pin_add = New pinned message:!n!#!key!!n!!value! +pin_add-de = Neue angeheftete Nachricht:!n!#!key!!n!!value! +pin_remove = Removed pinned message:!n!#!key!!n!!value! +pin_remove-de = Angeheftete Nachricht entfernt:!n!#!key!!n!!value! +cluster_pin_add = New pinned message:!n!#!key!!n!!value! +cluster_pin_add-de = Neue angeheftete Nachricht:!n!#!key!!n!!value! + +# Reply messages. (Single message to the user.) +reply_signature = Info: Signature invalid! +reply_signature-de = Info: Signatur ungültig! +reply_cluster_enabled = Info: Cluster disabled! +reply_cluster_enabled-de = Info: Cluster deaktiviert! +reply_cluster_right = Info: No authorization for cluster messages! +reply_cluster_right-de = Keine Berechtigung für Clusternachrichten! +reply_interface_enabled = Info: Commands disabled! +reply_interface_enabled-de = Info: Befehle deaktiviert! +reply_interface_right = Info: No authorization for commands! +reply_interface_right-de = Info: Keine Berechtigung für Befehle! +reply_local_enabled = Info: Group deactivated! +reply_local_enabled-de = Info: Gruppe deaktiviert! +reply_local_right = Info: No authorization to send messages! +reply_local_right-de = Info: Keine Berechtigung zum senden von Nachrichten! +reply_block = Info: You are blocked! +reply_block-de = Info: Sie sind geblockt! +reply_length_min = Info: Minimum message length not reached! +reply_length_min-de = Info: Minimale Nachrichtenlänge unterschritten! +reply_length_max = Info: Maximum message length exceeded! +reply_length_max-de = Info: Maximale Nachrichtenlänge überschritten! + + + + +#### Interface settings - Menu/command #### + +# Define menu/command texts. +# These texts are used within the menu the user has to start. +[interface_menu] + +# "/help" command. +help_admin = Group:!n!!display_name!!n!!n!Description:!n!!description!!n!!n!Number of members: !count_members!!n!Nickname: !source_name!!n!User right/type: !source_right!!n!!n!!interface_help!!n!Commands:!n!!interface_help_command! +help_admin-de = Gruppe:!n!!display_name!!n!!n!Beschreibung:!n!!description!!n!!n!Anzahl Mitglieder: !count_members!!n!Nickname: !source_name!!n!Benutzer Recht/Typ: !source_right!!n!!n!!interface_help!!n!Befehle:!n!!interface_help_command! +help_mod = Group:!n!!display_name!!n!!n!Description:!n!!description!!n!!n!Number of members: !count_members!!n!Nickname: !source_name!!n!User right/type: !source_right!!n!!n!!interface_help!!n!Commands:!n!!interface_help_command! +help_mod-de = Gruppe:!n!!display_name!!n!!n!Beschreibung:!n!!description!!n!!n!Anzahl Mitglieder: !count_members!!n!Nickname: !source_name!!n!Benutzer Recht/Typ: !source_right!!n!!n!!interface_help!!n!Befehle:!n!!interface_help_command! +help_user = Group:!n!!display_name!!n!!n!Description:!n!!description!!n!!n!Number of members: !count_members!!n!Nickname: !source_name!!n!User right/type: !source_right!!n!!n!!interface_help!!n!Commands:!n!!interface_help_command! +help_user-de = Gruppe:!n!!display_name!!n!!n!Beschreibung:!n!!description!!n!!n!Anzahl Mitglieder: !count_members!!n!Nickname: !source_name!!n!Benutzer Recht/Typ: !source_right!!n!!n!!interface_help!!n!Befehle:!n!!interface_help_command! +help_guest = +help_guest-de = + +# "/leave" command. +leave_ok = OK: You leaved the group. +leave_ok-de = OK: Sie haben die Gruppe verlassen. +leave_error = ERROR: While leaving group. +leave_error-de = FEHLER: Beim Verlassen der Gruppe. + +# "/name" command. +name = Current nickname: !source_name! +name-de = Aktueller Nickname: !source_name! +name_ok = OK: Changed name: +name_ok-de = OK: Name geändert: +name_error = ERROR: Changing name: +name_error-de = FEHLER: Name ändern: + +# "/address" command. +address_admin = Group address:!n!!n!!n!Propagation node:!n! +address_admin-de = Gruppenadresse:!n!!n!!n!Propagation Node:!n! +address_mod = Group address:!n!!n!!n!Propagation node:!n! +address_mod-de = Gruppenadresse:!n!!n!!n!Propagation Node:!n! +address_user = Group address:!n!!n!!n!Propagation node:!n! +address_user-de = Gruppenadresse:!n!!n!!n!Propagation Node:!n! +address_guest = Group address:!n!!n!!n!Propagation node:!n! +address_guest-de = Gruppenadresse:!n!!n!!n!Propagation Node:!n! + +# "/info" command. +info_admin = Group:!n!!display_name!!n!!n!Description:!n!!description!!n!!n!Number of members: !count_members!!n!Pinned messages: !count_pin!!n!Use the command /pin to display them. +info_admin-de = Gruppe:!n!!display_name!!n!!n!Beschreibung:!n!!description!!n!!n!Anzahl Mitglieder: !count_members!!n!Angepinnte Nachrichten: !count_pin!!n!Verwenden Sie den Befehl /pin um sie anzuzeigen. +info_mod = Group:!n!!display_name!!n!!n!Description:!n!!description!!n!!n!Number of members: !count_members!!n!Pinned messages: !count_pin!!n!Use the command /pin to display them. +info_mod-de = Gruppe:!n!!display_name!!n!!n!Beschreibung:!n!!description!!n!!n!Anzahl Mitglieder: !count_members!!n!Angepinnte Nachrichten: !count_pin!!n!Verwenden Sie den Befehl /pin um sie anzuzeigen. +info_user = Group:!n!!display_name!!n!!n!Description:!n!!description!!n!!n!Number of members: !count_members!!n!Pinned messages: !count_pin!!n!Use the command /pin to display them. +info_user-de = Gruppe:!n!!display_name!!n!!n!Beschreibung:!n!!description!!n!!n!Anzahl Mitglieder: !count_members!!n!Angepinnte Nachrichten: !count_pin!!n!Verwenden Sie den Befehl /pin um sie anzuzeigen. +info_guest = Group:!n!!display_name!!n!!n!Description:!n!!description!!n!!n!Number of members: !count_members!!n!Pinned messages: !count_pin!!n!Use the command /pin to display them. +info_guest-de = Gruppe:!n!!display_name!!n!!n!Beschreibung:!n!!description!!n!!n!Anzahl Mitglieder: !count_members!!n!Angepinnte Nachrichten: !count_pin!!n!Verwenden Sie den Befehl /pin um sie anzuzeigen. + +# "/pin" command. +pin_header = Pinned messages (!count!):!n!!n! +pin_header-de = Angeheftete Nachrichten (!count!):!n!!n! +pin = !source_name!!n!!n!!value! +pin-de = !source_name!!n!!n!!value! +pin_add_ok = OK: Message pinned +pin_add_ok-de = OK: Nachricht angeheftet +pin_remove_ok = OK: Message removed +pin_remove_ok-de = OK: Nachricht entfernt +pin_found_error = ERROR: Message ID not found +pin_found_error-de = FEHLER: Nachrichten ID nicht gefunden +cluster_pin = !value! +cluster_pin-de = !value! + +# "/version" command. +version_header = Version info:!n!!n! +version_header-de = Versionsinformationen:!n!!n! + +# "/groups" command. +groups_header = Groups/Cluster (!count!):!n!!n! +groups_header-de = Gruppen/Cluster (!count!):!n!!n! +groups_member = !source_name!!n! +groups_member-de = !source_name!!n! +groups_search_header = Found groups/cluster (!count!):!n!!n! +groups_search_header-de = Gefundene Gruppen/Cluster (!count!):!n!!n! +groups_search_member = !source_name!!n! +groups_search_member-de = !source_name!!n! +groups_search_found_error = ERROR: Group/Cluster not found +groups_search_found_error-de = FEHLER: Gruppe/Cluster nicht gefunden + +# "/members" command. +members_header = Group members (!count!):!n!!n! +members_header-de = Gruppenmitglieder (!count!):!n!!n! +members_member = !source_name!!n!!n!!n! +members_member-de = !source_name!!n!!n!!n! + +# "/admins" command. +admins_header = Group admins (!count!):!n!!n! +admins_header-de = Gruppenadmins (!count!):!n!!n! +admins_member = !source_name!!n!!n!!n! +admins_member-de = !source_name!!n!!n!!n! + +# "/moderators" command. +moderators_header = Group moderators (!count!):!n!!n! +moderators_header-de = Gruppenmoderatoren (!count!):!n!!n! +moderators_member = !source_name!!n!!n!!n! +moderators_member-de = !source_name!!n!!n!!n! + +# "/users" command. +users_header = Group users (!count!):!n!!n! +users_header-de = Gruppenbenutzer (!count!):!n!!n! +users_member = !source_name!!n!!n!!n! +users_member-de = !source_name!!n!!n!!n! + +# "/guests" command. +guests_header = Group guests (!count!):!n!!n! +guests_header-de = Gruppengäste (!count!):!n!!n! +guests_member = !source_name!!n!!n!!n! +guests_member-de = !source_name!!n!!n!!n! + +# "/search" command. +search_header = Found members (!count!):!n!!n! +search_header-de = Gefundene Mitglieder (!count!):!n!!n! +search_member = !source_name!!n!!n!!activity_receive! / !activity_send!!n!!n! +search_member-de = !source_name!!n!!n!!activity_receive! / !activity_send!!n!!n! +search_found_error = ERROR: Nickname or address not found +search_found_error-de = FEHLER: Benutzername oder Adresse nicht gefunden + +# "/activitys" command. +activitys_header = User activitys (!count!):!n!(receive / send)!n!!n! +activitys_header-de = Benutzeraktivitäten (!count!):!n!(empf. / gesendet)!n!!n! +activitys_member = !source_name!!n!!n!!activity_receive! / !activity_send!!n!!n! +activitys_member-de = !source_name!!n!!n!!activity_receive! / !activity_send!!n!!n! + +# "/statistic" command. +statistic_header_cluster = -- Cluster statistics - !value! --!n! +statistic_header_cluster-de = -- Cluster-Statistik - !value! --!n! +statistic_header_local = -- Group statistics - !value! --!n! +statistic_header_local-de = -- Gruppen-Statistik - !value! --!n! +statistic_header_interface = -- Interface statistics - !value! --!n! +statistic_header_interface-de = -- Interface-Statistik - !value! --!n! +statistic_header_user = -- User statistics - !value! --!n! +statistic_header_user-de = -- Benutzer-Statistik - !value! --!n! +statistic_header_self = -- Own statistics --!n! +statistic_header_self-de = -- Eigene-Statistik --!n! +statistic_found_error = ERROR: Statistic type not found +statistic_found_error-de = FEHLER: Statistik typ nicht vorhanden + +# "/status" command. +status_admin = Status:!n!!n!Local message routing:!enabled_local!!n!Cluster message routing:!enabled_cluster!!n! +status_admin-de = Status:!n!!n!Lokales Nachrichten Routing:!enabled_local!!n!Cluster Nachrichten Routing:!enabled_cluster!!n! +status_mod = Status:!n!!n!Local message routing:!enabled_local!!n!Cluster message routing:!enabled_cluster!!n! +status_mod-de = Status:!n!!n!Lokales Nachrichten Routing:!enabled_local!!n!Cluster Nachrichten Routing:!enabled_cluster!!n! +status_user = Status:!n!!n!Local message routing:!enabled_local!!n!Cluster message routing:!enabled_cluster!!n! +status_user-de = Status:!n!!n!Lokales Nachrichten Routing:!enabled_local!!n!Cluster Nachrichten Routing:!enabled_cluster!!n! +status_guest = Status:!n!!n!Local message routing:!enabled_local!!n!Cluster message routing:!enabled_cluster!!n! +status_guest-de = Status:!n!!n!Lokales Nachrichten Routing:!enabled_local!!n!Cluster Nachrichten Routing:!enabled_cluster!!n! + +# "/delivery" command. +# todo + +# "/enable_local" command. +enable_local_true = OK: Local message routing enabled. +enable_local_true-de = OK: Lokales Nachrichten Routing aktiviert. +enable_local_false = OK: Local message routing disabled. +enable_local_false-de = OK: Lokales Nachrichten Routing deaktiviert. +enable_local_error = ERROR: Local message routing change. +enable_local_error-de = FEHLER: Änderung Lokales Nachrichten Routing. + +# "/enable_cluster" command. +enable_cluster_true = OK: Cluster message routing enabled. +enable_cluster_true-de = OK: Cluster Nachrichten Routing aktiviert. +enable_cluster_false = OK: Cluster message routing disabled. +enable_cluster_false-de = OK: Cluster Nachrichten Routing deaktiviert. +enable_cluster_error = ERROR: Cluster message routing change. +enable_cluster_error-de = FEHLER: Änderung Cluster Nachrichten Routing. + +# "/auto_add_user" command. +auto_add_user_true = OK: Auto add user enabled. +auto_add_user_true-de = OK: Benutzer automatisch hinzufügen aktiviert. +auto_add_user_false = OK: Auto add user disabled. +auto_add_user_false-de = OK: Benutzer automatisch hinzufügen deaktiviert. +auto_add_user_error = ERROR: Auto add user change. +auto_add_user_error-de = FEHLER: Änderung Benutzer automatisch hinzufügen. + +# "/auto_add_user_type" command. +auto_add_user_type = OK: User type changed to: +auto_add_user_type-de = OK: Benutzertyp geändert in: +auto_add_user_type_error = ERROR: User type change. +auto_add_user_type_error-de = FEHLER: Änderung des Benutzertyps. + +# "/auto_add_cluster" command. +auto_add_cluster_true = OK: Auto add cluster enabled. +auto_add_cluster_true-de = OK: Cluster/Gruppen automatisch hinzufügen aktiviert. +auto_add_cluster_false = OK: Auto add cluster disabled. +auto_add_cluster_false-de = OK: Cluster/Gruppen automatisch hinzufügen deaktiviert. +auto_add_cluster_error = ERROR: Auto add cluster change. +auto_add_cluster_error-de = FEHLER: Änderung Cluster/Gruppen automatisch hinzufügen. + +# "/invite_user" command. +invite_user_true = OK: Invite user enabled. +invite_user_true-de = OK: Benutzer einladen aktiviert. +invite_user_false = OK: Invite user disabled. +invite_user_false-de = OK: Benutzer einladen deaktiviert. +invite_user_error = ERROR: Invite user change. +invite_user_error-de = FEHLER: Änderung Benutzer einladen. + +# "/invite_user_type" command. +invite_user_type = OK: User type changed to: +invite_user_type-de = OK: Benutzertyp geändert in: +invite_user_type_error = ERROR: User type change. +invite_user_type_error-de = FEHLER: Änderung des Benutzertyps. + +# "/allow_user" command. +allow_user_true = OK: Allow user enabled. +allow_user_true-de = OK: Benutzer erlauben aktiviert. +allow_user_false = OK: Allow user disabled. +allow_user_false-de = OK: Benutzer erlauben deaktiviert. +allow_user_error = ERROR: Allow user change. +allow_user_error-de = FEHLER: Änderung Benutzer erlauben. + +# "/allow_user_type" command. +allow_user_type = OK: User type changed to: +allow_user_type-de = OK: Benutzertyp geändert in: +allow_user_type_error = ERROR: User type change. +allow_user_type_error-de = FEHLER: Änderung des Benutzertyps. + +# "/deny_user" command. +deny_user_true = OK: Deny user enabled. +deny_user_true-de = OK: Benutzer ablehnen aktiviert. +deny_user_false = OK: Deny user disabled. +deny_user_false-de = OK: Benutzer ablehnen deaktiviert. +deny_user_error = ERROR: Deny user change. +deny_user_error-de = FEHLER: Änderung Benutzer ablehnen. + +# "/deny_user_type" command. +deny_user_type = OK: User type changed to: +deny_user_type-de = OK: Benutzertyp geändert in: +deny_user_type_error = ERROR: User type change. +deny_user_type_error-de = FEHLER: Änderung des Benutzertyps. + +# "/description" command. +description = OK: Description changed to: +description-de = OK: Beschreibung geändert in: +description_error = ERROR: Description change. +description_error-de = FEHLER: Beschreibung ändern. + +# "/rules" command. +rules = OK: Rules changed to: +rules-de = OK: Regeln geändert in: +rules_error = ERROR: Rules change. +rules_error-de = FEHLER: Regeln ändern. + +# "/readme" command. +readme = +readme-de = + +# "/time" command. +time = Current server time: %%Y-%%m-%%d %%H:%%M:%%S +time-de = Aktuelle Server-Zeit: %%Y-%%m-%%d %%H:%%M:%%S + +# "/announce" command. +announce = Announce send. +announce-de = Announce gesendet. + +# "/sync" command. +sync = Synchronize messages with propagation node . +sync-de = Synchronisiere Nachrichten mit Propagation node . + +# "/show run" command. +show_run_header = Current settings/data:!n!!n! +show_run_header-de = Aktuelle Konfiguration/Daten:!n!!n! + +# "/show" command. +show_header = +show_header-de = + +# "/user" command. +user_add = OK: Added user -> group: +user_add-de = OK: Benutzer -> Gruppe hinzugefügt: +user_del = OK: Removed user -> group: +user_del-de = OK: Entfernter Benutzer -> Gruppe: +user_move = OK: Moved user -> group: +user_move-de = OK: Benutzer -> Gruppe verschoben: +user_error = ERROR: Unknown user -> group: +user_error-de = FEHLER: Unbekannter Benutzer -> Gruppe: +user_format_error = ERROR: Wrong user format +user_format_error-de = FEHLER: Falsches Benutzerformat +user_type_error = ERROR: Unknown user type +user_type_error-de = FEHLER: Unbekannter Benutzertyp + +# "/invite" command. +invite_ok = OK: Invited user: +invite_ok-de = OK: Benutzer eingeladen: +invite_error = ERROR: Inviting user: +invite_error-de = FEHLER: Benutzer einladen: +invite_format_error = ERROR: Wrong user format +invite_format_error-de = FEHLER: Falsches Benutzerformat +invite_type_error = ERROR: Unknown user type +invite_type_error-de = FEHLER: Unbekannter Benutzertyp + +# "/kick" command. +kick_ok = OK: User kicked out: !user_name! +kick_ok-de = OK: Benutzer rausgeworfen: !user_name! +kick_found_error = ERROR: User address not found +kick_found_error-de = FEHLER: Benutzer Adresse nicht gefunden +kick_format_error = ERROR: Wrong user format +kick_format_error-de = FEHLER: Falsches Benutzerformat + +# "/block" command. +block_ok = OK: User blocked: !user_name! +block_ok-de = OK: Benutzer blockiert: !user_name! +block_found_error = ERROR: User address not found +block_found_error-de = FEHLER: Benutzer Adresse nicht gefunden +block_format_error = ERROR: Wrong user format +block_format_error-de = FEHLER: Falsches Benutzerformat + +# "/unblock" command. +unblock_ok = OK: User unblocked: !user_name! +unblock_ok-de = OK: Blockerierung des Benutzers aufgehoben: !user_name! +unblock_found_error = ERROR: User address not found +unblock_found_error-de = FEHLER: Benutzer Adresse nicht gefunden +unblock_format_error = ERROR: Wrong user format +unblock_format_error-de = FEHLER: Falsches Benutzerformat + +# "/allow" command. +allow_ok = OK: User allowed: !user_name! +allow_ok-de = OK: Benutzer erlaubt: !user_name! +allow_found_error = ERROR: User address not found +allow_found_error-de = FEHLER: Benutzer Adresse nicht gefunden +allow_format_error = ERROR: Wrong user format +allow_format_error-de = FEHLER: Falsches Benutzerformat +allow_type_error = ERROR: Unknown user type +allow_type_error-de = FEHLER: Unbekannter Benutzertyp + +# "/deny" command. +deny_ok = OK: User denied: !user_name! +deny_ok-de = OK: Benutzer abgelehnt: !user_name! +deny_found_error = ERROR: User address not found +deny_found_error-de = FEHLER: Benutzer Adresse nicht gefunden +deny_format_error = ERROR: Wrong user format +deny_format_error-de = FEHLER: Falsches Benutzerformat +deny_type_error = ERROR: Unknown user type +deny_type_error-de = FEHLER: Unbekannter Benutzertyp + +# "/load" command. +load_ok = OK: Loading configuration/data. +load_ok-de = OK: Konfiguration/Daten werden geladen. +load_error = ERROR: Loading configuration/data. +load_error-de = FEHLER: Konfiguration/Daten werden geladen. + +# "/save" command. +save_ok = OK: Saved configuration/data. +save_ok-de = OK: Konfiguration/Daten gespeichert. +save_error = ERROR: Saving configuration/data. +save_error-de = FEHLER: Speichern der Konfiguration/Daten. +save_info = INFO: Unsaved changes! Please run the command '/save' to save these changes permanently! +save_info-de = INFO: Nicht gespeicherte Änderungen! Bitte führen Sie den Befehl '/save' aus, um diese Änderungen dauerhaft zu speichern! + +# "/reload" command. +reload_ok = OK: Reloaded configuration/data. +reload_ok-de = OK: Konfiguration/Daten neu geladen. +reload_error = ERROR: Reload configuration/data. +reload_error-de = FEHLER: Neu laden der Konfiguration/Daten. + +# "/reset" command. +reset_statistic_ok = OK: Reset statistic. +reset_statistic_ok-de = OK: Statistik zurückgesetzt. +reset_statistic_error = ERROR: Reset statistic. +reset_statistic_error-de = FEHLER: Statistik zurücksetzen. + +# Cluster messages. +cluster_found_error = ERROR: Cluster name not found +cluster_found_error-de = FEHLER: Clustername nicht gefunden +cluster_format_error = ERROR: Wrong cluster format +cluster_format_error-de = FEHLER: Falsches Clusterformat + +# General messages. +cmd_error = ERROR: Processing command. +cmd_error-de = FEHLER: Verarbeitung des Befehls. +cmd_unknown = ERROR: Unknown command. Type /? for help. +cmd_unknown-de = FEHLER: Unbekannter Befehl. Geben Sie /? für Hilfe ein. + + + + +#### Interface settings - Help #### + +# Define help texts. +# These texts are used within the help-menu. +# Only the commands defined in the user rights are displayed. +# If a message is to be deactivated simply comment it out. +[interface_help] +send_local = To send a message simply enter any text.!n!!n! +send_local-de = Um eine Nachricht zu senden, geben Sie einfach einen beliebigen Text ein.!n!!n! +send_cluster = To send a message to another group enter the destination group with the following command followed by the message: @destination message.!n!!n! +send_cluster-de = Um eine Nachricht an eine andere Gruppe zu senden, geben Sie die Zielgruppe mit dem folgenden Befehl gefolgt von der Nachricht ein: @Zielname Nachricht.!n!!n! +interface = If the sent message is displayed as delivered and no error message is received, everything has worked fine.!n!!n! +interface-de = Wenn die gesendete Nachricht als zugestellt angezeigt wird und keine Fehlermeldung eingeht, hat alles fehlerfrei funktioniert.!n!!n! + + +# Define help texts. +# These texts are used within the help-menu. +# Only the commands defined in the user rights are displayed. +# If a message is to be deactivated simply comment it out. +[interface_help_command] + +help = /help or /? = Shows this help!n! +help-de = /help oder /? = Zeigt diese Hilfe an!n! + +leave = /leave or /part = Leave group!n! +leave-de = /leave oder /part = Gruppe verlassen!n! + +name = /name = Show current nickname!n!/nick = Show current nickname!n!/name = Change/Define nickname!n!/nick = Change/Define nickname!n! +name-de = /name = Aktueller Nickname anzeigen!n!/nick = Aktueller Nickname anzeigen!n!/name = Ändern/Definieren des Nickname!n!/nick = Ändern/Definieren des Nickname!n! + +address = /address = Dislay address info!n! +address-de = /address = Adressinfos anzeigen!n! + +info = /info = Show group info!n! +info-de = /info = Gruppeninfos anzeigen!n! + +pin = /pin = Show pinned messages!n! +pin-de = /pin = Angeheftete Nachrichten anzeigen!n! + +pin_add = /pin = Pin a message!n! +pin_add-de = /pin = Nachricht anheften!n! + +pin_remove = /unpin <#id> = Removes a pinned message!n! +pin_remove-de = /unpin <#id> = Angeheftete Nachricht entfernen!n! + +version = /version = Show version info!n! +version-de = /version = Versionsinformationen anzeigen!n! + +groups = /groups or /cluster = Show all groups/clusters!n!/groups = Searches for a group/cluster by name!n! +groups-de = /groups oder /cluster = Alle Gruppen/Cluster anzeigen!n!/groups = Sucht nach einer Gruppe/Cluster nach Namen!n! + +members = /members or /names or /who = Show all group members!n! +members-de = /members oder /names oder /who = Alle Gruppenmitglieder anzeigen!n! + +admins = /admins = Show group admins!n! +admins-de = /admins = Gruppenadmins anzeigen!n! + +moderators = /moderators or /mods = Show group moderators!n! +moderators-de = /moderators oder /mods = Gruppenmoderatoren anzeigen!n! + +users = /users = Show group users!n! +users-de = /users = Gruppenbenutzer anzeigen!n! + +guests = /guests = Show group guests!n! +guests-de = /guests = Gruppengäste anzeigen!n! + +search = /search = Searches for a user by nickname or address!n!/whois = Searches for a user by nickname or address!n! +search-de = /search = Sucht einen Benutzer anhand des Nicknamens oder Adresse!n!/whois = Sucht einen Benutzer anhand des Nicknamens oder Adresse!n! + +activitys = /activitys = Show user activitys!n! +activitys-de = /activitys = Benutzeraktivitäten anzeigen!n! + +statistic = /statistic or /stat = Show group statistic!n!/statistic or /stat = Show group statistic!n! +statistic-de = /statistic oder /stat = Gruppenstatistik anzeigen!n!/statistic oder /stat = Gruppenstatistik anzeigen!n! + +status = /status = Show status!n! +status-de = /status = Status anzeigen!n! + +delivery = /delivery or /message = Show delivery status of last message!n! +delivery-de = /delivery oder /message = Lieferstatus der letzten Nachricht anzeigen!n! + +enable_local = /enable_local = Local message routing!n! +enable_local-de = /enable_local Lokales Nachrichten Routing!n! + +enable_cluster = /enable_cluster = Cluster message routing!n! +enable_cluster-de = /enable_cluster Cluster Nachrichten Routing!n! + +auto_add_user = /auto_add_user = Add unknown user functionality!n! +auto_add_user-de = /auto_add_user = Unbekannten Benutzer hinzufügen Funktionalität!n! + +auto_add_user_type = /auto_add_user_type !n! +auto_add_user_type-de = /auto_add_user_type !n! + +auto_add_cluster = /auto_add_cluster = Add unknown cluster functionality!n! +auto_add_cluster-de = /auto_add_cluster = Unbekannten Cluster/Gruppen hinzufügen Funktionalität!n! + +invite_user = /invite_user = Invite functionality!n! +invite_user-de = /invite_user = Einladung Funktionalität!n! + +invite_user_type = /invite_user_type !n! +invite_user_type-de = /invite_user_type !n! + +allow_user = /allow_user = Allow user functionality!n! +allow_user-de = /allow_user = Benutzer erlauben Funktionalität!n! + +allow_user_type = /allow_user_type !n! +allow_user_type-de = /allow_user_type !n! + +deny_user = /deny_user = Deny user functionality!n! +deny_user-de = /deny_user = Benutzer ablehnen Funktionalität!n! + +deny_user_type = /deny_user_type !n! +deny_user_type-de = /deny_user_type !n! + +description = /description = Show current description!n! +description-de = /description = Aktuelle Beschreibung anzeigen!n! + +description_set = /description = Change description!n! +description_set-de = /description !n! = Beschreibung ändern + +rules = /rules = Show current rules!n! +rules-de = /rules = Aktuelle Regeln anzeigen!n! + +rules_set = /rules = Change rules!n! +rules_set-de = /rules !n! = Regeln ändern + +readme = /readme = Show readme!n! +readme-de = /readme = Liesmich anzeigen!n! + +time = /time = Show date/time!n! +time-de = /time = Datum/Uhrzeit anzeigen!n! + +announce = /announce = Send announce!n! +announce-de = /announce = Announce senden!n! + +sync = /sync = Synchronize messages with propagation node!n! +sync-de = /sync = Nachrichten mit Propagation Node synchronisieren!n! + +show_run = /show run = Show current configuration!n! +show_run-de = /show run = Aktuelle Konfiguration anzeigen!n! + +show = /show or /list!n!/show or /list !n! +show-de = /show oder /list!n!/show oder /list !n! + +add = /add !n! +add-de = /add !n! + +del = /del or /rm !n!/del or /rm !n! +del-de = /del oder /rm !n!/del oder /rm !n! + +move = /move !n! +move-de = /move !n! + +invite = /invite = Invites user to group!n! +invite-de = /invite = Lädt Benutzer zur Gruppe ein!n! + +kick = /kick = Kicks user out of group!n! +kick-de = /kick = Wirft den Benutzer aus der Gruppe!n! + +block = /block = Block user!n!/ban = Block user!n! +block-de = /block = Blockiert Benutzer!n!/ban = Blockiert Benutzer!n! + +unblock = /unblock = Unblock user!n!/unban = Unblock user!n! +unblock-de = /unblock = Blockierung des Benutzers aufheben!n!/unban = Blockierung des Benutzers aufheben!n! + +allow = /allow = Allow user!n! +allow-de = /allow = Benutzer erlauben!n! + +deny = /deny = Deny user!n! +deny-de = /deny = Benutzer ablehnen!n! + +load = /load or /read = Read the configuration/data!n! +load-de = /load oder /read = Lesen der Konfiguration/Daten!n! + +save = /save or /wr = Saves the current configuration/data!n! +save-de = /save oder /wr = Speichert die aktuelle Konfiguration/Daten!n! + +reload = /reload = Reload the current configuration/data!n! +reload-de = /reload = Neu laden der aktuelle Konfiguration/Daten!n! + +reset = /reset statistic = Reset statistic!n! +reset-de = /reset statistic = Statisktik zurücksetzenn!n! +''' + + +#### Default data file #### +DEFAULT_DATA = '''# This is the data file. It is automatically created and saved/overwritten. +# It contains data managed by the software itself. +# If manual adjustments are made here, the program must be shut down first! + + +#### High availability settings #### +[high_availability] +role = master +last_heartbeat = 0000-00-00 00:00:00 + + +#### Main program settings #### +[main] +enabled_local = True +enabled_cluster = True +auto_add_user = True +auto_add_user_type = user +auto_add_cluster = True +invite_user = True +invite_user_type = user +allow_user = True +allow_user_type = user +deny_user = True +deny_user_type = block_wait +description = This group is for a first test of functionality.!n!!n!To receive offline messages please use the following propagation node: +description-de = Diese Gruppe dient einem ersten Test der Funktionalität.!n!!n!Um offline Nachrichten zu empfangen bitte folgender Propagation Node verwenden: +rules = Please follow the general rules of etiquette which should be taken for granted!!n!Prohibited are:!n!Spam, insults, violence, sex, illegal topics +rules-de = Bitte befolgen Sie die allgemeinen benimm-dich-Regeln welche als selbstverständlich gelten sollten!!n!Verboten sind:!n!Spam, Beleidigungen, Gewalt, Sex, illegale Themen + + +#### Admin user #### +[admin] + +#### Mod/Moderator user #### +[mod] + +#### User #### +[user] + +#### Guest user #### +[guest] + +#### Wait user #### +[wait] + +#### Blocked user #### +[block_admin] + +[block_mod] + +[block_user] + +[block_guest] + +[block_wait] + +#### Cluster (Automatically or manual created) #### +[cluster] + +[block_cluster] + +#### Pinned messages (Automatically created) #### +[pin] +''' + + +############################################################################################################## +# Init + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/lxmf_distribution_group_minimal/CHANGELOG.md b/lxmf_distribution_group_minimal/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/lxmf_distribution_group_minimal/README.md b/lxmf_distribution_group_minimal/README.md new file mode 100644 index 0000000..9929ebc --- /dev/null +++ b/lxmf_distribution_group_minimal/README.md @@ -0,0 +1,295 @@ +# lxmf_distribution_group_minimal +This program is a minimalist version of the normal distribution group. The functionality is reduced to a minimum. Only sender and receiver users can be defined. Messages are then sent to the other users accordingly. There is no user interface or other notifications. Only the messages are distributed 1:1. The administration is done completely by the respective configuration files which are to be edited accordingly. + +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 +- Compatible with all LXMF applications (NomadNet, Sideband, ...) +- Server/Node based message routing and processing +- Direct or propagated message delivery (receive/send) +- Easy distribution of incoming messages to recipients + + +## Examples of use + +### Local self-sufficient group +In a small group of people, this group software can be hosted on a centrally located node. This then allows users to communicate with each other via this group. + +### Multiple local self-sufficient group +On the same node/server several groups can be operated independently of each other. How this works is described below in the installation instructions. + +### General info how the messages are transported +All messages between client<->group-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 the group. + + +## Current Status +It should currently be considered beta software. All core features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. +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 + ``` +- Change the Reticulum configuration to suit your needs and use-case. + ```bash + nano /.reticulum/config + ``` +- Download the [file](lxmf_distribution_group_minimal.py) from this repository. + ```bash + wget https://raw.githubusercontent.com/SebastianObi/LXMF-Tools/main/lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py + ``` +- Make it executable with the following command + ```bash + chmod +x lxmf_distribution_group_minimal.py + ``` + +### Start: +- Start it + ```bash + ./lxmf_distribution_group_minimal.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_distribution_group_minimal/config.cfg.owr + ``` + ```bash + # 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. + + #### LXMF connection settings #### + [lxmf] + + # The name will be visible to other peers + # on the network, and included in announces. + # It is also used in the group description/info. + display_name = Distribution Group + + # Propagation node address/hash. + propagation_node = ca2762fe5283873719aececfb9e18835 + + # 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 + ``` +- Start it again. Finished! + ```bash + ./lxmf_distribution_group_minimal.py + ``` + + +### Run as a system service/deamon: +- Create a service file. + ```bash + nano /etc/systemd/system/lxmf_distribution_group_minimal.service + ``` +- Copy and edit the following content to your own needs. + ```bash + [Unit] + Description=lxmf_distribution_group_minimal.py Daemon + After=multi-user.target + [Service] + # ExecStartPre=/bin/sleep 10 + Type=simple + Restart=always + RestartSec=3 + User=root + ExecStart=/root/lxmf_distribution_group_minimal.py + [Install] + WantedBy=multi-user.target + ``` +- Enable the service. + ```bash + systemctl enable lxmf_distribution_group_minimal + ``` +- Start the service. + ```bash + systemctl start lxmf_distribution_group_minimal + ``` + + +### Start/Stop service: + ```bash + systemctl start lxmf_distribution_group_minimal + systemctl stop lxmf_distribution_group_minimal + ``` + + +### Enable/Disable service: + ```bash + systemctl enable lxmf_distribution_group_minimal + systemctl disable lxmf_distribution_group_minimal + ``` + + +### Run several instances (To copy the same application): +- Run the program with a different configuration path. + ```bash + ./lxmf_distribution_group_minimal.py -p /root/.lxmf_distribution_group_minimal_2nd + ./lxmf_distribution_group_minimal.py -p /root/.lxmf_distribution_group_minimal_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 group LXMF address is displayed: + ``` + [] ............................................................................... + [] LXMF - Address: <801f48d54bc71cb3e0886944832aaf8d> + [] ...............................................................................` + ``` +- This address is also annouced at startup in the default setting. +- The users need to be entered manually in the `data.cfg` file. +- Now the group can be used. + + +### Startup parameters: +```bash +usage: lxmf_distribution_group_minimal.py [-h] [-p PATH] [-pr PATH_RNS] [-pl PATH_LOG] [-l LOGLEVEL] [-s] [--exampleconfig] [--exampleconfigoverride] [--exampledata] + +LXMF Distribution Group - Server-Side group functions for LXMF based apps + +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 + --exampledata 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. + +- data.cfg + + This is the data file. + It contains the user data. + + +## 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`. + + +### Members: +All data here (`data.cfg`) contains the end users. The users must be maintained manually. There is no automatic joining to the group. +Please do not forget to restart the program after a modification! + +- Receive only and send only members `data.cfg` + ``` + [send] + 04652a820cc69d47940ce39050c455a6 = Test user with send only right 1 + + [receive] + d1b551e1b89fff5a4a6f2aaff2464971 = Test user with receive only right 1 + 801f48d54bc71cb3e0886944832aaf8d = Test user with receive only right 2 + + [receive_send] + ``` + +- Receive and send members (Anyone can communicate with anyone)`data.cfg` + ``` + [send] + + [receive] + + [receive_send] + 04652a820cc69d47940ce39050c455a6 = Test user 1 + d1b551e1b89fff5a4a6f2aaff2464971 = Test user 2 + 801f48d54bc71cb3e0886944832aaf8d = Test user 3 + ``` + + +## Admin manual +This guide applies to all admins. Here are briefly explained the administative possibilities. + +### Manage users: +All users are maintained directly in the `data.cfg` file. +There is no automatic joining to the group. +Please do not forget to restart the program after a modification! + + ``` + # This is the data file. It is automatically created and saved/overwritten. + # It contains data managed by the software itself. + # If manual adjustments are made here, the program must be shut down first! + + + #### User with send only rights #### + [send] + 04652a820cc69d47940ce39050c455a6 = Test user 1 + + #### User with receive only rights #### + [receive] + d1b551e1b89fff5a4a6f2aaff2464971 = Test user 2 + + #### User with receive and send rights #### + [receive_send] + 801f48d54bc71cb3e0886944832aaf8d = Test user 3 + ``` + + +## User manual +This guide applies to users or admins. Here are briefly explained the normal possibilities of the software. + + +### Start/Join the group: +The administrator must create new users manually. + + +### Send message: +Any text will be interpreted as a normal message and sent to all members accordingly. There is nothing else to consider here. + + +## FAQ + +### Why this server based group function and no direct groups in the client software? +At the time of the development of these group functions there is no other possibility to use groups via Sideband/Nomadnet. Therefore this software was developed as a workaround. +This software also offers other functions than a normal group broadcast. + +### 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_distribution_group_minimal/lxmf_distribution_group_minimal.py b/lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py new file mode 100755 index 0000000..c52dd6e --- /dev/null +++ b/lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py @@ -0,0 +1,1297 @@ +#!/usr/bin/env python3 +############################################################################################################## +# +# Copyright (c) 2022 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 + +#### 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 + + +############################################################################################################## +# Globals + + +#### Global Variables - Configuration #### +NAME = "LXMF Distribution Group" +DESCRIPTION = "Server-Side group functions for LXMF based apps" +VERSION = "0.0.1 (2022-10-21)" +COPYRIGHT = "(c) 2022 Sebastian Obele / obele.eu" +PATH = os.path.expanduser("~") + "/." + os.path.splitext(os.path.basename(__file__))[0] +PATH_RNS = None + + + + +#### Global Variables - System (Not changeable) #### +DATA = None +CONFIG = None +RNS_CONNECTION = None +LXMF_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 + + + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", send_delay=0, desired_method="direct", propagation_node=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.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.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 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) + + 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) + + 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) + + self.autoselect_propagation_node() + + if self.announce_startup or self.announce_periodic: + self.announce(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 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=None, fields=None, timestamp=None, app_data=""): + 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 + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return + + destination_identity = RNS.Identity.recall(destination) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + + + def send_message(self, destination, source, content="", title=None, 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) + 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 + + + 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, 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() + return + + self.announce_now() + + + def announce_now(self, app_data=None): + if app_data: + self.destination.announce(app_data.encode("utf-8")) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ":" + app_data, 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 autoselect_propagation_node(self): + if self.propagation_node is not None: + if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + else: + try: + propagation_hash = bytes.fromhex(self.propagation_node) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return + + node_identity = RNS.Identity.recall(propagation_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) + propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(propagation_hash) + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + + + 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) + + +############################################################################################################## +# 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: + 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 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 + + content = message.content.decode('utf-8') + content = content.strip() + if content == "": + return + + title = message.title.decode('utf-8') + title = title.strip() + + source_hash = RNS.hexrep(message.source_hash, False) + source_name = "" + source_right = "" + + for section in DATA.sections(): + for (key, val) in DATA.items(section): + if key == source_hash: + source_name = val + source_right = section + + if source_right == "": + for section in DATA.sections(): + if "send" in section: + if DATA.has_option(section, "any") or DATA.has_option(section, "all") or DATA.has_option(section, "anybody"): + source_right = section + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not exist -> any allowed", LOG_DEBUG) + break + if source_right == "": + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not exist", LOG_DEBUG) + return + + length = config_getint(CONFIG, "message", "receive_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "receive_length_max", 0) + if length > 0: + if len(content) > length: + return + + if "send" in source_right: + length = config_getint(CONFIG, "message", "send_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "send_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "send_prefix") + content_suffix = config_get(CONFIG, "message", "send_suffix") + + content_prefix = content_prefix.replace("!source_address!", source_hash) + content_prefix = content_prefix.replace("!source_name!", source_name) + content_prefix = content_prefix.replace("!name!", config_get(CONFIG, "main", "name")) + content_prefix = content_prefix.replace("!display_name!", config_get(CONFIG, "lxmf", "display_name")) + content_prefix = content_prefix.replace("!n!", "\n") + + content_suffix = content_suffix.replace("!source_address!", source_hash) + content_suffix = content_suffix.replace("!source_name!", source_name) + content_suffix = content_suffix.replace("!name!", config_get(CONFIG, "main", "name")) + content_suffix = content_suffix.replace("!display_name!", config_get(CONFIG, "lxmf", "display_name")) + content_suffix = content_suffix.replace("!n!", "\n") + + search = config_get(CONFIG, "message", "send_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "send_replace")) + + search = config_get(CONFIG, "message", "send_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "send_regex_replace"), content) + + content = content_prefix + content + content_suffix + + title = message.title.decode('utf-8') + + if config_get(CONFIG, "message", "timestamp") == "client": + timestamp = message.timestamp + else: + timestamp = time.time() + + for section in DATA.sections(): + if "receive" in section: + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content, title, timestamp) + return + else: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " 'send' not allowed", LOG_DEBUG) + + return + + +############################################################################################################## +# 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 - 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: + return False + else: + DATA = configparser.ConfigParser(allow_no_value=True, inline_comment_prefixes="#") + DATA.sections() + if os.path.isfile(file): + try: + DATA.read(file) + except Exception as e: + return False + else: + if not data_default(file=file): + return False + return True + + + + +#### Data - Save ##### +def data_save(file=None): + global DATA + + if file is None: + return False + else: + if os.path.isfile(file): + try: + with open(file,"w") as file: + DATA.write(file) + except Exception as e: + return False + else: + return False + return True + + + + +#### Data - Save ##### +def data_save_periodic(initial=False): + data_timer = threading.Timer(CONFIG.getint("main", "periodic_save_data_interval")*60, data_save_periodic) + data_timer.daemon = True + data_timer.start() + + if initial: + return + + global DATA + if DATA.has_section("main"): + if DATA["main"].getboolean("unsaved"): + DATA.remove_option("main", "unsaved") + if not data_save(PATH + "/data.cfg"): + DATA["main"]["unsaved"] = "True" + + + + +#### Data - Default ##### +def data_default(file=None): + global DATA + + if file is None: + return False + elif DEFAULT_DATA != "": + if not os.path.isdir(os.path.dirname(file)): + try: + os.makedirs(os.path.dirname(file)) + except Exception: + return False + try: + data_file = open(file, "w") + data_file.write(DEFAULT_DATA) + data_file.close() + if not data_read(file=file): + return False + except: + return False + else: + return False + return True + + +############################################################################################################## +# Value convert + + +def val_to_bool(val): + 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 True + else: + return False + + +############################################################################################################## +# 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 RNS_CONNECTION + global LXMF_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.cfg"): + print("Data - Error reading data file " + PATH + "/data.cfg") + 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() + + 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(" Data File: " + PATH + "/data.cfg", LOG_INFO) + log(" Version: " + VERSION, LOG_INFO) + log(" Copyright: " + COPYRIGHT, LOG_INFO) + log("...............................................................................", LOG_INFO) + + 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 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"], + send_delay=CONFIG["lxmf"]["send_delay"], + desired_method=CONFIG["lxmf"]["desired_method"], + propagation_node=config_propagation_node, + 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) + + log("LXMF - Connected", LOG_DEBUG) + + log("...............................................................................", LOG_FORCE) + log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE) + log("...............................................................................", LOG_FORCE) + + 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") + parser.add_argument("--exampledata", 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() + + if params.exampledata: + print("Data File: " + PATH + "/data.cfg") + print("Content:") + print(DEFAULT_DATA) + 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. + +#### LXMF connection settings #### +[lxmf] + +# The name will be visible to other peers +# on the network, and included in announces. +# It is also used in the group description/info. +display_name = Distribution Group + +# Propagation node address/hash. +propagation_node = ca2762fe5283873719aececfb9e18835 + +# 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 +''' + + +#### 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 = Distribution Group + + + + +#### 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 = Distribution Group + +# Default send method. +desired_method = direct #direct/propagated + +# Propagation node address/hash. +propagation_node = ca2762fe5283873719aececfb9e18835 + +# 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 = Yes +announce_startup_delay = 0 #Seconds + +# The peer is announced periodically +# to let other peers reach it. +announce_periodic = Yes +announce_periodic_interval = 120 #Minutes + +# 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 = 8 + +# Allow only messages with valid signature. +signature_validated = No + + + + +#### Message settings #### +[message] +## Each message received (message and command) ## + +# Text is added. +receive_prefix = +receive_suffix = + +# Text is replaced. +receive_search = +receive_replace = + +# Text is replaced by regular expression. +receive_regex_search = +receive_regex_replace = + +# Length limitation. +receive_length_min = 0 #0=any length +receive_length_max = 0 #0=any length + + +## Each message send (message) ## + +# Text is added. +send_prefix = !source_name!!n!!n! +send_suffix = + +# Text is replaced. +send_search = +send_replace = + +# Text is replaced by regular expression. +send_regex_search = +send_regex_replace = + +# Length limitation. +send_length_min = 0 #0=any length +send_length_max = 0 #0=any length + + +# Define which message timestamp should be used. +timestamp = client #client/server +''' + + +#### Default data file #### +DEFAULT_DATA = '''# This is the data file. It is automatically created and saved/overwritten. +# It contains data managed by the software itself. +# If manual adjustments are made here, the program must be shut down first! + + +#### User with send only rights #### +[send] + +#### User with receive only rights #### +[receive] + +#### User with receive and send rights #### +[receive_send] +''' + + +############################################################################################################## +# Init + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/lxmf_echo/CHANGELOG.md b/lxmf_echo/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/lxmf_echo/README.md b/lxmf_echo/README.md new file mode 100644 index 0000000..6b9a227 --- /dev/null +++ b/lxmf_echo/README.md @@ -0,0 +1,197 @@ +# lxmf_echo +This program is a simple echo server. All received messages are sent back 1:1 as an answer. This can be used as a simple counterpart to test the chat functionality of applications. + +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 +- Compatible with all LXMF applications (NomadNet, Sideband, ...) + + +## 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 the group. + + +## Current Status +It should currently be considered beta software. All core features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. +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 + ``` +- Change the Reticulum configuration to suit your needs and use-case. + ```bash + nano /.reticulum/config + ``` +- Download the [file](lxmf_echo.py) from this repository. + ```bash + wget https://raw.githubusercontent.com/SebastianObi/LXMF-Tools/main/lxmf_echo/lxmf_echo.py + ``` +- Make it executable with the following command + ```bash + chmod +x lxmf_echo.py + ``` + +### Start: +- Start it + ```bash + ./lxmf_echo.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_echo/config.cfg.owr + ``` + ```bash + ``` +- Start it again. Finished! + ```bash + ./lxmf_echo.py + ``` + + +### Run as a system service/deamon: +- Create a service file. + ```bash + nano /etc/systemd/system/lxmf_echo.service + ``` +- Copy and edit the following content to your own needs. + ```bash + [Unit] + Description=lxmf_echo.py Daemon + After=multi-user.target + [Service] + # ExecStartPre=/bin/sleep 10 + Type=simple + Restart=always + RestartSec=3 + User=root + ExecStart=/root/lxmf_echo.py + [Install] + WantedBy=multi-user.target + ``` +- Enable the service. + ```bash + systemctl enable lxmf_echo + ``` +- Start the service. + ```bash + systemctl start lxmf_echo + ``` + + +### Start/Stop service: + ```bash + systemctl start lxmf_echo + systemctl stop lxmf_echo + ``` + + +### Enable/Disable service: + ```bash + systemctl enable lxmf_echo + systemctl disable lxmf_echo + ``` + + +### Run several instances (To copy the same application): +- Run the program with a different configuration path. + ```bash + ./lxmf_echo.py -p /root/.lxmf_echo_2nd + ./lxmf_echo.py -p /root/.lxmf_echo_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_echo.py [-h] [-p PATH] [-pr PATH_RNS] [-pl PATH_LOG] [-l LOGLEVEL] [-s] [--exampleconfig] [--exampleconfigoverride] + +LXMF Echo - + +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_echo/lxmf_echo.py b/lxmf_echo/lxmf_echo.py new file mode 100755 index 0000000..7cadc7f --- /dev/null +++ b/lxmf_echo/lxmf_echo.py @@ -0,0 +1,1126 @@ +#!/usr/bin/env python3 +############################################################################################################## +# +# Copyright (c) 2022 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 argparse + +#### Config #### +import configparser + +#### JSON #### +import json +import pickle + +#### String #### +import string + +#### 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 + + +############################################################################################################## +# Globals + + +#### Global Variables - Configuration #### +NAME = "LXMF Echo" +DESCRIPTION = "" +VERSION = "0.0.1 (2022-10-21)" +COPYRIGHT = "(c) 2022 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 +RNS_CONNECTION = None +LXMF_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 + + + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", send_delay=0, desired_method="direct", propagation_node=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.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.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 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) + + 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) + + 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) + + self.autoselect_propagation_node() + + if self.announce_startup or self.announce_periodic: + self.announce(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 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=None, fields=None, timestamp=None, app_data=""): + 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 + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return + + destination_identity = RNS.Identity.recall(destination) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + + + def send_message(self, destination, source, content="", title=None, 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) + 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 + + + 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, 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() + return + + self.announce_now() + + + def announce_now(self, app_data=None): + if app_data: + self.destination.announce(app_data.encode("utf-8")) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ":" + app_data, 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 autoselect_propagation_node(self): + if self.propagation_node is not None: + if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + else: + try: + propagation_hash = bytes.fromhex(self.propagation_node) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return + + node_identity = RNS.Identity.recall(propagation_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) + propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(propagation_hash) + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + + + 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) + + +############################################################################################################## +# 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: + 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 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)): + + content = message.content.decode('utf-8').strip() + + length = config_getint(CONFIG, "message", "receive_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "receive_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "receive_prefix") + content_suffix = config_get(CONFIG, "message", "receive_suffix") + + search = config_get(CONFIG, "message", "receive_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "receive_replace")) + + search = config_get(CONFIG, "message", "receive_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "receive_regex_replace"), content) + + content = content_prefix + content + content_suffix + + length = config_getint(CONFIG, "message", "send_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "send_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "send_prefix") + content_suffix = config_get(CONFIG, "message", "send_suffix") + + search = config_get(CONFIG, "message", "send_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "send_replace")) + + search = config_get(CONFIG, "message", "send_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "send_regex_replace"), content) + + content = content_prefix + content + content_suffix + + LXMF_CONNECTION.send(message.source_hash, content, message.title.decode('utf-8').strip()) + else: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not allowed", LOG_DEBUG) + return + + +############################################################################################################## +# 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 - 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 + + +############################################################################################################## +# Value convert + + +def val_to_bool(val): + 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 True + else: + return False + + +############################################################################################################## +# 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 RNS_CONNECTION + global LXMF_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 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() + + 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) + + 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 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"], + send_delay=CONFIG["lxmf"]["send_delay"], + desired_method=CONFIG["lxmf"]["desired_method"], + propagation_node=config_propagation_node, + 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) + + log("LXMF - Connected", LOG_DEBUG) + + log("...............................................................................", LOG_FORCE) + log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE) + log("...............................................................................", LOG_FORCE) + + 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 = Echo Test + + + + +#### 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 = Echo Test + +# Default send method. +desired_method = direct #direct/propagated + +# Propagation node address/hash. +#propagation_node = + +# Try to deliver a message via the LXMF propagation network, +# if a direct delivery to the recipient is not possible. +try_propagation_on_fail = No + +# The peer is announced at startup +# to let other peers reach it immediately. +announce_startup = Yes +announce_startup_delay = 0 #Seconds + +# The peer is announced periodically +# to let other peers reach it. +announce_periodic = Yes +announce_periodic_interval = 360 #Minutes + +# 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 = 8 + +# Allow only messages with valid signature. +signature_validated = Yes + + + + +#### Message settings #### +[message] + +# Text is added. +receive_prefix = +receive_suffix = + +# Text is replaced. +receive_search = +receive_replace = + +# Text is replaced by regular expression. +receive_regex_search = +receive_regex_replace = + +# Length limitation. +receive_length_min = 0 #0=any length +receive_length_max = 0 #0=any length + + +# Text is added. +send_prefix = +send_suffix = + +# Text is replaced. +send_search = +send_replace = + +# Text is replaced by regular expression. +send_regex_search = +send_regex_replace = + +# Length limitation. +send_length_min = 0 #0=any length +send_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_ping/CHANGELOG.md b/lxmf_ping/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/lxmf_ping/README.md b/lxmf_ping/README.md new file mode 100644 index 0000000..26ef7df --- /dev/null +++ b/lxmf_ping/README.md @@ -0,0 +1,100 @@ +# lxmf_ping +This program sends an adjustable number of LXMF messages to a destination. Then a simple statistic is created to check the success or failure of a single message. This tool can be useful to load the LXMF/Reticulum network with a defined load of messages. This can be used to simulate a certain amount of users. + +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 +- Compatible with all LXMF applications (NomadNet, Sideband, ...) + + +## 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 the group. + + +## Current Status +It should currently be considered beta software. All core features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. +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 + ``` +- Change the Reticulum configuration to suit your needs and use-case. + ```bash + nano /.reticulum/config + ``` +- Download the [file](lxmf_ping.py) from this repository. + ```bash + wget https://raw.githubusercontent.com/SebastianObi/LXMF-Tools/main/lxmf_ping/lxmf_ping.py + ``` +- Make it executable with the following command + ```bash + chmod +x lxmf_ping.py + ``` + +### Start: +- Start it + ```bash + ./lxmf_ping.py + ``` + + +### Startup parameters: +```bash +usage: lxmf_ping.py [-h] [-p PATH] [-pr PATH_RNS] [-pl PATH_LOG] [-l LOGLEVEL] -d DEST [-t TIME] [-s SIZE] [-c COUNT] [-i INST] + +LXMF Ping - Periodically sends pings/messages and evaluates the status + +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 + -d DEST, --dest DEST Single destination hash or ,-separated list with destination hashs or . for random destination + -t TIME, --time TIME Time between messages in seconds + -s SIZE, --size SIZE Size (lenght) of the message content + -c COUNT, --count COUNT + Maximum message send count (0=no end) + -i INST, --inst INST Parallel instances (different sender addresses) +``` + + +## 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_ping/lxmf_ping.py b/lxmf_ping/lxmf_ping.py new file mode 100755 index 0000000..fee26a7 --- /dev/null +++ b/lxmf_ping/lxmf_ping.py @@ -0,0 +1,782 @@ +#!/usr/bin/env python3 +############################################################################################################## +# +# Copyright (c) 2022 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 argparse + +#### JSON #### +import json +import pickle + +#### String #### +import string + +#### Other #### +import random +import secrets + +#### 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 + + +############################################################################################################## +# Globals + + +#### Global Variables - Configuration #### +NAME = "LXMF Ping" +DESCRIPTION = "Periodically sends pings/messages and evaluates the status" +VERSION = "0.0.1 (2022-10-21)" +COPYRIGHT = "(c) 2022 Sebastian Obele / obele.eu" +PATH = os.path.expanduser("~") + "/." + os.path.splitext(os.path.basename(__file__))[0] +PATH_RNS = None + + + + +#### Global Variables - System (Not changeable) #### +DATA = None +RNS_CONNECTION = None +LXMF_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 + + + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", send_delay=0, desired_method="direct", propagation_node=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.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.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 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) + + 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) + + 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) + + self.autoselect_propagation_node() + + if self.announce_startup or self.announce_periodic: + self.announce(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 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=None, fields=None, timestamp=None, app_data=""): + 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 + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return + + destination_identity = RNS.Identity.recall(destination) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + + + def send_message(self, destination, source, content="", title=None, 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) + 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 + + + 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, 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() + return + + self.announce_now() + + + def announce_now(self, app_data=None): + if app_data: + self.destination.announce(app_data.encode("utf-8")) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ":" + app_data, 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 autoselect_propagation_node(self): + if self.propagation_node is not None: + if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + else: + try: + propagation_hash = bytes.fromhex(self.propagation_node) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return + + node_identity = RNS.Identity.recall(propagation_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) + propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(propagation_hash) + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + + + 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) + + +############################################################################################################## +# LXMF Functions + + +#### LXMF - Success #### +def lxmf_success(message): + global DATA + key = RNS.hexrep(message.destination_hash, False) + if not key in DATA: + key = "." + if not key in DATA: + return + DATA[key]["count_success"] = DATA[key]["count_success"] + 1 + timestamp = round(float(time.time()) - float(message.timestamp), 4) + if DATA[key]["time_min"] == 0 or DATA[key]["time_min"] > timestamp: + DATA[key]["time_min"] = timestamp + if DATA[key]["time_max"] == 0 or DATA[key]["time_max"] < timestamp: + DATA[key]["time_max"] = timestamp + DATA[key]["time"] = DATA[key]["time"] + timestamp + DATA[key]["time_avg"] = round(DATA[key]["time"]/DATA[key]["count_success"], 4) + count = str(message.content) + if "#" in count: + count = count.split("#", 1)[1] + count = count.split(" ", 1)[0] + else: + count = "" + print("Destination: " + str (key) + " | #: " + str(count) + " | Messages delivered: " + str(DATA[key]["count_success"]) + "/" + str(DATA[key]["count"]) + " (" + str(round(100/DATA[key]["count"]*DATA[key]["count_success"], 2)) + "%) | Time (min / max / avg): " + str(DATA[key]["time_min"]) + " / " + str(DATA[key]["time_max"]) + " / " + str(DATA[key]["time_avg"]) + " | Info: Success") + + + + +#### LXMF - Failed #### +def lxmf_failed(message): + global DATA + key = RNS.hexrep(message.destination_hash, False) + if not key in DATA: + key = "." + if not key in DATA: + return + DATA[key]["count_failed"] = DATA[key]["count_failed"] + 1 + count = str(message.content) + if "#" in count: + count = count.split("#", 1)[1] + count = count.split(" ", 1)[0] + else: + count = "" + print("Destination: " + str (key) + " | #: " + str(count) + " | Messages delivered: " + str(DATA[key]["count_success"]) + "/" + str(DATA[key]["count"]) + " (" + str(round(100/DATA[key]["count"]*DATA[key]["count_success"], 2)) + "%) | Time (min / max / avg): " + str(DATA[key]["time_min"]) + " / " + str(DATA[key]["time_max"]) + " / " + str(DATA[key]["time_avg"]) + " | Info: Failed") + + +############################################################################################################## +# Value convert + + +def val_to_bool(val): + 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 True + else: + return False + + +############################################################################################################## +# 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, dest="", interval=1, size=128, count=0, inst=1): + global DATA + global PATH + global PATH_RNS + global LOG_LEVEL + global LOG_FILE + global RNS_CONNECTION + global LXMF_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 + + RNS_CONNECTION = RNS.Reticulum(configdir=PATH_RNS, loglevel=rns_loglevel) + + print("...............................................................................") + print(" Name: " + NAME + " - " + DESCRIPTION) + print("Program File: " + __file__) + print(" Version: " + VERSION) + print(" Copyright: " + COPYRIGHT) + print("...............................................................................") + + log("LXMF - Connecting ...", LOG_DEBUG) + + if path is None: + path = PATH + + LXMF_CONNECTION = lxmf_connection(storage_path=path) + + LXMF_CONNECTION.register_message_notification_success_callback(lxmf_success) + LXMF_CONNECTION.register_message_notification_failed_callback(lxmf_failed) + + log("LXMF - Connected", LOG_DEBUG) + + log("...............................................................................", LOG_FORCE) + log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE) + log("...............................................................................", LOG_FORCE) + + DATA = {} + destinations = dest.split(",") + for key in destinations: + DATA[key] = {} + DATA[key]["count"] = 0 + DATA[key]["count_success"] = 0 + DATA[key]["count_failed"] = 0 + DATA[key]["time"] = 0 + DATA[key]["time_min"] = 0 + DATA[key]["time_max"] = 0 + DATA[key]["time_avg"] = 0 + + count_current = 0 + while True: + if count == 0 or count_current < count: + count_current = count_current + 1 + letters = string.ascii_lowercase + content = ''.join(random.choice(letters) for i in range(size)) + for key in DATA: + DATA[key]["count"] = DATA[key]["count"] + 1 + content = "#"+ str(DATA[key]["count"]) + " " + content + content = content[:size] + if key == ".": + LXMF_CONNECTION.send(secrets.token_hex(nbytes=10), content, "") + else: + LXMF_CONNECTION.send(key, content, "") + print("Destination: " + str (key) + " | #: " + str(DATA[key]["count"]) + " | Messages delivered: " + str(DATA[key]["count_success"]) + "/" + str(DATA[key]["count"]) + " (" + str(round(100/DATA[key]["count"]*DATA[key]["count_success"], 2)) + "%) | Time (min / max / avg): " + str(DATA[key]["time_min"]) + " / " + str(DATA[key]["time_max"]) + " / " + str(DATA[key]["time_avg"]) + " | Info: Sending/Queued") + time.sleep(interval) + + + + +#### 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("-d", "--dest", action="store", required=True, type=str, default=None, help="Single destination hash or ,-separated list with destination hashs or . for random destination") + parser.add_argument("-t", "--time", action="store", type=float, default=1, help="Time between messages in seconds") + parser.add_argument("-s", "--size", action="store", type=int, default=128, help="Size (lenght) of the message content") + parser.add_argument("-c", "--count", action="store", type=float, default=0, help="Maximum message send count (0=no end)") + parser.add_argument("-i", "--inst", action="store", type=int, default=1, help="Parallel instances (different sender addresses)") + + params = parser.parse_args() + + setup(path=params.path, path_rns=params.path_rns, path_log=params.path_log, loglevel=params.loglevel, dest=params.dest, interval=params.time, size=params.size, count=params.count, inst=params.inst) + + except KeyboardInterrupt: + print("Terminated by CTRL-C") + exit() + + +############################################################################################################## +# Init + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/lxmf_terminal/CHANGELOG.md b/lxmf_terminal/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/lxmf_terminal/README.md b/lxmf_terminal/README.md new file mode 100644 index 0000000..f93e9d8 --- /dev/null +++ b/lxmf_terminal/README.md @@ -0,0 +1,197 @@ +# lxmf_terminal +This program provides a complete terminal session on the server. Any commands can be executed on the target device. The communication is done by single LXMF messages. This offers the advantage that simple terminal commands can be used by any LXMF capable application. + +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 +- Compatible with all LXMF applications (NomadNet, Sideband, ...) + + +## 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 the group. + + +## Current Status +It should currently be considered beta software. All core features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. +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 + ``` +- Change the Reticulum configuration to suit your needs and use-case. + ```bash + nano /.reticulum/config + ``` +- Download the [file](lxmf_terminal.py) from this repository. + ```bash + wget https://raw.githubusercontent.com/SebastianObi/LXMF-Tools/main/lxmf_terminal/lxmf_terminal.py + ``` +- Make it executable with the following command + ```bash + chmod +x lxmf_terminal.py + ``` + +### Start: +- Start it + ```bash + ./lxmf_terminal.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_terminal/config.cfg.owr + ``` + ```bash + ``` +- Start it again. Finished! + ```bash + ./lxmf_terminal.py + ``` + + +### Run as a system service/deamon: +- Create a service file. + ```bash + nano /etc/systemd/system/lxmf_terminal.service + ``` +- Copy and edit the following content to your own needs. + ```bash + [Unit] + Description=lxmf_terminal.py Daemon + After=multi-user.target + [Service] + # ExecStartPre=/bin/sleep 10 + Type=simple + Restart=always + RestartSec=3 + User=root + ExecStart=/root/lxmf_terminal.py + [Install] + WantedBy=multi-user.target + ``` +- Enable the service. + ```bash + systemctl enable lxmf_terminal + ``` +- Start the service. + ```bash + systemctl start lxmf_terminal + ``` + + +### Start/Stop service: + ```bash + systemctl start lxmf_terminal + systemctl stop lxmf_terminal + ``` + + +### Enable/Disable service: + ```bash + systemctl enable lxmf_terminal + systemctl disable lxmf_terminal + ``` + + +### Run several instances (To copy the same application): +- Run the program with a different configuration path. + ```bash + ./lxmf_terminal.py -p /root/.lxmf_terminal_2nd + ./lxmf_terminal.py -p /root/.lxmf_terminal_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_terminal.py [-h] [-p PATH] [-pr PATH_RNS] [-pl PATH_LOG] [-l LOGLEVEL] [-s] [--exampleconfig] [--exampleconfigoverride] + +LXMF Terminal - + +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_terminal/lxmf_terminal.py b/lxmf_terminal/lxmf_terminal.py new file mode 100755 index 0000000..2cf4cdd --- /dev/null +++ b/lxmf_terminal/lxmf_terminal.py @@ -0,0 +1,1337 @@ +#!/usr/bin/env python3 +############################################################################################################## +# +# Copyright (c) 2022 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 argparse + +#### Config #### +import configparser + +#### JSON #### +import json +import pickle + +#### String #### +import string + +#### Regex #### +import re + +#### Process #### +import threading + +#### Terminal #### +import subprocess +import pty +import termios +import select +import struct +import fcntl +import shlex +import signal + +#### Reticulum, LXMF #### +# Install: pip3 install rns lxmf +# Source: https://markqvist.github.io +import RNS +import LXMF +import RNS.vendor.umsgpack as umsgpack + + +############################################################################################################## +# Globals + + +#### Global Variables - Configuration #### +NAME = "LXMF Terminal" +DESCRIPTION = "" +VERSION = "0.0.1 (2022-10-21)" +COPYRIGHT = "(c) 2022 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 +RNS_CONNECTION = None +LXMF_CONNECTION = None +SESSION = {} +TERMINAL = None + + +############################################################################################################## +# Terminal Class + + +class terminal_class: + fd = None + pid = None + + + def size(self, rows, cols, xpix=0, ypix=0): + if self.fd: + size = struct.pack("HHHH", rows, cols, xpix, ypix) + fcntl.ioctl(self.fd, termios.TIOCSWINSZ, size) + + + def get(self, timeout=None, read_bytes=None): + if not timeout: + timeout = self.timeout + if not read_bytes: + read_bytes = self.read_bytes + if not self.fd: return ["", 0] + (data_ready, _, _) = select.select([self.fd], [], [], self.timeout) + if not data_ready: return ["", 0] + output = "" + state = 0 + try: + read_bytes = 1024 * read_bytes + output = os.read(self.fd, read_bytes).decode() + except Exception as e: + output = str(e) + state = e.errno + if e.errno == 5: self.stop() + return [output, state] + + + def set(self, cmd): + if not self.fd and not self.restart_session: return False + if not self.fd and self.restart_session: self.start() + if not self.fd: return False + try: + cmd = cmd.strip() + "\n" + os.write(self.fd, cmd.encode()) + except: + return False + return True + + + def start(self): + if not self.pid: + (pid, fd) = pty.fork() + if pid == 0: + cmd = [self.cmd] + shlex.split(self.cmd_args) + if self.path != "": os.chdir(self.path) + subprocess.run(cmd) + else: + self.fd = fd + self.pid = pid + self.size(self.fd, self.rows, self.cols) + + + def stop(self): + if self.fd: + fd = self.fd + self.fd = None + self.pid = None + try: + os.kill(fd, signal.SIGTERM) + except: + return False + return True + + + def __init__(self, path="", cmd="bash", cmd_args="", timeout=0, read_bytes=20, rows=100, cols=200, restart_session=True): + self.path = path + self.cmd = cmd + self.cmd_args = cmd_args + self.timeout = timeout + self.read_bytes = read_bytes + self.rows = rows + self.cols = cols + self.restart_session = restart_session + self.start() + return + + + def __del__(self): + self.stop() + return + + +############################################################################################################## +# LXMF Class + + +class lxmf_connection: + message_received_callback = None + message_notification_callback = None + message_notification_success_callback = None + message_notification_failed_callback = None + + + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", send_delay=0, desired_method="direct", propagation_node=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.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.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 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) + + 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) + + 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) + + self.autoselect_propagation_node() + + if self.announce_startup or self.announce_periodic: + self.announce(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 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=None, fields=None, timestamp=None, app_data=""): + 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 + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return + + destination_identity = RNS.Identity.recall(destination) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + + + def send_message(self, destination, source, content="", title=None, 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) + 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 + + + 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, 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() + return + + self.announce_now() + + + def announce_now(self, app_data=None): + if app_data: + self.destination.announce(app_data.encode("utf-8")) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ":" + app_data, 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 autoselect_propagation_node(self): + if self.propagation_node is not None: + if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + else: + try: + propagation_hash = bytes.fromhex(self.propagation_node) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return + + node_identity = RNS.Identity.recall(propagation_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) + propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(propagation_hash) + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + + + 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) + + +############################################################################################################## +# 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: + 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): + global SESSION + + 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)): + + if TERMINAL: + SESSION["source"] = message.source_hash + content = message.content.decode('utf-8') + + length = config_getint(CONFIG, "message", "receive_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "receive_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "receive_prefix") + content_suffix = config_get(CONFIG, "message", "receive_suffix") + + search = config_get(CONFIG, "message", "receive_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "receive_replace")) + + search = config_get(CONFIG, "message", "receive_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "receive_regex_replace"), content) + + content = content_prefix + content + content_suffix + SESSION["cmd"] = content + TERMINAL.set(content) + output() + else: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not allowed", LOG_DEBUG) + return + + +############################################################################################################## +# Functions + + +def output(initial=False): + timer = threading.Timer(CONFIG.getint("terminal", "interval"), output) + timer.daemon = True + timer.start() + if initial: return + output_now() + + + + +def output_now(): + global SESSION + global TERMINAL + + if not "source" in SESSION: return True + + if not TERMINAL: return True + output, state = TERMINAL.get() + if output == "": return True + + log("Output - RAW:" + output, LOG_EXTREME) + + if CONFIG["message"].getboolean("replace_cmd"): + output = re.sub(r'\u001b\[\?2004h.*?#\s', '', output) + output = output.replace("\u001b[?2004l", "") + output = re.sub(r'.*@.*#', '', output) + if output.startswith(SESSION["cmd"]): + output = output.replace(SESSION["cmd"], "", 1) + output = re.sub(r'^\s+|\s+$', '', output) + if output.startswith(SESSION["cmd"]): + output = output.replace(SESSION["cmd"], "", 1) + output = re.sub(r'^\s+|\s+$', '', output) + + if CONFIG["message"].getboolean("replace_ansi"): + ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]') + output = ansi_escape.sub('', output) + + if CONFIG["message"].getboolean("replace_log"): + output = re.sub(r'[\[\(]\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[\]\)]', '', output) + + if CONFIG["message"].getboolean("replace_whitespace"): + output = re.sub(r'(\r)+(\n)+', '\n', output) + output = re.sub(r'(\n)+((\s+)?\r)+', '\n', output) + output = re.sub(r'((\s+)?\r)+', '\n', output) + output = re.sub(r'^\s+|\s+$', '', output) + output = re.sub(r'^ +| +$', '', output, flags=re.M) + + if CONFIG["message"].getboolean("replace_double_lines"): + output = re.sub(r'^(.*)(\r?\n\1)+$', r'\1', output, flags=re.M) + + if CONFIG["message"].getboolean("replace_special_characters"): + output = output.replace(" :", ":") + output = output.replace("?:", "?") + + if CONFIG["message"].getboolean("replace_unnecessary_characters"): + output = re.sub(r'^[\-\_]+|[\-\_]+$', '', output) + output = re.sub(r'^[^a-zA-Z0-9]$', '', output, flags=re.M) + output = re.sub(r'^#\?$', '', output, flags=re.M) + output = re.sub(r'\?\s?:', '', output) + + if CONFIG["message"].getboolean("replace_whitespace"): + output = re.sub(r'^\s+|\s+$', '', output) + output = re.sub(r'^ +| +$', '', output, flags=re.M) + + if output != "": + length = config_getint(CONFIG, "message", "send_length_min", 0) + if length> 0: + if len(output) < length: + return True + + length = config_getint(CONFIG, "message", "send_length_max", 0) + if length > 0: + if len(output) > length: + return True + + content_prefix = config_get(CONFIG, "message", "send_prefix") + content_suffix = config_get(CONFIG, "message", "send_suffix") + + search = config_get(CONFIG, "message", "send_search") + if search != "": + output = output.replace(search, config_get(CONFIG, "message", "send_replace")) + + search = config_get(CONFIG, "message", "send_regex_search") + if search != "": + output = re.sub(search, config_get(CONFIG, "message", "send_regex_replace"), output) + + LXMF_CONNECTION.send(SESSION["source"], content_prefix + output + content_suffix, "") + + return True + + +############################################################################################################## +# 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 - 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 + + +############################################################################################################## +# Value convert + + +def val_to_bool(val): + 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 True + else: + return False + + +############################################################################################################## +# 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 RNS_CONNECTION + global LXMF_CONNECTION + global TERMINAL + + 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 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() + + 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) + + 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 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"], + send_delay=CONFIG["lxmf"]["send_delay"], + desired_method=CONFIG["lxmf"]["desired_method"], + propagation_node=config_propagation_node, + 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) + + log("LXMF - Connected", LOG_DEBUG) + + log("...............................................................................", LOG_FORCE) + log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE) + log("...............................................................................", LOG_FORCE) + + log("Terminal - Connecting ...", LOG_DEBUG) + TERMINAL = terminal_class(CONFIG["terminal"]["path"], CONFIG["terminal"]["cmd"], CONFIG["terminal"]["cmd_args"], CONFIG.getint("terminal", "timeout"), CONFIG.getint("terminal", "read_bytes"), CONFIG.getint("terminal", "size_rows"), CONFIG.getint("terminal", "size_cols"), CONFIG["terminal"].getboolean("restart_session")) + output(True) + log("Terminal - Connected", LOG_DEBUG) + 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 = Terminal + + + + +#### 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 = CMD + +# Default send method. +desired_method = direct #direct/propagated + +# Propagation node address/hash. +#propagation_node = + +# Try to deliver a message via the LXMF propagation network, +# if a direct delivery to the recipient is not possible. +try_propagation_on_fail = No + +# 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 + +# 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 = 8 + +# Allow only messages with valid signature. +signature_validated = Yes + + + + +#### Terminal settings #### +[terminal] + +path = /tmp +cmd = bash +cmd_args = +timeout = 0 +read_bytes = 20 +size_rows = 100 +size_cols = 200 +restart_session = True +interval = 5 #Seconds + + + + +#### Message settings #### +[message] + +# Text is added. +receive_prefix = +receive_suffix = + +# Text is replaced. +receive_search = +receive_replace = + +# Text is replaced by regular expression. +receive_regex_search = +receive_regex_replace = + +# Length limitation. +receive_length_min = 0 #0=any length +receive_length_max = 0 #0=any length + + +# Text is added. +send_prefix = +send_suffix = + +# Text is replaced. +send_search = +send_replace = + +# Text is replaced by regular expression. +send_regex_search = +send_regex_replace = + +# Length limitation. +send_length_min = 0 #0=any length +send_length_max = 0 #0=any length + + +replace_cmd = False +replace_ansi = True +replace_log = True +replace_whitespace = True +replace_double_lines = True +replace_special_characters = True +replace_unnecessary_characters = True + + + + +#### Right settings #### +# Allow only specific source addresses/hashs or any. +[allowed] + +#any +#2858b7a096899116cd529559cc679ffe +''' + + +############################################################################################################## +# Init + + +if __name__ == "__main__": + main() \ No newline at end of file