Compare commits

...

23 Commits

Author SHA1 Message Date
Ivan
8dd0e468f0 update 2025-05-11 16:08:28 -05:00
Ivan
2318672a75 remove flatpak 2025-05-11 16:08:09 -05:00
Ivan
6b3713e58f flatpak 2025-05-11 14:56:33 -05:00
Ivan
09bd78e194 node 22 and python 3.13 2025-05-11 14:43:47 -05:00
Ivan
3288fea934 update 2025-05-11 14:31:50 -05:00
Ivan
47f544e2ee ruff format and fixes.
Improve page download.
2025-05-11 14:28:57 -05:00
Ivan
be3d8f1e80 update 2025-05-11 14:22:39 -05:00
Ivan
01b1251589 dark mode by default 2025-05-11 14:22:35 -05:00
Ivan
936c298e15 update 2025-05-11 14:21:01 -05:00
Ivan
f5dc06ab88 update 2025-05-11 14:20:10 -05:00
Ivan
24e2ac9c65 update 2025-05-09 18:45:33 -05:00
Ivan
349f50b87f update 2025-05-09 18:18:36 -05:00
Ivan
5f8c476f18 fix 2025-04-22 17:58:46 -05:00
Ivan
dbf5361fe4 fix 2025-04-22 17:55:30 -05:00
Ivan
54a92ad5d5 update 2025-04-22 17:54:45 -05:00
Ivan
d59e91ced3 update 2025-04-22 17:53:27 -05:00
Ivan
31dacb357f update 2025-04-22 17:51:42 -05:00
Ivan
daeda58b80 add bearer 2025-04-22 17:47:22 -05:00
Ivan
195daf343d update 2025-04-22 17:47:03 -05:00
Ivan
c41e022e4f use my image 2025-04-22 17:46:54 -05:00
Ivan
15c4355a58 update package-lock 2025-04-22 17:46:39 -05:00
Ivan
a23f64067a update 2025-04-22 17:34:08 -05:00
Ivan
cf72ac1ec8 update 2025-04-22 17:20:07 -05:00
27 changed files with 3237 additions and 1585 deletions

36
.github/workflows/bearer.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Security Scan
on:
push:
branches:
- master
pull_request:
branches:
- master
schedule:
- cron: '0 0 * * 0' # Run weekly on Sunday
permissions:
contents: read
security-events: write
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Bearer Security Scan
uses: bearer/bearer-action@v2
with:
scanner: sast
format: sarif
output: bearer.sarif
severity: critical,high
path: .
exit-code: 0
- name: Upload SARIF results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: bearer.sarif

View File

@@ -17,12 +17,12 @@ jobs:
- name: Install NodeJS - name: Install NodeJS
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 18 node-version: 22
- name: Install Python - name: Install Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.13"
- name: Install Python Deps - name: Install Python Deps
run: pip install -r requirements.txt run: pip install -r requirements.txt
@@ -44,43 +44,43 @@ jobs:
omitNameDuringUpdate: true omitNameDuringUpdate: true
artifacts: "dist/*-win-installer.exe,dist/*-win-portable.exe" artifacts: "dist/*-win-installer.exe,dist/*-win-portable.exe"
build_mac: # build_mac:
runs-on: macos-13 # runs-on: macos-13
permissions: # permissions:
contents: write # contents: write
steps: # steps:
- name: Clone Repo # - name: Clone Repo
uses: actions/checkout@v1 # uses: actions/checkout@v1
- name: Install NodeJS # - name: Install NodeJS
uses: actions/setup-node@v1 # uses: actions/setup-node@v1
with: # with:
node-version: 18 # node-version: 20
- name: Install Python # - name: Install Python
uses: actions/setup-python@v5 # uses: actions/setup-python@v5
with: # with:
python-version: "3.11" # python-version: "3.13"
- name: Install Python Deps # - name: Install Python Deps
run: pip install -r requirements.txt # run: pip install -r requirements.txt
- name: Install NodeJS Deps # - name: Install NodeJS Deps
run: npm install # run: npm install
- name: Build Electron App # - name: Build Electron App
run: npm run dist # run: npm run dist
- name: Create Release # - name: Create Release
id: create_release # id: create_release
uses: ncipollo/release-action@v1 # uses: ncipollo/release-action@v1
with: # with:
draft: true # draft: true
allowUpdates: true # allowUpdates: true
replacesArtifacts: true # replacesArtifacts: true
omitDraftDuringUpdate: true # omitDraftDuringUpdate: true
omitNameDuringUpdate: true # omitNameDuringUpdate: true
artifacts: "dist/*-mac.dmg" # artifacts: "dist/*-mac.dmg"
build_linux: build_linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -93,12 +93,12 @@ jobs:
- name: Install NodeJS - name: Install NodeJS
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 18 node-version: 22
- name: Install Python - name: Install Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.13"
- name: Install Python Deps - name: Install Python Deps
run: pip install -r requirements.txt run: pip install -r requirements.txt
@@ -149,9 +149,9 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: | tags: |
ghcr.io/liamcottle/reticulum-meshchat:latest ghcr.io/sudo-ivan/reticulum-meshchat:latest
ghcr.io/liamcottle/reticulum-meshchat:${{ github.ref_name }} ghcr.io/sudo-ivan/reticulum-meshchat:${{ github.ref_name }}
labels: | labels: |
org.opencontainers.image.title=Reticulum MeshChat org.opencontainers.image.title=Reticulum MeshChat
org.opencontainers.image.description=Docker image for Reticulum MeshChat org.opencontainers.image.description=Docker image for Reticulum MeshChat
org.opencontainers.image.url=https://github.com/liamcottle/reticulum-meshchat/pkgs/container/reticulum-meshchat/ org.opencontainers.image.url=https://github.com/Sudo-Ivan/reticulum-meshchat/pkgs/container/reticulum-meshchat/

