feat: support for authenticated media

This commit is contained in:
td 2024-08-16 14:35:50 +05:30
parent 987dd750b3
commit f79096dfbb
No known key found for this signature in database
GPG Key ID: 62A30523D4D6CE28
7 changed files with 564 additions and 135 deletions

View File

@ -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) => {

View File

@ -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.

View File

@ -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;

View File

@ -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,

View File

@ -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;
}
}

View File

@ -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]!;
}

View File

@ -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');
});
});
}