clean up all code
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
lxmf==0.4.3
|
lxmf==0.4.3
|
||||||
rns==0.7.3
|
rns==0.7.3
|
||||||
sanic==23.12.1
|
|
||||||
websockets==12.0
|
websockets==12.0
|
||||||
|
|||||||
401
web.py
401
web.py
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import http
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import RNS
|
import RNS
|
||||||
@@ -9,240 +10,217 @@ import asyncio
|
|||||||
import websockets
|
import websockets
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from sanic import Sanic, Request, Websocket, file
|
|
||||||
|
|
||||||
# global references
|
class ReticulumWebChat:
|
||||||
app_name = "ReticulumWebChat"
|
|
||||||
reticulum: RNS.Reticulum | None = None
|
|
||||||
identity: RNS.Identity | None = None
|
|
||||||
message_router: LXMF.LXMRouter | None = None
|
|
||||||
local_lxmf_destination: RNS.Destination | None = None
|
|
||||||
websocket_clients = []
|
|
||||||
|
|
||||||
# create sanic app
|
def __init__(self, identity: RNS.Identity):
|
||||||
app = Sanic(app_name)
|
|
||||||
|
|
||||||
|
# init reticulum
|
||||||
|
self.reticulum = RNS.Reticulum(None)
|
||||||
|
self.identity = identity
|
||||||
|
|
||||||
def main():
|
# init lxmf router
|
||||||
|
self.message_router = LXMF.LXMRouter(identity=self.identity, storagepath="storage/lxmf")
|
||||||
|
|
||||||
# parse command line args
|
# register lxmf identity
|
||||||
parser = argparse.ArgumentParser(description="ReticulumWebChat")
|
self.local_lxmf_destination = self.message_router.register_delivery_identity(self.identity, display_name="ReticulumWebChat")
|
||||||
parser.add_argument("--host", nargs='?', default="0.0.0.0", type=str, help="The address the web server should listen on.")
|
|
||||||
parser.add_argument("--port", nargs='?', default="8000", type=int, help="The port the web server should listen on.")
|
|
||||||
parser.add_argument("--identity-file", type=str, help="Path to a Reticulum Identity file to use as your LXMF address.")
|
|
||||||
parser.add_argument("--identity-base64", type=str, help="A base64 encoded Reticulum Identity to use as your LXMF address.")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# use provided identity, or fallback to a random one
|
# set a callback for when an lxmf message is received
|
||||||
global identity
|
self.message_router.register_delivery_callback(self.on_lxmf_delivery)
|
||||||
if args.identity_file is not None:
|
|
||||||
identity = RNS.Identity(create_keys=False)
|
|
||||||
identity.load(args.identity_file)
|
|
||||||
print("Reticulum Identity has been loaded from file.")
|
|
||||||
print(identity)
|
|
||||||
elif args.identity_base64 is not None:
|
|
||||||
identity = RNS.Identity(create_keys=False)
|
|
||||||
identity.load_private_key(base64.b64decode(args.identity_base64))
|
|
||||||
print("Reticulum Identity has been loaded from base64.")
|
|
||||||
print(identity)
|
|
||||||
else:
|
|
||||||
identity = RNS.Identity(create_keys=True)
|
|
||||||
print("Reticulum Identity has been randomly generated.")
|
|
||||||
print(identity)
|
|
||||||
|
|
||||||
# init reticulum
|
# set a callback for when an lxmf announce is received
|
||||||
global reticulum
|
RNS.Transport.register_announce_handler(LXMFAnnounceHandler(self.on_lxmf_announce_received))
|
||||||
reticulum = RNS.Reticulum(None)
|
|
||||||
|
|
||||||
# init lxmf router
|
# remember websocket clients
|
||||||
global message_router
|
self.websocket_clients = []
|
||||||
message_router = LXMF.LXMRouter(identity=identity, storagepath="storage/lxmf")
|
|
||||||
|
|
||||||
# register lxmf identity
|
async def run(self, host, port):
|
||||||
global local_lxmf_destination
|
|
||||||
local_lxmf_destination = message_router.register_delivery_identity(identity, display_name="ReticulumWebChat")
|
|
||||||
|
|
||||||
# set a callback for when an lxmf message is received
|
# start websocket server
|
||||||
message_router.register_delivery_callback(lxmf_delivery)
|
async with websockets.serve(self.on_websocket_client_connected, host, port, process_request=self.process_request):
|
||||||
|
print("ReticulumWebChat server running at http://{}:{}".format(host, port))
|
||||||
|
await asyncio.Future() # run forever
|
||||||
|
|
||||||
# set a callback for when an lxmf announce is received
|
# handle serving custom http paths
|
||||||
RNS.Transport.register_announce_handler(LXMFAnnounceHandler(on_lxmf_announce_received))
|
async def process_request(self, path, request_headers):
|
||||||
|
|
||||||
|
# serve index.html
|
||||||
|
if path == "/":
|
||||||
|
with open("index.html") as f:
|
||||||
|
file_content = f.read()
|
||||||
|
return http.HTTPStatus.OK, [
|
||||||
|
('Content-Type', 'text/html')
|
||||||
|
], file_content.encode("utf-8")
|
||||||
|
|
||||||
|
# by default, websocket is always served, but we only want it to be available at /ws
|
||||||
|
# so we will return 404 for everything other than /ws
|
||||||
|
elif path != "/ws":
|
||||||
|
return http.HTTPStatus.NOT_FOUND, [
|
||||||
|
('Content-Type', 'text/html')
|
||||||
|
], b"Not Found"
|
||||||
|
|
||||||
# run sanic app
|
# handle new client connecting to websocket
|
||||||
app.run(
|
async def on_websocket_client_connected(self, client):
|
||||||
host=args.host,
|
|
||||||
port=args.port,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# add client to connected clients list
|
||||||
|
self.websocket_clients.append(client)
|
||||||
|
|
||||||
@app.get("/")
|
# handle client messages until disconnected
|
||||||
async def hello_world(request):
|
while True:
|
||||||
return await file("index.html")
|
try:
|
||||||
|
message = await client.recv()
|
||||||
|
data = json.loads(message)
|
||||||
|
await self.on_websocket_data_received(client, data)
|
||||||
|
except websockets.ConnectionClosedOK:
|
||||||
|
# client disconnected, we can stop looping
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
# ignore errors while handling message
|
||||||
|
print("failed to process client message")
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
# loop finished, client is no longer connected
|
||||||
|
self.websocket_clients.remove(client)
|
||||||
|
|
||||||
# handle websocket messages
|
# handle data received from websocket client
|
||||||
@app.websocket("/ws")
|
async def on_websocket_data_received(self, client, data):
|
||||||
async def on_websocket_client_connected(request: Request, client: Websocket):
|
|
||||||
|
|
||||||
# add client to connected clients list
|
# get type from client data
|
||||||
websocket_clients.append(client)
|
_type = data["type"]
|
||||||
|
|
||||||
# handle client messages until disconnected
|
# handle sending an lxmf message
|
||||||
while True:
|
if _type == "lxmf.delivery":
|
||||||
try:
|
|
||||||
message = await client.recv()
|
|
||||||
data = json.loads(message)
|
|
||||||
await on_data(client, data)
|
|
||||||
except websockets.ConnectionClosedOK:
|
|
||||||
# client disconnected, we can stop looping
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
# ignore errors while handling message
|
|
||||||
print("failed to process client message")
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
# loop finished, client is no longer connected
|
# send lxmf message to destination
|
||||||
websocket_clients.remove(client)
|
destination_hash = data["destination_hash"]
|
||||||
|
message = data["message"]
|
||||||
|
self.send_message(destination_hash, message)
|
||||||
|
|
||||||
|
# # TODO: send response to client when marked as delivered?
|
||||||
|
# await client.send(json.dumps({
|
||||||
|
# "type": "lxmf.sent",
|
||||||
|
# }))
|
||||||
|
|
||||||
async def on_data(client, data):
|
# handle sending an announce
|
||||||
|
elif _type == "announce":
|
||||||
|
|
||||||
# get type from client data
|
# send announce for lxmf
|
||||||
_type = data["type"]
|
self.local_lxmf_destination.announce()
|
||||||
|
|
||||||
# handle sending an lxmf message
|
# unhandled type
|
||||||
if _type == "lxmf.delivery":
|
else:
|
||||||
|
print("unhandled client message type: " + _type)
|
||||||
|
|
||||||
# send lxmf message to destination
|
|
||||||
destination_hash = data["destination_hash"]
|
|
||||||
message = data["message"]
|
|
||||||
send_message(destination_hash, message)
|
|
||||||
|
|
||||||
# # TODO: send response to client when marked as delivered?
|
|
||||||
# await client.send(json.dumps({
|
|
||||||
# "type": "lxmf.sent",
|
|
||||||
# }))
|
|
||||||
|
|
||||||
# handle sending an announce
|
|
||||||
elif _type == "announce":
|
|
||||||
|
|
||||||
# send announce for lxmf
|
|
||||||
local_lxmf_destination.announce()
|
|
||||||
|
|
||||||
# unhandled type
|
|
||||||
else:
|
|
||||||
print("unhandled client message type: " + _type)
|
|
||||||
|
|
||||||
|
|
||||||
def websocket_broadcast(data):
|
|
||||||
# broadcast provided data to all connected websocket clients
|
# broadcast provided data to all connected websocket clients
|
||||||
for websocket_client in websocket_clients:
|
def websocket_broadcast(self, data):
|
||||||
asyncio.run(websocket_client.send(data))
|
for websocket_client in self.websocket_clients:
|
||||||
|
asyncio.run(websocket_client.send(data))
|
||||||
|
|
||||||
|
# handle an lxmf delivery from reticulum
|
||||||
|
def on_lxmf_delivery(self, message):
|
||||||
|
try:
|
||||||
|
|
||||||
def lxmf_delivery(message):
|
# get message data
|
||||||
try:
|
message_content = message.content.decode('utf-8')
|
||||||
|
source_hash_text = RNS.hexrep(message.source_hash, delimit=False)
|
||||||
|
|
||||||
# get message data
|
fields = {}
|
||||||
message_content = message.content.decode('utf-8')
|
message_fields = message.get_fields()
|
||||||
source_hash_text = RNS.hexrep(message.source_hash, delimit=False)
|
for field_type in message_fields:
|
||||||
|
|
||||||
fields = {}
|
value = message_fields[field_type]
|
||||||
message_fields = message.get_fields()
|
|
||||||
for field_type in message_fields:
|
|
||||||
|
|
||||||
value = message_fields[field_type]
|
# handle file attachments field
|
||||||
|
if field_type == LXMF.FIELD_FILE_ATTACHMENTS:
|
||||||
|
|
||||||
# handle file attachments field
|
# process file attachments
|
||||||
if field_type == LXMF.FIELD_FILE_ATTACHMENTS:
|
file_attachments = []
|
||||||
|
for file_attachment in value:
|
||||||
|
file_name = file_attachment[0]
|
||||||
|
file_bytes = base64.b64encode(file_attachment[1]).decode("utf-8")
|
||||||
|
file_attachments.append({
|
||||||
|
"file_name": file_name,
|
||||||
|
"file_bytes": file_bytes,
|
||||||
|
})
|
||||||
|
|
||||||
# process file attachments
|
# add to fields
|
||||||
file_attachments = []
|
fields["file_attachments"] = file_attachments
|
||||||
for file_attachment in value:
|
|
||||||
file_name = file_attachment[0]
|
|
||||||
file_bytes = base64.b64encode(file_attachment[1]).decode("utf-8")
|
|
||||||
file_attachments.append({
|
|
||||||
"file_name": file_name,
|
|
||||||
"file_bytes": file_bytes,
|
|
||||||
})
|
|
||||||
|
|
||||||
# add to fields
|
# handle image field
|
||||||
fields["file_attachments"] = file_attachments
|
if field_type == LXMF.FIELD_IMAGE:
|
||||||
|
image_type = value[0]
|
||||||
|
image_bytes = base64.b64encode(value[1]).decode("utf-8")
|
||||||
|
fields["image"] = {
|
||||||
|
"image_type": image_type,
|
||||||
|
"image_bytes": image_bytes,
|
||||||
|
}
|
||||||
|
|
||||||
# handle image field
|
# send received lxmf message data to all websocket clients
|
||||||
if field_type == LXMF.FIELD_IMAGE:
|
self.websocket_broadcast(json.dumps({
|
||||||
image_type = value[0]
|
"type": "lxmf.delivery",
|
||||||
image_bytes = base64.b64encode(value[1]).decode("utf-8")
|
"source_hash": source_hash_text,
|
||||||
fields["image"] = {
|
"message": {
|
||||||
"image_type": image_type,
|
"content": message_content,
|
||||||
"image_bytes": image_bytes,
|
"fields": fields,
|
||||||
}
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
# send received lxmf message data to all websocket clients
|
except Exception as e:
|
||||||
websocket_broadcast(json.dumps({
|
# do nothing on error
|
||||||
"type": "lxmf.delivery",
|
print("lxmf_delivery error: {}".format(e))
|
||||||
"source_hash": source_hash_text,
|
|
||||||
"message": {
|
# handle sending an lxmf message to reticulum
|
||||||
"content": message_content,
|
def send_message(self, destination_hash, message_content):
|
||||||
"fields": fields,
|
|
||||||
},
|
try:
|
||||||
|
|
||||||
|
# convert destination hash to bytes
|
||||||
|
destination_hash = bytes.fromhex(destination_hash)
|
||||||
|
|
||||||
|
# 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 have to bail out of sending, since we don't have the path yet
|
||||||
|
return
|
||||||
|
|
||||||
|
# create destination for recipients lxmf delivery address
|
||||||
|
lxmf_destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery")
|
||||||
|
|
||||||
|
# create lxmf message
|
||||||
|
lxm = LXMF.LXMessage(lxmf_destination, self.local_lxmf_destination, message_content, desired_method=LXMF.LXMessage.DIRECT)
|
||||||
|
lxm.try_propagation_on_fail = True
|
||||||
|
|
||||||
|
# send lxmf message to be routed to destination
|
||||||
|
self.message_router.handle_outbound(lxm)
|
||||||
|
|
||||||
|
except:
|
||||||
|
# FIXME send error to websocket?
|
||||||
|
print("failed to send lxmf message")
|
||||||
|
|
||||||
|
# handle an announce received from reticulum, for an lxmf address
|
||||||
|
def on_lxmf_announce_received(self, destination_hash, announced_identity, app_data):
|
||||||
|
|
||||||
|
# log received announce
|
||||||
|
RNS.log("Received an announce from " + RNS.prettyhexrep(destination_hash))
|
||||||
|
|
||||||
|
# parse app data
|
||||||
|
parsed_app_data = None
|
||||||
|
if app_data is not None:
|
||||||
|
parsed_app_data = app_data.decode("utf-8")
|
||||||
|
|
||||||
|
# send received lxmf announce to all websocket clients
|
||||||
|
self.websocket_broadcast(json.dumps({
|
||||||
|
"type": "announce",
|
||||||
|
"destination_hash": destination_hash.hex(),
|
||||||
|
"app_data": parsed_app_data,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# do nothing on error
|
|
||||||
print("lxmf_delivery error: {}".format(e))
|
|
||||||
|
|
||||||
|
|
||||||
def send_message(destination_hash, message_content):
|
|
||||||
|
|
||||||
try:
|
|
||||||
|
|
||||||
# convert destination hash to bytes
|
|
||||||
destination_hash = bytes.fromhex(destination_hash)
|
|
||||||
|
|
||||||
# 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 have to bail out of sending, since we don't have the path yet
|
|
||||||
return
|
|
||||||
|
|
||||||
# create destination for recipients lxmf delivery address
|
|
||||||
lxmf_destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery")
|
|
||||||
|
|
||||||
# create lxmf message
|
|
||||||
lxm = LXMF.LXMessage(lxmf_destination, local_lxmf_destination, message_content, desired_method=LXMF.LXMessage.DIRECT)
|
|
||||||
lxm.try_propagation_on_fail = True
|
|
||||||
|
|
||||||
# send lxmf message to be routed to destination
|
|
||||||
message_router.handle_outbound(lxm)
|
|
||||||
|
|
||||||
except:
|
|
||||||
# FIXME send error to websocket?
|
|
||||||
print("failed to send lxmf message")
|
|
||||||
|
|
||||||
|
|
||||||
def on_lxmf_announce_received(destination_hash, announced_identity, app_data):
|
|
||||||
|
|
||||||
# log received announce
|
|
||||||
RNS.log("Received an announce from " + RNS.prettyhexrep(destination_hash))
|
|
||||||
|
|
||||||
# parse app data
|
|
||||||
parsed_app_data = None
|
|
||||||
if app_data is not None:
|
|
||||||
parsed_app_data = app_data.decode("utf-8")
|
|
||||||
|
|
||||||
# send received lxmf announce to all websocket clients
|
|
||||||
websocket_broadcast(json.dumps({
|
|
||||||
"type": "announce",
|
|
||||||
"destination_hash": destination_hash.hex(),
|
|
||||||
"app_data": parsed_app_data,
|
|
||||||
}))
|
|
||||||
|
|
||||||
|
|
||||||
|
# an announce handler for lxmf.delivery aspect that just forwards to a provided callback
|
||||||
class LXMFAnnounceHandler:
|
class LXMFAnnounceHandler:
|
||||||
|
|
||||||
def __init__(self, received_announce_callback):
|
def __init__(self, received_announce_callback):
|
||||||
@@ -258,7 +236,36 @@ class LXMFAnnounceHandler:
|
|||||||
# ignore failure to handle received announce
|
# ignore failure to handle received announce
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
|
||||||
|
# parse command line args
|
||||||
|
parser = argparse.ArgumentParser(description="ReticulumWebChat")
|
||||||
|
parser.add_argument("--host", nargs='?', default="0.0.0.0", type=str, help="The address the web server should listen on.")
|
||||||
|
parser.add_argument("--port", nargs='?', default="8000", type=int, help="The port the web server should listen on.")
|
||||||
|
parser.add_argument("--identity-file", type=str, help="Path to a Reticulum Identity file to use as your LXMF address.")
|
||||||
|
parser.add_argument("--identity-base64", type=str, help="A base64 encoded Reticulum Identity to use as your LXMF address.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# use provided identity, or fallback to a random one
|
||||||
|
if args.identity_file is not None:
|
||||||
|
identity = RNS.Identity(create_keys=False)
|
||||||
|
identity.load(args.identity_file)
|
||||||
|
print("Reticulum Identity has been loaded from file.")
|
||||||
|
print(identity)
|
||||||
|
elif args.identity_base64 is not None:
|
||||||
|
identity = RNS.Identity(create_keys=False)
|
||||||
|
identity.load_private_key(base64.b64decode(args.identity_base64))
|
||||||
|
print("Reticulum Identity has been loaded from base64.")
|
||||||
|
print(identity)
|
||||||
|
else:
|
||||||
|
identity = RNS.Identity(create_keys=True)
|
||||||
|
print("Reticulum Identity has been randomly generated.")
|
||||||
|
print(identity)
|
||||||
|
|
||||||
|
# init app
|
||||||
|
reticulum_webchat = ReticulumWebChat(identity)
|
||||||
|
await reticulum_webchat.run(args.host, args.port)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
asyncio.run(main())
|
||||||
|
|||||||
Reference in New Issue
Block a user