Skip to content

Commit a0f4462

Browse files
chrisbobbegnprice
authored andcommitted
api: In send-message requests, override user agent for old servers
This resolves zulip#440 for older servers.
1 parent 0b32457 commit a0f4462

File tree

3 files changed

+61
-5
lines changed

3 files changed

+61
-5
lines changed

lib/api/core.dart

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,16 @@ class ApiConnection {
7474
bool _isOpen = true;
7575

7676
Future<T> send<T>(String routeName, T Function(Map<String, dynamic>) fromJson,
77-
http.BaseRequest request) async {
77+
http.BaseRequest request, {String? overrideUserAgent}) async {
7878
assert(_isOpen);
7979

8080
assert(debugLog("${request.method} ${request.url}"));
8181

8282
addAuth(request);
8383
request.headers.addAll(userAgentHeader());
84+
if (overrideUserAgent != null) {
85+
request.headers['User-Agent'] = overrideUserAgent;
86+
}
8487

8588
final http.StreamedResponse response;
8689
try {
@@ -137,13 +140,13 @@ class ApiConnection {
137140
}
138141

139142
Future<T> post<T>(String routeName, T Function(Map<String, dynamic>) fromJson,
140-
String path, Map<String, dynamic>? params) async {
143+
String path, Map<String, dynamic>? params, {String? overrideUserAgent}) async {
141144
final url = realmUrl.replace(path: "/api/v1/$path");
142145
final request = http.Request('POST', url);
143146
if (params != null) {
144147
request.bodyFields = encodeParameters(params)!;
145148
}
146-
return send(routeName, fromJson, request);
149+
return send(routeName, fromJson, request, overrideUserAgent: overrideUserAgent);
147150
}
148151

149152
Future<T> postFileFromStream<T>(String routeName, T Function(Map<String, dynamic>) fromJson,

lib/api/route/messages.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ Future<SendMessageResult> sendMessage(
180180
bool? readBySender,
181181
}) {
182182
final supportsTypeDirect = connection.zulipFeatureLevel! >= 174; // TODO(server-7)
183+
final supportsReadBySender = connection.zulipFeatureLevel! >= 236; // TODO(server-8)
183184
return connection.post('sendMessage', SendMessageResult.fromJson, 'messages', {
184185
if (destination is StreamDestination) ...{
185186
'type': RawParameter('stream'),
@@ -195,6 +196,23 @@ Future<SendMessageResult> sendMessage(
195196
if (queueId != null) 'queue_id': queueId, // TODO should this use RawParameter?
196197
if (localId != null) 'local_id': localId, // TODO should this use RawParameter?
197198
if (readBySender != null) 'read_by_sender': readBySender,
199+
},
200+
overrideUserAgent: switch ((supportsReadBySender, readBySender)) {
201+
// Old servers use the user agent to decide if we're a UI client
202+
// and so whether the message should be marked as read for its author
203+
// (see #440). We are a UI client; so, use a value those servers will
204+
// interpret correctly. With newer servers, passing `readBySender: true`
205+
// gives the same result.
206+
// TODO(#467) include platform, platform version, and app version
207+
(false, _ ) => 'ZulipMobile/flutter',
208+
209+
// According to the doc, a user-agent heuristic is still used in this case:
210+
// https://zulip.com/api/send-message#parameter-read_by_sender
211+
// TODO find out if our default user agent would work with that.
212+
// TODO(#467) include platform, platform version, and app version
213+
(true, null) => 'ZulipMobile/flutter',
214+
215+
_ => null,
198216
});
199217
}
200218

test/api/route/messages_test.dart

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ 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/core.dart';
67
import 'package:zulip/api/model/model.dart';
78
import 'package:zulip/api/model/narrow.dart';
89
import 'package:zulip/api/route/messages.dart';
@@ -304,6 +305,7 @@ void main() {
304305
String? localId,
305306
bool? readBySender,
306307
required Map<String, String> expectedBodyFields,
308+
String? expectedUserAgent,
307309
}) async {
308310
connection.prepare(json: SendMessageResult(id: 42).toJson());
309311
final result = await sendMessage(connection,
@@ -313,7 +315,8 @@ void main() {
313315
check(connection.lastRequest).isA<http.Request>()
314316
..method.equals('POST')
315317
..url.path.equals('/api/v1/messages')
316-
..bodyFields.deepEquals(expectedBodyFields);
318+
..bodyFields.deepEquals(expectedBodyFields)
319+
..headers['User-Agent'].equals(expectedUserAgent ?? userAgentHeader()['User-Agent']!);
317320
}
318321

319322
test('smoke', () {
@@ -374,7 +377,39 @@ void main() {
374377
'to': jsonEncode(userIds),
375378
'content': content,
376379
'read_by_sender': 'true',
377-
});
380+
},
381+
expectedUserAgent: 'ZulipMobile/flutter');
382+
});
383+
});
384+
385+
test('when readBySender is null, sends a User-Agent we know the server will recognize', () {
386+
return FakeApiConnection.with_((connection) async {
387+
await checkSendMessage(connection,
388+
destination: StreamDestination(streamId, topic), content: content,
389+
readBySender: null,
390+
expectedBodyFields: {
391+
'type': 'stream',
392+
'to': streamId.toString(),
393+
'topic': topic,
394+
'content': content,
395+
},
396+
expectedUserAgent: 'ZulipMobile/flutter');
397+
});
398+
});
399+
400+
test('legacy: when server does not support readBySender, sends a User-Agent the server will recognize', () {
401+
return FakeApiConnection.with_(zulipFeatureLevel: 235, (connection) async {
402+
await checkSendMessage(connection,
403+
destination: StreamDestination(streamId, topic), content: content,
404+
readBySender: true,
405+
expectedBodyFields: {
406+
'type': 'stream',
407+
'to': streamId.toString(),
408+
'topic': topic,
409+
'content': content,
410+
'read_by_sender': 'true',
411+
},
412+
expectedUserAgent: 'ZulipMobile/flutter');
378413
});
379414
});
380415
});

0 commit comments

Comments
 (0)