diff --git a/public/index.html b/public/index.html
index c15b5f3..97af93a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1424,21 +1424,6 @@
break;
- }
- case 'lxmf_outbound_message_created': {
-
- // add outbound message to ui
- this.chatItems.push({
- "type": "lxmf_message",
- "lxmf_message": json.lxmf_message,
- "is_outbound": true,
- });
-
- // always scroll to bottom since we just sent a message
- this.scrollMessagesToBottom();
-
- break;
-
}
case 'lxmf_message_state_updated': {
@@ -1593,12 +1578,6 @@
return;
}
- // do nothing if not connected to websocket
- if(!this.isWebsocketConnected){
- this.alert("Not connected to WebSocket!");
- return;
- }
-
// do nothing if no peer selected
if(!this.selectedPeer){
return;
@@ -1642,15 +1621,24 @@
};
}
- // send message to reticulum via websocket
- this.ws.send(JSON.stringify({
- "type": "lxmf.delivery",
+ // send message to reticulum
+ const response = await window.axios.post(`/api/v1/lxmf-messages/send`, {
"lxmf_message": {
"destination_hash": this.selectedPeer.destination_hash,
"content": this.newMessageText,
"fields": fields,
},
- }));
+ });
+
+ // add outbound message to ui
+ this.chatItems.push({
+ "type": "lxmf_message",
+ "lxmf_message": response.data.lxmf_message,
+ "is_outbound": true,
+ });
+
+ // always scroll to bottom since we just sent a message
+ this.scrollMessagesToBottom();
// clear message inputs
this.newMessageText = "";
@@ -1663,16 +1651,15 @@
} catch(e) {
- // todo handle error
- console.error(e);
+ // show error
+ const message = e.response?.data?.message ?? "failed to send message";
+ this.alert(message);
+ console.log(e);
} finally {
this.isSendingMessage = false;
}
- // scroll to bottom
- this.scrollMessagesToBottom();
-
},
async retrySendingMessage(chatItem) {
@@ -1687,22 +1674,34 @@
try {
- // send message to reticulum via websocket
- this.ws.send(JSON.stringify({
- "type": "lxmf.delivery",
- "lxmf_message": chatItem.lxmf_message,
- }));
+ // send message to reticulum
+ const response = await window.axios.post(`/api/v1/lxmf-messages/send`, {
+ "lxmf_message": {
+ "destination_hash": chatItem.lxmf_message.destination_hash,
+ "content": chatItem.lxmf_message.content,
+ "fields": chatItem.lxmf_message.fields,
+ },
+ });
+
+ // add outbound message to ui
+ this.chatItems.push({
+ "type": "lxmf_message",
+ "lxmf_message": response.data.lxmf_message,
+ "is_outbound": true,
+ });
+
+ // always scroll to bottom since we just sent a message
+ this.scrollMessagesToBottom();
} catch(e) {
- // todo handle error
- console.error(e);
+ // show error
+ const message = e.response?.data?.message ?? "failed to send message";
+ this.alert(message);
+ console.log(e);
}
- // scroll to bottom
- this.scrollMessagesToBottom();
-
},
async updateConfig(config) {
diff --git a/web.py b/web.py
index d122cdb..7ee89f2 100644
--- a/web.py
+++ b/web.py
@@ -796,6 +796,63 @@ class ReticulumMeshChat:
"path_table": path_table,
})
+ # send lxmf message
+ @routes.post("/api/v1/lxmf-messages/send")
+ async def index(request):
+
+ # get request body as json
+ data = await request.json()
+
+ # get data from json
+ destination_hash = data["lxmf_message"]["destination_hash"]
+ content = data["lxmf_message"]["content"]
+ fields = {}
+ if "fields" in data["lxmf_message"]:
+ fields = data["lxmf_message"]["fields"]
+
+ # parse image field
+ image_field = None
+ if "image" in fields:
+ image_type = data["lxmf_message"]["fields"]["image"]["image_type"]
+ image_bytes = base64.b64decode(data["lxmf_message"]["fields"]["image"]["image_bytes"])
+ image_field = LxmfImageField(image_type, image_bytes)
+
+ # parse audio field
+ audio_field = None
+ if "audio" in fields:
+ audio_mode = data["lxmf_message"]["fields"]["audio"]["audio_mode"]
+ audio_bytes = base64.b64decode(data["lxmf_message"]["fields"]["audio"]["audio_bytes"])
+ audio_field = LxmfAudioField(audio_mode, audio_bytes)
+
+ # parse file attachments field
+ file_attachments_field = None
+ if "file_attachments" in fields:
+
+ file_attachments = []
+ for file_attachment in data["lxmf_message"]["fields"]["file_attachments"]:
+ file_name = file_attachment["file_name"]
+ file_bytes = base64.b64decode(file_attachment["file_bytes"])
+ file_attachments.append(LxmfFileAttachment(file_name, file_bytes))
+
+ file_attachments_field = LxmfFileAttachmentsField(file_attachments)
+
+ try:
+
+ # send lxmf message to destination
+ lxmf_message = await self.send_message(destination_hash, content,
+ image_field=image_field,
+ audio_field=audio_field,
+ file_attachments_field=file_attachments_field)
+
+ return web.json_response({
+ "lxmf_message": self.convert_lxmf_message_to_dict(lxmf_message),
+ })
+
+ except Exception as e:
+ return web.json_response({
+ "message": "Sending Failed: {}".format(str(e)),
+ }, status=503)
+
# delete lxmf message
@routes.delete("/api/v1/lxmf-messages/{hash}")
async def index(request):
@@ -1011,53 +1068,6 @@ class ReticulumMeshChat:
# send config to websocket clients
await self.send_config_to_websocket_clients()
- # handle sending an lxmf message
- elif _type == "lxmf.delivery":
-
- # get data from websocket client
- destination_hash = data["lxmf_message"]["destination_hash"]
- content = data["lxmf_message"]["content"]
- fields = {}
- if "fields" in data["lxmf_message"]:
- fields = data["lxmf_message"]["fields"]
-
- # parse image field
- image_field = None
- if "image" in fields:
- image_type = data["lxmf_message"]["fields"]["image"]["image_type"]
- image_bytes = base64.b64decode(data["lxmf_message"]["fields"]["image"]["image_bytes"])
- image_field = LxmfImageField(image_type, image_bytes)
-
- # parse audio field
- audio_field = None
- if "audio" in fields:
- audio_mode = data["lxmf_message"]["fields"]["audio"]["audio_mode"]
- audio_bytes = base64.b64decode(data["lxmf_message"]["fields"]["audio"]["audio_bytes"])
- audio_field = LxmfAudioField(audio_mode, audio_bytes)
-
- # parse file attachments field
- file_attachments_field = None
- if "file_attachments" in fields:
-
- file_attachments = []
- for file_attachment in data["lxmf_message"]["fields"]["file_attachments"]:
- file_name = file_attachment["file_name"]
- file_bytes = base64.b64decode(file_attachment["file_bytes"])
- file_attachments.append(LxmfFileAttachment(file_name, file_bytes))
-
- file_attachments_field = LxmfFileAttachmentsField(file_attachments)
-
- # send lxmf message to destination
- await self.send_message(destination_hash, content,
- image_field=image_field,
- audio_field=audio_field,
- file_attachments_field=file_attachments_field)
-
- # # TODO: send response to client when marked as delivered?
- # await client.send(json.dumps({
- # "type": "lxmf.sent",
- # }))
-
# handle sending an announce
elif _type == "announce":
await self.announce()
@@ -1463,83 +1473,72 @@ class ReticulumMeshChat:
async def send_message(self, destination_hash, content: str,
image_field: LxmfImageField = None,
audio_field: LxmfAudioField = None,
- file_attachments_field: LxmfFileAttachmentsField = None):
+ file_attachments_field: LxmfFileAttachmentsField = None) -> LXMF.LXMessage:
- try:
+ # convert destination hash to bytes
+ destination_hash = bytes.fromhex(destination_hash)
- # convert destination hash to bytes
- destination_hash = bytes.fromhex(destination_hash)
+ # FIXME: can this be removed, and just rely on the router to check paths?
+ # find destination identity from hash
+ destination_identity = RNS.Identity.recall(destination_hash)
+ if destination_identity is None:
- # FIXME: can this be removed, and just rely on the router to check paths?
- # find destination identity from hash
- destination_identity = RNS.Identity.recall(destination_hash)
- if destination_identity is None:
+ # we don't know the path/identity for this destination hash, we will request it
+ RNS.Transport.request_path(destination_hash)
- # we don't know the path/identity for this destination hash, we will request it
- RNS.Transport.request_path(destination_hash)
+ # we have to bail out of sending, since we don't have the path yet
+ raise Exception("Destination identity is not known. Try again later.")
- # we have to bail out of sending, since we don't have the path yet
- # FIXME: we just ate the message, and didn't tell the user it failed...
- return
+ # create destination for recipients lxmf delivery address
+ lxmf_destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery")
- # create destination for recipients lxmf delivery address
- lxmf_destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery")
+ # create lxmf message
+ lxmf_message = LXMF.LXMessage(lxmf_destination, self.local_lxmf_destination, content, desired_method=LXMF.LXMessage.DIRECT)
+ lxmf_message.try_propagation_on_fail = True
- # create lxmf message
- lxmf_message = LXMF.LXMessage(lxmf_destination, self.local_lxmf_destination, content, desired_method=LXMF.LXMessage.DIRECT)
- lxmf_message.try_propagation_on_fail = True
+ lxmf_message.fields = {}
- lxmf_message.fields = {}
+ # add file attachments field
+ if file_attachments_field is not None:
- # add file attachments field
- if file_attachments_field is not None:
+ # create array of [[file_name, file_bytes], [file_name, file_bytes], ...]
+ file_attachments = []
+ for file_attachment in file_attachments_field.file_attachments:
+ file_attachments.append([file_attachment.file_name, file_attachment.file_bytes])
- # create array of [[file_name, file_bytes], [file_name, file_bytes], ...]
- file_attachments = []
- for file_attachment in file_attachments_field.file_attachments:
- file_attachments.append([file_attachment.file_name, file_attachment.file_bytes])
+ # set field attachments field
+ lxmf_message.fields[LXMF.FIELD_FILE_ATTACHMENTS] = file_attachments
- # set field attachments field
- lxmf_message.fields[LXMF.FIELD_FILE_ATTACHMENTS] = file_attachments
+ # add image field
+ if image_field is not None:
+ lxmf_message.fields[LXMF.FIELD_IMAGE] = [
+ image_field.image_type,
+ image_field.image_bytes,
+ ]
- # add image field
- if image_field is not None:
- lxmf_message.fields[LXMF.FIELD_IMAGE] = [
- image_field.image_type,
- image_field.image_bytes,
- ]
+ # add audio field
+ if audio_field is not None:
+ lxmf_message.fields[LXMF.FIELD_AUDIO] = [
+ audio_field.audio_mode,
+ audio_field.audio_bytes,
+ ]
- # add audio field
- if audio_field is not None:
- lxmf_message.fields[LXMF.FIELD_AUDIO] = [
- audio_field.audio_mode,
- audio_field.audio_bytes,
- ]
+ # register delivery callbacks
+ lxmf_message.register_delivery_callback(self.on_lxmf_sending_state_updated)
+ lxmf_message.register_failed_callback(self.on_lxmf_sending_failed)
- # register delivery callbacks
- lxmf_message.register_delivery_callback(self.on_lxmf_sending_state_updated)
- lxmf_message.register_failed_callback(self.on_lxmf_sending_failed)
+ # send lxmf message to be routed to destination
+ self.message_router.handle_outbound(lxmf_message)
- # send lxmf message to be routed to destination
- self.message_router.handle_outbound(lxmf_message)
+ # upsert lxmf message to database
+ self.db_upsert_lxmf_message(lxmf_message)
- # upsert lxmf message to database
- self.db_upsert_lxmf_message(lxmf_message)
+ # handle lxmf message progress loop without blocking or awaiting
+ # otherwise other incoming websocket packets will not be processed until sending is complete
+ # which results in the next message not showing up until the first message is finished
+ asyncio.create_task(self.handle_lxmf_message_progress(lxmf_message))
- # send outbound lxmf message to websocket (after passing to router so hash is available)
- await self.websocket_broadcast(json.dumps({
- "type": "lxmf_outbound_message_created",
- "lxmf_message": self.convert_lxmf_message_to_dict(lxmf_message),
- }))
-
- # handle lxmf message progress loop without blocking or awaiting
- # otherwise other incoming websocket packets will not be processed until sending is complete
- # which results in the next message not showing up until the first message is finished
- asyncio.create_task(self.handle_lxmf_message_progress(lxmf_message))
-
- except:
- # FIXME send error to websocket?
- print("failed to send lxmf message")
+ return lxmf_message
# updates lxmf message in database and broadcasts to websocket until it's delivered, or it fails
async def handle_lxmf_message_progress(self, lxmf_message):