Skip to content

Commit 6c3ee14

Browse files
PIG208gnprice
authored andcommitted
api: Extract and parse UpdateMessageMoveData from UpdateMessageEvent
This is mostly NFC except that we were logging and ignoring malformed event data before, and now start to throw errors within the parsing code. This data structure encapsulates some checks so that we can make all fields non-nullable, with reasonable fallback values. As of writing, we do not use origStreamId (a.k.a.: 'stream_id') when there was no message move, even though it is present if there were content edits This makes dropping 'stream_id' when parsing `moveData` into `null` acceptable for now. Signed-off-by: Zixuan James Li <[email protected]>
1 parent 5149268 commit 6c3ee14

File tree

8 files changed

+193
-101
lines changed

8 files changed

+193
-101
lines changed

lib/api/model/events.dart

Lines changed: 83 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -718,16 +718,8 @@ class UpdateMessageEvent extends Event {
718718

719719
// final String? streamName; // ignore
720720

721-
@JsonKey(name: 'stream_id')
722-
final int? origStreamId;
723-
final int? newStreamId;
724-
725-
final PropagateMode? propagateMode;
726-
727-
@JsonKey(name: 'orig_subject')
728-
final TopicName? origTopic;
729-
@JsonKey(name: 'subject')
730-
final TopicName? newTopic;
721+
@JsonKey(readValue: _readMoveData, fromJson: UpdateMessageMoveData.tryParseFromJson, includeToJson: false)
722+
final UpdateMessageMoveData? moveData;
731723

732724
// final List<TopicLink> topicLinks; // TODO handle
733725

@@ -747,25 +739,101 @@ class UpdateMessageEvent extends Event {
747739
required this.messageIds,
748740
required this.flags,
749741
required this.editTimestamp,
750-
required this.origStreamId,
751-
required this.newStreamId,
752-
required this.propagateMode,
753-
required this.origTopic,
754-
required this.newTopic,
742+
required this.moveData,
755743
required this.origContent,
756744
required this.origRenderedContent,
757745
required this.content,
758746
required this.renderedContent,
759747
required this.isMeMessage,
760748
});
761749

750+
static Map<String, dynamic> _readMoveData(Map<dynamic, dynamic> json, String key) {
751+
// Parsing [UpdateMessageMoveData] requires `json`, not the default `json[key]`.
752+
assert(json is Map<String, dynamic>); // value came through `fromJson` with this type
753+
return json as Map<String, dynamic>;
754+
}
755+
762756
factory UpdateMessageEvent.fromJson(Map<String, dynamic> json) =>
763757
_$UpdateMessageEventFromJson(json);
764758

765759
@override
766760
Map<String, dynamic> toJson() => _$UpdateMessageEventToJson(this);
767761
}
768762

