From 7dc2d291363dd0d6c3fe94462ddf777554ef9733 Mon Sep 17 00:00:00 2001 From: Abdo Date: Sat, 18 Oct 2025 02:45:27 +0300 Subject: [PATCH] Handle deeply nested OpChanges Messages such as AddNotesResponse were not handled correctly. --- proto/anki/collection.proto | 4 ++++ pylib/anki/collection.py | 1 + qt/aqt/mediasrv.py | 25 +++++++++++++++++-------- rslib/proto/typescript.rs | 26 +++++++++++++++++++------- 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/proto/anki/collection.proto b/proto/anki/collection.proto index 330413613..0ed1a155c 100644 --- a/proto/anki/collection.proto +++ b/proto/anki/collection.proto @@ -78,6 +78,10 @@ message OpChangesOnly { collection.OpChanges changes = 1; } +message NestedOpChanges { + OpChangesOnly changes = 1; +} + message OpChangesWithCount { OpChanges changes = 1; uint32 count = 2; diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 60360470c..fb50f0691 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -35,6 +35,7 @@ Preferences = config_pb2.Preferences UndoStatus = collection_pb2.UndoStatus OpChanges = collection_pb2.OpChanges OpChangesOnly = collection_pb2.OpChangesOnly +NestedOpChanges = collection_pb2.NestedOpChanges OpChangesWithCount = collection_pb2.OpChangesWithCount OpChangesWithId = collection_pb2.OpChangesWithId OpChangesAfterUndo = collection_pb2.OpChangesAfterUndo diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 550a47ad3..7a028b002 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -30,7 +30,13 @@ import aqt import aqt.main import aqt.operations from anki import frontend_pb2, generic_pb2, hooks -from anki.collection import OpChanges, OpChangesOnly, Progress, SearchNode +from anki.collection import ( + NestedOpChanges, + OpChanges, + OpChangesOnly, + Progress, + SearchNode, +) from anki.decks import UpdateDeckConfigs from anki.scheduler.v3 import SchedulingStatesWithContext, SetSchedulingStatesRequest from anki.utils import dev_mode, from_json_bytes, to_json_bytes @@ -1001,16 +1007,19 @@ def raw_backend_request(endpoint: str) -> Callable[[], bytes]: output = getattr(aqt.mw.col._backend, f"{endpoint}_raw")(request.data) op_changes_type = int(request.headers.get("Anki-Op-Changes", "0")) if op_changes_type: - response: OpChanges | OpChangesOnly - if op_changes_type == 1: - response = OpChanges() - else: - response = OpChangesOnly() - response.ParseFromString(output) + op_message_types = (OpChanges, OpChangesOnly, NestedOpChanges) + try: + response = op_message_types[op_changes_type - 1]() + response.ParseFromString(output) + changes: Any = response + for _ in range(op_changes_type - 1): + changes = changes.changes + except IndexError: + raise ValueError(f"unhandled op changes level: {op_changes_type}") def handle_on_main() -> None: handler = aqt.mw.app.activeWindow() - on_op_finished(aqt.mw, response, handler) + on_op_finished(aqt.mw, changes, handler) aqt.mw.taskman.run_on_main(handle_on_main) diff --git a/rslib/proto/typescript.rs b/rslib/proto/typescript.rs index 6572eb949..e5802fdab 100644 --- a/rslib/proto/typescript.rs +++ b/rslib/proto/typescript.rs @@ -101,6 +101,7 @@ enum OpChangesType { None = 0, OpChanges = 1, OpChangesOnly = 2, + NestedOpChanges = 3, } struct MethodDetails { @@ -117,7 +118,8 @@ impl MethodDetails { let input_type = full_name_to_imported_reference(method.proto.input().full_name()); let output_type = full_name_to_imported_reference(method.proto.output().full_name()); let comments = method.comments.clone(); - let op_changes_type = get_op_changes_type(&method.proto.output(), true); + let op_changes_type = + get_op_changes_type(&method.proto.output(), &method.proto.output(), 1); Self { method_name: name, input_type, @@ -128,16 +130,26 @@ impl MethodDetails { } } -fn get_op_changes_type(message: &MessageDescriptor, root: bool) -> OpChangesType { +fn get_op_changes_type( + root_message: &MessageDescriptor, + message: &MessageDescriptor, + level: u8, +) -> OpChangesType { if message.full_name() == "anki.collection.OpChanges" { - if root { - OpChangesType::OpChanges - } else { - OpChangesType::OpChangesOnly + match level { + 0 => OpChangesType::None, + 1 => OpChangesType::OpChanges, + 2 => OpChangesType::OpChangesOnly, + 3 => OpChangesType::NestedOpChanges, + _ => panic!( + "unhandled op changes level for message {}: {}", + root_message.full_name(), + level + ), } } else if let Some(field) = message.get_field(1) { if let Some(field_message) = field.kind().as_message() { - get_op_changes_type(field_message, false) + get_op_changes_type(root_message, field_message, level + 1) } else { OpChangesType::None }