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 ## Features of this fork
- [x] Custom UI/UX (actively being improved) - [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] Better config parsing
- [x] Cancel page fetching or file downloads - [x] Cancel page fetching or file downloads
- [ ] Block users - [x] Block users
- [ ] Spam filter (based on keywords) - [x] Spam filter (based on keywords)
- [ ] Multi-identity support - [ ] Multi-identity support
- [x] More stats on about page - [x] More stats on about page
- [x] Actions are pinned to full-length SHA hashes. - [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 datetime import datetime, timezone
from peewee import * 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 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) migrator = SqliteMigrator(database)
# migrates the database # migrates the database
def migrate(current_version): def migrate(current_version):
# migrate to version 2 # migrate to version 2
if current_version < 2: if current_version < 2:
migrate_database( migrate_database(
migrator.add_column("lxmf_messages", 'delivery_attempts', LxmfMessage.delivery_attempts), migrator.add_column(
migrator.add_column("lxmf_messages", 'next_delivery_attempt_at', LxmfMessage.next_delivery_attempt_at), "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 # migrate to version 3
if current_version < 3: if current_version < 3:
migrate_database( migrate_database(
migrator.add_column("lxmf_messages", 'rssi', LxmfMessage.rssi), migrator.add_column("lxmf_messages", "rssi", LxmfMessage.rssi),
migrator.add_column("lxmf_messages", 'snr', LxmfMessage.snr), migrator.add_column("lxmf_messages", "snr", LxmfMessage.snr),
migrator.add_column("lxmf_messages", 'quality', LxmfMessage.quality), migrator.add_column("lxmf_messages", "quality", LxmfMessage.quality),
) )
# migrate to version 4 # migrate to version 4
if current_version < 4: if current_version < 4:
migrate_database( migrate_database(
migrator.add_column("lxmf_messages", 'method', LxmfMessage.method), migrator.add_column("lxmf_messages", "method", LxmfMessage.method),
) )
# migrate to version 5 # migrate to version 5
if current_version < 5: if current_version < 5:
migrate_database( migrate_database(
migrator.add_column("announces", 'rssi', Announce.rssi), migrator.add_column("announces", "rssi", Announce.rssi),
migrator.add_column("announces", 'snr', Announce.snr), migrator.add_column("announces", "snr", Announce.snr),
migrator.add_column("announces", 'quality', Announce.quality), migrator.add_column("announces", "quality", Announce.quality),
) )
# migrate to version 6 # migrate to version 6
if current_version < 6: if current_version < 6:
migrate_database( 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 return latest_version
@@ -55,7 +63,6 @@ class BaseModel(Model):
class Config(BaseModel): class Config(BaseModel):
id = BigAutoField() id = BigAutoField()
key = CharField(unique=True) key = CharField(unique=True)
value = TextField() value = TextField()
@@ -68,12 +75,19 @@ class Config(BaseModel):
class Announce(BaseModel): class Announce(BaseModel):
id = BigAutoField() id = BigAutoField()
destination_hash = CharField(unique=True) # unique destination hash that was announced destination_hash = CharField(
aspect = TextField(index=True) # aspect is not included in announce, but we want to filter saved announces by aspect unique=True,
identity_hash = CharField(index=True) # identity hash that announced the destination ) # unique destination hash that was announced
identity_public_key = CharField() # base64 encoded public key, incase we want to recreate the identity manually 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 app_data = TextField(null=True) # base64 encoded app data bytes
rssi = IntegerField(null=True) rssi = IntegerField(null=True)
snr = FloatField(null=True) snr = FloatField(null=True)
@@ -88,7 +102,6 @@ class Announce(BaseModel):
class CustomDestinationDisplayName(BaseModel): class CustomDestinationDisplayName(BaseModel):
id = BigAutoField() id = BigAutoField()
destination_hash = CharField(unique=True) # unique destination hash destination_hash = CharField(unique=True) # unique destination hash
display_name = CharField() # custom display name for the destination hash display_name = CharField() # custom display name for the destination hash
@@ -102,7 +115,6 @@ class CustomDestinationDisplayName(BaseModel):
class FavouriteDestination(BaseModel): class FavouriteDestination(BaseModel):
id = BigAutoField() id = BigAutoField()
destination_hash = CharField(unique=True) # unique destination hash destination_hash = CharField(unique=True) # unique destination hash
display_name = CharField() # custom display name for the destination hash display_name = CharField() # custom display name for the destination hash
@@ -117,21 +129,30 @@ class FavouriteDestination(BaseModel):
class LxmfMessage(BaseModel): class LxmfMessage(BaseModel):
id = BigAutoField() id = BigAutoField()
hash = CharField(unique=True) # unique lxmf message hash hash = CharField(unique=True) # unique lxmf message hash
source_hash = CharField(index=True) source_hash = CharField(index=True)
destination_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) 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 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 method = CharField(
delivery_attempts = IntegerField(default=0) # how many times delivery has been attempted for this message null=True,
next_delivery_attempt_at = FloatField(null=True) # timestamp of when the message will attempt delivery again ) # 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() title = TextField()
content = TextField() content = TextField()
fields = TextField() # json string 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) rssi = IntegerField(null=True)
snr = FloatField(null=True) snr = FloatField(null=True)
quality = FloatField(null=True) quality = FloatField(null=True)
@@ -145,7 +166,6 @@ class LxmfMessage(BaseModel):
class LxmfConversationReadState(BaseModel): class LxmfConversationReadState(BaseModel):
id = BigAutoField() id = BigAutoField()
destination_hash = CharField(unique=True) # unique destination hash destination_hash = CharField(unique=True) # unique destination hash
last_read_at = DateTimeField() last_read_at = DateTimeField()
@@ -159,12 +179,13 @@ class LxmfConversationReadState(BaseModel):
class LxmfUserIcon(BaseModel): class LxmfUserIcon(BaseModel):
id = BigAutoField() id = BigAutoField()
destination_hash = CharField(unique=True) # unique destination hash destination_hash = CharField(unique=True) # unique destination hash
icon_name = CharField() # material design icon name for the destination hash icon_name = CharField() # material design icon name for the destination hash
foreground_colour = CharField() # hex colour to use for foreground (icon colour) 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)) created_at = DateTimeField(default=lambda: datetime.now(timezone.utc))
updated_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): class BlockedDestination(BaseModel):
id = BigAutoField() 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)) created_at = DateTimeField(default=lambda: datetime.now(timezone.utc))
updated_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): class SpamKeyword(BaseModel):
id = BigAutoField() 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)) created_at = DateTimeField(default=lambda: datetime.now(timezone.utc))
updated_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( setup(
name='ReticulumMeshChatX', name="ReticulumMeshChatX",
version='1.0.0', version="1.0.0",
description='A simple mesh network communications app powered by the Reticulum Network Stack', description="A simple mesh network communications app powered by the Reticulum Network Stack",
executables=[ executables=[
Executable( Executable(
script='meshchat.py', # this script to run script="meshchat.py", # this script to run
base=None, # we are running a console application, not a gui base=None, # we are running a console application, not a gui
target_name='ReticulumMeshChatX', # creates ReticulumMeshChatX.exe target_name="ReticulumMeshChatX", # creates ReticulumMeshChatX.exe
shortcut_name='ReticulumMeshChatX', # name shown in shortcut shortcut_name="ReticulumMeshChatX", # name shown in shortcut
shortcut_dir='ProgramMenuFolder', # put the shortcut in windows start menu shortcut_dir="ProgramMenuFolder", # put the shortcut in windows start menu
icon='logo/icon.ico', # set the icon for the exe icon="logo/icon.ico", # set the icon for the exe
copyright='Copyright (c) 2024 Liam Cottle', copyright="Copyright (c) 2024 Liam Cottle",
), ),
], ],
options={ options={
'build_exe': { "build_exe": {
# libs that are required # libs that are required
'packages': [ "packages": [
# required for dynamic import fix # required for dynamic import fix
# https://github.com/marcelotduarte/cx_Freeze/discussions/2039 # https://github.com/marcelotduarte/cx_Freeze/discussions/2039
# https://github.com/marcelotduarte/cx_Freeze/issues/2041 # https://github.com/marcelotduarte/cx_Freeze/issues/2041
'RNS', "RNS",
'RNS.Interfaces', "RNS.Interfaces",
'LXMF', "LXMF",
], ],
# files that are required # files that are required
'include_files': [ "include_files": [
'package.json', # used to determine app version from python "package.json", # used to determine app version from python
'public/', # static files served by web server "public/", # static files served by web server
], ],
# slim down the build by excluding these unused libs # slim down the build by excluding these unused libs
'excludes': [ "excludes": [
'PIL', # saves ~200MB "PIL", # saves ~200MB
], ],
# this has the same effect as the -O command line option when executing CPython directly. # 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. # it also prevents assert statements from executing, removes docstrings and sets __debug__ to False.
# https://stackoverflow.com/a/57948104 # https://stackoverflow.com/a/57948104
"optimize": 2, "optimize": 2,
# change where exe is built to # change where exe is built to
'build_exe': 'build/exe', "build_exe": "build/exe",
# make the build relocatable by replacing absolute paths # 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 # this class forces stream writes to be flushed immediately
class ImmediateFlushingStreamWrapper: class ImmediateFlushingStreamWrapper:
def __init__(self, stream): def __init__(self, stream):
self.stream = 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 # 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 # this handler exists so we can have access to the original aspect, as this is not provided in the announce itself
class AnnounceHandler: class AnnounceHandler:
def __init__(self, aspect_filter: str, received_announce_callback): def __init__(self, aspect_filter: str, received_announce_callback):
self.aspect_filter = aspect_filter self.aspect_filter = aspect_filter
self.received_announce_callback = received_announce_callback self.received_announce_callback = received_announce_callback
# we will just pass the received announce back to the provided 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: try:
# handle received announce # 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: except:
# ignore failure to handle received announce # ignore failure to handle received announce
pass pass

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,5 @@
from typing import List
# helper class for passing around an lxmf audio field # helper class for passing around an lxmf audio field
class LxmfAudioField: class LxmfAudioField:
def __init__(self, audio_mode: int, audio_bytes: bytes): def __init__(self, audio_mode: int, audio_bytes: bytes):
self.audio_mode = audio_mode self.audio_mode = audio_mode
self.audio_bytes = audio_bytes self.audio_bytes = audio_bytes
@@ -11,7 +7,6 @@ class LxmfAudioField:
# helper class for passing around an lxmf image field # helper class for passing around an lxmf image field
class LxmfImageField: class LxmfImageField:
def __init__(self, image_type: str, image_bytes: bytes): def __init__(self, image_type: str, image_bytes: bytes):
self.image_type = image_type self.image_type = image_type
self.image_bytes = image_bytes self.image_bytes = image_bytes
@@ -19,7 +14,6 @@ class LxmfImageField:
# helper class for passing around an lxmf file attachment # helper class for passing around an lxmf file attachment
class LxmfFileAttachment: class LxmfFileAttachment:
def __init__(self, file_name: str, file_bytes: bytes): def __init__(self, file_name: str, file_bytes: bytes):
self.file_name = file_name self.file_name = file_name
self.file_bytes = file_bytes self.file_bytes = file_bytes
@@ -27,7 +21,5 @@ class LxmfFileAttachment:
# helper class for passing around an lxmf file attachments field # helper class for passing around an lxmf file attachments field
class LxmfFileAttachmentsField: class LxmfFileAttachmentsField:
def __init__(self, file_attachments: list[LxmfFileAttachment]):
def __init__(self, file_attachments: List[LxmfFileAttachment]):
self.file_attachments = file_attachments 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 class="setting-toggle__description">Failed direct deliveries are queued on your preferred propagation node.</span>
</span> </span>
</label> </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> </div>
</section> </section>
@@ -214,6 +221,13 @@
<span v-else>Last synced: never.</span> <span v-else>Last synced: never.</span>
</div> </div>
</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> </div>
</section> </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, "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() { async onIsTransportEnabledChange() {
if(this.config.is_transport_enabled){ if(this.config.is_transport_enabled){
try { try {