Skip to content

Commit

Permalink
api: Add getMessageCompat helper, for servers with and without FL 120
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
chrisbobbe authored and gnprice committed Jun 13, 2023
1 parent 8644036 commit 631f4d6
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 0 deletions.
17 changes: 17 additions & 0 deletions lib/api/model/narrow.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,20 @@ class ApiNarrowPmWith extends ApiNarrowDm {

ApiNarrowPmWith._(super.operand, {super.negated});
}

class ApiNarrowMessageId extends ApiNarrowElement {
@override String get operator => 'id';

// The API requires a string, even though message IDs are ints:
// https://chat.zulip.org/#narrow/stream/378-api-design/topic/.60id.3A123.60.20narrow.20in.20.60GET.20.2Fmessages.60/near/1591465
// TODO(server-future) Send ints to future servers that support them. For how
// to handle the migration, see [ApiNarrowDm.resolve].
@override final String operand;

ApiNarrowMessageId(int operand, {super.negated}) : operand = operand.toString();

factory ApiNarrowMessageId.fromJson(Map<String, dynamic> json) => ApiNarrowMessageId(
int.parse(json['operand'] as String),
negated: json['negated'] as bool? ?? false,
);
}
44 changes: 44 additions & 0 deletions lib/api/route/messages.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,55 @@
import 'package:json_annotation/json_annotation.dart';

import '../core.dart';
import '../exception.dart';
import '../model/model.dart';
import '../model/narrow.dart';

part 'messages.g.dart';

/// Convenience function to get a single message from any server.
///
/// This encapsulates a server-feature check.
///
/// Gives null if the server reports that the message doesn't exist.
// TODO(server-5) Simplify this away; just use getMessage.
Future<Message?> getMessageCompat(ApiConnection connection, {
required int messageId,
bool? applyMarkdown,
}) async {
final useLegacyApi = connection.zulipFeatureLevel! < 120;
if (useLegacyApi) {
final response = await getMessages(connection,
narrow: [ApiNarrowMessageId(messageId)],
anchor: NumericAnchor(messageId),
numBefore: 0,
numAfter: 0,
applyMarkdown: applyMarkdown,

// Hard-code this param to `true`, as the new single-message API
// effectively does:
// https://chat.zulip.org/#narrow/stream/378-api-design/topic/.60client_gravatar.60.20in.20.60messages.2F.7Bmessage_id.7D.60/near/1418337
clientGravatar: true,
);
return response.messages.firstOrNull;
} else {
try {
final response = await getMessage(connection,
messageId: messageId,
applyMarkdown: applyMarkdown,
);
return response.message;
} on ZulipApiException catch (e) {
if (e.code == 'BAD_REQUEST') {
// Servers use this code when the message doesn't exist, according to
// the example in the doc.
return null;
}
rethrow;
}
}
}

/// https://zulip.com/api/get-message
///
/// This binding only supports feature levels 120+.
Expand Down
110 changes: 110 additions & 0 deletions test/api/route/messages_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,126 @@ import 'dart:convert';
import 'package:checks/checks.dart';
import 'package:http/http.dart' as http;
import 'package:test/scaffolding.dart';
import 'package:zulip/api/model/model.dart';
import 'package:zulip/api/model/narrow.dart';
import 'package:zulip/api/route/messages.dart';
import 'package:zulip/model/narrow.dart';

import '../../example_data.dart' as eg;
import '../../stdlib_checks.dart';
import '../fake_api.dart';
import '../model/model_checks.dart';
import 'route_checks.dart';

void main() {
group('getMessageCompat', () {
Future<Message?> checkGetMessageCompat(FakeApiConnection connection, {
required bool expectLegacy,
required int messageId,
bool? applyMarkdown,
}) async {
final result = await getMessageCompat(connection,
messageId: messageId,
applyMarkdown: applyMarkdown,
);
if (expectLegacy) {
check(connection.lastRequest).isA<http.Request>()
..method.equals('GET')
..url.path.equals('/api/v1/messages')
..url.queryParameters.deepEquals({
'narrow': jsonEncode([ApiNarrowMessageId(messageId)]),
'anchor': messageId.toString(),
'num_before': '0',
'num_after': '0',
if (applyMarkdown != null) 'apply_markdown': applyMarkdown.toString(),
'client_gravatar': 'true',
});
} else {
check(connection.lastRequest).isA<http.Request>()
..method.equals('GET')
..url.path.equals('/api/v1/messages/$messageId')
..url.queryParameters.deepEquals({
if (applyMarkdown != null) 'apply_markdown': applyMarkdown.toString(),
});
}
return result;
}

test('modern; message found', () {
return FakeApiConnection.with_((connection) async {
final message = eg.streamMessage();
final fakeResult = GetMessageResult(message: message);
connection.prepare(json: fakeResult.toJson());
final result = await checkGetMessageCompat(connection,
expectLegacy: false,
messageId: message.id,
applyMarkdown: true,
);
check(result).isNotNull().jsonEquals(message);
});
});

test('modern; message not found', () {
return FakeApiConnection.with_((connection) async {
final message = eg.streamMessage();
final fakeResponseJson = {
'code': 'BAD_REQUEST',
'msg': 'Invalid message(s)',
'result': 'error',
};
connection.prepare(httpStatus: 400, json: fakeResponseJson);
final result = await checkGetMessageCompat(connection,
expectLegacy: false,
messageId: message.id,
applyMarkdown: true,
);
check(result).isNull();
});
});

test('legacy; message found', () {
return FakeApiConnection.with_(zulipFeatureLevel: 119, (connection) async {
final message = eg.streamMessage();
final fakeResult = GetMessagesResult(
anchor: message.id,
foundNewest: false,
foundOldest: false,
foundAnchor: true,
historyLimited: false,
messages: [message],
);
connection.prepare(json: fakeResult.toJson());
final result = await checkGetMessageCompat(connection,
expectLegacy: true,
messageId: message.id,
applyMarkdown: true,
);
check(result).isNotNull().jsonEquals(message);
});
});

test('legacy; message not found', () {
return FakeApiConnection.with_(zulipFeatureLevel: 119, (connection) async {
final message = eg.streamMessage();
final fakeResult = GetMessagesResult(
anchor: message.id,
foundNewest: false,
foundOldest: false,
foundAnchor: false,
historyLimited: false,
messages: [],
);
connection.prepare(json: fakeResult.toJson());
final result = await checkGetMessageCompat(connection,
expectLegacy: true,
messageId: message.id,
applyMarkdown: true,
);
check(result).isNull();
});
});
});

group('getMessage', () {
Future<GetMessageResult> checkGetMessage(
FakeApiConnection connection, {
Expand Down

0 comments on commit 631f4d6

Please sign in to comment.