code cleanup

This commit is contained in:
2025-11-30 20:51:30 -06:00
parent 6dffe70e9b
commit cc30e6abc1
16 changed files with 2296 additions and 1251 deletions

View File

@@ -5,11 +5,11 @@ A heavily customized fork of [Reticulum MeshChat](https://github.com/liamcottle/
## Features of this fork
- [x] Custom UI/UX (actively being improved)
- [ ] Ability to set stamps
- [x] Ability to set inbound and propagation node stamps
- [x] Better config parsing
- [x] Cancel page fetching or file downloads
- [ ] Block users
- [ ] Spam filter (based on keywords)
- [x] Block users
- [x] Spam filter (based on keywords)
- [ ] Multi-identity support
- [x] More stats on about page
- [x] Actions are pinned to full-length SHA hashes.

31
TODO.md Normal file
View File

@@ -0,0 +1,31 @@
1. for messages fix:
convo goes off edge, near edge should be ... 3 dots
long names push the last message/announced seconds/time to right and nearly off the side, fix please
2. interfaces:
3 dots background circle is a oval, fix to be circle
on 3 dots clicked there is still white background the buttons have dark backgrounds though but main dropdown window is white fix depdning on theme
also on 3 dots drop down it still makes me scroll down in that interfaces window, we can expand that interfaces box os something so this crap doesnt hapen or if dropdown is above it
rework propagation nodes page with new UI/UX please like rest of app.
1. the attachment dropups/popups are white on dark mode, they need a ui/ux rework.
2. for settings add ability to set inbound stamp, ref lxmf via python -c if needed.
3. add multi-identity / account suport and a switcher at bottom with ability to create, delete or import/export identies from other apps.
for all this you will likely need to look at my ren chat app for stamps, multi-identity, /mnt/projects/ren-messenger/
its pretty simple.
translator tool
reticulum documentation tool
lxmfy bot tool
page downloader tool
page snapshots

View File

@@ -1,49 +1,57 @@
from datetime import datetime, timezone
from peewee import *
from playhouse.migrate import migrate as migrate_database, SqliteMigrator
from playhouse.migrate import SqliteMigrator
from playhouse.migrate import migrate as migrate_database
latest_version = 6 # increment each time new database migrations are added
database = DatabaseProxy() # use a proxy object, as we will init real db client inside meshchat.py
database = (
DatabaseProxy()
) # use a proxy object, as we will init real db client inside meshchat.py
migrator = SqliteMigrator(database)
# migrates the database
def migrate(current_version):
# migrate to version 2
if current_version < 2:
migrate_database(
migrator.add_column("lxmf_messages", 'delivery_attempts', LxmfMessage.delivery_attempts),
migrator.add_column("lxmf_messages", 'next_delivery_attempt_at', LxmfMessage.next_delivery_attempt_at),
migrator.add_column(
"lxmf_messages", "delivery_attempts", LxmfMessage.delivery_attempts,
),
migrator.add_column(
"lxmf_messages",
"next_delivery_attempt_at",
LxmfMessage.next_delivery_attempt_at,
),
)
# migrate to version 3
if current_version < 3:
migrate_database(
migrator.add_column("lxmf_messages", 'rssi', LxmfMessage.rssi),
migrator.add_column("lxmf_messages", 'snr', LxmfMessage.snr),
migrator.add_column("lxmf_messages", 'quality', LxmfMessage.quality),
migrator.add_column("lxmf_messages", "rssi", LxmfMessage.rssi),
migrator.add_column("lxmf_messages", "snr", LxmfMessage.snr),
migrator.add_column("lxmf_messages", "quality", LxmfMessage.quality),
)
# migrate to version 4
if current_version < 4:
migrate_database(
migrator.add_column("lxmf_messages", 'method', LxmfMessage.method),
migrator.add_column("lxmf_messages", "method", LxmfMessage.method),
)
# migrate to version 5
if current_version < 5:
migrate_database(
migrator.add_column("announces", 'rssi', Announce.rssi),
migrator.add_column("announces", 'snr', Announce.snr),
migrator.add_column("announces", 'quality', Announce.quality),
migrator.add_column("announces", "rssi", Announce.rssi),
migrator.add_column("announces", "snr", Announce.snr),
migrator.add_column("announces", "quality", Announce.quality),
)
# migrate to version 6
if current_version < 6:
migrate_database(
migrator.add_column("lxmf_messages", 'is_spam', LxmfMessage.is_spam),
migrator.add_column("lxmf_messages", "is_spam", LxmfMessage.is_spam),
)
return latest_version
@@ -55,7 +63,6 @@ class BaseModel(Model):
class Config(BaseModel):
id = BigAutoField()
key = CharField(unique=True)
value = TextField()
@@ -68,12 +75,19 @@ class Config(BaseModel):
class Announce(BaseModel):
id = BigAutoField()
destination_hash = CharField(unique=True) # unique destination hash that was announced
aspect = TextField(index=True) # aspect is not included in announce, but we want to filter saved announces by aspect
identity_hash = CharField(index=True) # identity hash that announced the destination
identity_public_key = CharField() # base64 encoded public key, incase we want to recreate the identity manually
destination_hash = CharField(
unique=True,
) # unique destination hash that was announced
aspect = TextField(
index=True,
) # aspect is not included in announce, but we want to filter saved announces by aspect
identity_hash = CharField(
index=True,
) # identity hash that announced the destination
identity_public_key = (
CharField()
) # base64 encoded public key, incase we want to recreate the identity manually
app_data = TextField(null=True) # base64 encoded app data bytes
rssi = IntegerField(null=True)
snr = FloatField(null=True)
@@ -88,7 +102,6 @@ class Announce(BaseModel):
class CustomDestinationDisplayName(BaseModel):
id = BigAutoField()
destination_hash = CharField(unique=True) # unique destination hash
display_name = CharField() # custom display name for the destination hash
@@ -102,7 +115,6 @@ class CustomDestinationDisplayName(BaseModel):
class FavouriteDestination(BaseModel):
id = BigAutoField()
destination_hash = CharField(unique=True) # unique destination hash
display_name = CharField() # custom display name for the destination hash
@@ -117,21 +129,30 @@ class FavouriteDestination(BaseModel):
class LxmfMessage(BaseModel):
id = BigAutoField()
hash = CharField(unique=True) # unique lxmf message hash
source_hash = CharField(index=True)
destination_hash = CharField(index=True)
state = CharField() # state is converted from internal int to a human friendly string
state = (
CharField()
) # state is converted from internal int to a human friendly string
progress = FloatField() # progress is converted from internal float 0.00-1.00 to float between 0.00/100 (2 decimal places)
is_incoming = BooleanField() # if true, we should ignore state, it's set to draft by default on incoming messages
method = CharField(null=True) # what method is being used to send the message, e.g: direct, propagated
delivery_attempts = IntegerField(default=0) # how many times delivery has been attempted for this message
next_delivery_attempt_at = FloatField(null=True) # timestamp of when the message will attempt delivery again
method = CharField(
null=True,
) # what method is being used to send the message, e.g: direct, propagated
delivery_attempts = IntegerField(
default=0,
) # how many times delivery has been attempted for this message
next_delivery_attempt_at = FloatField(
null=True,
) # timestamp of when the message will attempt delivery again
title = TextField()
content = TextField()
fields = TextField() # json string
timestamp = FloatField() # timestamp of when the message was originally created (before ever being sent)
timestamp = (
FloatField()
) # timestamp of when the message was originally created (before ever being sent)
rssi = IntegerField(null=True)
snr = FloatField(null=True)
quality = FloatField(null=True)
@@ -145,7 +166,6 @@ class LxmfMessage(BaseModel):
class LxmfConversationReadState(BaseModel):
id = BigAutoField()
destination_hash = CharField(unique=True) # unique destination hash
last_read_at = DateTimeField()
@@ -159,12 +179,13 @@ class LxmfConversationReadState(BaseModel):
class LxmfUserIcon(BaseModel):
id = BigAutoField()
destination_hash = CharField(unique=True) # unique destination hash
icon_name = CharField() # material design icon name for the destination hash
foreground_colour = CharField() # hex colour to use for foreground (icon colour)
background_colour = CharField() # hex colour to use for background (background colour)
background_colour = (
CharField()
) # hex colour to use for background (background colour)
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc))
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc))
@@ -175,9 +196,10 @@ class LxmfUserIcon(BaseModel):
class BlockedDestination(BaseModel):
id = BigAutoField()
destination_hash = CharField(unique=True, index=True) # unique destination hash that is blocked
destination_hash = CharField(
unique=True, index=True,
) # unique destination hash that is blocked
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc))
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc))
@@ -187,9 +209,10 @@ class BlockedDestination(BaseModel):
class SpamKeyword(BaseModel):
id = BigAutoField()
keyword = CharField(unique=True, index=True) # keyword to match against message content
keyword = CharField(
unique=True, index=True,
) # keyword to match against message content
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc))
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc))

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,49 +1,49 @@
from cx_Freeze import setup, Executable
from cx_Freeze import Executable, setup
setup(
name='ReticulumMeshChatX',
version='1.0.0',
description='A simple mesh network communications app powered by the Reticulum Network Stack',
name="ReticulumMeshChatX",
version="1.0.0",
description="A simple mesh network communications app powered by the Reticulum Network Stack",
executables=[
Executable(
script='meshchat.py', # this script to run
base=None, # we are running a console application, not a gui
target_name='ReticulumMeshChatX', # creates ReticulumMeshChatX.exe
shortcut_name='ReticulumMeshChatX', # name shown in shortcut
shortcut_dir='ProgramMenuFolder', # put the shortcut in windows start menu
icon='logo/icon.ico', # set the icon for the exe
copyright='Copyright (c) 2024 Liam Cottle',
script="meshchat.py", # this script to run
base=None, # we are running a console application, not a gui
target_name="ReticulumMeshChatX", # creates ReticulumMeshChatX.exe
shortcut_name="ReticulumMeshChatX", # name shown in shortcut
shortcut_dir="ProgramMenuFolder", # put the shortcut in windows start menu
icon="logo/icon.ico", # set the icon for the exe
copyright="Copyright (c) 2024 Liam Cottle",
),
],
options={
'build_exe': {
"build_exe": {
# libs that are required
'packages': [
"packages": [
# required for dynamic import fix
# https://github.com/marcelotduarte/cx_Freeze/discussions/2039
# https://github.com/marcelotduarte/cx_Freeze/issues/2041
'RNS',
'RNS.Interfaces',
'LXMF',
"RNS",
"RNS.Interfaces",
"LXMF",
],
# files that are required
'include_files': [
'package.json', # used to determine app version from python
'public/', # static files served by web server
"include_files": [
"package.json", # used to determine app version from python
"public/", # static files served by web server
],
# slim down the build by excluding these unused libs
'excludes': [
'PIL', # saves ~200MB
"excludes": [
"PIL", # saves ~200MB
],
# this has the same effect as the -O command line option when executing CPython directly.
# it also prevents assert statements from executing, removes docstrings and sets __debug__ to False.
# https://stackoverflow.com/a/57948104
"optimize": 2,
# change where exe is built to
'build_exe': 'build/exe',
"build_exe": "build/exe",
# make the build relocatable by replacing absolute paths
'replace_paths': [
('*', ''),
"replace_paths": [
("*", ""),
],
},
},

View File

@@ -7,7 +7,6 @@ import sys
# this class forces stream writes to be flushed immediately
class ImmediateFlushingStreamWrapper:
def __init__(self, stream):
self.stream = stream

View File

@@ -1,16 +1,23 @@
# an announce handler that forwards announces to a provided callback for the provided aspect filter
# this handler exists so we can have access to the original aspect, as this is not provided in the announce itself
class AnnounceHandler:
def __init__(self, aspect_filter: str, received_announce_callback):
self.aspect_filter = aspect_filter
self.received_announce_callback = received_announce_callback
# we will just pass the received announce back to the provided callback
def received_announce(self, destination_hash, announced_identity, app_data, announce_packet_hash):
def received_announce(
self, destination_hash, announced_identity, app_data, announce_packet_hash,
):
try:
# handle received announce
self.received_announce_callback(self.aspect_filter, destination_hash, announced_identity, app_data, announce_packet_hash)
self.received_announce_callback(
self.aspect_filter,
destination_hash,
announced_identity,
app_data,
announce_packet_hash,
)
except:
# ignore failure to handle received announce
pass

View File

@@ -1,9 +1,8 @@
import asyncio
from typing import Coroutine
from collections.abc import Coroutine
class AsyncUtils:
# remember main loop
main_loop: asyncio.AbstractEventLoop | None = None
@@ -15,7 +14,6 @@ class AsyncUtils:
# it will run the async function on the main event loop if possible, otherwise it logs a warning
@staticmethod
def run_async(coroutine: Coroutine):
# run provided coroutine on main event loop, ensuring thread safety
if AsyncUtils.main_loop and AsyncUtils.main_loop.is_running():
asyncio.run_coroutine_threadsafe(coroutine, AsyncUtils.main_loop)

View File

@@ -1,11 +1,10 @@
import asyncio
import time
from typing import List
import RNS
# todo optionally identity self over link
# todo allowlist/denylist for incoming calls
# TODO optionally identity self over link
# TODO allowlist/denylist for incoming calls
class CallFailedException(Exception):
@@ -13,7 +12,6 @@ class CallFailedException(Exception):
class AudioCall:
def __init__(self, link: RNS.Link, is_outbound: bool):
self.link = link
self.is_outbound = is_outbound
@@ -41,21 +39,25 @@ class AudioCall:
# handle packet received over link
def on_packet(self, message, packet):
# send audio received from call initiator to all audio packet listeners
for audio_packet_listener in self.audio_packet_listeners:
audio_packet_listener(message)
# send an audio packet over the link
def send_audio_packet(self, data):
# do nothing if link is not active
if self.is_active() is False:
return
# drop audio packet if it is too big to send
if len(data) > RNS.Link.MDU:
print("[AudioCall] dropping audio packet " + str(len(data)) + " bytes exceeds the link packet MDU of " + str(RNS.Link.MDU) + " bytes")
print(
"[AudioCall] dropping audio packet "
+ str(len(data))
+ " bytes exceeds the link packet MDU of "
+ str(RNS.Link.MDU)
+ " bytes",
)
return
# send codec2 audio received from call receiver to call initiator over reticulum link
@@ -73,13 +75,10 @@ class AudioCall:
def hangup(self):
print("[AudioCall] hangup")
self.link.teardown()
pass
class AudioCallManager:
def __init__(self, identity: RNS.Identity, is_destination_blocked_callback=None):
self.identity = identity
self.on_incoming_call_callback = None
self.on_outgoing_call_callback = None
@@ -87,12 +86,15 @@ class AudioCallManager:
self.audio_call_receiver = AudioCallReceiver(manager=self)
# remember audio calls
self.audio_calls: List[AudioCall] = []
self.audio_calls: list[AudioCall] = []
# announces the audio call destination
def announce(self, app_data=None):
self.audio_call_receiver.destination.announce(app_data)
print("[AudioCallManager] announced destination: " + RNS.prettyhexrep(self.audio_call_receiver.destination.hash))
print(
"[AudioCallManager] announced destination: "
+ RNS.prettyhexrep(self.audio_call_receiver.destination.hash),
)
# set the callback for incoming calls
def register_incoming_call_callback(self, callback):
@@ -104,7 +106,6 @@ class AudioCallManager:
# handle incoming calls from audio call receiver
def handle_incoming_call(self, audio_call: AudioCall):
# remember it
self.audio_calls.append(audio_call)
@@ -114,7 +115,6 @@ class AudioCallManager:
# handle outgoing calls
def handle_outgoing_call(self, audio_call: AudioCall):
# remember it
self.audio_calls.append(audio_call)
@@ -143,22 +143,24 @@ class AudioCallManager:
def hangup_all(self):
for audio_call in self.audio_calls:
audio_call.hangup()
return None
# attempts to initiate a call to the provided destination and returns the link hash on success
async def initiate(self, destination_hash: bytes, timeout_seconds: int = 15) -> AudioCall:
async def initiate(
self, destination_hash: bytes, timeout_seconds: int = 15,
) -> AudioCall:
# determine when to timeout
timeout_after_seconds = time.time() + timeout_seconds
# check if we have a path to the destination
if not RNS.Transport.has_path(destination_hash):
# we don't have a path, so we need to request it
RNS.Transport.request_path(destination_hash)
# wait until we have a path, or give up after the configured timeout
while not RNS.Transport.has_path(destination_hash) and time.time() < timeout_after_seconds:
while (
not RNS.Transport.has_path(destination_hash)
and time.time() < timeout_after_seconds
):
await asyncio.sleep(0.1)
# if we still don't have a path, we can't establish a link, so bail out
@@ -172,14 +174,16 @@ class AudioCallManager:
RNS.Destination.OUT,
RNS.Destination.SINGLE,
"call",
"audio"
"audio",
)
# create link
link = RNS.Link(server_destination)
# wait until we have established a link, or give up after the configured timeout
while link.status is not RNS.Link.ACTIVE and time.time() < timeout_after_seconds:
while (
link.status is not RNS.Link.ACTIVE and time.time() < timeout_after_seconds
):
await asyncio.sleep(0.1)
# if we still haven't established a link, bail out
@@ -192,16 +196,14 @@ class AudioCallManager:
# handle new outgoing call
self.handle_outgoing_call(audio_call)
# todo: this can be optional, it's only being sent by default for ui, can be removed
# TODO: this can be optional, it's only being sent by default for ui, can be removed
link.identify(self.identity)
return audio_call
class AudioCallReceiver:
def __init__(self, manager: AudioCallManager):
self.manager = manager
# create destination for receiving audio calls
@@ -225,7 +227,6 @@ class AudioCallReceiver:
# client connected to us, set up an audio call instance
def client_connected(self, link: RNS.Link):
# check if source is blocked
if self.manager.is_destination_blocked_callback is not None:
try:
@@ -234,14 +235,16 @@ class AudioCallReceiver:
if remote_identity is not None:
source_hash = remote_identity.hash.hex()
if self.manager.is_destination_blocked_callback(source_hash):
print(f"Rejecting audio call from blocked source: {source_hash}")
print(
f"Rejecting audio call from blocked source: {source_hash}",
)
link.teardown()
return
except:
# if we can't get identity yet, we'll check later
pass
# todo: this can be optional, it's only being sent by default for ui, can be removed
# TODO: this can be optional, it's only being sent by default for ui, can be removed
link.identify(self.manager.identity)
# create audio call

View File

@@ -1,10 +1,8 @@
class ColourUtils:
@staticmethod
def hex_colour_to_byte_array(hex_colour):
# remove leading "#"
hex_colour = hex_colour.lstrip('#')
hex_colour = hex_colour.lstrip("#")
# convert the remaining hex string to bytes
return bytes.fromhex(hex_colour)

View File

@@ -2,10 +2,8 @@ import RNS.vendor.configobj
class InterfaceConfigParser:
@staticmethod
def parse(text):
# get lines from provided text
lines = text.splitlines()
stripped_lines = [line.strip() for line in lines]
@@ -30,7 +28,6 @@ class InterfaceConfigParser:
# process interfaces
interfaces = []
for interface_name in config_interfaces:
# ensure interface has a name
interface_config = config_interfaces[interface_name]
interface_config["name"] = interface_name

View File

@@ -1,8 +1,6 @@
class InterfaceEditor:
@staticmethod
def update_value(interface_details: dict, data: dict, key: str):
# update value if provided and not empty
value = data.get(key)
if value is not None and value != "":
@@ -10,5 +8,4 @@ class InterfaceEditor:
return
# otherwise remove existing value
if key in interface_details:
del interface_details[key]
interface_details.pop(key, None)

View File

@@ -8,7 +8,6 @@ from websockets.sync.connection import Connection
class WebsocketClientInterface(Interface):
# TODO: required?
DEFAULT_IFAC_SIZE = 16
@@ -18,7 +17,6 @@ class WebsocketClientInterface(Interface):
return f"WebsocketClientInterface[{self.name}/{self.target_url}]"
def __init__(self, owner, configuration, websocket: Connection = None):
super().__init__()
self.owner = owner
@@ -26,8 +24,8 @@ class WebsocketClientInterface(Interface):
self.IN = True
self.OUT = False
self.HW_MTU = 262144 # 256KiB
self.bitrate = 1_000_000_000 # 1Gbps
self.HW_MTU = 262144 # 256KiB
self.bitrate = 1_000_000_000 # 1Gbps
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
# parse config
@@ -48,7 +46,6 @@ class WebsocketClientInterface(Interface):
# called when a full packet has been received over the websocket
def process_incoming(self, data):
# do nothing if offline or detached
if not self.online or self.detached:
return
@@ -65,7 +62,6 @@ class WebsocketClientInterface(Interface):
# the running reticulum transport instance will call this method whenever the interface must transmit a packet
def process_outgoing(self, data):
# do nothing if offline or detached
if not self.online or self.detached:
return
@@ -74,8 +70,10 @@ class WebsocketClientInterface(Interface):
try:
self.websocket.send(data)
except Exception as e:
RNS.log(f"Exception occurred while transmitting via {str(self)}", RNS.LOG_ERROR)
RNS.log(f"The contained exception was: {str(e)}", RNS.LOG_ERROR)
RNS.log(
f"Exception occurred while transmitting via {self!s}", RNS.LOG_ERROR,
)
RNS.log(f"The contained exception was: {e!s}", RNS.LOG_ERROR)
return
# update sent bytes counter
@@ -87,27 +85,27 @@ class WebsocketClientInterface(Interface):
# connect to the configured websocket server
def connect(self):
# do nothing if interface is detached
if self.detached:
return
# connect to websocket server
try:
RNS.log(f"Connecting to Websocket for {str(self)}...", RNS.LOG_DEBUG)
self.websocket = connect(f"{self.target_url}", max_size=None, compression=None)
RNS.log(f"Connected to Websocket for {str(self)}", RNS.LOG_DEBUG)
RNS.log(f"Connecting to Websocket for {self!s}...", RNS.LOG_DEBUG)
self.websocket = connect(
f"{self.target_url}", max_size=None, compression=None,
)
RNS.log(f"Connected to Websocket for {self!s}", RNS.LOG_DEBUG)
self.read_loop()
except Exception as e:
RNS.log(f"{self} failed with error: {e}", RNS.LOG_ERROR)
# auto reconnect after delay
RNS.log(f"Websocket disconnected for {str(self)}...", RNS.LOG_DEBUG)
RNS.log(f"Websocket disconnected for {self!s}...", RNS.LOG_DEBUG)
time.sleep(self.RECONNECT_DELAY_SECONDS)
self.connect()
def read_loop(self):
self.online = True
try:
@@ -119,7 +117,6 @@ class WebsocketClientInterface(Interface):
self.online = False
def detach(self):
# mark as offline
self.online = False
@@ -130,5 +127,6 @@ class WebsocketClientInterface(Interface):
# mark as detached
self.detached = True
# set interface class RNS should use when importing this external interface
interface_class = WebsocketClientInterface

View File

@@ -3,33 +3,31 @@ import time
import RNS
from RNS.Interfaces.Interface import Interface
from websockets.sync.server import Server
from websockets.sync.server import serve
from websockets.sync.server import ServerConnection
from websockets.sync.server import Server, ServerConnection, serve
from src.backend.interfaces.WebsocketClientInterface import WebsocketClientInterface
class WebsocketServerInterface(Interface):
# TODO: required?
DEFAULT_IFAC_SIZE = 16
RESTART_DELAY_SECONDS = 5
def __str__(self):
return f"WebsocketServerInterface[{self.name}/{self.listen_ip}:{self.listen_port}]"
return (
f"WebsocketServerInterface[{self.name}/{self.listen_ip}:{self.listen_port}]"
)
def __init__(self, owner, configuration):
super().__init__()
self.owner = owner
self.IN = True
self.OUT = False
self.HW_MTU = 262144 # 256KiB
self.bitrate = 1_000_000_000 # 1Gbps
self.HW_MTU = 262144 # 256KiB
self.bitrate = 1_000_000_000 # 1Gbps
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
self.server: Server | None = None
@@ -61,12 +59,12 @@ class WebsocketServerInterface(Interface):
def clients(self):
return len(self.spawned_interfaces)
# todo docs
# TODO docs
def received_announce(self, from_spawned=False):
if from_spawned:
self.ia_freq_deque.append(time.time())
# todo docs
# TODO docs
def sent_announce(self, from_spawned=False):
if from_spawned:
self.oa_freq_deque.append(time.time())
@@ -80,17 +78,19 @@ class WebsocketServerInterface(Interface):
pass
def serve(self):
# handle new websocket client connections
def on_websocket_client_connected(websocket: ServerConnection):
# create new child interface
RNS.log("Accepting incoming WebSocket connection", RNS.LOG_VERBOSE)
spawned_interface = WebsocketClientInterface(self.owner, {
"name": f"Client on {self.name}",
"target_host": websocket.remote_address[0],
"target_port": str(websocket.remote_address[1]),
}, websocket=websocket)
spawned_interface = WebsocketClientInterface(
self.owner,
{
"name": f"Client on {self.name}",
"target_host": websocket.remote_address[0],
"target_port": str(websocket.remote_address[1]),
},
websocket=websocket,
)
# configure child interface
spawned_interface.IN = self.IN
@@ -101,16 +101,19 @@ class WebsocketServerInterface(Interface):
spawned_interface.parent_interface = self
spawned_interface.online = True
# todo implement?
# TODO implement?
spawned_interface.announce_rate_target = None
spawned_interface.announce_rate_grace = None
spawned_interface.announce_rate_penalty = None
# todo ifac?
# todo announce rates?
# TODO ifac?
# TODO announce rates?
# activate child interface
RNS.log(f"Spawned new WebsocketClientInterface: {spawned_interface}", RNS.LOG_VERBOSE)
RNS.log(
f"Spawned new WebsocketClientInterface: {spawned_interface}",
RNS.LOG_VERBOSE,
)
RNS.Transport.interfaces.append(spawned_interface)
# associate child interface with this interface
@@ -126,8 +129,13 @@ class WebsocketServerInterface(Interface):
# run websocket server
try:
RNS.log(f"Starting Websocket server for {str(self)}...", RNS.LOG_DEBUG)
with serve(on_websocket_client_connected, self.listen_ip, self.listen_port, compression=None) as server:
RNS.log(f"Starting Websocket server for {self!s}...", RNS.LOG_DEBUG)
with serve(
on_websocket_client_connected,
self.listen_ip,
self.listen_port,
compression=None,
) as server:
self.online = True
self.server = server
server.serve_forever()
@@ -136,12 +144,11 @@ class WebsocketServerInterface(Interface):
# websocket server is no longer running, let's restart it
self.online = False
RNS.log(f"Websocket server stopped for {str(self)}...", RNS.LOG_DEBUG)
RNS.log(f"Websocket server stopped for {self!s}...", RNS.LOG_DEBUG)
time.sleep(self.RESTART_DELAY_SECONDS)
self.serve()
def detach(self):
# mark as offline
self.online = False
@@ -152,5 +159,6 @@ class WebsocketServerInterface(Interface):
# mark as detached
self.detached = True
# set interface class RNS should use when importing this external interface
interface_class = WebsocketServerInterface

View File

@@ -1,9 +1,5 @@
from typing import List
# helper class for passing around an lxmf audio field
class LxmfAudioField:
def __init__(self, audio_mode: int, audio_bytes: bytes):
self.audio_mode = audio_mode
self.audio_bytes = audio_bytes
@@ -11,7 +7,6 @@ class LxmfAudioField:
# helper class for passing around an lxmf image field
class LxmfImageField:
def __init__(self, image_type: str, image_bytes: bytes):
self.image_type = image_type
self.image_bytes = image_bytes
@@ -19,7 +14,6 @@ class LxmfImageField:
# helper class for passing around an lxmf file attachment
class LxmfFileAttachment:
def __init__(self, file_name: str, file_bytes: bytes):
self.file_name = file_name
self.file_bytes = file_bytes
@@ -27,7 +21,5 @@ class LxmfFileAttachment:
# helper class for passing around an lxmf file attachments field
class LxmfFileAttachmentsField:
def __init__(self, file_attachments: List[LxmfFileAttachment]):
def __init__(self, file_attachments: list[LxmfFileAttachment]):
self.file_attachments = file_attachments

View File

@@ -161,6 +161,13 @@
<span class="setting-toggle__description">Failed direct deliveries are queued on your preferred propagation node.</span>
</span>
</label>
<div class="space-y-2">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">Inbound Message Stamp Cost</div>
<input v-model.number="config.lxmf_inbound_stamp_cost" @input="onLxmfInboundStampCostChange" type="number" min="1" max="254" placeholder="8" class="input-field">
<div class="text-xs text-gray-600 dark:text-gray-400">
Require proof-of-work stamps for direct delivery messages sent to you. Higher values require more computational work from senders. Range: 1-254. Default: 8.
</div>
</div>
</div>
</section>
@@ -214,6 +221,13 @@
<span v-else>Last synced: never.</span>
</div>
</div>
<div v-if="config.lxmf_local_propagation_node_enabled" class="space-y-2">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">Propagation Node Stamp Cost</div>
<input v-model.number="config.lxmf_propagation_node_stamp_cost" @input="onLxmfPropagationNodeStampCostChange" type="number" min="13" max="254" placeholder="16" class="input-field">
<div class="text-xs text-gray-600 dark:text-gray-400">
Require proof-of-work stamps for messages propagated through your node. Higher values require more computational work. Range: 13-254. Default: 16. <strong>Note:</strong> Changing this requires restarting the app.
</div>
</div>
</div>
</section>
@@ -350,6 +364,16 @@ export default {
"lxmf_preferred_propagation_node_auto_sync_interval_seconds": this.config.lxmf_preferred_propagation_node_auto_sync_interval_seconds,
});
},
async onLxmfInboundStampCostChange() {
await this.updateConfig({
"lxmf_inbound_stamp_cost": this.config.lxmf_inbound_stamp_cost,
});
},
async onLxmfPropagationNodeStampCostChange() {
await this.updateConfig({
"lxmf_propagation_node_stamp_cost": this.config.lxmf_propagation_node_stamp_cost,
});
},
async onIsTransportEnabledChange() {
if(this.config.is_transport_enabled){
try {