Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9c2fdfae4 | ||
|
|
85d1f71b0e | ||
|
|
b2ca3f25f6 | ||
|
|
8dd0e468f0 | ||
|
|
2318672a75 | ||
|
|
6b3713e58f | ||
|
|
09bd78e194 | ||
|
|
3288fea934 | ||
|
|
47f544e2ee | ||
|
|
be3d8f1e80 | ||
|
|
01b1251589 | ||
|
|
936c298e15 | ||
|
|
f5dc06ab88 | ||
|
|
24e2ac9c65 | ||
|
|
349f50b87f | ||
|
|
5f8c476f18 | ||
|
|
dbf5361fe4 | ||
|
|
54a92ad5d5 | ||
|
|
d59e91ced3 | ||
|
|
31dacb357f | ||
|
|
daeda58b80 | ||
|
|
195daf343d | ||
|
|
c41e022e4f | ||
|
|
15c4355a58 | ||
|
|
a23f64067a | ||
|
|
cf72ac1ec8 |
36
.github/workflows/bearer.yml
vendored
Normal file
36
.github/workflows/bearer.yml
vendored
Normal 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
|
||||||
76
.github/workflows/build.yml
vendored
76
.github/workflows/build.yml
vendored
@@ -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/
|
||||||
|
|||||||
6
.github/workflows/manual-docker-build.yml
vendored
6
.github/workflows/manual-docker-build.yml
vendored
@@ -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
4
.gitignore
vendored
@@ -9,3 +9,7 @@ node_modules
|
|||||||
|
|
||||||
# local storage
|
# local storage
|
||||||
storage/
|
storage/
|
||||||
|
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
config/
|
||||||
49
Dockerfile
49
Dockerfile
@@ -1,33 +1,42 @@
|
|||||||
# Build the frontend
|
FROM node:22-alpine AS build-frontend
|
||||||
FROM node:20-bookworm-slim AS build-frontend
|
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
# Copy required source files
|
COPY --chown=node:node *.json .
|
||||||
COPY *.json .
|
COPY --chown=node:node *.js .
|
||||||
COPY *.js .
|
COPY --chown=node:node src/frontend ./src/frontend
|
||||||
COPY src/frontend ./src/frontend
|
|
||||||
|
|
||||||
# Install NodeJS deps, exluding electron
|
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
|
FROM python:3.13-alpine
|
||||||
FROM python:3.11-bookworm
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install Python deps
|
RUN apk add --no-cache \
|
||||||
COPY ./requirements.txt .
|
gcc \
|
||||||
RUN pip install -r requirements.txt
|
musl-dev \
|
||||||
|
python3-dev \
|
||||||
|
libffi-dev \
|
||||||
|
openssl-dev
|
||||||
|
|
||||||
# Copy prebuilt frontend
|
RUN mkdir -p /config/.reticulum /config/.meshchat && \
|
||||||
COPY --from=build-frontend /src/public public
|
chown -R 1000:1000 /config
|
||||||
|
|
||||||
# Copy other required source files
|
COPY --chown=1000:1000 ./requirements.txt .
|
||||||
COPY *.py .
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
COPY src/__init__.py ./src/__init__.py
|
|
||||||
COPY src/backend ./src/backend
|
|
||||||
COPY *.json .
|
|
||||||
|
|
||||||
CMD ["python", "meshchat.py", "--host=0.0.0.0", "--reticulum-config-dir=/config/.reticulum", "--storage-dir=/config/.meshchat", "--headless"]
|
RUN mkdir -p /app/public
|
||||||
|
COPY --from=build-frontend --chown=1000:1000 /src/public/ /app/public/
|
||||||
|
|
||||||
|
COPY --chown=1000:1000 *.py .
|
||||||
|
COPY --chown=1000:1000 src/__init__.py ./src/__init__.py
|
||||||
|
COPY --chown=1000:1000 src/backend ./src/backend
|
||||||
|
COPY --chown=1000:1000 *.json .
|
||||||
|
|
||||||
|
USER 1000
|
||||||
|
ENTRYPOINT ["python"]
|
||||||
|
CMD ["meshchat.py", "--host=0.0.0.0", "--reticulum-config-dir=/config/.reticulum", "--storage-dir=/config/.meshchat", "--headless"]
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -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>
|
||||||
|
|||||||
75
database.py
75
database.py
@@ -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
40
docker-compose.dev.yml
Normal 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
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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){
|
||||||
|
|||||||
2032
meshchat.py
2032
meshchat.py
File diff suppressed because it is too large
Load Diff
1751
package-lock.json
generated
1751
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "reticulum-meshchat",
|
"name": "reticulum-meshchat",
|
||||||
"version": "1.21.0",
|
"version": "1.24.0",
|
||||||
"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
34
poetry.lock
generated
Normal 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
20
pyproject.toml
Normal 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"
|
||||||
|
|
||||||
@@ -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.7.1
|
||||||
peewee>=3.17.3
|
peewee>=3.18.1
|
||||||
rns>=0.9.3
|
rns>=0.9.6
|
||||||
websockets>=14.2
|
websockets>=15.0.1
|
||||||
36
setup.py
36
setup.py
@@ -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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 != "":
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -11,17 +11,17 @@ 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
|
||||||
@@ -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(
|
||||||
|
self.owner,
|
||||||
|
{
|
||||||
"name": f"Client on {self.name}",
|
"name": f"Client on {self.name}",
|
||||||
"target_host": websocket.remote_address[0],
|
"target_host": websocket.remote_address[0],
|
||||||
"target_port": str(websocket.remote_address[1]),
|
"target_port": str(websocket.remote_address[1]),
|
||||||
}, websocket=websocket)
|
},
|
||||||
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user