codebase restructure and organization.

This commit is contained in:
2025-11-30 23:16:57 -06:00
parent 80cf812e54
commit 84f887df90
121 changed files with 1952 additions and 17368 deletions

View File

@@ -3,24 +3,35 @@ README.md
LICENSE
donate.md
screenshots/
docs/
# Development files
.github/
electron/
scripts/
Makefile
# Build artifacts and cache
build/
dist/
public/
node_modules/
__pycache__/
*.pyc
*.pyo
*.pyd
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
*.egg
python-dist/
# Virtual environments
env/
venv/
ENV/
env.bak/
venv.bak/
.venv/
# IDE and editor files
.vscode/
@@ -47,9 +58,19 @@ Dockerfile*
docker-compose*.yml
.dockerignore
# Local storage and runtime data
storage/
testing/
telemetry_test_lxmf/
# Logs
*.log
# Temporary files
*.tmp
*.temp
# Environment variables
.env
.env.local
.env.*.local

View File

@@ -47,11 +47,14 @@ jobs:
with:
python-version: "3.12"
- name: Install Poetry
run: python -m pip install --upgrade pip poetry
- name: Sync versions
run: python scripts/sync_version.py
- name: Install Python Deps
run: |
python -m venv venv
venv\Scripts\pip install --upgrade pip
venv\Scripts\pip install -r requirements.txt
run: python -m poetry install
- name: Install NodeJS Deps
run: npm install
@@ -89,11 +92,14 @@ jobs:
with:
python-version: "3.11"
- name: Install Poetry
run: python -m pip install --upgrade pip poetry
- name: Sync versions
run: python scripts/sync_version.py
- name: Install Python Deps
run: |
python3 -m venv venv
venv/bin/pip install --upgrade pip
venv/bin/pip install -r requirements.txt
run: python -m poetry install
- name: Install NodeJS Deps
run: npm install
@@ -134,11 +140,21 @@ jobs:
- name: Install patchelf
run: sudo apt-get update && sudo apt-get install -y patchelf
- name: Install Poetry
run: python -m pip install --upgrade pip poetry
- name: Sync versions
run: python scripts/sync_version.py
- name: Install Python Deps
run: python -m poetry install
- name: Build Python wheel
run: |
python3 -m venv venv
venv/bin/pip install --upgrade pip
venv/bin/pip install -r requirements.txt
python -m poetry build -f wheel
mkdir -p python-dist
mv dist/*.whl python-dist/
rm -rf dist
- name: Install NodeJS Deps
run: npm install
@@ -155,7 +171,7 @@ jobs:
replacesArtifacts: true
omitDraftDuringUpdate: true
omitNameDuringUpdate: true
artifacts: "dist/*-linux.AppImage,dist/*-linux.deb"
artifacts: "dist/*-linux.AppImage,dist/*-linux.deb,python-dist/*.whl"
build_docker:
runs-on: ubuntu-latest

53
.gitignore vendored
View File

@@ -1,13 +1,56 @@
# IDE and editor files
.idea
node_modules
.vscode/
*.swp
*.swo
*~
# build files
# Dependencies
node_modules/
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
dist/
*.egg
# Virtual environments
venv/
env/
ENV/
env.bak/
venv.bak/
.venv/
# Build files
/build/
/dist/
/public/
/meshchatx/public/
/electron/build/exe/
python-dist/
# local storage
# Local storage and runtime data
storage/
testing/
telemetry_test_lxmf/
*.pyc
# Logs
*.log
# OS files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Environment variables
.env
.env.local
.env.*.local

View File

@@ -1,26 +1,29 @@
.PHONY: install run clean build build-appimage build-exe dist
.PHONY: install run develop clean build build-appimage build-exe dist sync-version wheel node_modules python
VENV = venv
PYTHON = $(VENV)/bin/python
PIP = $(VENV)/bin/pip
PYTHON ?= python
POETRY = $(PYTHON) -m poetry
NPM = npm
install: $(VENV) node_modules
$(VENV):
python3 -m venv $(VENV)
$(PIP) install --upgrade pip
$(PIP) install -r requirements.txt
install: sync-version node_modules python
node_modules:
$(NPM) install
python:
$(POETRY) install
run: install
$(PYTHON) meshchat.py
$(POETRY) run meshchat
develop: run
build: install
$(NPM) run build
wheel: install
$(POETRY) build -f wheel
$(PYTHON) scripts/move_wheels.py
build-appimage: build
$(NPM) run electron-postinstall
$(NPM) run dist -- --linux AppImage
@@ -32,10 +35,10 @@ build-exe: build
dist: build-appimage
clean:
rm -rf $(VENV)
rm -rf node_modules
rm -rf build
rm -rf dist
rm -rf python-dist
sync-version:
$(PYTHON) scripts/sync_version.py

View File

@@ -14,12 +14,12 @@ A heavily customized fork of [Reticulum MeshChat](https://github.com/liamcottle/
- [ ] Multi-language support
- [ ] Offline Reticulum documentation tool
- [ ] More tools (translate, LoRa calculator, LXMFy bots, etc)
- [ ] Codebase reorginization and cleanup.
- [x] Codebase reorganization and cleanup.
- [ ] Tests and proper CI/CD pipeline.
- [ ] RNS hot reload
- [ ] Backup/Import identities, messages and interfaces.
- [ ] Full LXST support.
- [ ] Move to Poetry and pyproject.toml for Python packaging.
- [x] Move to Poetry and pyproject.toml for Python packaging.
- [x] More stats on about page.
- [x] Actions are pinned to full-length SHA hashes.
- [x] Docker images are smaller and use SHA256 hashes for the images.
@@ -35,23 +35,30 @@ Check [releases](https://github.com/Sudo-Ivan/reticulum-meshchatX/releases) for
## Building
```bash
make install
make install # installs Python deps via Poetry and Node deps via npm
make build
```
You can run `make run` or `make develop` (a thin alias) to start the backend + frontend loop locally through `poetry run meshchat`.
### Python packaging
The Python build is driven entirely by Poetry now. Run `python scripts/sync_version.py` or `make sync-version` before packaging so `pyproject.toml` and `src/version.py` match `package.json`. After that:
```bash
python -m poetry install
make wheel # produces a wheel in python-dist/ that bundles the public assets
```
The wheel includes the frontend `public/` assets, `logo/`, and the CLI entry point, and `python-dist/` keeps the artifact separate from the Electron `dist/` output.
### Building in Docker
```bash
make docker-build
```
The build will be in the `dist` directory.
## Development
```bash
make develop
```
The Electron build artifacts will still live under `dist/` for releases.
## Python packaging

View File

@@ -133,6 +133,14 @@ app.whenReady().then(async () => {
webPreferences: {
// used to inject logging over ipc
preload: path.join(__dirname, 'preload.js'),
// Security: disable node integration in renderer
nodeIntegration: false,
// Security: enable context isolation (default in Electron 12+)
contextIsolation: true,
// Security: enable sandbox for additional protection
sandbox: true,
// Security: disable remote module (deprecated but explicit)
enableRemoteModule: false,
},
});

3
meshchatx/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""Reticulum MeshChatX - A mesh network communications app."""
__version__ = "2.41.0"

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime
from peewee import * # noqa: F403
from playhouse.migrate import SqliteMigrator
@@ -68,8 +68,8 @@ class Config(BaseModel):
id = BigAutoField() # noqa: F405
key = CharField(unique=True) # noqa: F405
value = TextField() # noqa: F405
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405
created_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
# define table name
class Meta:
@@ -95,8 +95,8 @@ class Announce(BaseModel):
snr = FloatField(null=True) # noqa: F405
quality = FloatField(null=True) # noqa: F405
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405
created_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
# define table name
class Meta:
@@ -108,8 +108,8 @@ class CustomDestinationDisplayName(BaseModel):
destination_hash = CharField(unique=True) # noqa: F405 # unique destination hash
display_name = CharField() # noqa: F405 # custom display name for the destination hash
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405
created_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
# define table name
class Meta:
@@ -122,8 +122,8 @@ class FavouriteDestination(BaseModel):
display_name = CharField() # noqa: F405 # custom display name for the destination hash
aspect = CharField() # noqa: F405 # e.g: nomadnetwork.node
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405
created_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
# define table name
class Meta:
@@ -159,8 +159,8 @@ class LxmfMessage(BaseModel):
snr = FloatField(null=True) # noqa: F405
quality = FloatField(null=True) # noqa: F405
is_spam = BooleanField(default=False) # noqa: F405 # if true, message is marked as spam
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405
created_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
# define table name
class Meta:
@@ -172,8 +172,8 @@ class LxmfConversationReadState(BaseModel):
destination_hash = CharField(unique=True) # noqa: F405 # unique destination hash
last_read_at = DateTimeField() # noqa: F405
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405
created_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
# define table name
class Meta:
@@ -189,8 +189,8 @@ class LxmfUserIcon(BaseModel):
CharField() # noqa: F405
) # hex colour to use for background (background colour)
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405
created_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
# define table name
class Meta:
@@ -203,8 +203,8 @@ class BlockedDestination(BaseModel):
unique=True,
index=True,
) # unique destination hash that is blocked
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405
created_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
# define table name
class Meta:
@@ -217,8 +217,8 @@ class SpamKeyword(BaseModel):
unique=True,
index=True,
) # keyword to match against message content
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405
created_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
# define table name
class Meta:

View File

@@ -13,7 +13,7 @@ import threading
import time
import webbrowser
from collections.abc import Callable
from datetime import datetime, timezone
from datetime import UTC, datetime
import LXMF
import psutil
@@ -24,20 +24,21 @@ from LXMF import LXMRouter
from peewee import SqliteDatabase
from serial.tools import list_ports
import database
from src.backend.announce_handler import AnnounceHandler
from src.backend.async_utils import AsyncUtils
from src.backend.audio_call_manager import AudioCall, AudioCallManager
from src.backend.colour_utils import ColourUtils
from src.backend.interface_config_parser import InterfaceConfigParser
from src.backend.interface_editor import InterfaceEditor
from src.backend.lxmf_message_fields import (
from meshchatx import database
from meshchatx.src.backend.announce_handler import AnnounceHandler
from meshchatx.src.backend.async_utils import AsyncUtils
from meshchatx.src.backend.audio_call_manager import AudioCall, AudioCallManager
from meshchatx.src.backend.colour_utils import ColourUtils
from meshchatx.src.backend.interface_config_parser import InterfaceConfigParser
from meshchatx.src.backend.interface_editor import InterfaceEditor
from meshchatx.src.backend.lxmf_message_fields import (
LxmfAudioField,
LxmfFileAttachment,
LxmfFileAttachmentsField,
LxmfImageField,
)
from src.backend.sideband_commands import SidebandCommands
from meshchatx.src.backend.sideband_commands import SidebandCommands
from meshchatx.src.version import __version__ as app_version
# NOTE: this is required to be able to pack our app with cxfreeze as an exe, otherwise it can't access bundled assets
@@ -46,9 +47,22 @@ from src.backend.sideband_commands import SidebandCommands
def get_file_path(filename):
if getattr(sys, "frozen", False):
datadir = os.path.dirname(sys.executable)
else:
datadir = os.path.dirname(__file__)
return os.path.join(datadir, filename)
return os.path.join(datadir, filename)
# Running from source or an installed wheel: assets live inside the meshchatx package
package_dir = os.path.dirname(__file__)
test_path = os.path.join(package_dir, filename)
if os.path.exists(test_path):
return test_path
# Fall back to repo root when running directly from the source tree
repo_root = os.path.dirname(package_dir)
repo_path = os.path.join(repo_root, filename)
if os.path.exists(repo_path):
return repo_path
# Return the package path even if it does not exist so callers raise a clear error
return test_path
class ReticulumMeshChat:
@@ -225,12 +239,10 @@ class ReticulumMeshChat:
thread.daemon = True
thread.start()
# gets app version from package.json
# gets app version from the synchronized Python version helper
@staticmethod
def get_app_version() -> str:
with open(get_file_path("package.json")) as f:
package_json = json.load(f)
return package_json["version"]
return app_version
# automatically announces based on user config
async def announce_loop(self):
@@ -3011,13 +3023,38 @@ class ReticulumMeshChat:
)
if message:
message.is_spam = is_spam
message.updated_at = datetime.now(timezone.utc)
message.updated_at = datetime.now(UTC)
message.save()
return web.json_response({"message": "ok"})
return web.json_response({"error": "Message not found"}, status=404)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# security headers middleware
@web.middleware
async def security_middleware(request, handler):
response = await handler(request)
# Add security headers to all responses
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
# CSP: allow localhost for development and Electron, websockets, and blob URLs
csp = (
"default-src 'self'; "
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data: blob:; "
"font-src 'self' data:; "
"connect-src 'self' ws://localhost:* wss://localhost:* blob:; "
"media-src 'self' blob:; "
"worker-src 'self' blob:; "
"object-src 'none'; "
"base-uri 'self';"
)
response.headers["Content-Security-Policy"] = csp
return response
# called when web app has started
async def on_startup(app):
# remember main event loop
@@ -3033,6 +3070,7 @@ class ReticulumMeshChat:
# create and run web app
app = web.Application(
client_max_size=1024 * 1024 * 50,
middlewares=[security_middleware],
) # allow uploading files up to 50mb
app.add_routes(routes)
app.add_routes(
@@ -3886,7 +3924,7 @@ class ReticulumMeshChat:
"icon_name": icon_name,
"foreground_colour": foreground_colour,
"background_colour": background_colour,
"updated_at": datetime.now(timezone.utc),
"updated_at": datetime.now(UTC),
}
# upsert to database
@@ -4108,7 +4146,7 @@ class ReticulumMeshChat:
"snr": lxmf_message_dict["snr"],
"quality": lxmf_message_dict["quality"],
"is_spam": is_spam,
"updated_at": datetime.now(timezone.utc),
"updated_at": datetime.now(UTC),
}
# upsert to database
@@ -4144,7 +4182,7 @@ class ReticulumMeshChat:
"rssi": rssi,
"snr": snr,
"quality": quality,
"updated_at": datetime.now(timezone.utc),
"updated_at": datetime.now(UTC),
}
# only set app data if provided, as we don't want to wipe existing data when we request keys from the network
@@ -4170,7 +4208,7 @@ class ReticulumMeshChat:
data = {
"destination_hash": destination_hash,
"display_name": display_name,
"updated_at": datetime.now(timezone.utc),
"updated_at": datetime.now(UTC),
}
# upsert to database
@@ -4193,7 +4231,7 @@ class ReticulumMeshChat:
"destination_hash": destination_hash,
"display_name": display_name,
"aspect": aspect,
"updated_at": datetime.now(timezone.utc),
"updated_at": datetime.now(UTC),
}
# upsert to database
@@ -4210,8 +4248,8 @@ class ReticulumMeshChat:
# prepare data to insert or update
data = {
"destination_hash": destination_hash,
"last_read_at": datetime.now(timezone.utc),
"updated_at": datetime.now(timezone.utc),
"last_read_at": datetime.now(UTC),
"updated_at": datetime.now(UTC),
}
# upsert to database
@@ -4878,7 +4916,7 @@ class Config:
data = {
"key": key,
"value": value,
"updated_at": datetime.now(timezone.utc),
"updated_at": datetime.now(UTC),
}
# upsert to database

View File

@@ -0,0 +1 @@
"""Backend utilities shared by the Reticulum MeshChatX CLI."""

View File

@@ -3,9 +3,8 @@ import time
import RNS
from RNS.Interfaces.Interface import Interface
from websockets.sync.server import Server, ServerConnection, serve
from src.backend.interfaces.WebsocketClientInterface import WebsocketClientInterface
from websockets.sync.server import Server, ServerConnection, serve
class WebsocketServerInterface(Interface):

View File

@@ -0,0 +1 @@
"""Shared transport interfaces for MeshChatX."""

View File

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

View File

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Some files were not shown because too many files have changed in this diff Show More