From ff0c90e04494ff9766d86000e959a9ac3096da76 Mon Sep 17 00:00:00 2001 From: TossSky Date: Wed, 2 Sep 2026 22:47:29 +0300 Subject: [PATCH] [OMEGA-318] Keep the Telegram authorization ledger usable after a damaged write - Append a leading newline when the previous record was left unterminated, so the next record is not swallowed by the damaged line. A revoked group used to come back after a restart because /unbind landed inside an invalid line. - Read the ledger with errors="replace" on the startup and group-lookup paths, so undecodable bytes are skipped like malformed JSON instead of stopping the agent with exit 2 before any channel starts. - Cover both cases with tests. --- channels/auth.py | 28 ++++++++++++++++++++-------- tests/test_auth_standalone.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/channels/auth.py b/channels/auth.py index 14bdba3f..84e93eb3 100644 --- a/channels/auth.py +++ b/channels/auth.py @@ -179,6 +179,22 @@ def _channel_auth_group_path(): return os.path.join(_MEMORY_DIRECTORY, _CHANNEL_DIR_NAME, _CHANNEL_AUTH_GROUP_FILE) +def _append_json_line(path, payload): + os.makedirs(os.path.dirname(path), exist_ok=True) + prefix = "" + try: + with open(path, "rb") as probe: + probe.seek(0, os.SEEK_END) + if probe.tell(): + probe.seek(-1, os.SEEK_END) + if probe.read(1) != b"\n": + prefix = "\n" + except FileNotFoundError: + pass + with open(path, "a", encoding="utf-8") as f: + f.write(prefix + json.dumps(payload, separators=(",", ":")) + "\n") + + def load_channel_auth_state(channel_identifier): """Validate and load one channel's persisted owner and active groups.""" channel_identifier = str(channel_identifier or "").strip() @@ -187,7 +203,7 @@ def load_channel_auth_state(channel_identifier): def read_records(path, label): try: - with open(path, "r", encoding="utf-8") as source: + with open(path, "r", encoding="utf-8", errors="replace") as source: records = [] for line_number, line in enumerate(source, 1): if not line.strip(): @@ -263,10 +279,8 @@ def store_channel_authenticated_group_id(channel_identifier, group_id, authorize "authorized_by": authorized_by_user_id, } path = _channel_auth_group_path() - os.makedirs(os.path.dirname(path), exist_ok=True) try: - with open(path, "a", encoding="utf-8") as f: - f.write(json.dumps(payload, separators=(",", ":")) + "\n") + _append_json_line(path, payload) except OSError as e: raise RuntimeError("Failed to write channel authenticated group record") from e return True @@ -284,7 +298,7 @@ def get_channel_saved_group_id(channel_identifier, group_id): authorized = False try: - with open(_channel_auth_group_path(), "r", encoding="utf-8") as f: + with open(_channel_auth_group_path(), "r", encoding="utf-8", errors="replace") as f: for line in f: try: record = json.loads(line) @@ -354,10 +368,8 @@ def revoke_channel_group(channel_identifier, group_id, requester_user_id): "revoked": True, } path = _channel_auth_group_path() - os.makedirs(os.path.dirname(path), exist_ok=True) try: - with open(path, "a", encoding="utf-8") as f: - f.write(json.dumps(payload, separators=(",", ":")) + "\n") + _append_json_line(path, payload) except OSError as e: raise RuntimeError("Failed to write channel group revocation record") from e diff --git a/tests/test_auth_standalone.py b/tests/test_auth_standalone.py index 011687bf..233684b4 100644 --- a/tests/test_auth_standalone.py +++ b/tests/test_auth_standalone.py @@ -190,3 +190,34 @@ def test_load_channel_auth_state_skips_malformed_records( "Skipping malformed channel authenticated group record at line 2" in warning for warning in warnings ) + + +def test_load_channel_auth_state_skips_undecodable_bytes(monkeypatch, tmp_path): + auth = load_auth_module(monkeypatch) + monkeypatch.setattr(auth, "_MEMORY_DIRECTORY", str(tmp_path)) + + assert auth.store_channel_authenticated_user_id("TELEGRAM", "owner") is True + assert auth.store_channel_authenticated_group_id( + "TELEGRAM", "active", "owner" + ) is True + path = tmp_path / ".channel" / "authenticated-group.json" + with path.open("ab") as target: + target.write(b"\xff\xfe garbage\n") + + assert auth.load_channel_auth_state("TELEGRAM") == ("owner", {"active"}) + + +def test_a_record_written_after_an_unterminated_one_is_kept(monkeypatch, tmp_path): + auth = load_auth_module(monkeypatch) + monkeypatch.setattr(auth, "_MEMORY_DIRECTORY", str(tmp_path)) + + assert auth.store_channel_authenticated_user_id("TELEGRAM", "owner") is True + assert auth.store_channel_authenticated_group_id( + "TELEGRAM", "active", "owner" + ) is True + path = tmp_path / ".channel" / "authenticated-group.json" + with path.open("a", encoding="utf-8") as target: + target.write('{"time":"2026-09-02T10:00:00Z","channel_ident') + + assert auth.revoke_channel_group("TELEGRAM", "active", "owner") == "group_unbound" + assert auth.load_channel_auth_state("TELEGRAM") == ("owner", set())