From 3b3e807518f4cb76c076cc5ecf274195b1e5f160 Mon Sep 17 00:00:00 2001 From: lichenblankie Date: Fri, 5 Jun 2026 00:55:03 +0000 Subject: [PATCH] truly public forum: auto-discover other instances via RNS announce handler --- tests/test_sync.py | 52 +++++++++++++++++++++++++++++++++++++++++++ tinyweb_forum/sync.py | 29 +++++++++++++++++++++++- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/tests/test_sync.py b/tests/test_sync.py index 8e535cb..6ad6833 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -378,6 +378,57 @@ def test_sync_peer_discovery(): shutil.rmtree(dir_c) +def test_announce_handler(): + """Announce handler should auto-discover peers.""" + dir_a = tempfile.mkdtemp() + try: + fdb = ForumDB(dir_a) + from tinyweb_forum.sync import _ForumAnnounceHandler + + # Mock identity — RNS identity.hash returns bytes + class MockIdentity: + def __init__(self, h): + self._hash_hex = h + @property + def hash(self): + return bytes.fromhex(self._hash_hex) + + local_hex = "aa" * 16 + peer_hex = "bb" * 16 + + handler = _ForumAnnounceHandler(fdb, MockIdentity(local_hex)) + + class MockAnnouncedIdentity: + def __init__(self, h): + self._hash_hex = h + @property + def hash(self): + return bytes.fromhex(self._hash_hex) + + # Announce from a peer + handler.received_announce( + destination_hash=bytes.fromhex(peer_hex), + announced_identity=MockAnnouncedIdentity(peer_hex), + app_data=b"tinyweb-forum", + ) + + known = [r["instance_hash"] for r in fdb.get_synced_instances()] + assert peer_hex in known, "peer should be added to sync list" + + # Announce from ourselves should be ignored + handler.received_announce( + destination_hash=bytes.fromhex(local_hex), + announced_identity=MockAnnouncedIdentity(local_hex), + app_data=b"tinyweb-forum", + ) + + known = [r["instance_hash"] for r in fdb.get_synced_instances()] + assert known.count(local_hex) == 0, "own announce should be ignored" + finally: + import shutil + shutil.rmtree(dir_a) + + if __name__ == "__main__": test_sync_thread() test_sync_reply() @@ -388,4 +439,5 @@ if __name__ == "__main__": test_sync_block_gossip() test_sync_updated_content() test_sync_peer_discovery() + test_announce_handler() print("all sync tests passed") diff --git a/tinyweb_forum/sync.py b/tinyweb_forum/sync.py index 05e6f5c..098dfc0 100644 --- a/tinyweb_forum/sync.py +++ b/tinyweb_forum/sync.py @@ -8,6 +8,24 @@ SYNC_INTERVAL = 300 # 5 minutes REQUEST_TIMEOUT = 60 +class _ForumAnnounceHandler: + """Receives announces from other forum instances and auto-discovers them.""" + + aspect_filter = FORUM_APP + receive_path_responses = False + + def __init__(self, fdb, identity): + self.fdb = fdb + self.my_hash = identity.hash.hex() if identity else "local" + + def received_announce(self, destination_hash, announced_identity, app_data): + if announced_identity is None: + return + peer_hash = announced_identity.hash.hex() + if peer_hash and peer_hash != self.my_hash: + self.fdb.add_known_peer(peer_hash) + + class ForumSync: def __init__(self, fdb, identity, reticulum, handlers_ref): self.fdb = fdb @@ -15,6 +33,7 @@ class ForumSync: self.reticulum = reticulum self.handlers_ref = handlers_ref self.destination = None + self._announce_handler = None self._running = False self._thread = None @@ -30,13 +49,21 @@ class ForumSync: response_generator=self._rns_handler, allow=RNS.Destination.ALLOW_ALL, ) - self.destination.announce() + self.destination.announce(app_data=FORUM_APP.encode("utf-8")) + # Auto-discover other forum instances via announces + self._announce_handler = _ForumAnnounceHandler(self.fdb, self.identity) + RNS.Transport.register_announce_handler(self._announce_handler) self._running = True self._thread = threading.Thread(target=self._sync_loop, daemon=True) self._thread.start() def stop(self): self._running = False + if self._announce_handler: + try: + RNS.Transport.deregister_announce_handler(self._announce_handler) + except Exception: + pass def _rns_handler(self, path, data, request_id, link_id, remote_identity, requested_at): if remote_identity: