feat: support for authenticated media
This commit is contained in:
parent
987dd750b3
commit
f79096dfbb
|
|
@ -91,6 +91,11 @@ class FakeMatrixApi extends BaseClient {
|
|||
return completer.future;
|
||||
}
|
||||
|
||||
Set<String> servers = {
|
||||
'https://fakeserver.notexisting',
|
||||
'https://fakeserverpriortoauthmedia.notexisting'
|
||||
};
|
||||
|
||||
FutureOr<Response> mockIntercept(Request request) async {
|
||||
// Collect data from Request
|
||||
var action = request.url.path;
|
||||
|
|
@ -125,8 +130,7 @@ class FakeMatrixApi extends BaseClient {
|
|||
if (data is Map<String, dynamic> && data['timeout'] is String) {
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
}
|
||||
|
||||
if (request.url.origin != 'https://fakeserver.notexisting') {
|
||||
if (!servers.contains(request.url.origin)) {
|
||||
return Response(
|
||||
'<html><head></head><body>Not found...</body></html>', 404);
|
||||
}
|
||||
|
|
@ -150,88 +154,105 @@ class FakeMatrixApi extends BaseClient {
|
|||
|
||||
// Call API
|
||||
(_calledEndpoints[action] ??= <dynamic>[]).add(data);
|
||||
final act = api[method]?[action];
|
||||
if (act != null) {
|
||||
res = act(data);
|
||||
if (res is Map && res.containsKey('errcode')) {
|
||||
if (res['errcode'] == 'M_NOT_FOUND') {
|
||||
statusCode = 404;
|
||||
} else {
|
||||
statusCode = 405;
|
||||
}
|
||||
}
|
||||
} else if (method == 'PUT' && action.contains('/client/v3/sendToDevice/')) {
|
||||
res = {};
|
||||
if (_failToDevice) {
|
||||
statusCode = 500;
|
||||
}
|
||||
} else if (method == 'GET' &&
|
||||
action.contains('/client/v3/rooms/') &&
|
||||
action.contains('/state/m.room.member/') &&
|
||||
!action.endsWith('%40alicyy%3Aexample.com') &&
|
||||
!action.contains('%40getme')) {
|
||||
res = {'displayname': '', 'membership': 'ban'};
|
||||
} else if (method == 'PUT' &&
|
||||
action.contains(
|
||||
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/')) {
|
||||
res = {'event_id': '\$event${_eventCounter++}'};
|
||||
} else if (method == 'PUT' &&
|
||||
action.contains(
|
||||
'/client/v3/rooms/!1234%3AfakeServer.notExisting/state/')) {
|
||||
res = {'event_id': '\$event${_eventCounter++}'};
|
||||
} else if (action.contains('/client/v3/sync')) {
|
||||
if (request.url.origin ==
|
||||
'https://fakeserverpriortoauthmedia.notexisting' &&
|
||||
action.contains('/client/versions')) {
|
||||
res = {
|
||||
'next_batch': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
'versions': [
|
||||
'r0.0.1',
|
||||
'ra.b.c',
|
||||
'v0.1',
|
||||
'v1.1',
|
||||
'v1.9',
|
||||
'v1.10.1',
|
||||
],
|
||||
'unstable_features': {'m.lazy_load_members': true},
|
||||
};
|
||||
} else if (method == 'PUT' &&
|
||||
_client != null &&
|
||||
action.contains('/account_data/') &&
|
||||
!action.contains('/rooms/')) {
|
||||
final type = Uri.decodeComponent(action.split('/').last);
|
||||
final syncUpdate = sdk.SyncUpdate(
|
||||
nextBatch: '',
|
||||
accountData: [sdk.BasicEvent(content: decodeJson(data), type: type)],
|
||||
);
|
||||
if (_client?.database != null) {
|
||||
await _client?.database?.transaction(() async {
|
||||
await _client?.handleSync(syncUpdate);
|
||||
});
|
||||
} else {
|
||||
await _client?.handleSync(syncUpdate);
|
||||
}
|
||||
res = {};
|
||||
} else if (method == 'PUT' &&
|
||||
_client != null &&
|
||||
action.contains('/account_data/') &&
|
||||
action.contains('/rooms/')) {
|
||||
final segments = action.split('/');
|
||||
final type = Uri.decodeComponent(segments.last);
|
||||
final roomId = Uri.decodeComponent(segments[segments.length - 3]);
|
||||
final syncUpdate = sdk.SyncUpdate(
|
||||
nextBatch: '',
|
||||
rooms: RoomsUpdate(
|
||||
join: {
|
||||
roomId: JoinedRoomUpdate(accountData: [
|
||||
sdk.BasicRoomEvent(
|
||||
content: decodeJson(data), type: type, roomId: roomId)
|
||||
])
|
||||
},
|
||||
),
|
||||
);
|
||||
if (_client?.database != null) {
|
||||
await _client?.database?.transaction(() async {
|
||||
await _client?.handleSync(syncUpdate);
|
||||
});
|
||||
} else {
|
||||
await _client?.handleSync(syncUpdate);
|
||||
}
|
||||
res = {};
|
||||
} else {
|
||||
res = {
|
||||
'errcode': 'M_UNRECOGNIZED',
|
||||
'error': 'Unrecognized request: $action'
|
||||
};
|
||||
statusCode = 405;
|
||||
final act = api[method]?[action];
|
||||
if (act != null) {
|
||||
res = act(data);
|
||||
if (res is Map && res.containsKey('errcode')) {
|
||||
if (res['errcode'] == 'M_NOT_FOUND') {
|
||||
statusCode = 404;
|
||||
} else {
|
||||
statusCode = 405;
|
||||
}
|
||||
}
|
||||
} else if (method == 'PUT' &&
|
||||
action.contains('/client/v3/sendToDevice/')) {
|
||||
res = {};
|
||||
if (_failToDevice) {
|
||||
statusCode = 500;
|
||||
}
|
||||
} else if (method == 'GET' &&
|
||||
action.contains('/client/v3/rooms/') &&
|
||||
action.contains('/state/m.room.member/') &&
|
||||
!action.endsWith('%40alicyy%3Aexample.com') &&
|
||||
!action.contains('%40getme')) {
|
||||
res = {'displayname': '', 'membership': 'ban'};
|
||||
} else if (method == 'PUT' &&
|
||||
action.contains(
|
||||
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/')) {
|
||||
res = {'event_id': '\$event${_eventCounter++}'};
|
||||
} else if (method == 'PUT' &&
|
||||
action.contains(
|
||||
'/client/v3/rooms/!1234%3AfakeServer.notExisting/state/')) {
|
||||
res = {'event_id': '\$event${_eventCounter++}'};
|
||||
} else if (action.contains('/client/v3/sync')) {
|
||||
res = {
|
||||
'next_batch': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
};
|
||||
} else if (method == 'PUT' &&
|
||||
_client != null &&
|
||||
action.contains('/account_data/') &&
|
||||
!action.contains('/rooms/')) {
|
||||
final type = Uri.decodeComponent(action.split('/').last);
|
||||
final syncUpdate = sdk.SyncUpdate(
|
||||
nextBatch: '',
|
||||
accountData: [sdk.BasicEvent(content: decodeJson(data), type: type)],
|
||||
);
|
||||
if (_client?.database != null) {
|
||||
await _client?.database?.transaction(() async {
|
||||
await _client?.handleSync(syncUpdate);
|
||||
});
|
||||
} else {
|
||||
await _client?.handleSync(syncUpdate);
|
||||
}
|
||||
res = {};
|
||||
} else if (method == 'PUT' &&
|
||||
_client != null &&
|
||||
action.contains('/account_data/') &&
|
||||
action.contains('/rooms/')) {
|
||||
final segments = action.split('/');
|
||||
final type = Uri.decodeComponent(segments.last);
|
||||
final roomId = Uri.decodeComponent(segments[segments.length - 3]);
|
||||
final syncUpdate = sdk.SyncUpdate(
|
||||
nextBatch: '',
|
||||
rooms: RoomsUpdate(
|
||||
join: {
|
||||
roomId: JoinedRoomUpdate(accountData: [
|
||||
sdk.BasicRoomEvent(
|
||||
content: decodeJson(data), type: type, roomId: roomId)
|
||||
])
|
||||
},
|
||||
),
|
||||
);
|
||||
if (_client?.database != null) {
|
||||
await _client?.database?.transaction(() async {
|
||||
await _client?.handleSync(syncUpdate);
|
||||
});
|
||||
} else {
|
||||
await _client?.handleSync(syncUpdate);
|
||||
}
|
||||
res = {};
|
||||
} else {
|
||||
res = {
|
||||
'errcode': 'M_UNRECOGNIZED',
|
||||
'error': 'Unrecognized request: $action'
|
||||
};
|
||||
statusCode = 405;
|
||||
}
|
||||
}
|
||||
|
||||
unawaited(Future.delayed(Duration(milliseconds: 1)).then((_) async {
|
||||
|
|
@ -1122,7 +1143,19 @@ class FakeMatrixApi extends BaseClient {
|
|||
'og:image:width': 48,
|
||||
'matrix:image:size': 102400
|
||||
},
|
||||
'/client/v1/media/preview_url?url=https%3A%2F%2Fmatrix.org&ts=10':
|
||||
(var req) => {
|
||||
'og:title': 'Matrix Blog Post',
|
||||
'og:description':
|
||||
'This is a really cool blog post from matrix.org',
|
||||
'og:image': 'mxc://example.com/ascERGshawAWawugaAcauga',
|
||||
'og:image:type': 'image/png',
|
||||
'og:image:height': 48,
|
||||
'og:image:width': 48,
|
||||
'matrix:image:size': 102400
|
||||
},
|
||||
'/media/v3/config': (var req) => {'m.upload.size': 50000000},
|
||||
'/client/v1/media/config': (var req) => {'m.upload.size': 50000000},
|
||||
'/.well-known/matrix/client': (var req) => {
|
||||
'm.homeserver': {'base_url': 'https://matrix.example.com'},
|
||||
'm.identity_server': {'base_url': 'https://identity.example.com'},
|
||||
|
|
@ -1690,10 +1723,7 @@ class FakeMatrixApi extends BaseClient {
|
|||
'/client/v3/rooms/!5345234234%3Aexample.com/messages?from=t_1234a&dir=b&limit=30&filter=%7B%22lazy_load_members%22%3Atrue%7D':
|
||||
(var req) => archivesMessageResponse,
|
||||
'/client/versions': (var req) => {
|
||||
'versions': [
|
||||
'v1.1',
|
||||
'v1.2',
|
||||
],
|
||||
'versions': ['v1.1', 'v1.2', 'v1.11'],
|
||||
'unstable_features': {'m.lazy_load_members': true},
|
||||
},
|
||||
'/client/v3/login': (var req) => {
|
||||
|
|
|
|||
|
|
@ -25,12 +25,14 @@ import 'dart:typed_data';
|
|||
import 'package:async/async.dart';
|
||||
import 'package:collection/collection.dart' show IterableExtension;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/http.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
import 'package:random_string/random_string.dart';
|
||||
|
||||
import 'package:matrix/encryption.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/matrix_api_lite/generated/fixed_model.dart';
|
||||
import 'package:matrix/msc_extensions/msc_unpublished_custom_refresh_token_lifetime/msc_unpublished_custom_refresh_token_lifetime.dart';
|
||||
import 'package:matrix/src/models/timeline_chunk.dart';
|
||||
import 'package:matrix/src/utils/cached_stream_controller.dart';
|
||||
|
|
@ -41,6 +43,7 @@ import 'package:matrix/src/utils/run_benchmarked.dart';
|
|||
import 'package:matrix/src/utils/run_in_root.dart';
|
||||
import 'package:matrix/src/utils/sync_update_item_count.dart';
|
||||
import 'package:matrix/src/utils/try_get_push_rule.dart';
|
||||
import 'package:matrix/src/utils/versions_comparator.dart';
|
||||
|
||||
typedef RoomSorter = int Function(Room a, Room b);
|
||||
|
||||
|
|
@ -506,7 +509,7 @@ class Client extends MatrixApi {
|
|||
}
|
||||
|
||||
// Check if server supports at least one supported version
|
||||
final versions = await getVersions();
|
||||
final versions = _versionsCache = await getVersions();
|
||||
if (!versions.versions
|
||||
.any((version) => supportedVersions.contains(version))) {
|
||||
throw BadServerVersionsException(
|
||||
|
|
@ -1158,12 +1161,222 @@ class Client extends MatrixApi {
|
|||
_archivedRooms.add(ArchivedRoom(room: archivedRoom, timeline: timeline));
|
||||
}
|
||||
|
||||
GetVersionsResponse? _versionsCache;
|
||||
|
||||
Future<bool> authenticatedMediaSupported() async {
|
||||
_versionsCache ??= await getVersions();
|
||||
return _versionsCache?.versions.any(
|
||||
(v) => isVersionGreaterThanOrEqualTo(v, 'v1.11'),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
final _serverConfigCache = AsyncCache<ServerConfig>(const Duration(hours: 1));
|
||||
|
||||
/// Gets the config of the content repository, such as upload limit.
|
||||
/// This endpoint allows clients to retrieve the configuration of the content
|
||||
/// repository, such as upload limitations.
|
||||
/// Clients SHOULD use this as a guide when using content repository endpoints.
|
||||
/// All values are intentionally left optional. Clients SHOULD follow
|
||||
/// the advice given in the field description when the field is not available.
|
||||
///
|
||||
/// **NOTE:** Both clients and server administrators should be aware that proxies
|
||||
/// between the client and the server may affect the apparent behaviour of content
|
||||
/// repository APIs, for example, proxies may enforce a lower upload size limit
|
||||
/// than is advertised by the server on this endpoint.
|
||||
@override
|
||||
Future<ServerConfig> getConfig() =>
|
||||
_serverConfigCache.fetch(() => super.getConfig());
|
||||
_serverConfigCache.fetch(() => _getAuthenticatedConfig());
|
||||
|
||||
// TODO: remove once we are able to autogen this
|
||||
Future<ServerConfig> _getAuthenticatedConfig() async {
|
||||
String path;
|
||||
if (await authenticatedMediaSupported()) {
|
||||
path = '_matrix/client/v1/media/config';
|
||||
} else {
|
||||
path = '_matrix/media/v3/config';
|
||||
}
|
||||
final requestUri = Uri(path: path);
|
||||
final request = Request('GET', baseUri!.resolveUri(requestUri));
|
||||
request.headers['authorization'] = 'Bearer ${bearerToken!}';
|
||||
final response = await httpClient.send(request);
|
||||
final responseBody = await response.stream.toBytes();
|
||||
if (response.statusCode != 200) unexpectedResponse(response, responseBody);
|
||||
final responseString = utf8.decode(responseBody);
|
||||
final json = jsonDecode(responseString);
|
||||
return ServerConfig.fromJson(json as Map<String, Object?>);
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
/// [serverName] The server name from the `mxc://` URI (the authoritory component)
|
||||
///
|
||||
///
|
||||
/// [mediaId] The media ID from the `mxc://` URI (the path component)
|
||||
///
|
||||
///
|
||||
/// [allowRemote] Indicates to the server that it should not attempt to fetch the media if it is deemed
|
||||
/// remote. This is to prevent routing loops where the server contacts itself. Defaults to
|
||||
/// true if not provided.
|
||||
///
|
||||
@override
|
||||
// TODO: remove once we are able to autogen this
|
||||
Future<FileResponse> getContent(String serverName, String mediaId,
|
||||
{bool? allowRemote}) async {
|
||||
String path;
|
||||
|
||||
if (await authenticatedMediaSupported()) {
|
||||
path =
|
||||
'_matrix/client/v1/media/download/${Uri.encodeComponent(serverName)}/${Uri.encodeComponent(mediaId)}';
|
||||
} else {
|
||||
path =
|
||||
'_matrix/media/v3/download/${Uri.encodeComponent(serverName)}/${Uri.encodeComponent(mediaId)}';
|
||||
}
|
||||
final requestUri = Uri(path: path, queryParameters: {
|
||||
if (allowRemote != null && !await authenticatedMediaSupported())
|
||||
// removed with msc3916, so just to be explicit
|
||||
'allow_remote': allowRemote.toString(),
|
||||
});
|
||||
final request = Request('GET', baseUri!.resolveUri(requestUri));
|
||||
request.headers['authorization'] = 'Bearer ${bearerToken!}';
|
||||
final response = await httpClient.send(request);
|
||||
final responseBody = await response.stream.toBytes();
|
||||
if (response.statusCode != 200) unexpectedResponse(response, responseBody);
|
||||
return FileResponse(
|
||||
contentType: response.headers['content-type'], data: responseBody);
|
||||
}
|
||||
|
||||
/// This will download content from the content repository (same as
|
||||
/// the previous endpoint) but replace the target file name with the one
|
||||
/// provided by the caller.
|
||||
///
|
||||
/// [serverName] The server name from the `mxc://` URI (the authoritory component)
|
||||
///
|
||||
///
|
||||
/// [mediaId] The media ID from the `mxc://` URI (the path component)
|
||||
///
|
||||
///
|
||||
/// [fileName] A filename to give in the `Content-Disposition` header.
|
||||
///
|
||||
/// [allowRemote] Indicates to the server that it should not attempt to fetch the media if it is deemed
|
||||
/// remote. This is to prevent routing loops where the server contacts itself. Defaults to
|
||||
/// true if not provided.
|
||||
///
|
||||
@override
|
||||
// TODO: remove once we are able to autogen this
|
||||
Future<FileResponse> getContentOverrideName(
|
||||
String serverName, String mediaId, String fileName,
|
||||
{bool? allowRemote}) async {
|
||||
String path;
|
||||
if (await authenticatedMediaSupported()) {
|
||||
path =
|
||||
'_matrix/client/v1/media/download/${Uri.encodeComponent(serverName)}/${Uri.encodeComponent(mediaId)}/${Uri.encodeComponent(fileName)}';
|
||||
} else {
|
||||
path =
|
||||
'_matrix/media/v3/download/${Uri.encodeComponent(serverName)}/${Uri.encodeComponent(mediaId)}/${Uri.encodeComponent(fileName)}';
|
||||
}
|
||||
final requestUri = Uri(path: path, queryParameters: {
|
||||
if (allowRemote != null && !await authenticatedMediaSupported())
|
||||
// removed with msc3916, so just to be explicit
|
||||
'allow_remote': allowRemote.toString(),
|
||||
});
|
||||
final request = Request('GET', baseUri!.resolveUri(requestUri));
|
||||
request.headers['authorization'] = 'Bearer ${bearerToken!}';
|
||||
final response = await httpClient.send(request);
|
||||
final responseBody = await response.stream.toBytes();
|
||||
if (response.statusCode != 200) unexpectedResponse(response, responseBody);
|
||||
return FileResponse(
|
||||
contentType: response.headers['content-type'], data: responseBody);
|
||||
}
|
||||
|
||||
/// Get information about a URL for the client. Typically this is called when a
|
||||
/// client sees a URL in a message and wants to render a preview for the user.
|
||||
///
|
||||
/// **Note:**
|
||||
/// Clients should consider avoiding this endpoint for URLs posted in encrypted
|
||||
/// rooms. Encrypted rooms often contain more sensitive information the users
|
||||
/// do not want to share with the homeserver, and this can mean that the URLs
|
||||
/// being shared should also not be shared with the homeserver.
|
||||
///
|
||||
/// [url] The URL to get a preview of.
|
||||
///
|
||||
/// [ts] The preferred point in time to return a preview for. The server may
|
||||
/// return a newer version if it does not have the requested version
|
||||
/// available.
|
||||
@override
|
||||
// TODO: remove once we are able to autogen this
|
||||
Future<GetUrlPreviewResponse> getUrlPreview(Uri url, {int? ts}) async {
|
||||
String path;
|
||||
if (await authenticatedMediaSupported()) {
|
||||
path = '_matrix/client/v1/media/preview_url';
|
||||
} else {
|
||||
path = '_matrix/media/v3/preview_url';
|
||||
}
|
||||
final requestUri = Uri(path: path, queryParameters: {
|
||||
'url': url.toString(),
|
||||
if (ts != null) 'ts': ts.toString(),
|
||||
});
|
||||
final request = Request('GET', baseUri!.resolveUri(requestUri));
|
||||
request.headers['authorization'] = 'Bearer ${bearerToken!}';
|
||||
final response = await httpClient.send(request);
|
||||
final responseBody = await response.stream.toBytes();
|
||||
if (response.statusCode != 200) unexpectedResponse(response, responseBody);
|
||||
final responseString = utf8.decode(responseBody);
|
||||
final json = jsonDecode(responseString);
|
||||
return GetUrlPreviewResponse.fromJson(json as Map<String, Object?>);
|
||||
}
|
||||
|
||||
/// Download a thumbnail of content from the content repository.
|
||||
/// See the [Thumbnails](https://spec.matrix.org/unstable/client-server-api/#thumbnails) section for more information.
|
||||
///
|
||||
/// [serverName] The server name from the `mxc://` URI (the authoritory component)
|
||||
///
|
||||
///
|
||||
/// [mediaId] The media ID from the `mxc://` URI (the path component)
|
||||
///
|
||||
///
|
||||
/// [width] The *desired* width of the thumbnail. The actual thumbnail may be
|
||||
/// larger than the size specified.
|
||||
///
|
||||
/// [height] The *desired* height of the thumbnail. The actual thumbnail may be
|
||||
/// larger than the size specified.
|
||||
///
|
||||
/// [method] The desired resizing method. See the [Thumbnails](https://spec.matrix.org/unstable/client-server-api/#thumbnails)
|
||||
/// section for more information.
|
||||
///
|
||||
/// [allowRemote] Indicates to the server that it should not attempt to fetch
|
||||
/// the media if it is deemed remote. This is to prevent routing loops
|
||||
/// where the server contacts itself. Defaults to true if not provided.
|
||||
@override
|
||||
// TODO: remove once we are able to autogen this
|
||||
Future<FileResponse> getContentThumbnail(
|
||||
String serverName, String mediaId, int width, int height,
|
||||
{Method? method, bool? allowRemote}) async {
|
||||
String path;
|
||||
if (await authenticatedMediaSupported()) {
|
||||
path =
|
||||
'_matrix/client/v1/media/thumbnail/${Uri.encodeComponent(serverName)}/${Uri.encodeComponent(mediaId)}';
|
||||
} else {
|
||||
path =
|
||||
'_matrix/media/v3/thumbnail/${Uri.encodeComponent(serverName)}/${Uri.encodeComponent(mediaId)}';
|
||||
}
|
||||
|
||||
final requestUri = Uri(path: path, queryParameters: {
|
||||
'width': width.toString(),
|
||||
'height': height.toString(),
|
||||
if (method != null) 'method': method.name,
|
||||
if (allowRemote != null && !await authenticatedMediaSupported())
|
||||
// removed with msc3916, so just to be explicit
|
||||
'allow_remote': allowRemote.toString(),
|
||||
});
|
||||
|
||||
final request = Request('GET', baseUri!.resolveUri(requestUri));
|
||||
request.headers['authorization'] = 'Bearer ${bearerToken!}';
|
||||
final response = await httpClient.send(request);
|
||||
final responseBody = await response.stream.toBytes();
|
||||
if (response.statusCode != 200) unexpectedResponse(response, responseBody);
|
||||
return FileResponse(
|
||||
contentType: response.headers['content-type'], data: responseBody);
|
||||
}
|
||||
|
||||
/// Uploads a file and automatically caches it in the database, if it is small enough
|
||||
/// and returns the mxc url.
|
||||
|
|
|
|||
|
|
@ -544,6 +544,66 @@ class Event extends MatrixEvent {
|
|||
/// [minNoThumbSize] is the minimum size that an original image may be to not fetch its thumbnail, defaults to 80k
|
||||
/// [useThumbnailMxcUrl] says weather to use the mxc url of the thumbnail, rather than the original attachment.
|
||||
/// [animated] says weather the thumbnail is animated
|
||||
///
|
||||
/// Throws an exception if the scheme is not `mxc` or the homeserver is not
|
||||
/// set.
|
||||
///
|
||||
/// Important! To use this link you have to set a http header like this:
|
||||
/// `headers: {"authorization": "Bearer ${client.accessToken}"}`
|
||||
Future<Uri?> getAttachmentUri(
|
||||
{bool getThumbnail = false,
|
||||
bool useThumbnailMxcUrl = false,
|
||||
double width = 800.0,
|
||||
double height = 800.0,
|
||||
ThumbnailMethod method = ThumbnailMethod.scale,
|
||||
int minNoThumbSize = _minNoThumbSize,
|
||||
bool animated = false}) async {
|
||||
if (![EventTypes.Message, EventTypes.Sticker].contains(type) ||
|
||||
!hasAttachment ||
|
||||
isAttachmentEncrypted) {
|
||||
return null; // can't url-thumbnail in encrypted rooms
|
||||
}
|
||||
if (useThumbnailMxcUrl && !hasThumbnail) {
|
||||
return null; // can't fetch from thumbnail
|
||||
}
|
||||
final thisInfoMap = useThumbnailMxcUrl ? thumbnailInfoMap : infoMap;
|
||||
final thisMxcUrl =
|
||||
useThumbnailMxcUrl ? infoMap['thumbnail_url'] : content['url'];
|
||||
// if we have as method scale, we can return safely the original image, should it be small enough
|
||||
if (getThumbnail &&
|
||||
method == ThumbnailMethod.scale &&
|
||||
thisInfoMap['size'] is int &&
|
||||
thisInfoMap['size'] < minNoThumbSize) {
|
||||
getThumbnail = false;
|
||||
}
|
||||
// now generate the actual URLs
|
||||
if (getThumbnail) {
|
||||
return await Uri.parse(thisMxcUrl).getThumbnailUri(
|
||||
room.client,
|
||||
width: width,
|
||||
height: height,
|
||||
method: method,
|
||||
animated: animated,
|
||||
);
|
||||
} else {
|
||||
return await Uri.parse(thisMxcUrl).getDownloadUri(room.client);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the attachment https URL to display in the timeline, taking into account if the original image is tiny.
|
||||
/// Returns null for encrypted rooms, if the image can't be fetched via http url or if the event does not contain an attachment.
|
||||
/// Set [getThumbnail] to true to fetch the thumbnail, set [width], [height] and [method]
|
||||
/// for the respective thumbnailing properties.
|
||||
/// [minNoThumbSize] is the minimum size that an original image may be to not fetch its thumbnail, defaults to 80k
|
||||
/// [useThumbnailMxcUrl] says weather to use the mxc url of the thumbnail, rather than the original attachment.
|
||||
/// [animated] says weather the thumbnail is animated
|
||||
///
|
||||
/// Throws an exception if the scheme is not `mxc` or the homeserver is not
|
||||
/// set.
|
||||
///
|
||||
/// Important! To use this link you have to set a http header like this:
|
||||
/// `headers: {"authorization": "Bearer ${client.accessToken}"}`
|
||||
@Deprecated('Use getAttachmentUri() instead')
|
||||
Uri? getAttachmentUrl(
|
||||
{bool getThumbnail = false,
|
||||
bool useThumbnailMxcUrl = false,
|
||||
|
|
@ -655,9 +715,13 @@ class Event extends MatrixEvent {
|
|||
final canDownloadFileFromServer = uint8list == null && !fromLocalStoreOnly;
|
||||
if (canDownloadFileFromServer) {
|
||||
final httpClient = room.client.httpClient;
|
||||
downloadCallback ??=
|
||||
(Uri url) async => (await httpClient.get(url)).bodyBytes;
|
||||
uint8list = await downloadCallback(mxcUrl.getDownloadLink(room.client));
|
||||
downloadCallback ??= (Uri url) async => (await httpClient.get(
|
||||
url,
|
||||
headers: {'authorization': 'Bearer ${room.client.accessToken}'},
|
||||
))
|
||||
.bodyBytes;
|
||||
uint8list =
|
||||
await downloadCallback(await mxcUrl.getDownloadUri(room.client));
|
||||
storeable = database != null &&
|
||||
storeable &&
|
||||
uint8list.lengthInBytes < database.maxFileSize;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,79 @@ import 'dart:core';
|
|||
import 'package:matrix/src/client.dart';
|
||||
|
||||
extension MxcUriExtension on Uri {
|
||||
/// Returns a download Link to this content.
|
||||
/// Transforms this `mxc://` Uri into a `http` resource, which can be used
|
||||
/// to download the content.
|
||||
///
|
||||
/// Throws an exception if the scheme is not `mxc` or the homeserver is not
|
||||
/// set.
|
||||
///
|
||||
/// Important! To use this link you have to set a http header like this:
|
||||
/// `headers: {"authorization": "Bearer ${client.accessToken}"}`
|
||||
Future<Uri> getDownloadUri(Client client) async {
|
||||
String uriPath;
|
||||
|
||||
if (await client.authenticatedMediaSupported()) {
|
||||
uriPath =
|
||||
'_matrix/client/v1/media/download/$host${hasPort ? ':$port' : ''}$path';
|
||||
} else {
|
||||
uriPath =
|
||||
'_matrix/media/v3/download/$host${hasPort ? ':$port' : ''}$path';
|
||||
}
|
||||
|
||||
return isScheme('mxc')
|
||||
? client.homeserver != null
|
||||
? client.homeserver?.resolve(uriPath) ?? Uri()
|
||||
: Uri()
|
||||
: Uri();
|
||||
}
|
||||
|
||||
/// Transforms this `mxc://` Uri into a `http` resource, which can be used
|
||||
/// to download the content with the given `width` and
|
||||
/// `height`. `method` can be `ThumbnailMethod.crop` or
|
||||
/// `ThumbnailMethod.scale` and defaults to `ThumbnailMethod.scale`.
|
||||
/// If `animated` (default false) is set to true, an animated thumbnail is requested
|
||||
/// as per MSC2705. Thumbnails only animate if the media repository supports that.
|
||||
///
|
||||
/// Throws an exception if the scheme is not `mxc` or the homeserver is not
|
||||
/// set.
|
||||
///
|
||||
/// Important! To use this link you have to set a http header like this:
|
||||
/// `headers: {"authorization": "Bearer ${client.accessToken}"}`
|
||||
Future<Uri> getThumbnailUri(Client client,
|
||||
{num? width,
|
||||
num? height,
|
||||
ThumbnailMethod? method = ThumbnailMethod.crop,
|
||||
bool? animated = false}) async {
|
||||
if (!isScheme('mxc')) return Uri();
|
||||
final homeserver = client.homeserver;
|
||||
if (homeserver == null) {
|
||||
return Uri();
|
||||
}
|
||||
|
||||
String requestPath;
|
||||
if (await client.authenticatedMediaSupported()) {
|
||||
requestPath =
|
||||
'/_matrix/client/v1/media/thumbnail/$host${hasPort ? ':$port' : ''}$path';
|
||||
} else {
|
||||
requestPath =
|
||||
'/_matrix/media/v3/thumbnail/$host${hasPort ? ':$port' : ''}$path';
|
||||
}
|
||||
|
||||
return Uri(
|
||||
scheme: homeserver.scheme,
|
||||
host: homeserver.host,
|
||||
path: requestPath,
|
||||
port: homeserver.port,
|
||||
queryParameters: {
|
||||
if (width != null) 'width': width.round().toString(),
|
||||
if (height != null) 'height': height.round().toString(),
|
||||
if (method != null) 'method': method.toString().split('.').last,
|
||||
if (animated != null) 'animated': animated.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@Deprecated('Use `getDownloadUri()` instead')
|
||||
Uri getDownloadLink(Client matrix) => isScheme('mxc')
|
||||
? matrix.homeserver != null
|
||||
? matrix.homeserver?.resolve(
|
||||
|
|
@ -35,6 +107,7 @@ extension MxcUriExtension on Uri {
|
|||
/// `ThumbnailMethod.scale` and defaults to `ThumbnailMethod.scale`.
|
||||
/// If `animated` (default false) is set to true, an animated thumbnail is requested
|
||||
/// as per MSC2705. Thumbnails only animate if the media repository supports that.
|
||||
@Deprecated('Use `getThumbnailUri()` instead')
|
||||
Uri getThumbnail(Client matrix,
|
||||
{num? width,
|
||||
num? height,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import 'package:matrix/matrix_api_lite/utils/logs.dart';
|
||||
|
||||
bool isVersionGreaterThanOrEqualTo(String version, String target) {
|
||||
try {
|
||||
final versionParts =
|
||||
version.substring(1).split('.').map(int.parse).toList();
|
||||
final targetParts = target.substring(1).split('.').map(int.parse).toList();
|
||||
|
||||
for (int i = 0; i < versionParts.length; i++) {
|
||||
if (i >= targetParts.length) return true; // reached the end, both equal
|
||||
if (versionParts[i] > targetParts[i]) return true; // ver greater
|
||||
if (versionParts[i] < targetParts[i]) return false; // tar greater
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e, s) {
|
||||
Logs().e(
|
||||
'[_isVersionGreaterThanOrEqualTo] Failed to parse version $version',
|
||||
e,
|
||||
s,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1310,8 +1310,8 @@ void main() {
|
|||
final THUMBNAIL_BUFF = Uint8List.fromList([2]);
|
||||
Future<Uint8List> downloadCallback(Uri uri) async {
|
||||
return {
|
||||
'/_matrix/media/v3/download/example.org/file': FILE_BUFF,
|
||||
'/_matrix/media/v3/download/example.org/thumb': THUMBNAIL_BUFF,
|
||||
'/_matrix/client/v1/media/download/example.org/file': FILE_BUFF,
|
||||
'/_matrix/client/v1/media/download/example.org/thumb': THUMBNAIL_BUFF,
|
||||
}[uri.path]!;
|
||||
}
|
||||
|
||||
|
|
@ -1366,22 +1366,23 @@ void main() {
|
|||
'mxc://example.org/file');
|
||||
expect(event.attachmentOrThumbnailMxcUrl(getThumbnail: true).toString(),
|
||||
'mxc://example.org/thumb');
|
||||
expect(event.getAttachmentUrl().toString(),
|
||||
'https://fakeserver.notexisting/_matrix/media/v3/download/example.org/file');
|
||||
expect(event.getAttachmentUrl(getThumbnail: true).toString(),
|
||||
'https://fakeserver.notexisting/_matrix/media/v3/thumbnail/example.org/file?width=800&height=800&method=scale&animated=false');
|
||||
expect(event.getAttachmentUrl(useThumbnailMxcUrl: true).toString(),
|
||||
'https://fakeserver.notexisting/_matrix/media/v3/download/example.org/thumb');
|
||||
expect((await event.getAttachmentUri()).toString(),
|
||||
'https://fakeserver.notexisting/_matrix/client/v1/media/download/example.org/file');
|
||||
expect((await event.getAttachmentUri(getThumbnail: true)).toString(),
|
||||
'https://fakeserver.notexisting/_matrix/client/v1/media/thumbnail/example.org/file?width=800&height=800&method=scale&animated=false');
|
||||
expect(
|
||||
event
|
||||
.getAttachmentUrl(getThumbnail: true, useThumbnailMxcUrl: true)
|
||||
.toString(),
|
||||
'https://fakeserver.notexisting/_matrix/media/v3/thumbnail/example.org/thumb?width=800&height=800&method=scale&animated=false');
|
||||
(await event.getAttachmentUri(useThumbnailMxcUrl: true)).toString(),
|
||||
'https://fakeserver.notexisting/_matrix/client/v1/media/download/example.org/thumb');
|
||||
expect(
|
||||
event
|
||||
.getAttachmentUrl(getThumbnail: true, minNoThumbSize: 9000000)
|
||||
(await event.getAttachmentUri(
|
||||
getThumbnail: true, useThumbnailMxcUrl: true))
|
||||
.toString(),
|
||||
'https://fakeserver.notexisting/_matrix/media/v3/download/example.org/file');
|
||||
'https://fakeserver.notexisting/_matrix/client/v1/media/thumbnail/example.org/thumb?width=800&height=800&method=scale&animated=false');
|
||||
expect(
|
||||
(await event.getAttachmentUri(
|
||||
getThumbnail: true, minNoThumbSize: 9000000))
|
||||
.toString(),
|
||||
'https://fakeserver.notexisting/_matrix/client/v1/media/download/example.org/file');
|
||||
|
||||
buffer = await event.downloadAndDecryptAttachment(
|
||||
downloadCallback: downloadCallback);
|
||||
|
|
@ -1404,8 +1405,9 @@ void main() {
|
|||
Uint8List.fromList([0x74, 0x68, 0x75, 0x6D, 0x62, 0x0A]);
|
||||
Future<Uint8List> downloadCallback(Uri uri) async {
|
||||
return {
|
||||
'/_matrix/media/v3/download/example.com/file': FILE_BUFF_ENC,
|
||||
'/_matrix/media/v3/download/example.com/thumb': THUMB_BUFF_ENC,
|
||||
'/_matrix/client/v1/media/download/example.com/file': FILE_BUFF_ENC,
|
||||
'/_matrix/client/v1/media/download/example.com/thumb':
|
||||
THUMB_BUFF_ENC,
|
||||
}[uri.path]!;
|
||||
}
|
||||
|
||||
|
|
@ -1508,7 +1510,7 @@ void main() {
|
|||
Future<Uint8List> downloadCallback(Uri uri) async {
|
||||
serverHits++;
|
||||
return {
|
||||
'/_matrix/media/v3/download/example.org/newfile': FILE_BUFF,
|
||||
'/_matrix/client/v1/media/download/example.org/newfile': FILE_BUFF,
|
||||
}[uri.path]!;
|
||||
}
|
||||
|
||||
|
|
@ -1550,7 +1552,7 @@ void main() {
|
|||
Future<Uint8List> downloadCallback(Uri uri) async {
|
||||
serverHits++;
|
||||
return {
|
||||
'/_matrix/media/v3/download/example.org/newfile': FILE_BUFF,
|
||||
'/_matrix/client/v1/media/download/example.org/newfile': FILE_BUFF,
|
||||
}[uri.path]!;
|
||||
}
|
||||
|
||||
|
|
@ -1599,7 +1601,7 @@ void main() {
|
|||
Future<Uint8List> downloadCallback(Uri uri) async {
|
||||
serverHits++;
|
||||
return {
|
||||
'/_matrix/media/v3/download/example.org/newfile': FILE_BUFF,
|
||||
'/_matrix/client/v1/media/download/example.org/newfile': FILE_BUFF,
|
||||
}[uri.path]!;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,19 +32,20 @@ void main() {
|
|||
final content = Uri.parse(mxc);
|
||||
expect(content.isScheme('mxc'), true);
|
||||
|
||||
expect(content.getDownloadLink(client).toString(),
|
||||
'${client.homeserver.toString()}/_matrix/media/v3/download/exampleserver.abc/abcdefghijklmn');
|
||||
expect(content.getThumbnail(client, width: 50, height: 50).toString(),
|
||||
'${client.homeserver.toString()}/_matrix/media/v3/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=crop&animated=false');
|
||||
expect((await content.getDownloadUri(client)).toString(),
|
||||
'${client.homeserver.toString()}/_matrix/client/v1/media/download/exampleserver.abc/abcdefghijklmn');
|
||||
expect(
|
||||
content
|
||||
.getThumbnail(client,
|
||||
(await content.getThumbnailUri(client, width: 50, height: 50))
|
||||
.toString(),
|
||||
'${client.homeserver.toString()}/_matrix/client/v1/media/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=crop&animated=false');
|
||||
expect(
|
||||
(await content.getThumbnailUri(client,
|
||||
width: 50,
|
||||
height: 50,
|
||||
method: ThumbnailMethod.scale,
|
||||
animated: true)
|
||||
animated: true))
|
||||
.toString(),
|
||||
'${client.homeserver.toString()}/_matrix/media/v3/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=scale&animated=true');
|
||||
'${client.homeserver.toString()}/_matrix/client/v1/media/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=scale&animated=true');
|
||||
});
|
||||
test('other port', () async {
|
||||
final client = Client('testclient', httpClient: FakeMatrixApi());
|
||||
|
|
@ -55,19 +56,20 @@ void main() {
|
|||
final content = Uri.parse(mxc);
|
||||
expect(content.isScheme('mxc'), true);
|
||||
|
||||
expect(content.getDownloadLink(client).toString(),
|
||||
'${client.homeserver.toString()}/_matrix/media/v3/download/exampleserver.abc/abcdefghijklmn');
|
||||
expect(content.getThumbnail(client, width: 50, height: 50).toString(),
|
||||
'${client.homeserver.toString()}/_matrix/media/v3/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=crop&animated=false');
|
||||
expect((await content.getDownloadUri(client)).toString(),
|
||||
'${client.homeserver.toString()}/_matrix/client/v1/media/download/exampleserver.abc/abcdefghijklmn');
|
||||
expect(
|
||||
content
|
||||
.getThumbnail(client,
|
||||
(await content.getThumbnailUri(client, width: 50, height: 50))
|
||||
.toString(),
|
||||
'${client.homeserver.toString()}/_matrix/client/v1/media/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=crop&animated=false');
|
||||
expect(
|
||||
(await content.getThumbnailUri(client,
|
||||
width: 50,
|
||||
height: 50,
|
||||
method: ThumbnailMethod.scale,
|
||||
animated: true)
|
||||
animated: true))
|
||||
.toString(),
|
||||
'https://fakeserver.notexisting:1337/_matrix/media/v3/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=scale&animated=true');
|
||||
'https://fakeserver.notexisting:1337/_matrix/client/v1/media/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=scale&animated=true');
|
||||
});
|
||||
test('other remote port', () async {
|
||||
final client = Client('testclient', httpClient: FakeMatrixApi());
|
||||
|
|
@ -77,18 +79,39 @@ void main() {
|
|||
final content = Uri.parse(mxc);
|
||||
expect(content.isScheme('mxc'), true);
|
||||
|
||||
expect(content.getDownloadLink(client).toString(),
|
||||
'${client.homeserver.toString()}/_matrix/media/v3/download/exampleserver.abc:1234/abcdefghijklmn');
|
||||
expect(content.getThumbnail(client, width: 50, height: 50).toString(),
|
||||
'${client.homeserver.toString()}/_matrix/media/v3/thumbnail/exampleserver.abc:1234/abcdefghijklmn?width=50&height=50&method=crop&animated=false');
|
||||
expect((await content.getDownloadUri(client)).toString(),
|
||||
'${client.homeserver.toString()}/_matrix/client/v1/media/download/exampleserver.abc:1234/abcdefghijklmn');
|
||||
expect(
|
||||
(await content.getThumbnailUri(client, width: 50, height: 50))
|
||||
.toString(),
|
||||
'${client.homeserver.toString()}/_matrix/client/v1/media/thumbnail/exampleserver.abc:1234/abcdefghijklmn?width=50&height=50&method=crop&animated=false');
|
||||
});
|
||||
test('Wrong scheme returns empty object', () async {
|
||||
test('Wrong scheme throw exception', () async {
|
||||
final client = Client('testclient', httpClient: FakeMatrixApi());
|
||||
await client.checkHomeserver(Uri.parse('https://fakeserver.notexisting'),
|
||||
checkWellKnown: false);
|
||||
final mxc = Uri.parse('https://wrong-scheme.com');
|
||||
expect(mxc.getDownloadLink(client).toString(), '');
|
||||
expect(mxc.getThumbnail(client).toString(), '');
|
||||
expect((await mxc.getDownloadUri(client)).toString(), '');
|
||||
expect((await mxc.getThumbnailUri(client)).toString(), '');
|
||||
});
|
||||
|
||||
test('auth media fallback', () async {
|
||||
final client = Client('testclient', httpClient: FakeMatrixApi());
|
||||
await client.checkHomeserver(
|
||||
Uri.parse('https://fakeserverpriortoauthmedia.notexisting'),
|
||||
checkWellKnown: false);
|
||||
|
||||
expect(await client.authenticatedMediaSupported(), false);
|
||||
final mxc = 'mxc://exampleserver.abc:1234/abcdefghijklmn';
|
||||
final content = Uri.parse(mxc);
|
||||
expect(content.isScheme('mxc'), true);
|
||||
|
||||
expect((await content.getDownloadUri(client)).toString(),
|
||||
'${client.homeserver.toString()}/_matrix/media/v3/download/exampleserver.abc:1234/abcdefghijklmn');
|
||||
expect(
|
||||
(await content.getThumbnailUri(client, width: 50, height: 50))
|
||||
.toString(),
|
||||
'${client.homeserver.toString()}/_matrix/media/v3/thumbnail/exampleserver.abc:1234/abcdefghijklmn?width=50&height=50&method=crop&animated=false');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue