mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-01 22:42:54 +02:00
fix: persist sender_prefix for signed room posts
TXT_TYPE_SIGNED_PLAIN room posts carry a 4-byte author pubkey prefix (QueuedMessage.sender_prefix, added in openhop_core). The SQLite persistence path dropped it, so posts replayed from persistence showed a zero-padded author prefix in the app frame while posts synced from the live in-memory queue were correct. - add sender_prefix column (hex text, default '') to companion_messages with an ALTER TABLE migration for existing databases - store/return the prefix in companion_push/pop/load_messages - pass sender_prefix when rebuilding QueuedMessage from persistence in the frame server and the startup preload paths - add round-trip, default, migration, and rebuild tests
This commit is contained in:
@@ -91,6 +91,9 @@ class CompanionFrameServer(_BaseFrameServer):
|
||||
msg_dict = self.sqlite_handler.companion_pop_message(self.companion_hash)
|
||||
if not msg_dict:
|
||||
return None
|
||||
sender_prefix = msg_dict.get("sender_prefix", b"")
|
||||
if isinstance(sender_prefix, str):
|
||||
sender_prefix = bytes.fromhex(sender_prefix) if sender_prefix else b""
|
||||
return QueuedMessage(
|
||||
sender_key=msg_dict.get("sender_key", b""),
|
||||
txt_type=msg_dict.get("txt_type", 0),
|
||||
@@ -99,6 +102,7 @@ class CompanionFrameServer(_BaseFrameServer):
|
||||
is_channel=bool(msg_dict.get("is_channel", False)),
|
||||
channel_idx=msg_dict.get("channel_idx", 0),
|
||||
path_len=msg_dict.get("path_len", 0),
|
||||
sender_prefix=sender_prefix,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
@@ -426,6 +426,7 @@ class SQLiteHandler:
|
||||
is_channel INTEGER NOT NULL DEFAULT 0,
|
||||
channel_idx INTEGER NOT NULL DEFAULT 0,
|
||||
path_len INTEGER NOT NULL DEFAULT 0,
|
||||
sender_prefix TEXT NOT NULL DEFAULT '',
|
||||
packet_hash TEXT,
|
||||
created_at REAL NOT NULL
|
||||
)
|
||||
@@ -581,6 +582,30 @@ class SQLiteHandler:
|
||||
)
|
||||
logger.info(f"Migration '{migration_name}' applied successfully")
|
||||
|
||||
# Migration 10: Add sender_prefix column (hex text) to
|
||||
# companion_messages. TXT_TYPE_SIGNED_PLAIN room posts carry a
|
||||
# 4-byte author pubkey prefix; without it, posts replayed from
|
||||
# SQLite show a zero-padded author in the app frame.
|
||||
migration_name = "add_sender_prefix_to_companion_messages"
|
||||
existing = conn.execute(
|
||||
"SELECT migration_name FROM migrations WHERE migration_name = ?",
|
||||
(migration_name,),
|
||||
).fetchone()
|
||||
if not existing:
|
||||
cursor = conn.execute("PRAGMA table_info(companion_messages)")
|
||||
columns = [column[1] for column in cursor.fetchall()]
|
||||
if "sender_prefix" not in columns:
|
||||
conn.execute(
|
||||
"ALTER TABLE companion_messages "
|
||||
"ADD COLUMN sender_prefix TEXT NOT NULL DEFAULT ''"
|
||||
)
|
||||
logger.info("Added sender_prefix column to companion_messages table")
|
||||
conn.execute(
|
||||
"INSERT INTO migrations (migration_name, applied_at) VALUES (?, ?)",
|
||||
(migration_name, time.time()),
|
||||
)
|
||||
logger.info(f"Migration '{migration_name}' applied successfully")
|
||||
|
||||
conn.commit()
|
||||
|
||||
except Exception as e:
|
||||
@@ -2652,13 +2677,17 @@ class SQLiteHandler:
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
SELECT sender_key, txt_type, timestamp, text, is_channel, channel_idx, path_len
|
||||
SELECT sender_key, txt_type, timestamp, text, is_channel, channel_idx,
|
||||
path_len, sender_prefix
|
||||
FROM companion_messages WHERE companion_hash = ?
|
||||
ORDER BY created_at ASC LIMIT ?
|
||||
""",
|
||||
(companion_hash, limit),
|
||||
)
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
rows = [dict(row) for row in cursor.fetchall()]
|
||||
for msg in rows:
|
||||
msg["sender_prefix"] = bytes.fromhex(msg.get("sender_prefix") or "")
|
||||
return rows
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load companion messages: {e}")
|
||||
return []
|
||||
@@ -2683,13 +2712,16 @@ class SQLiteHandler:
|
||||
if isinstance(packet_hash, bytes):
|
||||
packet_hash = packet_hash.decode("utf-8", errors="replace") if packet_hash else None
|
||||
sender_key = msg.get("sender_key", b"")
|
||||
sender_prefix = msg.get("sender_prefix", b"")
|
||||
if not isinstance(sender_prefix, str):
|
||||
sender_prefix = bytes(sender_prefix or b"").hex()
|
||||
with self._connect() as conn:
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO companion_messages
|
||||
(companion_hash, sender_key, txt_type, timestamp, text,
|
||||
is_channel, channel_idx, path_len, packet_hash, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
is_channel, channel_idx, path_len, sender_prefix, packet_hash, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
companion_hash,
|
||||
@@ -2700,6 +2732,7 @@ class SQLiteHandler:
|
||||
int(msg.get("is_channel", False)),
|
||||
msg.get("channel_idx", 0),
|
||||
msg.get("path_len", 0),
|
||||
sender_prefix,
|
||||
packet_hash,
|
||||
time.time(),
|
||||
),
|
||||
@@ -2732,7 +2765,8 @@ class SQLiteHandler:
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
SELECT id, sender_key, txt_type, timestamp, text, is_channel, channel_idx, path_len
|
||||
SELECT id, sender_key, txt_type, timestamp, text, is_channel, channel_idx,
|
||||
path_len, sender_prefix
|
||||
FROM companion_messages WHERE companion_hash = ?
|
||||
ORDER BY created_at ASC LIMIT 1
|
||||
""",
|
||||
@@ -2742,6 +2776,7 @@ class SQLiteHandler:
|
||||
if not row:
|
||||
return None
|
||||
msg = dict(row)
|
||||
msg["sender_prefix"] = bytes.fromhex(msg.get("sender_prefix") or "")
|
||||
conn.execute("DELETE FROM companion_messages WHERE id = ?", (msg["id"],))
|
||||
conn.commit()
|
||||
return {k: v for k, v in msg.items() if k != "id"}
|
||||
|
||||
@@ -656,6 +656,9 @@ class RepeaterDaemon:
|
||||
sk = msg_dict.get("sender_key", b"")
|
||||
if isinstance(sk, str):
|
||||
sk = bytes.fromhex(sk)
|
||||
sp = msg_dict.get("sender_prefix", b"")
|
||||
if isinstance(sp, str):
|
||||
sp = bytes.fromhex(sp) if sp else b""
|
||||
bridge.message_queue.push(
|
||||
QueuedMessage(
|
||||
sender_key=sk,
|
||||
@@ -665,6 +668,7 @@ class RepeaterDaemon:
|
||||
is_channel=bool(msg_dict.get("is_channel", False)),
|
||||
channel_idx=msg_dict.get("channel_idx", 0),
|
||||
path_len=msg_dict.get("path_len", 0),
|
||||
sender_prefix=sp,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -842,6 +846,9 @@ class RepeaterDaemon:
|
||||
sk = msg_dict.get("sender_key", b"")
|
||||
if isinstance(sk, str):
|
||||
sk = bytes.fromhex(sk)
|
||||
sp = msg_dict.get("sender_prefix", b"")
|
||||
if isinstance(sp, str):
|
||||
sp = bytes.fromhex(sp) if sp else b""
|
||||
bridge.message_queue.push(
|
||||
QueuedMessage(
|
||||
sender_key=sk,
|
||||
@@ -851,6 +858,7 @@ class RepeaterDaemon:
|
||||
is_channel=bool(msg_dict.get("is_channel", False)),
|
||||
channel_idx=msg_dict.get("channel_idx", 0),
|
||||
path_len=msg_dict.get("path_len", 0),
|
||||
sender_prefix=sp,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -196,6 +196,117 @@ class TestSqliteRetentionTrim:
|
||||
assert len(h.companion_load_messages("0x02")) == 3
|
||||
|
||||
|
||||
class TestSenderPrefixPersistence:
|
||||
"""sender_prefix (signed room-post author prefix) survives the SQLite round-trip."""
|
||||
|
||||
PREFIX = b"\xaa\xbb\xcc\xdd"
|
||||
|
||||
@staticmethod
|
||||
def _handler(tmp_path):
|
||||
from repeater.data_acquisition.sqlite_handler import SQLiteHandler
|
||||
|
||||
return SQLiteHandler(tmp_path)
|
||||
|
||||
def _push(self, h, sender_prefix=PREFIX):
|
||||
return h.companion_push_message(
|
||||
"0x01",
|
||||
{
|
||||
"sender_key": b"\x01" * 32,
|
||||
"txt_type": 2,
|
||||
"timestamp": 42,
|
||||
"text": "signed post",
|
||||
"sender_prefix": sender_prefix,
|
||||
"packet_hash": "ph-1",
|
||||
},
|
||||
)
|
||||
|
||||
def test_push_pop_round_trip(self, tmp_path):
|
||||
h = self._handler(tmp_path)
|
||||
assert self._push(h)
|
||||
msg = h.companion_pop_message("0x01")
|
||||
assert msg["sender_prefix"] == self.PREFIX
|
||||
assert msg["text"] == "signed post"
|
||||
|
||||
def test_load_messages_returns_prefix_bytes(self, tmp_path):
|
||||
h = self._handler(tmp_path)
|
||||
assert self._push(h)
|
||||
msgs = h.companion_load_messages("0x01")
|
||||
assert len(msgs) == 1
|
||||
assert msgs[0]["sender_prefix"] == self.PREFIX
|
||||
|
||||
def test_missing_prefix_defaults_empty(self, tmp_path):
|
||||
h = self._handler(tmp_path)
|
||||
assert h.companion_push_message(
|
||||
"0x01", {"text": "plain", "timestamp": 1, "packet_hash": "ph-2"}
|
||||
)
|
||||
msg = h.companion_pop_message("0x01")
|
||||
assert msg["sender_prefix"] == b""
|
||||
|
||||
def test_migration_adds_column_to_existing_db(self, tmp_path):
|
||||
import sqlite3
|
||||
|
||||
# Build a current DB, then rewind companion_messages to the
|
||||
# pre-sender_prefix schema and drop the migration marker.
|
||||
h = self._handler(tmp_path)
|
||||
conn = sqlite3.connect(str(h.sqlite_path))
|
||||
conn.execute(
|
||||
"DELETE FROM migrations "
|
||||
"WHERE migration_name = 'add_sender_prefix_to_companion_messages'"
|
||||
)
|
||||
conn.execute("ALTER TABLE companion_messages RENAME TO companion_messages_old")
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE companion_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
companion_hash TEXT NOT NULL,
|
||||
sender_key BLOB NOT NULL,
|
||||
txt_type INTEGER NOT NULL DEFAULT 0,
|
||||
timestamp INTEGER NOT NULL DEFAULT 0,
|
||||
text TEXT NOT NULL,
|
||||
is_channel INTEGER NOT NULL DEFAULT 0,
|
||||
channel_idx INTEGER NOT NULL DEFAULT 0,
|
||||
path_len INTEGER NOT NULL DEFAULT 0,
|
||||
packet_hash TEXT,
|
||||
created_at REAL NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute("DROP TABLE companion_messages_old")
|
||||
conn.execute(
|
||||
"INSERT INTO companion_messages "
|
||||
"(companion_hash, sender_key, text, created_at) VALUES ('0x01', X'01', 'old', 1.0)"
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
h2 = self._handler(tmp_path) # re-runs migrations
|
||||
# Pre-migration row decodes with an empty prefix.
|
||||
old = h2.companion_pop_message("0x01")
|
||||
assert old["sender_prefix"] == b""
|
||||
# New rows round-trip through the migrated column.
|
||||
assert self._push(h2)
|
||||
assert h2.companion_pop_message("0x01")["sender_prefix"] == self.PREFIX
|
||||
|
||||
def test_sync_next_from_persistence_rebuilds_prefix(self):
|
||||
from repeater.companion.frame_server import CompanionFrameServer
|
||||
|
||||
fs = CompanionFrameServer.__new__(CompanionFrameServer)
|
||||
fs.sqlite_handler = MagicMock()
|
||||
fs.companion_hash = "0x01"
|
||||
fs.sqlite_handler.companion_pop_message.return_value = {
|
||||
"sender_key": b"\x01" * 32,
|
||||
"txt_type": 2,
|
||||
"timestamp": 42,
|
||||
"text": "signed post",
|
||||
"is_channel": 0,
|
||||
"channel_idx": 0,
|
||||
"path_len": 0,
|
||||
"sender_prefix": self.PREFIX,
|
||||
}
|
||||
msg = fs._sync_next_from_persistence()
|
||||
assert msg.sender_prefix == self.PREFIX
|
||||
|
||||
|
||||
class TestTrimContactsOnOverflowPolicy:
|
||||
@staticmethod
|
||||
def _contacts(n, favourites=0):
|
||||
|
||||
Reference in New Issue
Block a user