mirror of
https://github.com/markqvist/LXMF-Tools.git
synced 2024-10-01 03:15:38 -04:00
796 lines
31 KiB
Python
Executable File
796 lines
31 KiB
Python
Executable File
#!/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="", announce_data=None, 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.announce_data = announce_data
|
|
|
|
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)
|
|
|
|
if self.destination_name == "lxmf" and self.destination_type == "delivery":
|
|
self.destination = self.message_router.register_delivery_identity(self.identity, display_name=self.display_name)
|
|
self.message_router.register_delivery_callback(self.process_lxmf_message_propagated)
|
|
else:
|
|
self.destination = RNS.Destination(self.identity, RNS.Destination.IN, RNS.Destination.SINGLE, self.destination_name, self.destination_type)
|
|
|
|
if self.display_name == "":
|
|
self.display_name = RNS.prettyhexrep(self.destination_hash())
|
|
|
|
self.destination.set_default_app_data(self.display_name.encode("utf-8"))
|
|
|
|
self.destination.set_proof_strategy(RNS.Destination.PROVE_ALL)
|
|
|
|
RNS.Identity.remember(packet_hash=None, destination_hash=self.destination.hash, public_key=self.identity.get_public_key(), app_data=None)
|
|
|
|
log("LXMF - Identity: " + str(self.identity), LOG_INFO)
|
|
log("LXMF - Destination: " + str(self.destination), LOG_INFO)
|
|
log("LXMF - Hash: " + RNS.prettyhexrep(self.destination_hash()), LOG_INFO)
|
|
|
|
self.destination.set_link_established_callback(self.client_connected)
|
|
|
|
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="", 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="", 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:
|
|
if isinstance(app_data, str):
|
|
self.destination.announce(app_data.encode("utf-8"))
|
|
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG)
|
|
else:
|
|
self.destination.announce(app_data)
|
|
log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
|
|
elif self.announce_data:
|
|
if isinstance(self.announce_data, str):
|
|
self.destination.announce(self.announce_data.encode("utf-8"))
|
|
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG)
|
|
else:
|
|
self.destination.announce(self.announce_data)
|
|
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
|
|
else:
|
|
self.destination.announce()
|
|
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ": " + self.display_name, LOG_DEBUG)
|
|
|
|
|
|
def sync(self, initial=False):
|
|
sync_timer = None
|
|
|
|
if self.sync_periodic and self.sync_periodic_interval > 0:
|
|
sync_timer = threading.Timer(self.sync_periodic_interval*60, self.sync)
|
|
sync_timer.daemon = True
|
|
sync_timer.start()
|
|
|
|
if initial:
|
|
if self.sync_startup:
|
|
if self.sync_startup_delay > 0:
|
|
if sync_timer is not None:
|
|
sync_timer.cancel()
|
|
sync_timer = threading.Timer(self.sync_startup_delay, self.sync)
|
|
sync_timer.daemon = True
|
|
sync_timer.start()
|
|
else:
|
|
self.sync_now(self.sync_limit)
|
|
return
|
|
|
|
self.sync_now(self.sync_limit)
|
|
|
|
|
|
def sync_now(self, limit=None):
|
|
if self.message_router.get_outbound_propagation_node() is not None:
|
|
if self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_IDLE or self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_COMPLETE:
|
|
log("LXMF - Message sync requested from propagation node " + RNS.prettyhexrep(self.message_router.get_outbound_propagation_node()) + " for " + str(self.identity))
|
|
self.message_router.request_messages_from_propagation_node(self.identity, max_messages = limit)
|
|
return True
|
|
else:
|
|
return False
|
|
else:
|
|
return False
|
|
|
|
|
|
def 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() |