View File

@@ -33,10 +33,10 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: | tags: |
ghcr.io/liamcottle/reticulum-meshchat:latest ghcr.io/sudo-ivan/reticulum-meshchat:latest
ghcr.io/liamcottle/reticulum-meshchat:${{ github.ref_name }} ghcr.io/sudo-ivan/reticulum-meshchat:${{ github.ref_name }}
labels: | labels: |
org.opencontainers.image.title=Reticulum MeshChat org.opencontainers.image.title=Reticulum MeshChat
org.opencontainers.image.description=Docker image for Reticulum MeshChat org.opencontainers.image.description=Docker image for Reticulum MeshChat
org.opencontainers.image.url=https://github.com/liamcottle/reticulum-meshchat/pkgs/container/reticulum-meshchat/ org.opencontainers.image.url=https://github.com/Sudo-Ivan/reticulum-meshchat/pkgs/container/reticulum-meshchat/

4
.gitignore vendored
View File

@@ -9,3 +9,7 @@ node_modules
# local storage # local storage
storage/ storage/
__pycache__/
config/

View File

@@ -1,33 +1,51 @@
# Build the frontend # Build the frontend
FROM node:20-bookworm-slim AS build-frontend FROM node:22-alpine AS build-frontend
WORKDIR /src WORKDIR /src
# Copy required source files # Copy required source files
COPY *.json . COPY --chown=node:node *.json .
COPY *.js . COPY --chown=node:node *.js .
COPY src/frontend ./src/frontend COPY --chown=node:node src/frontend ./src/frontend
# Install NodeJS deps, exluding electron # Fix permissions and install NodeJS deps
USER root
RUN chown -R node:node /src
USER node
RUN npm install --omit=dev && \ RUN npm install --omit=dev && \
npm run build-frontend npm run build-frontend
# Main app build # Main app build
FROM python:3.11-bookworm FROM python:3.13-alpine
WORKDIR /app WORKDIR /app
# Install Python deps # Install system dependencies
COPY ./requirements.txt . RUN apk add --no-cache \
RUN pip install -r requirements.txt gcc \
musl-dev \
python3-dev \
libffi-dev \
openssl-dev
# Copy prebuilt frontend # Create config directories with proper permissions
COPY --from=build-frontend /src/public public RUN mkdir -p /config/.reticulum /config/.meshchat && \
chown -R 1000:1000 /config
# Install Python deps
COPY --chown=1000:1000 ./requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Create public directory and copy frontend
RUN mkdir -p /app/public
COPY --from=build-frontend --chown=1000:1000 /src/public/ /app/public/
# Copy other required source files # Copy other required source files
COPY *.py . COPY --chown=1000:1000 *.py .
COPY src/__init__.py ./src/__init__.py COPY --chown=1000:1000 src/__init__.py ./src/__init__.py
COPY src/backend ./src/backend COPY --chown=1000:1000 src/backend ./src/backend
COPY *.json . COPY --chown=1000:1000 *.json .
CMD ["python", "meshchat.py", "--host=0.0.0.0", "--reticulum-config-dir=/config/.reticulum", "--storage-dir=/config/.meshchat", "--headless"] USER 1000
ENTRYPOINT ["python"]
CMD ["meshchat.py", "--host=0.0.0.0", "--reticulum-config-dir=/config/.reticulum", "--storage-dir=/config/.meshchat", "--headless"]

