Skip to content

Commit 631f4d6

Browse files
chrisbobbegnprice
authored andcommitted
api: Add getMessageCompat helper, for servers with and without FL 120
We can use this to get raw Markdown content for quote-and-reply (zulip#116) and for the "Share" option on a message. For those, we only care about the raw Markdown content and so could just as well have used the `raw_content` field on the get-single-message response, for servers pre-120. But... We can also use this for zulip#73, "Handle Zulip-internal links by navigation", to follow /near/<id> links through topic/stream moves (see implementation in zulip-mobile). For that, we'll need more than just the message's raw Markdown.
1 parent 8644036 commit 631f4d6

File tree

3 files changed

+171
-0
lines changed

3 files changed

+171
-0
lines changed

lib/api/model/narrow.dart

+17
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,20 @@ class ApiNarrowPmWith extends ApiNarrowDm {
9797

9898
ApiNarrowPmWith._(super.operand, {super.negated});
9999
}
100+
101+
class ApiNarrowMessageId extends ApiNarrowElement {
102+
@override String get operator => 'id';
103+
104+
// The API requires a string, even though message IDs are ints:
105+
// https://chat.zulip.org/#narrow/stream/378-api-design/topic/.60id.3A123.60.20narrow.20in.20.60GET.20.2Fmessages.60/near/1591465
106+
// TODO(server-future) Send ints to future servers that support them. For how
107+
// to handle the migration, see [ApiNarrowDm.resolve].
108+
@override final String operand;
109+
110+
ApiNarrowMessageId(int operand, {super.negated}) : operand = operand.toString();
111+
112+
factory ApiNarrowMessageId.fromJson(Map<String, dynamic> json) => ApiNarrowMessageId(
113+
int.parse(json['operand'] as String),
114+
negated: json['negated'] as bool? ?? false,
115+
);
116+
}

lib/api/route/messages.dart

+44
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,55 @@
11
import 'package:json_annotation/json_annotation.dart';
22

33
import '../core.dart';
4+
import '../exception.dart';
45
import '../model/model.dart';
56
import '../model/narrow.dart';
67

78
part 'messages.g.dart';
89

10+
/// Convenience function to get a single message from any server.
11+
///
12+
/// This encapsulates a server-feature check.
13+
///
14+
/// Gives null if the server reports that the message doesn't exist.
15+
// TODO(server-5) Simplify this away; just use getMessage.
16+
Future<Message?> getMessageCompat(ApiConnection connection, {
17+
required int messageId,
18+
bool? applyMarkdown,
19+
}) async {
20+
final useLegacyApi = connection.zulipFeatureLevel! < 120;
21+
if (useLegacyApi) {
22+
final response = await getMessages(connection,
23+
narrow: [ApiNarrowMessageId(messageId)],
24+
anchor: NumericAnchor(messageId),
25+
numBefore: 0,
26+
numAfter: 0,
27+
applyMarkdown: applyMarkdown,
28+
29+
// Hard-code this param to `true`, as the new single-message API
30+
// effectively does:
31+
// https://chat.zulip.org/#narrow/stream/378-api-design/topic/.60client_gravatar.60.20in.20.60messages.2F.7Bmessage_id.7D.60/near/1418337
32+
clientGravatar: true,
33+
);
34+
return response.messages.firstOrNull;
35+
} else {
36+
try {
37+
final response = await getMessage(connection,
38+
messageId: messageId,
39+
applyMarkdown: applyMarkdown,
40+
);
41+
return response.message;
42+
} on ZulipApiException catch (e) {
43+
if (e.code == 'BAD_REQUEST') {
44+
// Servers use this code when the message doesn't exist, according to
45+
// the example in the doc.
46+
return null;
47+
}
48+
rethrow;
49+
}
50+
}
51+
}
52+
953
/// https://zulip.com/api/get-message
1054
///
1155
/// This binding only supports feature levels 120+.

test/api/route/messages_test.dart

+110
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,126 @@ import 'dart:convert';
33
import 'package:checks/checks.dart';
44
import 'package:http/http.dart' as http;
55
import 'package:test/scaffolding.dart';
6+
import 'package:zulip/api/model/model.dart';
67
import 'package:zulip/api/model/narrow.dart';
78
import 'package:zulip/api/route/messages.dart';
89
import 'package:zulip/model/narrow.dart';
910

1011
import '../../example_data.dart' as eg;
1112
import '../../stdlib_checks.dart';
1213
import '../fake_api.dart';
14+
import '../model/model_checks.dart';
1315
import 'route_checks.dart';
1416

1517
void main() {
18+
group('getMessageCompat', () {
19+
Future<Message?> checkGetMessageCompat(FakeApiConnection connection, {
20+
required bool expectLegacy,
21+
required int messageId,
22+
bool? applyMarkdown,
23+
}) async {
24+
final result = await getMessageCompat(connection,
25+
messageId: messageId,
26+
applyMarkdown: applyMarkdown,
27+
);
28+
if (expectLegacy) {
29+
check(connection.lastRequest).isA<http.Request>()
30+
..method.equals('GET')
31+
..url.path.equals('/api/v1/messages')
32+
..url.queryParameters.deepEquals({
33+
'narrow': jsonEncode([ApiNarrowMessageId(messageId)]),
34+
'anchor': messageId.toString(),
35+
'num_before': '0',
36+
'num_after': '0',
37+
if (applyMarkdown != null) 'apply_markdown': applyMarkdown.toString(),
38+
'client_gravatar': 'true',
39+
});
40+
} else {
41+
check(connection.lastRequest).isA<http.Request>()
42+
..method.equals('GET')
43+
..url.path.equals('/api/v1/messages/$messageId')
44+
..url.queryParameters.deepEquals({
45+
if (applyMarkdown != null) 'apply_markdown': applyMarkdown.toString(),
46+
});
47+
}
48+
return result;
49+
}
50+
51+
test('modern; message found', () {
52+
return FakeApiConnection.with_((connection) async {
53+
final message = eg.streamMessage();
54+
final fakeResult = GetMessageResult(message: message);
55+
connection.prepare(json: fakeResult.toJson());
56+
final result = await checkGetMessageCompat(connection,
57+
expectLegacy: false,
58+
messageId: message.id,
59+
applyMarkdown: true,
60+
);
61+
check(result).isNotNull().jsonEquals(message);
62+
});
63+
});
64+
65+
test('modern; message not found', () {
66+
return FakeApiConnection.with_((connection) async {
67+
final message = eg.streamMessage();
68+
final fakeResponseJson = {
69+
'code': 'BAD_REQUEST',
70+
'msg': 'Invalid message(s)',
71+
'result': 'error',
72+
};
73+
connection.prepare(httpStatus: 400, json: fakeResponseJson);
74+
final result = await checkGetMessageCompat(connection,
75+
expectLegacy: false,
76+
messageId: message.id,
77+
applyMarkdown: true,
78+
);
79+
check(result).isNull();
80+
});
81+
});
82+
83+
test('legacy; message found', () {
84+
return FakeApiConnection.with_(zulipFeatureLevel: 119, (connection) async {
85+
final message = eg.streamMessage();
86+
final fakeResult = GetMessagesResult(
87+
anchor: message.id,
88+
foundNewest: false,
89+
foundOldest: false,
90+
foundAnchor: true,
91+
historyLimited: false,
92+
messages: [message],
93+
);
94+
connection.prepare(json: fakeResult.toJson());
95+
final result = await checkGetMessageCompat(connection,
96+
expectLegacy: true,
97+
messageId: message.id,
98+
applyMarkdown: true,
99+
);
100+
check(result).isNotNull().jsonEquals(message);
101+
});
102+
});
103+
104+
test('legacy; message not found', () {
105+
return FakeApiConnection.with_(zulipFeatureLevel: 119, (connection) async {
106+
final message = eg.streamMessage();
107+
final fakeResult = GetMessagesResult(
108+
anchor: message.id,
109+
foundNewest: false,
110+
foundOldest: false,
111+
foundAnchor: false,
112+
historyLimited: false,
113+
messages: [],
114+
);
115+
connection.prepare(json: fakeResult.toJson());
116+
final result = await checkGetMessageCompat(connection,
117+
expectLegacy: true,
118+
messageId: message.id,
119+
applyMarkdown: true,
120+
);
121+
check(result).isNull();
122+
});
123+
});
124+
});
125+
16126
group('getMessage', () {
17127
Future<GetMessageResult> checkGetMessage(
18128
FakeApiConnection connection, {

0 commit comments

Comments
 (0)