clean up all code

This commit is contained in:
liamcottle
2024-04-29 20:26:45 +12:00
parent e1be1126a8
commit bce99b05b9
2 changed files with 204 additions and 198 deletions

View File

@@ -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
View File

@@ -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())