763+
/// Data structure representing a message move.
764+
class UpdateMessageMoveData {
765+
final int origStreamId;
766+
final int newStreamId;
767+
final TopicName origTopic;
768+
final TopicName newTopic;
769+
final PropagateMode propagateMode;
770+
771+
UpdateMessageMoveData({
772+
required this.origStreamId,
773+
required this.newStreamId,
774+
required this.origTopic,
775+
required this.newTopic,
776+
required this.propagateMode,
777+
}) : assert(newStreamId != origStreamId || newTopic != origTopic);
778+
779+
/// Try to extract [UpdateMessageMoveData] from the JSON object for an
780+
/// [UpdateMessageEvent].
781+
///
782+
/// Returns `null` if there was no message move.
783+
///
784+
/// Throws an error if the data is malformed.
785+
// When parsing this, 'stream_id', which is also present when there was only
786+
// a content edit, cannot be recovered if this ends up returning `null`.
787+
// This may matter if we ever need 'stream_id' when no message move occurred.
788+
static UpdateMessageMoveData? tryParseFromJson(Map<String, Object?> json) {
789+
final origStreamId = (json['stream_id'] as num?)?.toInt();
790+
final newStreamIdRaw = (json['new_stream_id'] as num?)?.toInt();
791+
final newStreamId = newStreamIdRaw ?? origStreamId;
792+
793+
final origTopic = json['orig_subject'] == null ? null
794+
: TopicName.fromJson(json['orig_subject'] as String);
795+
final newTopicRaw = json['subject'] == null ? null
796+
: TopicName.fromJson(json['subject'] as String);
797+
final newTopic = newTopicRaw ?? origTopic;
798+
799+
final propagateModeString = json['propagate_mode'] as String?;
800+
final propagateMode = propagateModeString == null ? null
801+
: PropagateMode.fromRawString(propagateModeString);
802+
803+
if (newStreamId == origStreamId && newTopic == origTopic) {
804+
if (propagateMode != null) {
805+
throw FormatException(
806+
'Malformed UpdateMessageEvent: incoherent message-move fields; '
807+
'propagate_mode present but no new channel or topic');
808+
}
809+
return null;
810+
}
811+
812+
if (origStreamId == null || newStreamId == null) {
813+
// The `stream_id` field (aka origStreamId) is documented to be present on moves;
814+
// newStreamId should not be null either because it falls back to origStreamId.
815+
throw FormatException('Malformed UpdateMessageEvent: move but no origStreamId');
816+
}
817+
if (origTopic == null || newTopic == null) {
818+
// The `orig_subject` field (aka origTopic) is documented to be present on moves;
819+
// newTopic should not be null either because it falls back to origTopic.
820+
throw FormatException('Malformed UpdateMessageEvent: move but no origTopic');
821+
}
822+
if (propagateMode == null) {
823+
// The `propagate_mode` field (aka propagateMode) is documented to be present on moves.
824+
throw FormatException('Malformed UpdateMessageEvent: move but no propagateMode');
825+
}
826+
827+
return UpdateMessageMoveData(
828+
origStreamId: origStreamId,
829+
newStreamId: newStreamId,
830+
origTopic: origTopic,
831+
newTopic: newTopic,
832+
propagateMode: propagateMode,
833+
);
834+
}
835+
}
836+
769837
/// A Zulip event of type `delete_message`: https://zulip.com/api/get-events#delete_message
770838
@JsonSerializable(fieldRename: FieldRename.snake)
771839
class DeleteMessageEvent extends Event {

lib/api/model/events.g.dart

Lines changed: 3 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/model/message.dart

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -169,42 +169,14 @@ class MessageStoreImpl with MessageStore {
169169
}
170170

171171
void _handleUpdateMessageEventMove(UpdateMessageEvent event) {
172-
// The interaction between the fields of these events are a bit tricky.
173-
// For reference, see: https://zulip.com/api/get-events#update_message
174-
175-
final origStreamId = event.origStreamId;
176-
final newStreamId = event.newStreamId ?? origStreamId;
177-
final origTopic = event.origTopic;
178-
final newTopic = event.newTopic ?? origTopic;
179-
final propagateMode = event.propagateMode;
180-
181-
if (newStreamId == origStreamId && newTopic == origTopic) {
172+
final messageMove = event.moveData;
173+
if (messageMove == null) {
182174
// There was no move.
183-
if (propagateMode != null) {
184-
assert(debugLog(
185-
'Malformed UpdateMessageEvent: incoherent message-move fields; '
186-
'propagate_mode present but no new channel or topic')); // TODO(log)
187-
}
188175
return;
189176
}
190177

191-
if (origStreamId == null || newStreamId == null) {
192-
// The `stream_id` field (aka origStreamId) is documented to be present on moves.
193-
// newStreamId should not be null either because it falls back to origStreamId.
194-
assert(debugLog('Malformed UpdateMessageEvent: move but no origStreamId')); // TODO(log)
195-
return;
196-
}
197-
if (origTopic == null || newTopic == null) {
198-
// The `orig_subject` field (aka origTopic) is documented to be present on moves.
199-
// newTopic should not be null either because it falls back to origTopic.
200-
assert(debugLog('Malformed UpdateMessageEvent: move but no origTopic')); // TODO(log)
201-
return;
202-
}
203-
if (propagateMode == null) {
204-
// The `propagate_mode` field (aka propagateMode) is documented to be present on moves.
205-
assert(debugLog('Malformed UpdateMessageEvent: move but no propagateMode')); // TODO(log)
206-
return;
207-
}
178+
final UpdateMessageMoveData(
179+
:origStreamId, :newStreamId, :origTopic, :newTopic) = messageMove;
208180

209181
final wasResolveOrUnresolve = newStreamId == origStreamId
210182
&& MessageEditState.topicMoveWasResolveOrUnresolve(origTopic, newTopic);
@@ -236,14 +208,7 @@ class MessageStoreImpl with MessageStore {
236208
}
237209

238210
for (final view in _messageListViews) {
239-
view.messagesMoved(
240-
origStreamId: origStreamId,
241-
newStreamId: newStreamId,
242-
origTopic: origTopic,
243-
newTopic: newTopic,
244-
messageIds: event.messageIds,
245-
propagateMode: propagateMode,
246-
);
211+
view.messagesMoved(messageMove: messageMove, messageIds: event.messageIds);
247212
}
248213
}
249214

lib/model/message_list.dart

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -720,13 +720,12 @@ class MessageListView with ChangeNotifier, _MessageSequence {
720720
}
721721

722722
void messagesMoved({
723-
required int origStreamId,
724-
required int newStreamId,
725-
required TopicName origTopic,
726-
required TopicName newTopic,
723+
required UpdateMessageMoveData messageMove,
727724
required List<int> messageIds,
728-
required PropagateMode propagateMode,
729725
}) {
726+
final UpdateMessageMoveData(
727+
:origStreamId, :newStreamId, :origTopic, :newTopic, :propagateMode,
728+
) = messageMove;
730729
switch (narrow) {
731730
case DmNarrow():
732731
// DMs can't be moved (nor created by moves),

test/api/model/events_checks.dart

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,22 @@ extension UpdateMessageEventChecks on Subject<UpdateMessageEvent> {
4848
Subject<List<int>> get messageIds => has((e) => e.messageIds, 'messageIds');
4949
Subject<List<MessageFlag>> get flags => has((e) => e.flags, 'flags');
5050
Subject<int?> get editTimestamp => has((e) => e.editTimestamp, 'editTimestamp');
51-
Subject<int?> get origStreamId => has((e) => e.origStreamId, 'origStreamId');
52-
Subject<int?> get newStreamId => has((e) => e.newStreamId, 'newStreamId');
53-
Subject<PropagateMode?> get propagateMode => has((e) => e.propagateMode, 'propagateMode');
54-
Subject<TopicName?> get origTopic => has((e) => e.origTopic, 'origTopic');
55-
Subject<TopicName?> get newTopic => has((e) => e.newTopic, 'newTopic');
51+
Subject<UpdateMessageMoveData?> get moveData => has((e) => e.moveData, 'moveData');
5652
Subject<String?> get origContent => has((e) => e.origContent, 'origContent');
5753
Subject<String?> get origRenderedContent => has((e) => e.origRenderedContent, 'origRenderedContent');
5854
Subject<String?> get content => has((e) => e.content, 'content');
5955
Subject<String?> get renderedContent => has((e) => e.renderedContent, 'renderedContent');
6056
Subject<bool?> get isMeMessage => has((e) => e.isMeMessage, 'isMeMessage');
6157
}
6258

59+
extension UpdateMessageMoveDataChecks on Subject<UpdateMessageMoveData> {
60+
Subject<int> get origStreamId => has((e) => e.origStreamId, 'origStreamId');
61+
Subject<int> get newStreamId => has((e) => e.newStreamId, 'newStreamId');
62+
Subject<TopicName> get origTopic => has((e) => e.origTopic, 'origTopic');
63+
Subject<TopicName> get newTopic => has((e) => e.newTopic, 'newTopic');
64+
Subject<PropagateMode> get propagateMode => has((e) => e.propagateMode, 'propagateMode');
65+
}
66+
6367
extension DeleteMessageEventChecks on Subject<DeleteMessageEvent> {
6468
Subject<MessageType?> get messageType => has((e) => e.messageType, 'messageType');
6569
}

test/api/model/events_test.dart

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,26 @@ void main() {
106106
'propagate_mode': 'change_all',
107107
};
108108

109+
test('smoke moveData', () {
110+
check(Event.fromJson({ ...baseMoveJson,
111+
'stream_id': 1,
112+
'new_stream_id': 2,
113+
'orig_subject': 'foo',
114+
'subject': 'bar',
115+
'propagate_mode': 'change_all',
116+
})).isA<UpdateMessageEvent>().moveData.isNotNull()
117+
..origStreamId.equals(1)
118+
..newStreamId.equals(2)
119+
..origTopic.equals(const TopicName('foo'))
120+
..newTopic.equals(const TopicName('bar'))
121+
..propagateMode.equals(PropagateMode.changeAll);
122+
});
123+
109124
test('stream_id -> origStreamId', () {
110125
check(Event.fromJson({ ...baseMoveJson,
111126
'stream_id': 1,
112127
'new_stream_id': 2,
113-
})).isA<UpdateMessageEvent>()
128+
})).isA<UpdateMessageEvent>().moveData.isNotNull()
114129
..origStreamId.equals(1)
115130
..newStreamId.equals(2);
116131
});
@@ -119,10 +134,73 @@ void main() {
119134
check(Event.fromJson({ ...baseMoveJson,
120135
'orig_subject': 'foo',
121136
'subject': 'bar',
122-
})).isA<UpdateMessageEvent>()
137+
})).isA<UpdateMessageEvent>().moveData.isNotNull()
123138
..origTopic.equals(const TopicName('foo'))
124139
..newTopic.equals(const TopicName('bar'));
125140
});
141+
142+
test('new channel, same topic: fill in newTopic', () {
143+
// The server omits 'subject' in this situation.
144+
check(Event.fromJson({ ...baseMoveJson,
145+
'stream_id': 1,
146+
'new_stream_id': 2,
147+
'orig_subject': 'foo',
148+
'subject': null,
149+
})).isA<UpdateMessageEvent>().moveData.isNotNull()
150+
..origTopic.equals(const TopicName('foo'))
151+
..newTopic.equals(const TopicName('foo'));
152+
});
153+
154+
test('same channel, new topic; fill in newStreamId', () {
155+
// The server omits 'new_stream_id' in this situation.
156+
check(Event.fromJson({ ...baseMoveJson,
157+
'stream_id': 1,
158+
'new_stream_id': null,
159+
'orig_subject': 'foo',
160+
'subject': 'bar',
161+
})).isA<UpdateMessageEvent>().moveData.isNotNull()
162+
..origStreamId.equals(1)
163+
..newStreamId.equals(1);
164+
});
165+
166+
test('no message move', () {
167+
check(Event.fromJson({ ...baseJson,
168+
'orig_content': 'foo',
169+
'orig_rendered_content': 'foo',
170+
'content': 'bar',
171+
'rendered_content': 'bar',
172+
})).isA<UpdateMessageEvent>().moveData.isNull();
173+
});
174+
175+
test('stream move but no orig_subject', () {
176+
check(() => Event.fromJson({ ...baseMoveJson,
177+
'stream_id': 1,
178+
'new_stream_id': 2,
179+
'orig_subject': null,
180+
})).throws<void>();
181+
});
182+
183+
test('move but no subject or new_stream_id', () {
184+
check(() => Event.fromJson({ ...baseMoveJson,
185+
'new_stream_id': null,
186+
'subject': null,
187+
})).throws<void>();
188+
});
189+
190+
test('move but no orig_stream_id', () {
191+
check(() => Event.fromJson({ ...baseMoveJson,
192+
'stream_id': null,
193+
'new_stream_id': 2,
194+
})).throws<void>();
195+
});
196+
197+
test('move but no propagate_mode', () {
198+
check(() => Event.fromJson({ ...baseMoveJson,
199+
'orig_subject': 'foo',
200+
'subject': 'bar',
201+
'propagate_mode': null,
202+
})).throws<void>();
203+
});
126204
});
127205

128206
test('delete_message: require streamId and topic for stream messages', () {

test/api/model/model_checks.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ extension TopicNameChecks on Subject<TopicName> {
5353

5454
extension StreamMessageChecks on Subject<StreamMessage> {
5555
Subject<String?> get displayRecipient => has((e) => e.displayRecipient, 'displayRecipient');
56+
Subject<int> get streamId => has((e) => e.streamId, 'streamId');
5657
Subject<TopicName> get topic => has((e) => e.topic, 'topic');
5758
}
5859

0 commit comments

Comments
 (0)