Compare commits
5 Commits
v2.40.0
...
deepsource
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73f677d319 | ||
|
4770c21499
|
|||
|
720bef90c7
|
|||
|
1c98a231fd
|
|||
|
f6a1be5e80
|
26
.github/workflows/build.yml
vendored
26
.github/workflows/build.yml
vendored
@@ -4,10 +4,33 @@ on:
|
||||
push:
|
||||
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:
|
||||
build_windows:
|
||||
runs-on: windows-latest
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.build_windows == 'true')
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
@@ -49,6 +72,7 @@ jobs:
|
||||
|
||||
build_mac:
|
||||
runs-on: macos-13
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.build_mac == 'true')
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
@@ -90,6 +114,7 @@ jobs:
|
||||
|
||||
build_linux:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.build_linux == 'true')
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
@@ -134,6 +159,7 @@ jobs:
|
||||
|
||||
build_docker:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.build_docker == 'true')
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
45
.github/workflows/manual-docker-build.yml
vendored
45
.github/workflows/manual-docker-build.yml
vendored
@@ -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/
|
||||
|
||||
89
meshchat.py
89
meshchat.py
@@ -221,7 +221,8 @@ class ReticulumMeshChat:
|
||||
thread.start()
|
||||
|
||||
# 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:
|
||||
package_json = json.load(f)
|
||||
return package_json["version"]
|
||||
@@ -447,7 +448,8 @@ class ReticulumMeshChat:
|
||||
)
|
||||
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:
|
||||
return False
|
||||
try:
|
||||
@@ -491,7 +493,8 @@ class ReticulumMeshChat:
|
||||
|
||||
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:
|
||||
return False
|
||||
value = value.lower()
|
||||
@@ -1986,14 +1989,14 @@ class ReticulumMeshChat:
|
||||
nomadnetwork_node_announce 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,
|
||||
)
|
||||
|
||||
# parse app_data so we can see if propagation is enabled or disabled for this node
|
||||
is_propagation_enabled = 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,
|
||||
)
|
||||
if propagation_node_data is not None:
|
||||
@@ -2753,8 +2756,8 @@ class ReticulumMeshChat:
|
||||
other_user_hash,
|
||||
),
|
||||
"destination_hash": other_user_hash,
|
||||
"is_unread": self.is_lxmf_conversation_unread(other_user_hash),
|
||||
"failed_messages_count": self.lxmf_conversation_failed_messages_count(
|
||||
"is_unread": ReticulumMeshChat.is_lxmf_conversation_unread(other_user_hash),
|
||||
"failed_messages_count": ReticulumMeshChat.lxmf_conversation_failed_messages_count(
|
||||
other_user_hash,
|
||||
),
|
||||
"has_attachments": has_attachments,
|
||||
@@ -3167,7 +3170,8 @@ class ReticulumMeshChat:
|
||||
# to the following map:
|
||||
# - var_field1: 123
|
||||
# - 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 = {}
|
||||
if path_data is not None:
|
||||
for field in path_data.split("|"):
|
||||
@@ -3178,7 +3182,8 @@ class ReticulumMeshChat:
|
||||
print(f"unhandled field: {field}")
|
||||
return data
|
||||
|
||||
def convert_nomadnet_field_data_to_map(self, field_data):
|
||||
@staticmethod
|
||||
def convert_nomadnet_field_data_to_map(field_data):
|
||||
data = {}
|
||||
if field_data is not None or "{}":
|
||||
try:
|
||||
@@ -3681,7 +3686,8 @@ class ReticulumMeshChat:
|
||||
}
|
||||
|
||||
# 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
|
||||
lxmf_message_state = "unknown"
|
||||
if lxmf_message.state == LXMF.LXMessage.GENERATING:
|
||||
@@ -3704,7 +3710,8 @@ class ReticulumMeshChat:
|
||||
return lxmf_message_state
|
||||
|
||||
# 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
|
||||
lxmf_message_method = "unknown"
|
||||
if lxmf_message.method == LXMF.LXMessage.OPPORTUNISTIC:
|
||||
@@ -3718,7 +3725,8 @@ class ReticulumMeshChat:
|
||||
|
||||
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
|
||||
state_map = {
|
||||
LXMRouter.PR_IDLE: "idle",
|
||||
@@ -3749,7 +3757,7 @@ class ReticulumMeshChat:
|
||||
if announce.aspect == "lxmf.delivery":
|
||||
display_name = self.parse_lxmf_display_name(announce.app_data)
|
||||
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
|
||||
lxmf_user_icon = None
|
||||
@@ -3787,7 +3795,8 @@ class ReticulumMeshChat:
|
||||
}
|
||||
|
||||
# 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 {
|
||||
"id": favourite.id,
|
||||
"destination_hash": favourite.destination_hash,
|
||||
@@ -3798,7 +3807,8 @@ class ReticulumMeshChat:
|
||||
}
|
||||
|
||||
# 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 {
|
||||
"id": db_lxmf_message.id,
|
||||
"hash": db_lxmf_message.hash,
|
||||
@@ -3823,8 +3833,8 @@ class ReticulumMeshChat:
|
||||
}
|
||||
|
||||
# updates the lxmf user icon for the provided destination hash
|
||||
@staticmethod
|
||||
def update_lxmf_user_icon(
|
||||
self,
|
||||
destination_hash: str,
|
||||
icon_name: str,
|
||||
foreground_colour: str,
|
||||
@@ -3852,7 +3862,8 @@ class ReticulumMeshChat:
|
||||
query.execute()
|
||||
|
||||
# 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:
|
||||
blocked = database.BlockedDestination.get_or_none(
|
||||
database.BlockedDestination.destination_hash == destination_hash,
|
||||
@@ -3862,7 +3873,8 @@ class ReticulumMeshChat:
|
||||
return False
|
||||
|
||||
# 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:
|
||||
spam_keywords = database.SpamKeyword.select()
|
||||
search_text = (title + " " + content).lower()
|
||||
@@ -3874,7 +3886,8 @@ class ReticulumMeshChat:
|
||||
return False
|
||||
|
||||
# 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:
|
||||
if LXMF.FIELD_FILE_ATTACHMENTS in lxmf_fields:
|
||||
return len(lxmf_fields[LXMF.FIELD_FILE_ATTACHMENTS]) > 0
|
||||
@@ -4108,8 +4121,9 @@ class ReticulumMeshChat:
|
||||
query.execute()
|
||||
|
||||
# upserts a custom destination display name to the database
|
||||
@staticmethod
|
||||
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
|
||||
data = {
|
||||
@@ -4127,8 +4141,9 @@ class ReticulumMeshChat:
|
||||
query.execute()
|
||||
|
||||
# upserts a custom destination display name to the database
|
||||
@staticmethod
|
||||
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
|
||||
data = {
|
||||
@@ -4147,7 +4162,8 @@ class ReticulumMeshChat:
|
||||
query.execute()
|
||||
|
||||
# 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
|
||||
data = {
|
||||
"destination_hash": destination_hash,
|
||||
@@ -4633,7 +4649,8 @@ class ReticulumMeshChat:
|
||||
)
|
||||
|
||||
# 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
|
||||
db_destination_display_name = database.CustomDestinationDisplayName.get_or_none(
|
||||
database.CustomDestinationDisplayName.destination_hash == destination_hash,
|
||||
@@ -4646,7 +4663,8 @@ class ReticulumMeshChat:
|
||||
# get name to show for an lxmf conversation
|
||||
# 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
|
||||
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
|
||||
lxmf_announce = (
|
||||
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
|
||||
# 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:
|
||||
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
|
||||
return "Anonymous Peer"
|
||||
|
||||
# reads the lxmf display name from the provided base64 app data
|
||||
@staticmethod
|
||||
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:
|
||||
app_data_bytes = base64.b64decode(app_data_base64)
|
||||
@@ -4678,7 +4697,8 @@ class ReticulumMeshChat:
|
||||
return default_value
|
||||
|
||||
# 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:
|
||||
app_data_bytes = base64.b64decode(app_data_base64)
|
||||
return LXMF.stamp_cost_from_app_data(app_data_bytes)
|
||||
@@ -4686,8 +4706,9 @@ class ReticulumMeshChat:
|
||||
return None
|
||||
|
||||
# reads the nomadnetwork node display name from the provided base64 app data
|
||||
@staticmethod
|
||||
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:
|
||||
app_data_bytes = base64.b64decode(app_data_base64)
|
||||
@@ -4696,7 +4717,8 @@ class ReticulumMeshChat:
|
||||
return default_value
|
||||
|
||||
# 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:
|
||||
app_data_bytes = base64.b64decode(app_data_base64)
|
||||
data = msgpack.unpackb(app_data_bytes)
|
||||
@@ -4709,7 +4731,8 @@ class ReticulumMeshChat:
|
||||
return None
|
||||
|
||||
# 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
|
||||
lxmf_conversation_read_state = (
|
||||
database.LxmfConversationReadState.select()
|
||||
@@ -4745,7 +4768,8 @@ class ReticulumMeshChat:
|
||||
return conversation_last_read_at < conversation_latest_message_at
|
||||
|
||||
# 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 (
|
||||
database.LxmfMessage.select()
|
||||
.where(database.LxmfMessage.state == "failed")
|
||||
@@ -4754,7 +4778,8 @@ class ReticulumMeshChat:
|
||||
)
|
||||
|
||||
# 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:
|
||||
interface_name = str(interface)
|
||||
if name == interface_name:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"scripts": {
|
||||
"watch": "npm run build-frontend -- --watch",
|
||||
"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",
|
||||
"electron-postinstall": "electron-builder install-app-deps",
|
||||
"electron": "npm run electron-postinstall && npm run build && electron .",
|
||||
|
||||
18
scripts/build-backend.js
Executable file
18
scripts/build-backend.js
Executable 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user