Compare commits

...

5 Commits

Author SHA1 Message Date
deepsource-autofix[bot]
73f677d319 refactor: change methods not using its bound instance to staticmethods
The method doesn't use its bound instance. Decorate this method with `@staticmethod` decorator, so that Python does not have to instantiate a bound method for every instance of this class thereby saving memory and computation. Read more about staticmethods [here](https://docs.python.org/3/library/functions.html#staticmethod).
2025-12-01 03:22:30 +00:00
4770c21499 update to add manual trigger 2025-11-30 21:18:49 -06:00
720bef90c7 remove old workflow 2025-11-30 21:18:42 -06:00
1c98a231fd Refactor ReticulumMeshChat methods to static
- Updated several instance methods in ReticulumMeshChat to static methods for improved clarity and usability.
- Adjusted method calls to reflect the new static context, enhancing code organization.
2025-11-30 21:17:09 -06:00
f6a1be5e80 Replace backend build script in package.json with a Node.js script for improved compatibility and maintainability. Added new build-backend.js script to handle the backend build process using Python. 2025-11-30 21:16:49 -06:00
5 changed files with 102 additions and 78 deletions

View File

@@ -4,10 +4,33 @@ on:
push: push:
tags: tags:
- "*" - "*"
workflow_dispatch:
inputs:
build_windows:
description: 'Build Windows'
required: false
default: 'true'
type: boolean
build_mac:
description: 'Build macOS'
required: false
default: 'true'
type: boolean
build_linux:
description: 'Build Linux'
required: false
default: 'true'
type: boolean
build_docker:
description: 'Build Docker'
required: false
default: 'true'
type: boolean
jobs: jobs:
build_windows: build_windows:
runs-on: windows-latest runs-on: windows-latest
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.build_windows == 'true')
permissions: permissions:
contents: write contents: write
steps: steps:
@@ -49,6 +72,7 @@ jobs:
build_mac: build_mac:
runs-on: macos-13 runs-on: macos-13
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.build_mac == 'true')
permissions: permissions:
contents: write contents: write
steps: steps:
@@ -90,6 +114,7 @@ jobs:
build_linux: build_linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.build_linux == 'true')
permissions: permissions:
contents: write contents: write
steps: steps:
@@ -134,6 +159,7 @@ jobs:
build_docker: build_docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.build_docker == 'true')
permissions: permissions:
packages: write packages: write
contents: read contents: read

View File

@@ -1,45 +0,0 @@
name: Temporary manual trigger for Docker build
on:
workflow_dispatch:
jobs:
build_docker:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Clone Repo
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Set lowercase repository owner
run: echo "REPO_OWNER_LC=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_ENV
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- name: Log in to the GitHub Container registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker images
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: >-
ghcr.io/${{ env.REPO_OWNER_LC }}/reticulum-meshchat:latest,
ghcr.io/${{ env.REPO_OWNER_LC }}/reticulum-meshchat:${{ github.ref_name }}
labels: >-
org.opencontainers.image.title=Reticulum MeshChat,
org.opencontainers.image.description=Docker image for Reticulum MeshChat,
org.opencontainers.image.url=https://github.com/${{ github.repository }}/pkgs/container/reticulum-meshchat/

View File