View File

@@ -1,3 +1,25 @@
# Ivans Custom Fork Edition
highly experimental and customized, only use if you live on the edge.
## Changes
- 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 22.
- Ruff formatting and fixes.
## Security
- Bearer Security Scan Action
- [Socket](https://socket.dev/) Supply Chain Security/Analysis
---
<p align="center"> <p align="center">
<a href="https://github.com/liamcottle/reticulum-meshchat"><img src="./logo/logo-chat-bubble.png" width="150"></a> <a href="https://github.com/liamcottle/reticulum-meshchat"><img src="./logo/logo-chat-bubble.png" width="150"></a>
</p> </p>

View File

@@ -4,40 +4,47 @@ from peewee import *
from playhouse.migrate import migrate as migrate_database, SqliteMigrator from playhouse.migrate import migrate as migrate_database, SqliteMigrator
latest_version = 5 # increment each time new database migrations are added 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) 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),
) )
return latest_version return latest_version
@@ -49,7 +56,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()
@@ -62,12 +68,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)
@@ -82,7 +95,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
@@ -96,21 +108,30 @@ class CustomDestinationDisplayName(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)
@@ -123,7 +144,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()
@@ -137,12 +157,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))

40
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,40 @@
services:
reticulum-meshchat:
container_name: reticulum-meshchat-dev
build:
context: .
dockerfile: Dockerfile
pull_policy: never
restart: unless-stopped
user: "1000:1000"
# Make the meshchat web interface accessible from the host on port 8000
ports:
- 0.0.0.0:8000:8000
volumes:
- meshchat-config:/config:rw
- .:/app:delegated
- /app/public
# Uncomment if you have a USB device connected, such as an RNode
# devices:
# - /dev/ttyUSB0:/dev/ttyUSB0
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
deploy:
resources:
limits:
cpus: '1'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
volumes:
meshchat-config:
driver: local
driver_opts:
type: none
o: bind
device: ${PWD}/config

View File

@@ -1,17 +1,31 @@
services: services:
reticulum-meshchat: reticulum-meshchat:
container_name: reticulum-meshchat container_name: reticulum-meshchat
image: ghcr.io/liamcottle/reticulum-meshchat:latest image: ghcr.io/sudo-ivan/reticulum-meshchat:latest
pull_policy: always pull_policy: always
restart: unless-stopped restart: unless-stopped
user: "1000:1000"
# Make the meshchat web interface accessible from the host on port 8000 # Make the meshchat web interface accessible from the host on port 8000
ports: ports:
- 0.0.0.0:8000:8000 - 0.0.0.0:8000:8000
volumes: volumes:
- meshchat-config:/config - meshchat-config:/config:rw
# Uncomment if you have a USB device connected, such as an RNode # Uncomment if you have a USB device connected, such as an RNode
# devices: # devices:
# - /dev/ttyUSB0:/dev/ttyUSB0 # - /dev/ttyUSB0:/dev/ttyUSB0
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
deploy:
resources:
limits:
cpus: '1'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
volumes: volumes:
meshchat-config: meshchat-config:

View File

@@ -99,7 +99,9 @@ function getDefaultReticulumConfigDir() {
app.whenReady().then(async () => { app.whenReady().then(async () => {
// get arguments passed to application, and remove the provided application path // 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"); const shouldLaunchHeadless = userProvidedArguments.includes("--headless");
if(!shouldLaunchHeadless){ if(!shouldLaunchHeadless){

View File

File diff suppressed because it is too large Load Diff

1751
package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "reticulum-meshchat", "name": "reticulum-meshchat",
"version": "1.21.0", "version": "1.23.6",
"description": "", "description": "",
"main": "electron/main.js", "main": "electron/main.js",
"scripts": { "scripts": {
@@ -14,7 +14,7 @@
}, },
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=22"
}, },
"devDependencies": { "devDependencies": {
"electron": "^30.0.8", "electron": "^30.0.8",
@@ -70,7 +70,11 @@
}, },
"linux": { "linux": {
"artifactName": "ReticulumMeshChat-v${version}-${os}.${ext}", "artifactName": "ReticulumMeshChat-v${version}-${os}.${ext}",
"target": "AppImage", "target": [
"AppImage"
],
"category": "Network",
"icon": "electron/build/icon.png",
"extraFiles": [ "extraFiles": [
{ {
"from": "build/exe", "from": "build/exe",
@@ -96,9 +100,9 @@
"dependencies": { "dependencies": {
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.4",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"axios": "^1.7.9", "axios": "^1.9.0",
"click-outside-vue3": "^4.0.1", "click-outside-vue3": "^4.0.1",
"compressorjs": "^1.2.1", "compressorjs": "^1.2.1",
"electron-prompt": "^1.7.0", "electron-prompt": "^1.7.0",
@@ -106,13 +110,13 @@
"mitt": "^3.0.1", "mitt": "^3.0.1",
"moment": "^2.30.1", "moment": "^2.30.1",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"protobufjs": "^7.4.0", "protobufjs": "^7.5.1",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"vis-data": "^7.1.9", "vis-data": "^7.1.9",
"vis-network": "^9.1.9", "vis-network": "^9.1.9",
"vite": "^6.0.5", "vite": "^6.3.5",
"vite-plugin-vuetify": "^2.0.4", "vite-plugin-vuetify": "^2.0.4",
"vue-router": "^4.5.0", "vue-router": "^4.5.1",
"vuetify": "^3.7.6" "vuetify": "^3.8.4"
} }
} }

34
poetry.lock generated Normal file
View File

@@ -0,0 +1,34 @@
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
[[package]]
name = "ruff"
version = "0.11.9"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "ruff-0.11.9-py3-none-linux_armv6l.whl", hash = "sha256:a31a1d143a5e6f499d1fb480f8e1e780b4dfdd580f86e05e87b835d22c5c6f8c"},
{file = "ruff-0.11.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:66bc18ca783b97186a1f3100e91e492615767ae0a3be584e1266aa9051990722"},
{file = "ruff-0.11.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bd576cd06962825de8aece49f28707662ada6a1ff2db848d1348e12c580acbf1"},
{file = "ruff-0.11.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b1d18b4be8182cc6fddf859ce432cc9631556e9f371ada52f3eaefc10d878de"},
{file = "ruff-0.11.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f3f46f759ac623e94824b1e5a687a0df5cd7f5b00718ff9c24f0a894a683be7"},
{file = "ruff-0.11.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f34847eea11932d97b521450cf3e1d17863cfa5a94f21a056b93fb86f3f3dba2"},
{file = "ruff-0.11.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f33b15e00435773df97cddcd263578aa83af996b913721d86f47f4e0ee0ff271"},
{file = "ruff-0.11.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b27613a683b086f2aca8996f63cb3dd7bc49e6eccf590563221f7b43ded3f65"},
{file = "ruff-0.11.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e0d88756e63e8302e630cee3ce2ffb77859797cc84a830a24473939e6da3ca6"},
{file = "ruff-0.11.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:537c82c9829d7811e3aa680205f94c81a2958a122ac391c0eb60336ace741a70"},
{file = "ruff-0.11.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:440ac6a7029f3dee7d46ab7de6f54b19e34c2b090bb4f2480d0a2d635228f381"},
{file = "ruff-0.11.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:71c539bac63d0788a30227ed4d43b81353c89437d355fdc52e0cda4ce5651787"},
{file = "ruff-0.11.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c67117bc82457e4501473c5f5217d49d9222a360794bfb63968e09e70f340abd"},
{file = "ruff-0.11.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e4b78454f97aa454586e8a5557facb40d683e74246c97372af3c2d76901d697b"},
{file = "ruff-0.11.9-py3-none-win32.whl", hash = "sha256:7fe1bc950e7d7b42caaee2a8a3bc27410547cc032c9558ee2e0f6d3b209e845a"},
{file = "ruff-0.11.9-py3-none-win_amd64.whl", hash = "sha256:52edaa4a6d70f8180343a5b7f030c7edd36ad180c9f4d224959c2d689962d964"},
{file = "ruff-0.11.9-py3-none-win_arm64.whl", hash = "sha256:bcf42689c22f2e240f496d0c183ef2c6f7b35e809f12c1db58f75d9aa8d630ca"},
{file = "ruff-0.11.9.tar.gz", hash = "sha256:ebd58d4f67a00afb3a30bf7d383e52d0e036e6195143c6db7019604a05335517"},
]
[metadata]
lock-version = "2.1"
python-versions = ">=3.13"
content-hash = "905709f505f867b18d0134c85302f436c08efba1f5010d10245fce49cac80fce"

20
pyproject.toml Normal file
View File

@@ -0,0 +1,20 @@
[project]
name = "reticulum-meshchat"
version = "0.1.0"
description = ""
authors = [
{name = "Ivan"}
]
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
]
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.group.dev.dependencies]
ruff = "^0.11.9"

View File

@@ -1,6 +1,6 @@
aiohttp>=3.9.5 aiohttp>=3.11.18
cx_freeze>=7.0.0 cx_freeze>=7.0.0
lxmf>=0.6.3 lxmf>=0.6.3
peewee>=3.17.3 peewee>=3.18.1
rns>=0.9.3 rns>=0.9.5
websockets>=14.2 websockets>=15.0.1

View File

@@ -1,44 +1,44 @@
from cx_Freeze import setup, Executable from cx_Freeze import setup, Executable
setup( setup(
name='ReticulumMeshChat', name="ReticulumMeshChat",
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='ReticulumMeshChat', # creates ReticulumMeshChat.exe target_name="ReticulumMeshChat", # creates ReticulumMeshChat.exe
shortcut_name='ReticulumMeshChat', # name shown in shortcut shortcut_name="ReticulumMeshChat", # 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",
], ],
# 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",
}, },
}, },
) )

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

