diff --git a/lib/fake_matrix_api.dart b/lib/fake_matrix_api.dart index a1df999e..39291992 100644 --- a/lib/fake_matrix_api.dart +++ b/lib/fake_matrix_api.dart @@ -91,6 +91,11 @@ class FakeMatrixApi extends BaseClient { return completer.future; } + Set servers = { + 'https://fakeserver.notexisting', + 'https://fakeserverpriortoauthmedia.notexisting' + }; + FutureOr 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 && 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( 'Not found...', 404); } @@ -150,88 +154,105 @@ class FakeMatrixApi extends BaseClient { // Call API (_calledEndpoints[action] ??= []).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 { + } else { + 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); - }); - } 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 { + } + 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 { - await _client?.handleSync(syncUpdate); + res = { + 'errcode': 'M_UNRECOGNIZED', + 'error': 'Unrecognized request: $action' + }; + statusCode = 405; } - 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) => { diff --git a/lib/src/client.dart b/lib/src/client.dart index 960bd832..3e9065a4 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -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 authenticatedMediaSupported() async { + _versionsCache ??= await getVersions(); + return _versionsCache?.versions.any( + (v) => isVersionGreaterThanOrEqualTo(v, 'v1.11'), + ) ?? + false; + } + final _serverConfigCache = AsyncCache(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 getConfig() => - _serverConfigCache.fetch(() => super.getConfig()); + _serverConfigCache.fetch(() => _getAuthenticatedConfig()); + + // TODO: remove once we are able to autogen this + Future _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); + } + + /// + /// + /// [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 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 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 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); + } + + /// 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 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. diff --git a/lib/src/event.dart b/lib/src/event.dart index 79695014..f086a1e9 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -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 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; diff --git a/lib/src/utils/uri_extension.dart b/lib/src/utils/uri_extension.dart index 77289763..7ac92c3e 100644 --- a/lib/src/utils/uri_extension.dart +++ b/lib/src/utils/uri_extension.dart @@ -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 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 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, diff --git a/lib/src/utils/versions_comparator.dart b/lib/src/utils/versions_comparator.dart new file mode 100644 index 00000000..8e1928bd --- /dev/null +++ b/lib/src/utils/versions_comparator.dart @@ -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; + } +} diff --git a/test/event_test.dart b/test/event_test.dart index ea139fa7..b9439503 100644 --- a/test/event_test.dart +++ b/test/event_test.dart @@ -1310,8 +1310,8 @@ void main() { final THUMBNAIL_BUFF = Uint8List.fromList([2]); Future 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) + (await event.getAttachmentUri(useThumbnailMxcUrl: true)).toString(), + 'https://fakeserver.notexisting/_matrix/client/v1/media/download/example.org/thumb'); + expect( + (await event.getAttachmentUri( + getThumbnail: true, useThumbnailMxcUrl: true)) .toString(), - 'https://fakeserver.notexisting/_matrix/media/v3/thumbnail/example.org/thumb?width=800&height=800&method=scale&animated=false'); + 'https://fakeserver.notexisting/_matrix/client/v1/media/thumbnail/example.org/thumb?width=800&height=800&method=scale&animated=false'); expect( - event - .getAttachmentUrl(getThumbnail: true, minNoThumbSize: 9000000) + (await event.getAttachmentUri( + getThumbnail: true, minNoThumbSize: 9000000)) .toString(), - 'https://fakeserver.notexisting/_matrix/media/v3/download/example.org/file'); + '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 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 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 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 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]!; } diff --git a/test/mxc_uri_extension_test.dart b/test/mxc_uri_extension_test.dart index 3e6480d0..39e05831 100644 --- a/test/mxc_uri_extension_test.dart +++ b/test/mxc_uri_extension_test.dart @@ -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( + (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( - content - .getThumbnail(client, + (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'); }); }); }