@@ -221,7 +221,8 @@ class ReticulumMeshChat:
thread.start() thread.start()
# gets app version from package.json # gets app version from package.json
def get_app_version(self) -> str: @staticmethod
def get_app_version() -> str:
with open(get_file_path("package.json")) as f: with open(get_file_path("package.json")) as f:
package_json = json.load(f) package_json = json.load(f)
return package_json["version"] return package_json["version"]
@@ -447,7 +448,8 @@ class ReticulumMeshChat:
) )
return query.exists() return query.exists()
def message_fields_have_attachments(self, fields_json: str | None): @staticmethod
def message_fields_have_attachments(fields_json: str | None):
if not fields_json: if not fields_json:
return False return False
try: try:
@@ -491,7 +493,8 @@ class ReticulumMeshChat:
return matches return matches
def parse_bool_query_param(self, value: str | None) -> bool: @staticmethod
def parse_bool_query_param(value: str | None) -> bool:
if value is None: if value is None:
return False return False
value = value.lower() value = value.lower()
@@ -1986,14 +1989,14 @@ class ReticulumMeshChat:
nomadnetwork_node_announce is not None nomadnetwork_node_announce is not None
and nomadnetwork_node_announce.app_data is not None and nomadnetwork_node_announce.app_data is not None
): ):
operator_display_name = self.parse_nomadnetwork_node_display_name( operator_display_name = ReticulumMeshChat.parse_nomadnetwork_node_display_name(
nomadnetwork_node_announce.app_data, None, nomadnetwork_node_announce.app_data, None,
) )
# parse app_data so we can see if propagation is enabled or disabled for this node # parse app_data so we can see if propagation is enabled or disabled for this node
is_propagation_enabled = None is_propagation_enabled = None
per_transfer_limit = None per_transfer_limit = None
propagation_node_data = self.parse_lxmf_propagation_node_app_data( propagation_node_data = ReticulumMeshChat.parse_lxmf_propagation_node_app_data(
announce.app_data, announce.app_data,
) )
if propagation_node_data is not None: if propagation_node_data is not None:
@@ -2753,8 +2756,8 @@ class ReticulumMeshChat:
other_user_hash, other_user_hash,
), ),
"destination_hash": other_user_hash, "destination_hash": other_user_hash,
"is_unread": self.is_lxmf_conversation_unread(other_user_hash), "is_unread": ReticulumMeshChat.is_lxmf_conversation_unread(other_user_hash),
"failed_messages_count": self.lxmf_conversation_failed_messages_count( "failed_messages_count": ReticulumMeshChat.lxmf_conversation_failed_messages_count(
other_user_hash, other_user_hash,
), ),
"has_attachments": has_attachments, "has_attachments": has_attachments,
@@ -3167,7 +3170,8 @@ class ReticulumMeshChat:
# to the following map: # to the following map:
# - var_field1: 123 # - var_field1: 123
# - var_field2: 456 # - var_field2: 456
def convert_nomadnet_string_data_to_map(self, path_data: str | None): @staticmethod
def convert_nomadnet_string_data_to_map(path_data: str | None):
data = {} data = {}
if path_data is not None: if path_data is not None:
for field in path_data.split("|"): for field in path_data.split("|"):
@@ -3178,7 +3182,8 @@ class ReticulumMeshChat:
print(f"unhandled field: {field}") print(f"unhandled field: {field}")
return data return data
def convert_nomadnet_field_data_to_map(self, field_data): @staticmethod
def convert_nomadnet_field_data_to_map(field_data):
data = {} data = {}
if field_data is not None or "{}": if field_data is not None or "{}":
try: try:
@@ -3681,7 +3686,8 @@ class ReticulumMeshChat:
} }
# convert lxmf state to a human friendly string # convert lxmf state to a human friendly string
def convert_lxmf_state_to_string(self, lxmf_message: LXMF.LXMessage): @staticmethod
def convert_lxmf_state_to_string(lxmf_message: LXMF.LXMessage):
# convert state to string # convert state to string
lxmf_message_state = "unknown" lxmf_message_state = "unknown"
if lxmf_message.state == LXMF.LXMessage.GENERATING: if lxmf_message.state == LXMF.LXMessage.GENERATING:
@@ -3704,7 +3710,8 @@ class ReticulumMeshChat:
return lxmf_message_state return lxmf_message_state
# convert lxmf method to a human friendly string # convert lxmf method to a human friendly string
def convert_lxmf_method_to_string(self, lxmf_message: LXMF.LXMessage): @staticmethod
def convert_lxmf_method_to_string(lxmf_message: LXMF.LXMessage):
# convert method to string # convert method to string
lxmf_message_method = "unknown" lxmf_message_method = "unknown"
if lxmf_message.method == LXMF.LXMessage.OPPORTUNISTIC: if lxmf_message.method == LXMF.LXMessage.OPPORTUNISTIC:
@@ -3718,7 +3725,8 @@ class ReticulumMeshChat:
return lxmf_message_method return lxmf_message_method
def convert_propagation_node_state_to_string(self, state): @staticmethod
def convert_propagation_node_state_to_string(state):
# map states to strings # map states to strings
state_map = { state_map = {
LXMRouter.PR_IDLE: "idle", LXMRouter.PR_IDLE: "idle",
@@ -3749,7 +3757,7 @@ class ReticulumMeshChat:
if announce.aspect == "lxmf.delivery": if announce.aspect == "lxmf.delivery":
display_name = self.parse_lxmf_display_name(announce.app_data) display_name = self.parse_lxmf_display_name(announce.app_data)
elif announce.aspect == "nomadnetwork.node": elif announce.aspect == "nomadnetwork.node":
display_name = self.parse_nomadnetwork_node_display_name(announce.app_data) display_name = ReticulumMeshChat.parse_nomadnetwork_node_display_name(announce.app_data)
# find lxmf user icon from database # find lxmf user icon from database
lxmf_user_icon = None lxmf_user_icon = None
@@ -3787,7 +3795,8 @@ class ReticulumMeshChat:
} }
# convert database favourite to a dictionary # convert database favourite to a dictionary
def convert_db_favourite_to_dict(self, favourite: database.FavouriteDestination): @staticmethod
def convert_db_favourite_to_dict(favourite: database.FavouriteDestination):
return { return {
"id": favourite.id, "id": favourite.id,
"destination_hash": favourite.destination_hash, "destination_hash": favourite.destination_hash,
@@ -3798,7 +3807,8 @@ class ReticulumMeshChat:
} }
# convert database lxmf message to a dictionary # convert database lxmf message to a dictionary
def convert_db_lxmf_message_to_dict(self, db_lxmf_message: database.LxmfMessage): @staticmethod
def convert_db_lxmf_message_to_dict(db_lxmf_message: database.LxmfMessage):
return { return {
"id": db_lxmf_message.id, "id": db_lxmf_message.id,
"hash": db_lxmf_message.hash, "hash": db_lxmf_message.hash,
@@ -3823,8 +3833,8 @@ class ReticulumMeshChat:
} }
# updates the lxmf user icon for the provided destination hash # updates the lxmf user icon for the provided destination hash
@staticmethod
def update_lxmf_user_icon( def update_lxmf_user_icon(
self,
destination_hash: str, destination_hash: str,
icon_name: str, icon_name: str,
foreground_colour: str, foreground_colour: str,
@@ -3852,7 +3862,8 @@ class ReticulumMeshChat:
query.execute() query.execute()
# check if a destination is blocked # check if a destination is blocked
def is_destination_blocked(self, destination_hash: str) -> bool: @staticmethod
def is_destination_blocked(destination_hash: str) -> bool:
try: try:
blocked = database.BlockedDestination.get_or_none( blocked = database.BlockedDestination.get_or_none(
database.BlockedDestination.destination_hash == destination_hash, database.BlockedDestination.destination_hash == destination_hash,
@@ -3862,7 +3873,8 @@ class ReticulumMeshChat:
return False return False
# check if message content matches spam keywords # check if message content matches spam keywords
def check_spam_keywords(self, title: str, content: str) -> bool: @staticmethod
def check_spam_keywords(title: str, content: str) -> bool:
try: try:
spam_keywords = database.SpamKeyword.select() spam_keywords = database.SpamKeyword.select()
search_text = (title + " " + content).lower() search_text = (title + " " + content).lower()
@@ -3874,7 +3886,8 @@ class ReticulumMeshChat:
return False return False
# check if message has attachments and should be rejected # check if message has attachments and should be rejected
def has_attachments(self, lxmf_fields: dict) -> bool: @staticmethod
def has_attachments(lxmf_fields: dict) -> bool:
try: try:
if LXMF.FIELD_FILE_ATTACHMENTS in lxmf_fields: if LXMF.FIELD_FILE_ATTACHMENTS in lxmf_fields:
return len(lxmf_fields[LXMF.FIELD_FILE_ATTACHMENTS]) > 0 return len(lxmf_fields[LXMF.FIELD_FILE_ATTACHMENTS]) > 0
@@ -4108,8 +4121,9 @@ class ReticulumMeshChat:
query.execute() query.execute()
# upserts a custom destination display name to the database # upserts a custom destination display name to the database
@staticmethod
def db_upsert_custom_destination_display_name( def db_upsert_custom_destination_display_name(
self, destination_hash: str, display_name: str, destination_hash: str, display_name: str,
): ):
# prepare data to insert or update # prepare data to insert or update
data = { data = {
@@ -4127,8 +4141,9 @@ class ReticulumMeshChat:
query.execute() query.execute()
# upserts a custom destination display name to the database # upserts a custom destination display name to the database
@staticmethod
def db_upsert_favourite( def db_upsert_favourite(
self, destination_hash: str, display_name: str, aspect: str, destination_hash: str, display_name: str, aspect: str,
): ):
# prepare data to insert or update # prepare data to insert or update
data = { data = {
@@ -4147,7 +4162,8 @@ class ReticulumMeshChat:
query.execute() query.execute()
# upserts lxmf conversation read state to the database # upserts lxmf conversation read state to the database
def db_mark_lxmf_conversation_as_read(self, destination_hash: str): @staticmethod
def db_mark_lxmf_conversation_as_read(destination_hash: str):
# prepare data to insert or update # prepare data to insert or update
data = { data = {
"destination_hash": destination_hash, "destination_hash": destination_hash,
@@ -4633,7 +4649,8 @@ class ReticulumMeshChat:
) )
# gets the custom display name a user has set for the provided destination hash # gets the custom display name a user has set for the provided destination hash
def get_custom_destination_display_name(self, destination_hash: str): @staticmethod
def get_custom_destination_display_name(destination_hash: str):
# get display name from database # get display name from database
db_destination_display_name = database.CustomDestinationDisplayName.get_or_none( db_destination_display_name = database.CustomDestinationDisplayName.get_or_none(
database.CustomDestinationDisplayName.destination_hash == destination_hash, database.CustomDestinationDisplayName.destination_hash == destination_hash,
@@ -4646,7 +4663,8 @@ class ReticulumMeshChat:
# get name to show for an lxmf conversation # get name to show for an lxmf conversation
# currently, this will use the app data from the most recent announce # currently, this will use the app data from the most recent announce
# TODO: we should fetch this from our contacts database, when it gets implemented, and if not found, fallback to app data # TODO: we should fetch this from our contacts database, when it gets implemented, and if not found, fallback to app data
def get_lxmf_conversation_name(self, destination_hash): @staticmethod
def get_lxmf_conversation_name(destination_hash):
# get lxmf.delivery announce from database for the provided destination hash # get lxmf.delivery announce from database for the provided destination hash
lxmf_announce = ( lxmf_announce = (
database.Announce.select() database.Announce.select()
@@ -4658,14 +4676,15 @@ class ReticulumMeshChat:
# if app data is available in database, it should be base64 encoded text that was announced # if app data is available in database, it should be base64 encoded text that was announced
# we will return the parsed lxmf display name as the conversation name # we will return the parsed lxmf display name as the conversation name
if lxmf_announce is not None and lxmf_announce.app_data is not None: if lxmf_announce is not None and lxmf_announce.app_data is not None:
return self.parse_lxmf_display_name(app_data_base64=lxmf_announce.app_data) return ReticulumMeshChat.parse_lxmf_display_name(app_data_base64=lxmf_announce.app_data)
# announce did not have app data, so provide a fallback name # announce did not have app data, so provide a fallback name
return "Anonymous Peer" return "Anonymous Peer"
# reads the lxmf display name from the provided base64 app data # reads the lxmf display name from the provided base64 app data
@staticmethod
def parse_lxmf_display_name( def parse_lxmf_display_name(
self, app_data_base64: str, default_value: str | None = "Anonymous Peer", app_data_base64: str, default_value: str | None = "Anonymous Peer",
): ):
try: try:
app_data_bytes = base64.b64decode(app_data_base64) app_data_bytes = base64.b64decode(app_data_base64)
@@ -4678,7 +4697,8 @@ class ReticulumMeshChat:
return default_value return default_value
# reads the lxmf stamp cost from the provided base64 app data # reads the lxmf stamp cost from the provided base64 app data
def parse_lxmf_stamp_cost(self, app_data_base64: str): @staticmethod
def parse_lxmf_stamp_cost(app_data_base64: str):
try: try:
app_data_bytes = base64.b64decode(app_data_base64) app_data_bytes = base64.b64decode(app_data_base64)
return LXMF.stamp_cost_from_app_data(app_data_bytes) return LXMF.stamp_cost_from_app_data(app_data_bytes)
@@ -4686,8 +4706,9 @@ class ReticulumMeshChat:
return None return None
# reads the nomadnetwork node display name from the provided base64 app data # reads the nomadnetwork node display name from the provided base64 app data
@staticmethod
def parse_nomadnetwork_node_display_name( def parse_nomadnetwork_node_display_name(
self, app_data_base64: str, default_value: str | None = "Anonymous Node", app_data_base64: str, default_value: str | None = "Anonymous Node",
): ):
try: try:
app_data_bytes = base64.b64decode(app_data_base64) app_data_bytes = base64.b64decode(app_data_base64)
@@ -4696,7 +4717,8 @@ class ReticulumMeshChat:
return default_value return default_value
# parses lxmf propagation node app data # parses lxmf propagation node app data
def parse_lxmf_propagation_node_app_data(self, app_data_base64: str): @staticmethod
def parse_lxmf_propagation_node_app_data(app_data_base64: str):
try: try:
app_data_bytes = base64.b64decode(app_data_base64) app_data_bytes = base64.b64decode(app_data_base64)
data = msgpack.unpackb(app_data_bytes) data = msgpack.unpackb(app_data_bytes)
@@ -4709,7 +4731,8 @@ class ReticulumMeshChat:
return None return None
# returns true if the conversation has messages newer than the last read at timestamp # returns true if the conversation has messages newer than the last read at timestamp
def is_lxmf_conversation_unread(self, destination_hash): @staticmethod
def is_lxmf_conversation_unread(destination_hash):
# get lxmf conversation read state from database for the provided destination hash # get lxmf conversation read state from database for the provided destination hash
lxmf_conversation_read_state = ( lxmf_conversation_read_state = (
database.LxmfConversationReadState.select() database.LxmfConversationReadState.select()
@@ -4745,7 +4768,8 @@ class ReticulumMeshChat:
return conversation_last_read_at < conversation_latest_message_at return conversation_last_read_at < conversation_latest_message_at
# returns number of messages that failed to send in a conversation # returns number of messages that failed to send in a conversation
def lxmf_conversation_failed_messages_count(self, destination_hash: str): @staticmethod
def lxmf_conversation_failed_messages_count(destination_hash: str):
return ( return (
database.LxmfMessage.select() database.LxmfMessage.select()
.where(database.LxmfMessage.state == "failed") .where(database.LxmfMessage.state == "failed")
@@ -4754,7 +4778,8 @@ class ReticulumMeshChat:
) )
# find an interface by name # find an interface by name
def find_interface_by_name(self, name: str): @staticmethod
def find_interface_by_name(name: str):
for interface in RNS.Transport.interfaces: for interface in RNS.Transport.interfaces:
interface_name = str(interface) interface_name = str(interface)
if name == interface_name: if name == interface_name:

View File

@@ -7,7 +7,7 @@
"scripts": { "scripts": {
"watch": "npm run build-frontend -- --watch", "watch": "npm run build-frontend -- --watch",
"build-frontend": "vite build", "build-frontend": "vite build",
"build-backend": "venv/bin/python setup.py build", "build-backend": "node scripts/build-backend.js",
"build": "npm run build-frontend && npm run build-backend", "build": "npm run build-frontend && npm run build-backend",
"electron-postinstall": "electron-builder install-app-deps", "electron-postinstall": "electron-builder install-app-deps",
"electron": "npm run electron-postinstall && npm run build && electron .", "electron": "npm run electron-postinstall && npm run build && electron .",

18
scripts/build-backend.js Executable file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env node
const { execSync } = require('child_process');
const path = require('path');
const os = require('os');
const platform = os.platform();
const venvPython = platform === 'win32'
? path.join('venv', 'Scripts', 'python.exe')
: path.join('venv', 'bin', 'python');
try {
execSync(`${venvPython} setup.py build`, { stdio: 'inherit' });
} catch (error) {
console.error('Build failed:', error.message);
process.exit(1);
}