@@ -2,12 +2,10 @@ import asyncio
class AsyncUtils: class AsyncUtils:
# this method allows running the provided async coroutine from within a sync function # 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 # it will run the async function on the existing event loop if available, otherwise it will start a new event loop
@staticmethod @staticmethod
def run_async(coroutine): def run_async(coroutine):
# attempt to get existing event loop # attempt to get existing event loop
existing_event_loop = None existing_event_loop = None
try: try:

View File

@@ -13,7 +13,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 +40,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
@@ -77,9 +80,7 @@ class AudioCall:
class AudioCallManager: class AudioCallManager:
def __init__(self, identity: RNS.Identity): def __init__(self, identity: RNS.Identity):
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
@@ -91,7 +92,10 @@ class AudioCallManager:
# 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):
@@ -103,7 +107,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)
@@ -113,7 +116,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)
@@ -145,19 +147,22 @@ class AudioCallManager:
return None 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
@@ -171,14 +176,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
@@ -198,9 +205,7 @@ class AudioCallManager:
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
@@ -224,7 +229,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):
# 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)

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()
@@ -22,7 +20,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 != "":

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,7 +70,9 @@ 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(
f"Exception occurred while transmitting via {str(self)}", RNS.LOG_ERROR
)
RNS.log(f"The contained exception was: {str(e)}", RNS.LOG_ERROR) RNS.log(f"The contained exception was: {str(e)}", RNS.LOG_ERROR)
return return
@@ -87,7 +85,6 @@ 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
@@ -95,7 +92,9 @@ class WebsocketClientInterface(Interface):
# 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 {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) RNS.log(f"Connected to Websocket for {str(self)}", RNS.LOG_DEBUG)
self.read_loop() self.read_loop()
except Exception as e: except Exception as e:
@@ -107,7 +106,6 @@ class WebsocketClientInterface(Interface):
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

@@ -11,25 +11,25 @@ from src.backend.interfaces.WebsocketClientInterface import WebsocketClientInter
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
@@ -80,17 +80,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
@@ -110,7 +112,10 @@ class WebsocketServerInterface(Interface):
# 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
@@ -127,7 +132,12 @@ 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 {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.online = True
self.server = server self.server = server
server.serve_forever() server.serve_forever()
@@ -141,7 +151,6 @@ class WebsocketServerInterface(Interface):
self.serve() self.serve()
def detach(self): def detach(self):
# mark as offline # mark as offline
self.online = False self.online = False
@@ -152,5 +161,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

@@ -3,7 +3,6 @@ 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 +10,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 +17,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 +24,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