diff --git a/database.py b/database.py index d9d6e9d..221257e 100644 --- a/database.py +++ b/database.py @@ -23,6 +23,23 @@ class Config(BaseModel): table_name = "config" +class Announce(BaseModel): + + id = BigAutoField() + destination_hash = CharField(unique=True) # unique destination hash that was announced + aspect = TextField(index=True) # aspect is not included in announce, but we want to filter saved announces by aspect + identity_hash = CharField(index=True) # identity hash that announced the destination + identity_public_key = CharField() # base64 encoded public key, incase we want to recreate the identity manually + app_data = TextField(null=True) # base64 encoded app data bytes + + created_at = DateTimeField(default=datetime.now) + updated_at = DateTimeField(default=datetime.now) + + # define table name + class Meta: + table_name = "announces" + + class LxmfMessage(BaseModel): id = BigAutoField() diff --git a/web.py b/web.py index 56b1000..feac40c 100644 --- a/web.py +++ b/web.py @@ -49,6 +49,7 @@ class ReticulumWebChat: self.db.connect() self.db.create_tables([ database.Config, + database.Announce, database.LxmfMessage, ]) @@ -73,6 +74,7 @@ class ReticulumWebChat: # set a callback for when an lxmf announce is received RNS.Transport.register_announce_handler(LXMFAnnounceHandler(self.on_lxmf_announce_received)) + RNS.Transport.register_announce_handler(NomadnetworkNodeAnnounceHandler(self.on_nomadnet_node_announce_received)) # remember websocket clients self.websocket_clients: List[web.WebSocketResponse] = [] @@ -600,6 +602,29 @@ class ReticulumWebChat: query = query.on_conflict(conflict_target=[database.LxmfMessage.hash], update=data) query.execute() + # upserts the provided announce to the database + def db_upsert_announce(self, identity: RNS.Identity, destination_hash: bytes, aspect: str, app_data: bytes): + + # parse app data + parsed_app_data = None + if app_data is not None: + parsed_app_data = base64.b64encode(app_data).decode("utf-8") + + # prepare data to insert or update + data = { + "destination_hash": destination_hash.hex(), + "aspect": aspect, + "identity_hash": identity.hash.hex(), + "identity_public_key": base64.b64encode(identity.get_public_key()).decode("utf-8"), + "app_data": parsed_app_data, + "updated_at": datetime.now(), + } + + # upsert to database + query = database.Announce.insert(data) + query = query.on_conflict(conflict_target=[database.Announce.destination_hash], update=data) + query.execute() + # handle sending an lxmf message to reticulum async def send_message(self, destination_hash, content: str, image_field: LxmfImageField = None, @@ -689,7 +714,10 @@ class ReticulumWebChat: 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)) + print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [lxmf.delivery]") + + # upsert announce to database + self.db_upsert_announce(announced_identity, destination_hash, "lxmf.delivery", app_data) # parse app data parsed_app_data = None @@ -706,6 +734,16 @@ class ReticulumWebChat: }, }))) + # handle an announce received from reticulum, for a nomadnet node + # NOTE: cant be async, as Reticulum doesn't await it + def on_nomadnet_node_announce_received(self, destination_hash, announced_identity, app_data): + + # log received announce + print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [nomadnetwork.node]") + + # upsert announce to database + self.db_upsert_announce(announced_identity, destination_hash, "nomadnetwork.node", app_data) + # class to manage config stored in database class Config: @@ -773,6 +811,23 @@ class LXMFAnnounceHandler: pass +# an announce handler for nomadnetwork.node aspect that just forwards to a provided callback +class NomadnetworkNodeAnnounceHandler: + + def __init__(self, received_announce_callback): + self.aspect_filter = "nomadnetwork.node" + self.received_announce_callback = received_announce_callback + + # we will just pass the received announce back to the provided callback + def received_announce(self, destination_hash, announced_identity, app_data): + try: + # handle received announce + self.received_announce_callback(destination_hash, announced_identity, app_data) + except: + # ignore failure to handle received announce + pass + + class NomadnetDownloader: def __init__(self, destination_hash: bytes, path: str, on_download_success: Callable[[bytes], None], on_download_failure: Callable[[str], None], on_progress_update: Callable[[float], None], timeout: int|None = None, auto_download=True):