Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09bd78e194 | ||
|
|
3288fea934 | ||
|
|
47f544e2ee | ||
|
|
be3d8f1e80 | ||
|
|
01b1251589 | ||
|
|
936c298e15 | ||
|
|
f5dc06ab88 | ||
|
|
24e2ac9c65 |
70
.github/workflows/build.yml
vendored
70
.github/workflows/build.yml
vendored
@@ -17,12 +17,12 @@ jobs:
|
||||
- name: Install NodeJS
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 22
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install Python Deps
|
||||
run: pip install -r requirements.txt
|
||||
@@ -44,43 +44,43 @@ jobs:
|
||||
omitNameDuringUpdate: true
|
||||
artifacts: "dist/*-win-installer.exe,dist/*-win-portable.exe"
|
||||
|
||||
build_mac:
|
||||
runs-on: macos-13
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Clone Repo
|
||||
uses: actions/checkout@v1
|
||||
# build_mac:
|
||||
# runs-on: macos-13
|
||||
# permissions:
|
||||
# contents: write
|
||||
# steps:
|
||||
# - name: Clone Repo
|
||||
# uses: actions/checkout@v1
|
||||
|
||||
- name: Install NodeJS
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 18
|
||||
# - name: Install NodeJS
|
||||
# uses: actions/setup-node@v1
|
||||
# with:
|
||||
# node-version: 20
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
# - name: Install Python
|
||||
# uses: actions/setup-python@v5
|
||||
# with:
|
||||
# python-version: "3.13"
|
||||
|
||||
- name: Install Python Deps
|
||||
run: pip install -r requirements.txt
|
||||
# - name: Install Python Deps
|
||||
# run: pip install -r requirements.txt
|
||||
|
||||
- name: Install NodeJS Deps
|
||||
run: npm install
|
||||
# - name: Install NodeJS Deps
|
||||
# run: npm install
|
||||
|
||||
- name: Build Electron App
|
||||
run: npm run dist
|
||||
# - name: Build Electron App
|
||||
# run: npm run dist
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
draft: true
|
||||
allowUpdates: true
|
||||
replacesArtifacts: true
|
||||
omitDraftDuringUpdate: true
|
||||
omitNameDuringUpdate: true
|
||||
artifacts: "dist/*-mac.dmg"
|
||||
# - name: Create Release
|
||||
# id: create_release
|
||||
# uses: ncipollo/release-action@v1
|
||||
# with:
|
||||
# draft: true
|
||||
# allowUpdates: true
|
||||
# replacesArtifacts: true
|
||||
# omitDraftDuringUpdate: true
|
||||
# omitNameDuringUpdate: true
|
||||
# artifacts: "dist/*-mac.dmg"
|
||||
|
||||
build_linux:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -93,12 +93,12 @@ jobs:
|
||||
- name: Install NodeJS
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 22
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install Python Deps
|
||||
run: pip install -r requirements.txt
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build the frontend
|
||||
FROM node:20-alpine AS build-frontend
|
||||
FROM node:22-alpine AS build-frontend
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
|
||||
14
README.md
14
README.md
@@ -1,11 +1,15 @@
|
||||
# Ivans Fork Edition
|
||||
|
||||
## Containers
|
||||
## Changes
|
||||
|
||||
- Drop unnecassary permissions
|
||||
- Rootless
|
||||
- Resource Limits
|
||||
- Alpine Image Variants
|
||||
- Drop unnecassary permissions (compose)
|
||||
- Rootless (user 1000:1000)
|
||||
- Resource Limits (compose)
|
||||
- Alpine Image Variants.
|
||||
- Updated Dependencies.
|
||||
- Dark mode by default.
|
||||
- Python 3.13 and Node 20.
|
||||
- Ruff formatting and fixes.
|
||||
|
||||
## Security
|
||||
|
||||
|
||||
75
database.py
75
database.py
@@ -4,40 +4,47 @@ from peewee import *
|
||||
from playhouse.migrate import migrate as migrate_database, SqliteMigrator
|
||||
|
||||
latest_version = 5 # increment each time new database migrations are added
|
||||
database = DatabaseProxy() # use a proxy object, as we will init real db client inside meshchat.py
|
||||
database = (
|
||||
DatabaseProxy()
|
||||
) # use a proxy object, as we will init real db client inside meshchat.py
|
||||
migrator = SqliteMigrator(database)
|
||||
|
||||
|
||||
# migrates the database
|
||||
def migrate(current_version):
|
||||
|
||||
# migrate to version 2
|
||||
if current_version < 2:
|
||||
migrate_database(
|
||||
migrator.add_column("lxmf_messages", 'delivery_attempts', LxmfMessage.delivery_attempts),
|
||||
migrator.add_column("lxmf_messages", 'next_delivery_attempt_at', LxmfMessage.next_delivery_attempt_at),
|
||||
migrator.add_column(
|
||||
"lxmf_messages", "delivery_attempts", LxmfMessage.delivery_attempts
|
||||
),
|
||||
migrator.add_column(
|
||||
"lxmf_messages",
|
||||
"next_delivery_attempt_at",
|
||||
LxmfMessage.next_delivery_attempt_at,
|
||||
),
|
||||
)
|
||||
|
||||
# migrate to version 3
|
||||
if current_version < 3:
|
||||
migrate_database(
|
||||
migrator.add_column("lxmf_messages", 'rssi', LxmfMessage.rssi),
|
||||
migrator.add_column("lxmf_messages", 'snr', LxmfMessage.snr),
|
||||
migrator.add_column("lxmf_messages", 'quality', LxmfMessage.quality),
|
||||
migrator.add_column("lxmf_messages", "rssi", LxmfMessage.rssi),
|
||||
migrator.add_column("lxmf_messages", "snr", LxmfMessage.snr),
|
||||
migrator.add_column("lxmf_messages", "quality", LxmfMessage.quality),
|
||||
)
|
||||
|
||||
# migrate to version 4
|
||||
if current_version < 4:
|
||||
migrate_database(
|
||||
migrator.add_column("lxmf_messages", 'method', LxmfMessage.method),
|
||||
migrator.add_column("lxmf_messages", "method", LxmfMessage.method),
|
||||
)
|
||||
|
||||
# migrate to version 5
|
||||
if current_version < 5:
|
||||
migrate_database(
|
||||
migrator.add_column("announces", 'rssi', Announce.rssi),
|
||||
migrator.add_column("announces", 'snr', Announce.snr),
|
||||
migrator.add_column("announces", 'quality', Announce.quality),
|
||||
migrator.add_column("announces", "rssi", Announce.rssi),
|
||||
migrator.add_column("announces", "snr", Announce.snr),
|
||||
migrator.add_column("announces", "quality", Announce.quality),
|
||||
)
|
||||
|
||||
return latest_version
|
||||
@@ -49,7 +56,6 @@ class BaseModel(Model):
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
|
||||
id = BigAutoField()
|
||||
key = CharField(unique=True)
|
||||
value = TextField()
|
||||
@@ -62,12 +68,19 @@ class Config(BaseModel):
|
||||
|
||||
|
||||
class Announce(BaseModel):
|
||||
|
||||
id = BigAutoField()
|
||||
destination_hash = CharField(unique=True) # unique destination hash that was announced
|
||||
aspect = TextField(index=True) # aspect is not included in announce, but we want to filter saved announces by aspect
|
||||
identity_hash = CharField(index=True) # identity hash that announced the destination
|
||||
identity_public_key = CharField() # base64 encoded public key, incase we want to recreate the identity manually
|
||||
destination_hash = CharField(
|
||||
unique=True
|
||||
) # unique destination hash that was announced
|
||||
aspect = TextField(
|
||||
index=True
|
||||
) # aspect is not included in announce, but we want to filter saved announces by aspect
|
||||
identity_hash = CharField(
|
||||
index=True
|
||||
) # identity hash that announced the destination
|
||||
identity_public_key = (
|
||||
CharField()
|
||||
) # base64 encoded public key, incase we want to recreate the identity manually
|
||||
app_data = TextField(null=True) # base64 encoded app data bytes
|
||||
rssi = IntegerField(null=True)
|
||||
snr = FloatField(null=True)
|
||||
@@ -82,7 +95,6 @@ class Announce(BaseModel):
|
||||
|
||||
|
||||
class CustomDestinationDisplayName(BaseModel):
|
||||
|
||||
id = BigAutoField()
|
||||
destination_hash = CharField(unique=True) # unique destination hash
|
||||
display_name = CharField() # custom display name for the destination hash
|
||||
@@ -96,21 +108,30 @@ class CustomDestinationDisplayName(BaseModel):
|
||||
|
||||
|
||||
class LxmfMessage(BaseModel):
|
||||
|
||||
id = BigAutoField()
|
||||
hash = CharField(unique=True) # unique lxmf message hash
|
||||
source_hash = CharField(index=True)
|
||||
destination_hash = CharField(index=True)
|
||||
state = CharField() # state is converted from internal int to a human friendly string
|
||||
state = (
|
||||
CharField()
|
||||
) # state is converted from internal int to a human friendly string
|
||||
progress = FloatField() # progress is converted from internal float 0.00-1.00 to float between 0.00/100 (2 decimal places)
|
||||
is_incoming = BooleanField() # if true, we should ignore state, it's set to draft by default on incoming messages
|
||||
method = CharField(null=True) # what method is being used to send the message, e.g: direct, propagated
|
||||
delivery_attempts = IntegerField(default=0) # how many times delivery has been attempted for this message
|
||||
next_delivery_attempt_at = FloatField(null=True) # timestamp of when the message will attempt delivery again
|
||||
method = CharField(
|
||||
null=True
|
||||
) # what method is being used to send the message, e.g: direct, propagated
|
||||
delivery_attempts = IntegerField(
|
||||
default=0
|
||||
) # how many times delivery has been attempted for this message
|
||||
next_delivery_attempt_at = FloatField(
|
||||
null=True
|
||||
) # timestamp of when the message will attempt delivery again
|
||||
title = TextField()
|
||||
content = TextField()
|
||||
fields = TextField() # json string
|
||||
timestamp = FloatField() # timestamp of when the message was originally created (before ever being sent)
|
||||
timestamp = (
|
||||
FloatField()
|
||||
) # timestamp of when the message was originally created (before ever being sent)
|
||||
rssi = IntegerField(null=True)
|
||||
snr = FloatField(null=True)
|
||||
quality = FloatField(null=True)
|
||||
@@ -123,7 +144,6 @@ class LxmfMessage(BaseModel):
|
||||
|
||||
|
||||
class LxmfConversationReadState(BaseModel):
|
||||
|
||||
id = BigAutoField()
|
||||
destination_hash = CharField(unique=True) # unique destination hash
|
||||
last_read_at = DateTimeField()
|
||||
@@ -137,12 +157,13 @@ class LxmfConversationReadState(BaseModel):
|
||||
|
||||
|
||||
class LxmfUserIcon(BaseModel):
|
||||
|
||||
id = BigAutoField()
|
||||
destination_hash = CharField(unique=True) # unique destination hash
|
||||
icon_name = CharField() # material design icon name for the destination hash
|
||||
foreground_colour = CharField() # hex colour to use for foreground (icon colour)
|
||||
background_colour = CharField() # hex colour to use for background (background colour)
|
||||
background_colour = (
|
||||
CharField()
|
||||
) # hex colour to use for background (background colour)
|
||||
|
||||
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
@@ -99,7 +99,9 @@ function getDefaultReticulumConfigDir() {
|
||||
app.whenReady().then(async () => {
|
||||
|
||||
// get arguments passed to application, and remove the provided application path
|
||||
const userProvidedArguments = process.argv.slice(1);
|
||||
const ignoredArguments = ["--no-sandbox"];
|
||||
const userProvidedArguments = process.argv.slice(1)
|
||||
.filter(arg => !ignoredArguments.includes(arg));
|
||||
const shouldLaunchHeadless = userProvidedArguments.includes("--headless");
|
||||
|
||||
if(!shouldLaunchHeadless){
|
||||
|
||||
2488
meshchat.py
2488
meshchat.py
File diff suppressed because it is too large
Load Diff
58
package-lock.json
generated
58
package-lock.json
generated
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "reticulum-meshchat",
|
||||
"version": "1.21.0",
|
||||
"version": "1.22.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "reticulum-meshchat",
|
||||
"version": "1.21.0",
|
||||
"version": "1.22.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^1.7.9",
|
||||
"axios": "^1.9.0",
|
||||
"click-outside-vue3": "^4.0.1",
|
||||
"compressorjs": "^1.2.1",
|
||||
"electron-prompt": "^1.7.0",
|
||||
@@ -21,21 +21,21 @@
|
||||
"mitt": "^3.0.1",
|
||||
"moment": "^2.30.1",
|
||||
"postcss": "^8.4.49",
|
||||
"protobufjs": "^7.4.0",
|
||||
"protobufjs": "^7.5.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"vis-data": "^7.1.9",
|
||||
"vis-network": "^9.1.9",
|
||||
"vite": "^6.0.5",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-vuetify": "^2.0.4",
|
||||
"vue-router": "^4.5.0",
|
||||
"vuetify": "^3.7.6"
|
||||
"vue-router": "^4.5.1",
|
||||
"vuetify": "^3.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^30.0.8",
|
||||
"electron-builder": "^24.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
@@ -1577,9 +1577,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz",
|
||||
"integrity": "sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==",
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
|
||||
"integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
@@ -2103,9 +2103,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.8.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
|
||||
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
@@ -5206,9 +5206,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.0.tgz",
|
||||
"integrity": "sha512-Z2E/kOY1QjoMlCytmexzYfDm/w5fKAiRwpSzGtdnXW1zC88Z2yXazHHrOtwCzn+7wSxyE8PYM4rvVcMphF9sOA==",
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.1.tgz",
|
||||
"integrity": "sha512-3qx3IRjR9WPQKagdwrKjO3Gu8RgQR2qqw+1KnigWhoVjFqegIj1K3bP11sGqhxrO46/XL7lekuG4jmjL+4cLsw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
@@ -6321,17 +6321,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.2.tgz",
|
||||
"integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==",
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.3",
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2",
|
||||
"postcss": "^8.5.3",
|
||||
"rollup": "^4.34.9",
|
||||
"tinyglobby": "^0.2.12"
|
||||
"tinyglobby": "^0.2.13"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@@ -6462,9 +6462,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz",
|
||||
"integrity": "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==",
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
||||
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.6.4"
|
||||
@@ -6477,9 +6477,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vuetify": {
|
||||
"version": "3.8.2",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.8.2.tgz",
|
||||
"integrity": "sha512-UJNFP4egmKJTQ3V3MKOq+7vIUKO7/Fko5G6yUsOW2Rm0VNBvAjgO6VY6EnK3DTqEKN6ugVXDEPw37NQSTGLZvw==",
|
||||
"version": "3.8.4",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.8.4.tgz",
|
||||
"integrity": "sha512-hfA1eqA6vhrF5LF8Yfk0uHdNUmh8Uckxn5wREiThO82HW/9Vfreh+IpxPgEtCsAhV33KW+NVamltQCu3HczRKw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20 || >=14.13"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "reticulum-meshchat",
|
||||
"version": "1.21.0",
|
||||
"version": "1.22.2",
|
||||
"description": "",
|
||||
"main": "electron/main.js",
|
||||
"scripts": {
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^30.0.8",
|
||||
|
||||
38
setup.py
38
setup.py
@@ -1,44 +1,44 @@
|
||||
from cx_Freeze import setup, Executable
|
||||
|
||||
setup(
|
||||
name='ReticulumMeshChat',
|
||||
version='1.0.0',
|
||||
description='A simple mesh network communications app powered by the Reticulum Network Stack',
|
||||
name="ReticulumMeshChat",
|
||||
version="1.0.0",
|
||||
description="A simple mesh network communications app powered by the Reticulum Network Stack",
|
||||
executables=[
|
||||
Executable(
|
||||
script='meshchat.py', # this script to run
|
||||
base=None, # we are running a console application, not a gui
|
||||
target_name='ReticulumMeshChat', # creates ReticulumMeshChat.exe
|
||||
shortcut_name='ReticulumMeshChat', # name shown in shortcut
|
||||
shortcut_dir='ProgramMenuFolder', # put the shortcut in windows start menu
|
||||
icon='logo/icon.ico', # set the icon for the exe
|
||||
copyright='Copyright (c) 2024 Liam Cottle',
|
||||
script="meshchat.py", # this script to run
|
||||
base=None, # we are running a console application, not a gui
|
||||
target_name="ReticulumMeshChat", # creates ReticulumMeshChat.exe
|
||||
shortcut_name="ReticulumMeshChat", # name shown in shortcut
|
||||
shortcut_dir="ProgramMenuFolder", # put the shortcut in windows start menu
|
||||
icon="logo/icon.ico", # set the icon for the exe
|
||||
copyright="Copyright (c) 2024 Liam Cottle",
|
||||
),
|
||||
],
|
||||
options={
|
||||
'build_exe': {
|
||||
"build_exe": {
|
||||
# libs that are required
|
||||
'packages': [
|
||||
"packages": [
|
||||
# required for dynamic import fix
|
||||
# https://github.com/marcelotduarte/cx_Freeze/discussions/2039
|
||||
# https://github.com/marcelotduarte/cx_Freeze/issues/2041
|
||||
'RNS',
|
||||
"RNS",
|
||||
],
|
||||
# files that are required
|
||||
'include_files': [
|
||||
'package.json', # used to determine app version from python
|
||||
'public/', # static files served by web server
|
||||
"include_files": [
|
||||
"package.json", # used to determine app version from python
|
||||
"public/", # static files served by web server
|
||||
],
|
||||
# slim down the build by excluding these unused libs
|
||||
'excludes': [
|
||||
'PIL', # saves ~200MB
|
||||
"excludes": [
|
||||
"PIL", # saves ~200MB
|
||||
],
|
||||
# this has the same effect as the -O command line option when executing CPython directly.
|
||||
# it also prevents assert statements from executing, removes docstrings and sets __debug__ to False.
|
||||
# https://stackoverflow.com/a/57948104
|
||||
"optimize": 2,
|
||||
# change where exe is built to
|
||||
'build_exe': 'build/exe',
|
||||
"build_exe": "build/exe",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -7,7 +7,6 @@ import sys
|
||||
|
||||
# this class forces stream writes to be flushed immediately
|
||||
class ImmediateFlushingStreamWrapper:
|
||||
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
# an announce handler that forwards announces to a provided callback for the provided aspect filter
|
||||
# this handler exists so we can have access to the original aspect, as this is not provided in the announce itself
|
||||
class AnnounceHandler:
|
||||
|
||||
def __init__(self, aspect_filter: str, received_announce_callback):
|
||||
self.aspect_filter = aspect_filter
|
||||
self.received_announce_callback = received_announce_callback
|
||||
|
||||
# we will just pass the received announce back to the provided callback
|
||||
def received_announce(self, destination_hash, announced_identity, app_data, announce_packet_hash):
|
||||
def received_announce(
|
||||
self, destination_hash, announced_identity, app_data, announce_packet_hash
|
||||
):
|
||||
try:
|
||||
# handle received announce
|
||||
self.received_announce_callback(self.aspect_filter, destination_hash, announced_identity, app_data, announce_packet_hash)
|
||||
self.received_announce_callback(
|
||||
self.aspect_filter,
|
||||
destination_hash,
|
||||
announced_identity,
|
||||
app_data,
|
||||
announce_packet_hash,
|
||||
)
|
||||
except:
|
||||
# ignore failure to handle received announce
|
||||
pass
|
||||
|
||||
@@ -2,12 +2,10 @@ import asyncio
|
||||
|
||||
|
||||
class AsyncUtils:
|
||||
|
||||
# this method allows running the provided async coroutine from within a sync function
|
||||
# it will run the async function on the existing event loop if available, otherwise it will start a new event loop
|
||||
@staticmethod
|
||||
def run_async(coroutine):
|
||||
|
||||
# attempt to get existing event loop
|
||||
existing_event_loop = None
|
||||
try:
|
||||
|
||||
@@ -13,7 +13,6 @@ class CallFailedException(Exception):
|
||||
|
||||
|
||||
class AudioCall:
|
||||
|
||||
def __init__(self, link: RNS.Link, is_outbound: bool):
|
||||
self.link = link
|
||||
self.is_outbound = is_outbound
|
||||
@@ -41,21 +40,25 @@ class AudioCall:
|
||||
|
||||
# handle packet received over link
|
||||
def on_packet(self, message, packet):
|
||||
|
||||
# send audio received from call initiator to all audio packet listeners
|
||||
for audio_packet_listener in self.audio_packet_listeners:
|
||||
audio_packet_listener(message)
|
||||
|
||||
# send an audio packet over the link
|
||||
def send_audio_packet(self, data):
|
||||
|
||||
# do nothing if link is not active
|
||||
if self.is_active() is False:
|
||||
return
|
||||
|
||||
# drop audio packet if it is too big to send
|
||||
if len(data) > RNS.Link.MDU:
|
||||
print("[AudioCall] dropping audio packet " + str(len(data)) + " bytes exceeds the link packet MDU of " + str(RNS.Link.MDU) + " bytes")
|
||||
print(
|
||||
"[AudioCall] dropping audio packet "
|
||||
+ str(len(data))
|
||||
+ " bytes exceeds the link packet MDU of "
|
||||
+ str(RNS.Link.MDU)
|
||||
+ " bytes"
|
||||
)
|
||||
return
|
||||
|
||||
# send codec2 audio received from call receiver to call initiator over reticulum link
|
||||
@@ -77,9 +80,7 @@ class AudioCall:
|
||||
|
||||
|
||||
class AudioCallManager:
|
||||
|
||||
def __init__(self, identity: RNS.Identity):
|
||||
|
||||
self.identity = identity
|
||||
self.on_incoming_call_callback = None
|
||||
self.on_outgoing_call_callback = None
|
||||
@@ -91,7 +92,10 @@ class AudioCallManager:
|
||||
# announces the audio call destination
|
||||
def announce(self, app_data=None):
|
||||
self.audio_call_receiver.destination.announce(app_data)
|
||||
print("[AudioCallManager] announced destination: " + RNS.prettyhexrep(self.audio_call_receiver.destination.hash))
|
||||
print(
|
||||
"[AudioCallManager] announced destination: "
|
||||
+ RNS.prettyhexrep(self.audio_call_receiver.destination.hash)
|
||||
)
|
||||
|
||||
# set the callback for incoming calls
|
||||
def register_incoming_call_callback(self, callback):
|
||||
@@ -103,7 +107,6 @@ class AudioCallManager:
|
||||
|
||||
# handle incoming calls from audio call receiver
|
||||
def handle_incoming_call(self, audio_call: AudioCall):
|
||||
|
||||
# remember it
|
||||
self.audio_calls.append(audio_call)
|
||||
|
||||
@@ -113,7 +116,6 @@ class AudioCallManager:
|
||||
|
||||
# handle outgoing calls
|
||||
def handle_outgoing_call(self, audio_call: AudioCall):
|
||||
|
||||
# remember it
|
||||
self.audio_calls.append(audio_call)
|
||||
|
||||
@@ -145,19 +147,22 @@ class AudioCallManager:
|
||||
return None
|
||||
|
||||
# attempts to initiate a call to the provided destination and returns the link hash on success
|
||||
async def initiate(self, destination_hash: bytes, timeout_seconds: int = 15) -> AudioCall:
|
||||
|
||||
async def initiate(
|
||||
self, destination_hash: bytes, timeout_seconds: int = 15
|
||||
) -> AudioCall:
|
||||
# determine when to timeout
|
||||
timeout_after_seconds = time.time() + timeout_seconds
|
||||
|
||||
# check if we have a path to the destination
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
|
||||
# we don't have a path, so we need to request it
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
|
||||
# wait until we have a path, or give up after the configured timeout
|
||||
while not RNS.Transport.has_path(destination_hash) and time.time() < timeout_after_seconds:
|
||||
while (
|
||||
not RNS.Transport.has_path(destination_hash)
|
||||
and time.time() < timeout_after_seconds
|
||||
):
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# if we still don't have a path, we can't establish a link, so bail out
|
||||
@@ -171,14 +176,16 @@ class AudioCallManager:
|
||||
RNS.Destination.OUT,
|
||||
RNS.Destination.SINGLE,
|
||||
"call",
|
||||
"audio"
|
||||
"audio",
|
||||
)
|
||||
|
||||
# create link
|
||||
link = RNS.Link(server_destination)
|
||||
|
||||
# wait until we have established a link, or give up after the configured timeout
|
||||
while link.status is not RNS.Link.ACTIVE and time.time() < timeout_after_seconds:
|
||||
while (
|
||||
link.status is not RNS.Link.ACTIVE and time.time() < timeout_after_seconds
|
||||
):
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# if we still haven't established a link, bail out
|
||||
@@ -198,9 +205,7 @@ class AudioCallManager:
|
||||
|
||||
|
||||
class AudioCallReceiver:
|
||||
|
||||
def __init__(self, manager: AudioCallManager):
|
||||
|
||||
self.manager = manager
|
||||
|
||||
# create destination for receiving audio calls
|
||||
@@ -224,7 +229,6 @@ class AudioCallReceiver:
|
||||
|
||||
# client connected to us, set up an audio call instance
|
||||
def client_connected(self, link: RNS.Link):
|
||||
|
||||
# todo: this can be optional, it's only being sent by default for ui, can be removed
|
||||
link.identify(self.manager.identity)
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
class ColourUtils:
|
||||
|
||||
@staticmethod
|
||||
def hex_colour_to_byte_array(hex_colour):
|
||||
|
||||
# remove leading "#"
|
||||
hex_colour = hex_colour.lstrip('#')
|
||||
hex_colour = hex_colour.lstrip("#")
|
||||
|
||||
# convert the remaining hex string to bytes
|
||||
return bytes.fromhex(hex_colour)
|
||||
|
||||
@@ -2,10 +2,8 @@ import RNS.vendor.configobj
|
||||
|
||||
|
||||
class InterfaceConfigParser:
|
||||
|
||||
@staticmethod
|
||||
def parse(text):
|
||||
|
||||
# get lines from provided text
|
||||
lines = text.splitlines()
|
||||
|
||||
@@ -22,7 +20,6 @@ class InterfaceConfigParser:
|
||||
# process interfaces
|
||||
interfaces = []
|
||||
for interface_name in config_interfaces:
|
||||
|
||||
# ensure interface has a name
|
||||
interface_config = config_interfaces[interface_name]
|
||||
interface_config["name"] = interface_name
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
class InterfaceEditor:
|
||||
|
||||
@staticmethod
|
||||
def update_value(interface_details: dict, data: dict, key: str):
|
||||
|
||||
# update value if provided and not empty
|
||||
value = data.get(key)
|
||||
if value is not None and value != "":
|
||||
|
||||
@@ -8,7 +8,6 @@ from websockets.sync.connection import Connection
|
||||
|
||||
|
||||
class WebsocketClientInterface(Interface):
|
||||
|
||||
# TODO: required?
|
||||
DEFAULT_IFAC_SIZE = 16
|
||||
|
||||
@@ -18,7 +17,6 @@ class WebsocketClientInterface(Interface):
|
||||
return f"WebsocketClientInterface[{self.name}/{self.target_url}]"
|
||||
|
||||
def __init__(self, owner, configuration, websocket: Connection = None):
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.owner = owner
|
||||
@@ -26,8 +24,8 @@ class WebsocketClientInterface(Interface):
|
||||
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
self.HW_MTU = 262144 # 256KiB
|
||||
self.bitrate = 1_000_000_000 # 1Gbps
|
||||
self.HW_MTU = 262144 # 256KiB
|
||||
self.bitrate = 1_000_000_000 # 1Gbps
|
||||
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
|
||||
|
||||
# parse config
|
||||
@@ -48,7 +46,6 @@ class WebsocketClientInterface(Interface):
|
||||
|
||||
# called when a full packet has been received over the websocket
|
||||
def process_incoming(self, data):
|
||||
|
||||
# do nothing if offline or detached
|
||||
if not self.online or self.detached:
|
||||
return
|
||||
@@ -65,7 +62,6 @@ class WebsocketClientInterface(Interface):
|
||||
|
||||
# the running reticulum transport instance will call this method whenever the interface must transmit a packet
|
||||
def process_outgoing(self, data):
|
||||
|
||||
# do nothing if offline or detached
|
||||
if not self.online or self.detached:
|
||||
return
|
||||
@@ -74,7 +70,9 @@ class WebsocketClientInterface(Interface):
|
||||
try:
|
||||
self.websocket.send(data)
|
||||
except Exception as e:
|
||||
RNS.log(f"Exception occurred while transmitting via {str(self)}", RNS.LOG_ERROR)
|
||||
RNS.log(
|
||||
f"Exception occurred while transmitting via {str(self)}", RNS.LOG_ERROR
|
||||
)
|
||||
RNS.log(f"The contained exception was: {str(e)}", RNS.LOG_ERROR)
|
||||
return
|
||||
|
||||
@@ -87,7 +85,6 @@ class WebsocketClientInterface(Interface):
|
||||
|
||||
# connect to the configured websocket server
|
||||
def connect(self):
|
||||
|
||||
# do nothing if interface is detached
|
||||
if self.detached:
|
||||
return
|
||||
@@ -95,7 +92,9 @@ class WebsocketClientInterface(Interface):
|
||||
# connect to websocket server
|
||||
try:
|
||||
RNS.log(f"Connecting to Websocket for {str(self)}...", RNS.LOG_DEBUG)
|
||||
self.websocket = connect(f"{self.target_url}", max_size=None, compression=None)
|
||||
self.websocket = connect(
|
||||
f"{self.target_url}", max_size=None, compression=None
|
||||
)
|
||||
RNS.log(f"Connected to Websocket for {str(self)}", RNS.LOG_DEBUG)
|
||||
self.read_loop()
|
||||
except Exception as e:
|
||||
@@ -107,7 +106,6 @@ class WebsocketClientInterface(Interface):
|
||||
self.connect()
|
||||
|
||||
def read_loop(self):
|
||||
|
||||
self.online = True
|
||||
|
||||
try:
|
||||
@@ -119,7 +117,6 @@ class WebsocketClientInterface(Interface):
|
||||
self.online = False
|
||||
|
||||
def detach(self):
|
||||
|
||||
# mark as offline
|
||||
self.online = False
|
||||
|
||||
@@ -130,5 +127,6 @@ class WebsocketClientInterface(Interface):
|
||||
# mark as detached
|
||||
self.detached = True
|
||||
|
||||
|
||||
# set interface class RNS should use when importing this external interface
|
||||
interface_class = WebsocketClientInterface
|
||||
|
||||
@@ -11,25 +11,25 @@ from src.backend.interfaces.WebsocketClientInterface import WebsocketClientInter
|
||||
|
||||
|
||||
class WebsocketServerInterface(Interface):
|
||||
|
||||
# TODO: required?
|
||||
DEFAULT_IFAC_SIZE = 16
|
||||
|
||||
RESTART_DELAY_SECONDS = 5
|
||||
|
||||
def __str__(self):
|
||||
return f"WebsocketServerInterface[{self.name}/{self.listen_ip}:{self.listen_port}]"
|
||||
return (
|
||||
f"WebsocketServerInterface[{self.name}/{self.listen_ip}:{self.listen_port}]"
|
||||
)
|
||||
|
||||
def __init__(self, owner, configuration):
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.owner = owner
|
||||
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
self.HW_MTU = 262144 # 256KiB
|
||||
self.bitrate = 1_000_000_000 # 1Gbps
|
||||
self.HW_MTU = 262144 # 256KiB
|
||||
self.bitrate = 1_000_000_000 # 1Gbps
|
||||
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
|
||||
|
||||
self.server: Server | None = None
|
||||
@@ -80,17 +80,19 @@ class WebsocketServerInterface(Interface):
|
||||
pass
|
||||
|
||||
def serve(self):
|
||||
|
||||
# handle new websocket client connections
|
||||
def on_websocket_client_connected(websocket: ServerConnection):
|
||||
|
||||
# create new child interface
|
||||
RNS.log("Accepting incoming WebSocket connection", RNS.LOG_VERBOSE)
|
||||
spawned_interface = WebsocketClientInterface(self.owner, {
|
||||
"name": f"Client on {self.name}",
|
||||
"target_host": websocket.remote_address[0],
|
||||
"target_port": str(websocket.remote_address[1]),
|
||||
}, websocket=websocket)
|
||||
spawned_interface = WebsocketClientInterface(
|
||||
self.owner,
|
||||
{
|
||||
"name": f"Client on {self.name}",
|
||||
"target_host": websocket.remote_address[0],
|
||||
"target_port": str(websocket.remote_address[1]),
|
||||
},
|
||||
websocket=websocket,
|
||||
)
|
||||
|
||||
# configure child interface
|
||||
spawned_interface.IN = self.IN
|
||||
@@ -110,7 +112,10 @@ class WebsocketServerInterface(Interface):
|
||||
# todo announce rates?
|
||||
|
||||
# activate child interface
|
||||
RNS.log(f"Spawned new WebsocketClientInterface: {spawned_interface}", RNS.LOG_VERBOSE)
|
||||
RNS.log(
|
||||
f"Spawned new WebsocketClientInterface: {spawned_interface}",
|
||||
RNS.LOG_VERBOSE,
|
||||
)
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
|
||||
# associate child interface with this interface
|
||||
@@ -127,7 +132,12 @@ class WebsocketServerInterface(Interface):
|
||||
# run websocket server
|
||||
try:
|
||||
RNS.log(f"Starting Websocket server for {str(self)}...", RNS.LOG_DEBUG)
|
||||
with serve(on_websocket_client_connected, self.listen_ip, self.listen_port, compression=None) as server:
|
||||
with serve(
|
||||
on_websocket_client_connected,
|
||||
self.listen_ip,
|
||||
self.listen_port,
|
||||
compression=None,
|
||||
) as server:
|
||||
self.online = True
|
||||
self.server = server
|
||||
server.serve_forever()
|
||||
@@ -141,7 +151,6 @@ class WebsocketServerInterface(Interface):
|
||||
self.serve()
|
||||
|
||||
def detach(self):
|
||||
|
||||
# mark as offline
|
||||
self.online = False
|
||||
|
||||
@@ -152,5 +161,6 @@ class WebsocketServerInterface(Interface):
|
||||
# mark as detached
|
||||
self.detached = True
|
||||
|
||||
|
||||
# set interface class RNS should use when importing this external interface
|
||||
interface_class = WebsocketServerInterface
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import List
|
||||
|
||||
# helper class for passing around an lxmf audio field
|
||||
class LxmfAudioField:
|
||||
|
||||
def __init__(self, audio_mode: int, audio_bytes: bytes):
|
||||
self.audio_mode = audio_mode
|
||||
self.audio_bytes = audio_bytes
|
||||
@@ -11,7 +10,6 @@ class LxmfAudioField:
|
||||
|
||||
# helper class for passing around an lxmf image field
|
||||
class LxmfImageField:
|
||||
|
||||
def __init__(self, image_type: str, image_bytes: bytes):
|
||||
self.image_type = image_type
|
||||
self.image_bytes = image_bytes
|
||||
@@ -19,7 +17,6 @@ class LxmfImageField:
|
||||
|
||||
# helper class for passing around an lxmf file attachment
|
||||
class LxmfFileAttachment:
|
||||
|
||||
def __init__(self, file_name: str, file_bytes: bytes):
|
||||
self.file_name = file_name
|
||||
self.file_bytes = file_bytes
|
||||
@@ -27,7 +24,5 @@ class LxmfFileAttachment:
|
||||
|
||||
# helper class for passing around an lxmf file attachments field
|
||||
class LxmfFileAttachmentsField:
|
||||
|
||||
def __init__(self, file_attachments: List[LxmfFileAttachment]):
|
||||
self.file_attachments = file_attachments
|
||||
|
||||
|
||||
Reference in New Issue
Block a user