diff options
Diffstat (limited to 'packages/meshbay-node')
5 files changed, 55 insertions, 52 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/chat/store.py b/packages/meshbay-node/src/meshbay_node/chat/store.py index 9e07aff..7610b33 100644 --- a/packages/meshbay-node/src/meshbay_node/chat/store.py +++ b/packages/meshbay-node/src/meshbay_node/chat/store.py @@ -54,8 +54,11 @@ CREATE TABLE IF NOT EXISTS messages ( sender_name TEXT DEFAULT '' ); CREATE INDEX IF NOT EXISTS idx_messages_ts ON messages(timestamp); -CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages(thread_id); """ +# `thread_id` is written and read by nothing: chat has no threads. The column +# stays, empty, because a node downgraded to an older version names it in every +# query, and its rows must keep opening. Its index goes; an older node makes it +# again on its own. # Additive, every one with a default, so an existing chat.db opens unchanged and # a node that is downgraded still reads its own rows. `CREATE TABLE IF NOT @@ -88,7 +91,6 @@ class StoredMessage: iteration: int payload: bytes timestamp: float - thread_id: str | None sender_name: str = "" format: int = FORMAT_PLAIN epoch: int = 0 @@ -97,16 +99,16 @@ class StoredMessage: sig: bytes | None = None -_COLUMNS = ("id, sender_id, iteration, payload, timestamp, thread_id, " +_COLUMNS = ("id, sender_id, iteration, payload, timestamp, " "sender_name, format, epoch, device, nonce, sig") def _row(r) -> StoredMessage: return StoredMessage( id=r[0], sender_id=r[1], iteration=r[2], payload=r[3], - timestamp=r[4], thread_id=r[5], sender_name=r[6] or "", - format=r[7] or FORMAT_PLAIN, epoch=r[8] or 0, - device=r[9], nonce=r[10], sig=r[11], + timestamp=r[4], sender_name=r[5] or "", + format=r[6] or FORMAT_PLAIN, epoch=r[7] or 0, + device=r[8], nonce=r[9], sig=r[10], ) @@ -132,6 +134,7 @@ class ChatStore: # nothing to show for it. pass await self._db.execute(_REPLAY_INDEX) + await self._db.execute("DROP INDEX IF EXISTS idx_messages_thread") await self._db.commit() async def close(self) -> None: @@ -151,7 +154,6 @@ class ChatStore: sender_id: str, iteration: int, payload: bytes, - thread_id: str | None = None, sender_name: str = "", *, format: int = FORMAT_PLAIN, @@ -171,9 +173,9 @@ class ChatStore: try: cursor = await self._db.execute( "INSERT INTO messages (sender_id, iteration, payload, timestamp, " - " thread_id, sender_name, format, epoch, device, nonce, sig) " - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - (sender_id, iteration, payload, ts, thread_id, sender_name, + " sender_name, format, epoch, device, nonce, sig) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + (sender_id, iteration, payload, ts, sender_name, format, epoch, device, nonce, sig), ) except aiosqlite.IntegrityError as e: @@ -234,16 +236,6 @@ class ChatStore: "SELECT 1 FROM messages WHERE id < ? LIMIT 1", (message_id,)) return await cursor.fetchone() is not None - async def get_thread(self, thread_id: str, limit: int = 100) -> list[StoredMessage]: - """Get messages in a thread.""" - cursor = await self._db.execute( - f"SELECT {_COLUMNS} " - "FROM messages WHERE thread_id = ? ORDER BY timestamp ASC LIMIT ?", - (thread_id, limit), - ) - rows = await cursor.fetchall() - return [_row(r) for r in rows] - @property def db_path(self) -> Path: """Where this store lives — the re-encryption command backs it up.""" diff --git a/packages/meshbay-node/src/meshbay_node/ops/chat.py b/packages/meshbay-node/src/meshbay_node/ops/chat.py index 9fc7b94..7caf85c 100644 --- a/packages/meshbay-node/src/meshbay_node/ops/chat.py +++ b/packages/meshbay-node/src/meshbay_node/ops/chat.py @@ -214,7 +214,6 @@ async def encrypt_chat_history(state: dict, group_id: str) -> dict: if isinstance(row.payload, bytes) else str(row.payload)) env = seal(key, group_id, epoch, device_b64, device_raw, sk_node, { "text": text, - "thread_id": row.thread_id, "sender_name": row.sender_name, "sent_at": int(row.timestamp), # The node sealed this after the fact; it did not witness it being diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py index ed2c43a..5bc9dbb 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py @@ -75,7 +75,7 @@ _LINK_PREVIEW_RATE_NODE = 60 # C5a; this is the same question asked of the path nobody had asked it of. # # 64 KB of ciphertext is about sixty thousand characters. The sealed payload is -# the text, a thread id, a display name and a timestamp: an attachment is a file +# the text, a display name and a timestamp: an attachment is a file # on a root and travels as a reference (§4.5), so nothing legitimate comes close. MAX_CHAT_CIPHERTEXT = 64 * 1024 # Per account per group, not per connection: a second tab does not make a person @@ -330,18 +330,14 @@ class ChatMixin: # anyone on the node. gctx = self._group_ctx() chat_store = gctx.get("chat_store") - # The two fields that travel in clear beside the ciphertext (the sealed - # envelope carries its own). Stored and relayed to every member, so they - # are what they claim to be and no larger: a name as long as a username, - # a thread id as long as a message id. Anything else is dropped. + # The one field that travels in clear beside the ciphertext (the sealed + # envelope carries its own). Stored and relayed to every member, so it is + # what it claims to be and no larger: a name as long as a username. + # Anything else is dropped. A `thread_id` from an older client is + # ignored: chat has no threads. sender_name = msg.get("sender_name", "") if not isinstance(sender_name, str) or len(sender_name) > 64: sender_name = "" - thread_id = msg.get("thread_id") - id_like = (isinstance(thread_id, int) and not isinstance(thread_id, bool) - or isinstance(thread_id, str) and len(thread_id) <= 64) - if thread_id is not None and not id_like: - thread_id = None # Two shapes, and keeping them apart is what makes this deployable. # @@ -387,7 +383,7 @@ class ChatMixin: self._spawn(self._store_chat_message( chat_store, iteration=msg.get("iteration", 0), payload=raw, - thread_id=thread_id, sender_name=sender_name, + sender_name=sender_name, format=fmt, epoch=epoch, device=device, nonce=nonce, sig=sig, )) @@ -398,7 +394,6 @@ class ChatMixin: "sender_id": self._user_id, "sender_name": sender_name, "payload": payload, - "thread_id": thread_id, "timestamp": time.time(), "format": fmt, "epoch": epoch, @@ -613,7 +608,6 @@ class ChatMixin: "sender_id": m.sender_id, "sender_name": m.sender_name or names.get(m.sender_id, ""), "timestamp": m.timestamp, - "thread_id": m.thread_id, "format": m.format, "epoch": m.epoch, "device": m.device, diff --git a/packages/meshbay-node/tests/test_chat_is_bounded.py b/packages/meshbay-node/tests/test_chat_is_bounded.py index c48f26d..f98fd81 100644 --- a/packages/meshbay-node/tests/test_chat_is_bounded.py +++ b/packages/meshbay-node/tests/test_chat_is_bounded.py @@ -210,10 +210,10 @@ async def test_one_member_at_their_limit_has_not_spent_anyone_elses(ctx, store): async def test_the_clear_fields_are_what_they_claim_and_no_larger(ctx, store): """ - `sender_name` and `thread_id` travel in clear beside the sealed envelope, - which carries its own. They are stored on the operator's disk and relayed - to every member, so a megabyte of name or a list for a thread id is dropped, - not kept. + `sender_name` travels in clear beside the sealed envelope, which carries its + own. It is stored on the operator's disk and relayed to every member, so a + megabyte of name is dropped, not kept — and a `thread_id`, which an older + client still sends, is neither. """ alice = _session(ctx, store, user="alice", conn="c1") bob = _session(ctx, store, user="bob", conn="c2") @@ -225,17 +225,19 @@ async def test_the_clear_fields_are_what_they_claim_and_no_larger(ctx, store): stored = (await store.get_recent(limit=1))[0] assert stored.sender_name in ("", None) - assert stored.thread_id is None relayed = [m for m in bob.sent if m.get("type") == "chat_msg"] - assert relayed and relayed[-1]["sender_name"] == "" and relayed[-1]["thread_id"] is None + assert relayed and relayed[-1]["sender_name"] == "" and "thread_id" not in relayed[-1] -async def test_ordinary_clear_fields_pass_unchanged(ctx, store): +async def test_an_older_clients_message_is_taken_its_thread_id_ignored(ctx, store): alice = _session(ctx, store, user="alice", conn="c1") + bob = _session(ctx, store, user="bob", conn="c2") msg = _message(alice, size=64) msg["sender_name"] = "Alice" msg["thread_id"] = "42" alice._do_chat_message(msg) await _drain(ctx) stored = (await store.get_recent(limit=1))[0] - assert (stored.sender_name, stored.thread_id) == ("Alice", "42") + assert stored.sender_name == "Alice" + relayed = [m for m in bob.sent if m.get("type") == "chat_msg"] + assert relayed and "thread_id" not in relayed[-1] diff --git a/packages/meshbay-node/tests/test_chat_store.py b/packages/meshbay-node/tests/test_chat_store.py index 2533921..c4dff6d 100644 --- a/packages/meshbay-node/tests/test_chat_store.py +++ b/packages/meshbay-node/tests/test_chat_store.py @@ -28,7 +28,6 @@ async def test_save_and_retrieve(store): assert msgs[0].sender_id == "alice" assert msgs[0].iteration == 0 assert msgs[0].payload == b"hello" - assert msgs[0].thread_id is None @pytest.mark.asyncio @@ -52,15 +51,32 @@ async def test_get_messages_since(store): @pytest.mark.asyncio -async def test_thread_messages(store): - await store.save_message("alice", 0, b"root", thread_id="t1") - await store.save_message("bob", 1, b"reply", thread_id="t1") - await store.save_message("carol", 2, b"other") - - thread = await store.get_thread("t1") - assert len(thread) == 2 - assert thread[0].sender_id == "alice" - assert thread[1].sender_id == "bob" +async def test_the_thread_column_stays_for_an_older_node_and_its_index_goes(tmp_path): + """Chat has no threads; a node downgraded to a version that still names + `thread_id` in every query must keep reading its rows.""" + import sqlite3 + path = tmp_path / "chat.db" + with sqlite3.connect(path) as c: # as an older node left it + c.executescript(""" + CREATE TABLE messages (id INTEGER PRIMARY KEY AUTOINCREMENT, + sender_id TEXT NOT NULL, iteration INTEGER NOT NULL, + payload BLOB NOT NULL, timestamp REAL NOT NULL, + thread_id TEXT DEFAULT NULL, sender_name TEXT DEFAULT ''); + CREATE INDEX idx_messages_thread ON messages(thread_id); + INSERT INTO messages (sender_id, iteration, payload, timestamp, thread_id) + VALUES ('alice', 0, X'6869', 1.0, 't1');""") + s = ChatStore(path) + await s.open() + await s.save_message("bob", 1, b"now") + assert [m.payload for m in await s.get_recent()] == [b"hi", b"now"] + await s.close() + with sqlite3.connect(path) as c: + assert "thread_id" in [r[1] for r in c.execute("PRAGMA table_info(messages)")] + assert not c.execute("SELECT 1 FROM sqlite_master WHERE name = " + "'idx_messages_thread'").fetchone() + # What an older node selects still answers. + assert c.execute("SELECT thread_id FROM messages ORDER BY id").fetchall() == [ + ("t1",), (None,)] @pytest.mark.asyncio |