code cleanup
This commit is contained in:
@@ -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
31
TODO.md
Normal 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
|
||||||
91
database.py
91
database.py
@@ -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))
|
||||||
|
|
||||||
|
|||||||
3164
meshchat.py
3164
meshchat.py
File diff suppressed because it is too large
Load Diff
48
setup.py
48
setup.py
@@ -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": [
|
||||||
('*', ''),
|
("*", ""),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user