Merge branch 'main' of https://github.com/famedly/matrix-dart-sdk
This commit is contained in:
commit
58c4cf19d0
|
|
@ -1,2 +1,2 @@
|
|||
flutter_version=3.27.4
|
||||
dart_version=3.6.2
|
||||
flutter_version=3.35.4
|
||||
dart_version=3.9.2
|
||||
|
|
@ -6,16 +6,13 @@ Matrix (matrix.org) SDK written in dart.
|
|||
|
||||
For E2EE, vodozemac must be provided.
|
||||
|
||||
Additionally, OpenSSL (libcrypto) must be provided on native platforms for E2EE.
|
||||
|
||||
For flutter apps you can easily import it with the [flutter_vodozemac](https://pub.dev/packages/flutter_vodozemac) and the [flutter_openssl_crypto](https://pub.dev/packages/flutter_openssl_crypto) packages.
|
||||
For flutter apps you can easily import it with the [flutter_vodozemac](https://pub.dev/packages/flutter_vodozemac) package.
|
||||
|
||||
```sh
|
||||
flutter pub add matrix
|
||||
|
||||
# Optional: For end to end encryption:
|
||||
flutter pub add flutter_vodozemac
|
||||
flutter pub add flutter_openssl_crypto
|
||||
```
|
||||
|
||||
## Get started
|
||||
|
|
|
|||
|
|
@ -6,12 +6,6 @@ For Flutter you can use [flutter_vodozemac](https://pub.dev/packages/flutter_vod
|
|||
flutter pub add flutter_vodozemac
|
||||
```
|
||||
|
||||
You also need [flutter_openssl_crypto](https://pub.dev/packages/flutter_openssl_crypto).
|
||||
|
||||
```sh
|
||||
flutter pub add flutter_openssl_crypto
|
||||
```
|
||||
|
||||
Now before you create your `Client`, init vodozemac:
|
||||
|
||||
```dart
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ In your `pubspec.yaml` file add the following dependencies:
|
|||
# (Optional) For end to end encryption, please head on the
|
||||
# encryption guide and add these dependencies:
|
||||
flutter_vodozemac: <latest-version>
|
||||
flutter_openssl_crypto: <latest-version>
|
||||
```
|
||||
|
||||
## Step 2: Create the client
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import 'dart:typed_data';
|
|||
|
||||
import 'package:base58check/base58.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:vodozemac/vodozemac.dart';
|
||||
|
||||
import 'package:matrix/encryption/encryption.dart';
|
||||
import 'package:matrix/encryption/utils/base64_unpadded.dart';
|
||||
|
|
@ -74,16 +74,18 @@ class SSSS {
|
|||
|
||||
static DerivedKeys deriveKeys(Uint8List key, String name) {
|
||||
final zerosalt = Uint8List(8);
|
||||
final prk = Hmac(sha256, zerosalt).convert(key);
|
||||
final prk = CryptoUtils.hmac(key: zerosalt, input: key);
|
||||
final b = Uint8List(1);
|
||||
b[0] = 1;
|
||||
final aesKey = Hmac(sha256, prk.bytes).convert(utf8.encode(name) + b);
|
||||
final aesKey = CryptoUtils.hmac(key: prk, input: utf8.encode(name) + b);
|
||||
b[0] = 2;
|
||||
final hmacKey =
|
||||
Hmac(sha256, prk.bytes).convert(aesKey.bytes + utf8.encode(name) + b);
|
||||
final hmacKey = CryptoUtils.hmac(
|
||||
key: prk,
|
||||
input: aesKey + utf8.encode(name) + b,
|
||||
);
|
||||
return DerivedKeys(
|
||||
aesKey: Uint8List.fromList(aesKey.bytes),
|
||||
hmacKey: Uint8List.fromList(hmacKey.bytes),
|
||||
aesKey: Uint8List.fromList(aesKey),
|
||||
hmacKey: Uint8List.fromList(hmacKey),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -105,14 +107,15 @@ class SSSS {
|
|||
final keys = deriveKeys(key, name);
|
||||
|
||||
final plain = Uint8List.fromList(utf8.encode(data));
|
||||
final ciphertext = await uc.aesCtr.encrypt(plain, keys.aesKey, iv);
|
||||
final ciphertext =
|
||||
CryptoUtils.aesCtr(input: plain, key: keys.aesKey, iv: iv);
|
||||
|
||||
final hmac = Hmac(sha256, keys.hmacKey).convert(ciphertext);
|
||||
final hmac = CryptoUtils.hmac(key: keys.hmacKey, input: ciphertext);
|
||||
|
||||
return EncryptedContent(
|
||||
iv: base64.encode(iv),
|
||||
ciphertext: base64.encode(ciphertext),
|
||||
mac: base64.encode(hmac.bytes),
|
||||
mac: base64.encode(hmac),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -124,13 +127,16 @@ class SSSS {
|
|||
final keys = deriveKeys(key, name);
|
||||
final cipher = base64decodeUnpadded(data.ciphertext);
|
||||
final hmac = base64
|
||||
.encode(Hmac(sha256, keys.hmacKey).convert(cipher).bytes)
|
||||
.encode(CryptoUtils.hmac(key: keys.hmacKey, input: cipher))
|
||||
.replaceAll(RegExp(r'=+$'), '');
|
||||
if (hmac != data.mac.replaceAll(RegExp(r'=+$'), '')) {
|
||||
throw Exception('Bad MAC');
|
||||
}
|
||||
final decipher = await uc.aesCtr
|
||||
.encrypt(cipher, keys.aesKey, base64decodeUnpadded(data.iv));
|
||||
final decipher = CryptoUtils.aesCtr(
|
||||
input: cipher,
|
||||
key: keys.aesKey,
|
||||
iv: base64decodeUnpadded(data.iv),
|
||||
);
|
||||
return String.fromCharCodes(decipher);
|
||||
}
|
||||
|
||||
|
|
@ -184,12 +190,10 @@ class SSSS {
|
|||
if (info.salt == null) {
|
||||
throw InvalidPassphraseException('Passphrase info without salt');
|
||||
}
|
||||
return await uc.pbkdf2(
|
||||
Uint8List.fromList(utf8.encode(passphrase)),
|
||||
Uint8List.fromList(utf8.encode(info.salt!)),
|
||||
uc.sha512,
|
||||
info.iterations!,
|
||||
info.bits ?? 256,
|
||||
return CryptoUtils.pbkdf2(
|
||||
passphrase: Uint8List.fromList(utf8.encode(passphrase)),
|
||||
salt: Uint8List.fromList(utf8.encode(info.salt!)),
|
||||
iterations: info.iterations!,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -742,7 +746,7 @@ class OpenSSSS {
|
|||
info: keyData.passphrase!,
|
||||
),
|
||||
),
|
||||
).timeout(Duration(seconds: 10));
|
||||
).timeout(Duration(minutes: 2));
|
||||
} else if (recoveryKey != null) {
|
||||
privateKey = SSSS.decodeRecoveryKey(recoveryKey);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import 'dart:convert';
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:canonical_json/canonical_json.dart';
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
import 'package:typed_data/typed_data.dart';
|
||||
import 'package:vodozemac/vodozemac.dart' as vod;
|
||||
|
||||
|
|
@ -748,9 +747,10 @@ class KeyVerification {
|
|||
// no need to request cache, we already have it
|
||||
return;
|
||||
}
|
||||
// ignore: unawaited_futures
|
||||
encryption.ssss
|
||||
.maybeRequestAll(_verifiedDevices.whereType<DeviceKeys>().toList());
|
||||
unawaited(
|
||||
encryption.ssss
|
||||
.maybeRequestAll(_verifiedDevices.whereType<DeviceKeys>().toList()),
|
||||
);
|
||||
if (requestInterval.length <= i) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1558,8 +1558,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
Future<String> _makeCommitment(String pubKey, String canonicalJson) async {
|
||||
if (hash == 'sha256') {
|
||||
final bytes = utf8.encoder.convert(pubKey + canonicalJson);
|
||||
final digest = crypto.sha256.convert(bytes);
|
||||
return encodeBase64Unpadded(digest.bytes);
|
||||
final digest = vod.CryptoUtils.sha256(input: bytes);
|
||||
return encodeBase64Unpadded(digest);
|
||||
}
|
||||
throw Exception('Unknown hash method');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1740,6 +1740,28 @@ class FakeMatrixApi extends BaseClient {
|
|||
'origin_server_ts': 1432735824653,
|
||||
'unsigned': {'age': 1234},
|
||||
},
|
||||
'/client/v3/rooms/!localpart%3Aserver.abc/messages?dir=b&limit=1&filter=%7B%22types%22%3A%5B%22m.room.message%22%2C%22m.room.encrypted%22%2C%22m.sticker%22%2C%22m.call.invite%22%2C%22m.call.answer%22%2C%22m.call.reject%22%2C%22m.call.hangup%22%2C%22com.famedly.call.member%22%5D%7D':
|
||||
(var req) => {
|
||||
'start': 't47429-4392820_219380_26003_2265',
|
||||
'end': 't47409-4357353_219380_26003_2265',
|
||||
'chunk': [
|
||||
{
|
||||
'content': {
|
||||
'body': 'This is an example text message',
|
||||
'msgtype': 'm.text',
|
||||
'format': 'org.matrix.custom.html',
|
||||
'formatted_body':
|
||||
'<b>This is an example text message</b>',
|
||||
},
|
||||
'type': 'm.room.message',
|
||||
'event_id': '3143273582443PhrSn:example.org',
|
||||
'room_id': '!1234:example.com',
|
||||
'sender': '@example:example.org',
|
||||
'origin_server_ts': 1432735824653,
|
||||
'unsigned': {'age': 1234},
|
||||
},
|
||||
],
|
||||
},
|
||||
'/client/v3/rooms/new_room_id/messages?from=emptyHistoryResponse&dir=b&limit=30&filter=%7B%22lazy_load_members%22%3Atrue%7D':
|
||||
(var req) => emptyHistoryResponse,
|
||||
'/client/v3/rooms/new_room_id/messages?from=1&dir=b&limit=30&filter=%7B%22lazy_load_members%22%3Atrue%7D':
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export 'msc_extensions/extension_timeline_export/timeline_export.dart';
|
|||
export 'msc_extensions/msc_4140_delayed_events/api.dart';
|
||||
|
||||
export 'src/utils/web_worker/web_worker_stub.dart'
|
||||
if (dart.library.html) 'src/utils/web_worker/web_worker.dart';
|
||||
if (dart.library.js_interop) 'src/utils/web_worker/web_worker.dart';
|
||||
|
||||
export 'src/utils/web_worker/native_implementations_web_worker_stub.dart'
|
||||
if (dart.library.html) 'src/utils/web_worker/native_implementations_web_worker.dart';
|
||||
if (dart.library.js_interop) 'src/utils/web_worker/native_implementations_web_worker.dart';
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ class Api {
|
|||
/// suitably namespaced for each application and reduces the risk of
|
||||
/// clashes.
|
||||
///
|
||||
/// **NOTE:**
|
||||
/// This endpoint should be accessed with the hostname of the homeserver's
|
||||
/// [server name](https://spec.matrix.org/unstable/appendices/#server-name) by making a
|
||||
/// GET request to `https://hostname/.well-known/matrix/client`.
|
||||
///
|
||||
///
|
||||
/// Note that this endpoint is not necessarily handled by the homeserver,
|
||||
/// but by another webserver, to be used for discovering the homeserver URL.
|
||||
Future<DiscoveryInformation> getWellknown() async {
|
||||
|
|
@ -49,10 +55,13 @@ class Api {
|
|||
|
||||
/// Gets server admin contact and support page of the domain.
|
||||
///
|
||||
/// Like the [well-known discovery URI](https://spec.matrix.org/unstable/client-server-api/#well-known-uri),
|
||||
/// this should be accessed with the hostname of the homeserver by making a
|
||||
/// **NOTE:**
|
||||
/// Like the [well-known discovery URI](https://spec.matrix.org/unstable/client-server-api/#well-known-uris),
|
||||
/// this endpoint should be accessed with the hostname of the homeserver's
|
||||
/// [server name](https://spec.matrix.org/unstable/appendices/#server-name) by making a
|
||||
/// GET request to `https://hostname/.well-known/matrix/support`.
|
||||
///
|
||||
///
|
||||
/// Note that this endpoint is not necessarily handled by the homeserver.
|
||||
/// It may be served by another webserver, used for discovering support
|
||||
/// information for the homeserver.
|
||||
|
|
@ -112,6 +121,36 @@ class Api {
|
|||
return json['duration_ms'] as int;
|
||||
}
|
||||
|
||||
/// Gets the OAuth 2.0 authorization server metadata, as defined in
|
||||
/// [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414), including the
|
||||
/// endpoint URLs and the supported parameters that can be used by the
|
||||
/// clients.
|
||||
///
|
||||
/// This endpoint definition includes only the fields that are meaningful in
|
||||
/// the context of the Matrix specification. The full list of possible
|
||||
/// fields is available in the [OAuth Authorization Server Metadata
|
||||
/// registry](https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#authorization-server-metadata),
|
||||
/// and normative definitions of them are available in their respective
|
||||
/// RFCs.
|
||||
///
|
||||
/// **NOTE:**
|
||||
/// The authorization server metadata is relatively large and may change
|
||||
/// over time. Clients should:
|
||||
///
|
||||
/// - Cache the metadata appropriately based on HTTP caching headers
|
||||
/// - Refetch the metadata if it is stale
|
||||
///
|
||||
Future<GetAuthMetadataResponse> getAuthMetadata() async {
|
||||
final requestUri = Uri(path: '_matrix/client/v1/auth_metadata');
|
||||
final request = Request('GET', baseUri!.resolveUri(requestUri));
|
||||
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 GetAuthMetadataResponse.fromJson(json as Map<String, Object?>);
|
||||
}
|
||||
|
||||
/// Optional endpoint - the server is not required to implement this endpoint if it does not
|
||||
/// intend to use or support this functionality.
|
||||
///
|
||||
|
|
@ -169,12 +208,12 @@ class Api {
|
|||
/// All values are intentionally left optional. Clients SHOULD follow
|
||||
/// the advice given in the field description when the field is not available.
|
||||
///
|
||||
/// {{% boxes/note %}}
|
||||
/// **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.
|
||||
/// {{% /boxes/note %}}
|
||||
///
|
||||
Future<MediaConfig> getConfigAuthed() async {
|
||||
final requestUri = Uri(path: '_matrix/client/v1/media/config');
|
||||
final request = Request('GET', baseUri!.resolveUri(requestUri));
|
||||
|
|
@ -187,11 +226,11 @@ class Api {
|
|||
return MediaConfig.fromJson(json as Map<String, Object?>);
|
||||
}
|
||||
|
||||
/// {{% boxes/note %}}
|
||||
/// **NOTE:**
|
||||
/// Clients SHOULD NOT generate or use URLs which supply the access token in
|
||||
/// the query string. These URLs may be copied by users verbatim and provided
|
||||
/// in a chat message to another user, disclosing the sender's access token.
|
||||
/// {{% /boxes/note %}}
|
||||
///
|
||||
///
|
||||
/// Clients MAY be redirected using the 307/308 responses below to download
|
||||
/// the request object. This is typical when the homeserver uses a Content
|
||||
|
|
@ -236,11 +275,11 @@ class Api {
|
|||
/// the previous endpoint) but replaces the target file name with the one
|
||||
/// provided by the caller.
|
||||
///
|
||||
/// {{% boxes/note %}}
|
||||
/// **NOTE:**
|
||||
/// Clients SHOULD NOT generate or use URLs which supply the access token in
|
||||
/// the query string. These URLs may be copied by users verbatim and provided
|
||||
/// in a chat message to another user, disclosing the sender's access token.
|
||||
/// {{% /boxes/note %}}
|
||||
///
|
||||
///
|
||||
/// Clients MAY be redirected using the 307/308 responses below to download
|
||||
/// the request object. This is typical when the homeserver uses a Content
|
||||
|
|
@ -287,12 +326,12 @@ class Api {
|
|||
/// 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.
|
||||
///
|
||||
/// {{% boxes/note %}}
|
||||
/// **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.
|
||||
/// {{% /boxes/note %}}
|
||||
///
|
||||
///
|
||||
/// [url] The URL to get a preview of.
|
||||
///
|
||||
|
|
@ -320,11 +359,11 @@ class Api {
|
|||
/// 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.
|
||||
///
|
||||
/// {{% boxes/note %}}
|
||||
/// **NOTE:**
|
||||
/// Clients SHOULD NOT generate or use URLs which supply the access token in
|
||||
/// the query string. These URLs may be copied by users verbatim and provided
|
||||
/// in a chat message to another user, disclosing the sender's access token.
|
||||
/// {{% /boxes/note %}}
|
||||
///
|
||||
///
|
||||
/// Clients MAY be redirected using the 307/308 responses below to download
|
||||
/// the request object. This is typical when the homeserver uses a Content
|
||||
|
|
@ -427,6 +466,48 @@ class Api {
|
|||
return json['valid'] as bool;
|
||||
}
|
||||
|
||||
/// Retrieves a summary for a room.
|
||||
///
|
||||
/// Clients should note that requests for rooms where the user's membership
|
||||
/// is `invite` or `knock` might yield outdated, partial or even no data
|
||||
/// since the server may not have access to the current state of the room.
|
||||
///
|
||||
/// Servers MAY allow unauthenticated access to this API if at least one of
|
||||
/// the following conditions holds true:
|
||||
///
|
||||
/// - The room has a [join rule](#mroomjoin_rules) of `public`, `knock` or
|
||||
/// `knock_restricted`.
|
||||
/// - The room has a `world_readable` [history visibility](#room-history-visibility).
|
||||
///
|
||||
/// Servers should consider rate limiting requests that require a federation
|
||||
/// request more heavily if the client is unauthenticated.
|
||||
///
|
||||
/// [roomIdOrAlias] The room identifier or alias to summarise.
|
||||
///
|
||||
/// [via] The servers to attempt to request the summary from when
|
||||
/// the local server cannot generate it (for instance, because
|
||||
/// it has no local user in the room).
|
||||
Future<GetRoomSummaryResponse$3> getRoomSummary(
|
||||
String roomIdOrAlias, {
|
||||
List<String>? via,
|
||||
}) async {
|
||||
final requestUri = Uri(
|
||||
path:
|
||||
'_matrix/client/v1/room_summary/${Uri.encodeComponent(roomIdOrAlias)}',
|
||||
queryParameters: {
|
||||
if (via != null) 'via': via,
|
||||
},
|
||||
);
|
||||
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 GetRoomSummaryResponse$3.fromJson(json as Map<String, Object?>);
|
||||
}
|
||||
|
||||
/// Paginates over the space tree in a depth-first manner to locate child rooms of a given space.
|
||||
///
|
||||
/// Where a child room is unknown to the local server, federation is used to fill in the details.
|
||||
|
|
@ -915,6 +996,11 @@ class Api {
|
|||
/// Homeservers should prevent the caller from adding a 3PID to their account if it has
|
||||
/// already been added to another user's account on the homeserver.
|
||||
///
|
||||
/// **WARNING:**
|
||||
/// Since this endpoint uses User-Interactive Authentication, it cannot be used when the access token was obtained
|
||||
/// via the [OAuth 2.0 API](https://spec.matrix.org/unstable/client-server-api/#oauth-20-api).
|
||||
///
|
||||
///
|
||||
/// [auth] Additional authentication information for the
|
||||
/// user-interactive authentication API.
|
||||
///
|
||||
|
|
@ -1588,9 +1674,23 @@ class Api {
|
|||
/// 2. An `m.room.member` event for the creator to join the room. This is
|
||||
/// needed so the remaining events can be sent.
|
||||
///
|
||||
/// 3. A default `m.room.power_levels` event, giving the room creator
|
||||
/// (and not other members) permission to send state events. Overridden
|
||||
/// by the `power_level_content_override` parameter.
|
||||
/// 3. A default `m.room.power_levels` event. Overridden by the
|
||||
/// `power_level_content_override` parameter.
|
||||
///
|
||||
/// In [room versions](https://spec.matrix.org/unstable/rooms) 1 through 11, the room creator (and not
|
||||
/// other members) will be given permission to send state events.
|
||||
///
|
||||
/// In room versions 12 and later, the room creator is given infinite
|
||||
/// power level and cannot be specified in the `users` field of
|
||||
/// `m.room.power_levels`, so is not listed explicitly.
|
||||
///
|
||||
/// **Note**: For `trusted_private_chat`, the users specified in the
|
||||
/// `invite` parameter SHOULD also be appended to `additional_creators`
|
||||
/// by the server, per the `creation_content` parameter.
|
||||
///
|
||||
/// If the room's version is 12 or higher, the power level for sending
|
||||
/// `m.room.tombstone` events MUST explicitly be higher than `state_default`.
|
||||
/// For example, set to 150 instead of 100.
|
||||
///
|
||||
/// 4. An `m.room.canonical_alias` event if `room_alias_name` is given.
|
||||
///
|
||||
|
|
@ -1616,13 +1716,20 @@ class Api {
|
|||
///
|
||||
/// The server will create a `m.room.create` event in the room with the
|
||||
/// requesting user as the creator, alongside other keys provided in the
|
||||
/// `creation_content`.
|
||||
/// `creation_content` or implied by behaviour of `creation_content`.
|
||||
///
|
||||
/// [creationContent] Extra keys, such as `m.federate`, to be added to the content
|
||||
/// of the [`m.room.create`](https://spec.matrix.org/unstable/client-server-api/#mroomcreate) event. The server will overwrite the following
|
||||
/// of the [`m.room.create`](https://spec.matrix.org/unstable/client-server-api/#mroomcreate) event.
|
||||
///
|
||||
/// The server will overwrite the following
|
||||
/// keys: `creator`, `room_version`. Future versions of the specification
|
||||
/// may allow the server to overwrite other keys.
|
||||
///
|
||||
/// When using the `trusted_private_chat` preset, the server SHOULD combine
|
||||
/// `additional_creators` specified here and the `invite` array into the
|
||||
/// eventual `m.room.create` event's `additional_creators`, deduplicating
|
||||
/// between the two parameters.
|
||||
///
|
||||
/// [initialState] A list of state events to set in the new room. This allows
|
||||
/// the user to override the default state events set in the new
|
||||
/// room. The expected format of the state events are an object
|
||||
|
|
@ -1641,9 +1748,10 @@ class Api {
|
|||
/// `m.room.member` events sent to the users in `invite` and
|
||||
/// `invite_3pid`. See [Direct Messaging](https://spec.matrix.org/unstable/client-server-api/#direct-messaging) for more information.
|
||||
///
|
||||
/// [name] If this is included, an `m.room.name` event will be sent
|
||||
/// into the room to indicate the name of the room. See Room
|
||||
/// Events for more information on `m.room.name`.
|
||||
/// [name] If this is included, an [`m.room.name`](https://spec.matrix.org/unstable/client-server-api/#mroomname) event
|
||||
/// will be sent into the room to indicate the name for the room.
|
||||
/// This overwrites any [`m.room.name`](https://spec.matrix.org/unstable/client-server-api/#mroomname)
|
||||
/// event in `initial_state`.
|
||||
///
|
||||
/// [powerLevelContentOverride] The power level content to override in the default power level
|
||||
/// event. This object is applied on top of the generated
|
||||
|
|
@ -1675,16 +1783,14 @@ class Api {
|
|||
/// 400 error with the errcode `M_UNSUPPORTED_ROOM_VERSION` if it does not
|
||||
/// support the room version.
|
||||
///
|
||||
/// [topic] If this is included, an `m.room.topic` event will be sent
|
||||
/// into the room to indicate the topic for the room. See Room
|
||||
/// Events for more information on `m.room.topic`.
|
||||
/// [topic] If this is included, an [`m.room.topic`](https://spec.matrix.org/unstable/client-server-api/#mroomtopic)
|
||||
/// event with a `text/plain` mimetype will be sent into the room
|
||||
/// to indicate the topic for the room. This overwrites any
|
||||
/// [`m.room.topic`](https://spec.matrix.org/unstable/client-server-api/#mroomtopic) event in `initial_state`.
|
||||
///
|
||||
/// [visibility] A `public` visibility indicates that the room will be shown
|
||||
/// in the published room list. A `private` visibility will hide
|
||||
/// the room from the published room list. Rooms default to
|
||||
/// `private` visibility if this key is not included. NB: This
|
||||
/// should not be confused with `join_rules` which also uses the
|
||||
/// word `public`.
|
||||
/// [visibility] The room's visibility in the server's
|
||||
/// [published room directory](https://spec.matrix.org/unstable/client-server-api#published-room-directory).
|
||||
/// Defaults to `private`.
|
||||
///
|
||||
/// returns `room_id`:
|
||||
/// The created room's ID.
|
||||
|
|
@ -1737,6 +1843,11 @@ class Api {
|
|||
///
|
||||
/// Deletes the given devices, and invalidates any access token associated with them.
|
||||
///
|
||||
/// **WARNING:**
|
||||
/// Since this endpoint uses User-Interactive Authentication, it cannot be used when the access token was obtained
|
||||
/// via the [OAuth 2.0 API](https://spec.matrix.org/unstable/client-server-api/#oauth-20-api).
|
||||
///
|
||||
///
|
||||
/// [auth] Additional authentication information for the
|
||||
/// user-interactive authentication API.
|
||||
///
|
||||
|
|
@ -1787,6 +1898,11 @@ class Api {
|
|||
///
|
||||
/// Deletes the given device, and invalidates any access token associated with it.
|
||||
///
|
||||
/// **WARNING:**
|
||||
/// Since this endpoint uses User-Interactive Authentication, it cannot be used when the access token was obtained
|
||||
/// via the [OAuth 2.0 API](https://spec.matrix.org/unstable/client-server-api/#oauth-20-api).
|
||||
///
|
||||
///
|
||||
/// [deviceId] The device to delete.
|
||||
///
|
||||
/// [auth] Additional authentication information for the
|
||||
|
|
@ -1851,11 +1967,12 @@ class Api {
|
|||
return ignore(json);
|
||||
}
|
||||
|
||||
/// Updates the visibility of a given room on the application service's room
|
||||
/// directory.
|
||||
/// Updates the visibility of a given room in the application service's
|
||||
/// published room directory.
|
||||
///
|
||||
/// This API is similar to the room directory visibility API used by clients
|
||||
/// to update the homeserver's more general room directory.
|
||||
/// This API is similar to the
|
||||
/// [visibility API](https://spec.matrix.org/unstable/client-server-api#put_matrixclientv3directorylistroomroomid)
|
||||
/// used by clients to update the homeserver's more general published room directory.
|
||||
///
|
||||
/// This API requires the use of an application service access token (`as_token`)
|
||||
/// instead of a typical client's access_token. This API cannot be invoked by
|
||||
|
|
@ -1894,7 +2011,8 @@ class Api {
|
|||
return json as Map<String, Object?>;
|
||||
}
|
||||
|
||||
/// Gets the visibility of a given room on the server's public room directory.
|
||||
/// Gets the visibility of a given room in the server's
|
||||
/// published room directory.
|
||||
///
|
||||
/// [roomId] The room ID.
|
||||
///
|
||||
|
|
@ -1916,17 +2034,16 @@ class Api {
|
|||
: null)(json['visibility']);
|
||||
}
|
||||
|
||||
/// Sets the visibility of a given room in the server's public room
|
||||
/// directory.
|
||||
/// Sets the visibility of a given room in the server's published room directory.
|
||||
///
|
||||
/// Servers may choose to implement additional access control checks
|
||||
/// here, for instance that room visibility can only be changed by
|
||||
/// the room creator or a server administrator.
|
||||
/// Servers MAY implement additional access control checks, for instance,
|
||||
/// to ensure that a room's visibility can only be changed by the room creator
|
||||
/// or a server administrator.
|
||||
///
|
||||
/// [roomId] The room ID.
|
||||
///
|
||||
/// [visibility] The new visibility setting for the room.
|
||||
/// Defaults to 'public'.
|
||||
/// Defaults to `public`.
|
||||
Future<void> setRoomVisibilityOnDirectory(
|
||||
String roomId, {
|
||||
Visibility? visibility,
|
||||
|
|
@ -2296,6 +2413,11 @@ class Api {
|
|||
/// makes this endpoint idempotent in the case where the response is lost over the network,
|
||||
/// which would otherwise cause a UIA challenge upon retry.
|
||||
///
|
||||
/// **WARNING:**
|
||||
/// When this endpoint requires User-Interactive Authentication, it cannot be used when the access token was obtained
|
||||
/// via the [OAuth 2.0 API](https://spec.matrix.org/unstable/client-server-api/#oauth-20-api).
|
||||
///
|
||||
///
|
||||
/// [auth] Additional authentication information for the
|
||||
/// user-interactive authentication API.
|
||||
///
|
||||
|
|
@ -2639,7 +2761,7 @@ class Api {
|
|||
/// deleted alongside the device.
|
||||
///
|
||||
/// This endpoint does not use the [User-Interactive Authentication API](https://spec.matrix.org/unstable/client-server-api/#user-interactive-authentication-api) because
|
||||
/// User-Interactive Authentication is designed to protect against attacks where the
|
||||
/// User-Interactive Authentication is designed to protect against attacks where
|
||||
/// someone gets hold of a single access token then takes over the account. This
|
||||
/// endpoint invalidates all access tokens for the user, including the token used in
|
||||
/// the request, and therefore the attacker is unable to take over the account in
|
||||
|
|
@ -2742,9 +2864,7 @@ class Api {
|
|||
return ignore(json);
|
||||
}
|
||||
|
||||
/// Get the combined profile information for this user. This API may be used
|
||||
/// to fetch the user's own profile information or other users; either
|
||||
/// locally or on remote homeservers.
|
||||
/// Get the complete profile for a user.
|
||||
///
|
||||
/// [userId] The user whose profile information to get.
|
||||
Future<ProfileInformation> getUserProfile(String userId) async {
|
||||
|
|
@ -2762,18 +2882,41 @@ class Api {
|
|||
return ProfileInformation.fromJson(json as Map<String, Object?>);
|
||||
}
|
||||
|
||||
/// Get the user's avatar URL. This API may be used to fetch the user's
|
||||
/// own avatar URL or to query the URL of other users; either locally or
|
||||
/// on remote homeservers.
|
||||
/// Remove a specific field from a user's profile.
|
||||
///
|
||||
/// [userId] The user whose avatar URL to get.
|
||||
/// [userId] The user whose profile field should be deleted.
|
||||
///
|
||||
/// returns `avatar_url`:
|
||||
/// The user's avatar URL if they have set one, otherwise not present.
|
||||
Future<Uri?> getAvatarUrl(String userId) async {
|
||||
/// [keyName] The name of the profile field to delete.
|
||||
Future<Map<String, Object?>> deleteProfileField(
|
||||
String userId,
|
||||
String keyName,
|
||||
) async {
|
||||
final requestUri = Uri(
|
||||
path:
|
||||
'_matrix/client/v3/profile/${Uri.encodeComponent(userId)}/avatar_url',
|
||||
'_matrix/client/v3/profile/${Uri.encodeComponent(userId)}/${Uri.encodeComponent(keyName)}',
|
||||
);
|
||||
final request = Request('DELETE', 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 json as Map<String, Object?>;
|
||||
}
|
||||
|
||||
/// Get the value of a profile field for a user.
|
||||
///
|
||||
/// [userId] The user whose profile field should be returned.
|
||||
///
|
||||
/// [keyName] The name of the profile field to retrieve.
|
||||
Future<Map<String, Object?>> getProfileField(
|
||||
String userId,
|
||||
String keyName,
|
||||
) async {
|
||||
final requestUri = Uri(
|
||||
path:
|
||||
'_matrix/client/v3/profile/${Uri.encodeComponent(userId)}/${Uri.encodeComponent(keyName)}',
|
||||
);
|
||||
final request = Request('GET', baseUri!.resolveUri(requestUri));
|
||||
if (bearerToken != null) {
|
||||
|
|
@ -2784,90 +2927,44 @@ class Api {
|
|||
if (response.statusCode != 200) unexpectedResponse(response, responseBody);
|
||||
final responseString = utf8.decode(responseBody);
|
||||
final json = jsonDecode(responseString);
|
||||
return ((v) =>
|
||||
v != null ? Uri.parse(v as String) : null)(json['avatar_url']);
|
||||
return json as Map<String, Object?>;
|
||||
}
|
||||
|
||||
/// This API sets the given user's avatar URL. You must have permission to
|
||||
/// set this user's avatar URL, e.g. you need to have their `access_token`.
|
||||
/// Set or update a profile field for a user. Must be authenticated with an
|
||||
/// access token authorised to make changes. Servers MAY impose size limits
|
||||
/// on individual fields, and the total profile MUST be under 64 KiB.
|
||||
///
|
||||
/// [userId] The user whose avatar URL to set.
|
||||
/// Servers MAY reject `null` values. Servers that accept `null` values SHOULD store
|
||||
/// them rather than treating `null` as a deletion request. Clients that want to delete a
|
||||
/// field, including its key and value, SHOULD use the `DELETE` endpoint instead.
|
||||
///
|
||||
/// [avatarUrl] The new avatar URL for this user.
|
||||
Future<void> setAvatarUrl(String userId, Uri? avatarUrl) async {
|
||||
/// [userId] The user whose profile field should be set.
|
||||
///
|
||||
/// [keyName] The name of the profile field to set. This MUST be either `avatar_url`, `displayname`, `m.tz`, or a custom field following the [Common Namespaced Identifier Grammar](https://spec.matrix.org/unstable/appendices/#common-namespaced-identifier-grammar).
|
||||
///
|
||||
/// [body] A JSON object containing the property whose name matches the `keyName` specified in the URL. See `additionalProperties` for further details.
|
||||
Future<Map<String, Object?>> setProfileField(
|
||||
String userId,
|
||||
String keyName,
|
||||
Map<String, Object?> body,
|
||||
) async {
|
||||
final requestUri = Uri(
|
||||
path:
|
||||
'_matrix/client/v3/profile/${Uri.encodeComponent(userId)}/avatar_url',
|
||||
'_matrix/client/v3/profile/${Uri.encodeComponent(userId)}/${Uri.encodeComponent(keyName)}',
|
||||
);
|
||||
final request = Request('PUT', baseUri!.resolveUri(requestUri));
|
||||
request.headers['authorization'] = 'Bearer ${bearerToken!}';
|
||||
request.headers['content-type'] = 'application/json';
|
||||
request.bodyBytes = utf8.encode(
|
||||
jsonEncode({
|
||||
if (avatarUrl != null) 'avatar_url': avatarUrl.toString(),
|
||||
}),
|
||||
);
|
||||
request.bodyBytes = utf8.encode(jsonEncode(body));
|
||||
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 ignore(json);
|
||||
return json as Map<String, Object?>;
|
||||
}
|
||||
|
||||
/// Get the user's display name. This API may be used to fetch the user's
|
||||
/// own displayname or to query the name of other users; either locally or
|
||||
/// on remote homeservers.
|
||||
///
|
||||
/// [userId] The user whose display name to get.
|
||||
///
|
||||
/// returns `displayname`:
|
||||
/// The user's display name if they have set one, otherwise not present.
|
||||
Future<String?> getDisplayName(String userId) async {
|
||||
final requestUri = Uri(
|
||||
path:
|
||||
'_matrix/client/v3/profile/${Uri.encodeComponent(userId)}/displayname',
|
||||
);
|
||||
final request = Request('GET', baseUri!.resolveUri(requestUri));
|
||||
if (bearerToken != null) {
|
||||
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 ((v) => v != null ? v as String : null)(json['displayname']);
|
||||
}
|
||||
|
||||
/// This API sets the given user's display name. You must have permission to
|
||||
/// set this user's display name, e.g. you need to have their `access_token`.
|
||||
///
|
||||
/// [userId] The user whose display name to set.
|
||||
///
|
||||
/// [displayname] The new display name for this user.
|
||||
Future<void> setDisplayName(String userId, String? displayname) async {
|
||||
final requestUri = Uri(
|
||||
path:
|
||||
'_matrix/client/v3/profile/${Uri.encodeComponent(userId)}/displayname',
|
||||
);
|
||||
final request = Request('PUT', baseUri!.resolveUri(requestUri));
|
||||
request.headers['authorization'] = 'Bearer ${bearerToken!}';
|
||||
request.headers['content-type'] = 'application/json';
|
||||
request.bodyBytes = utf8.encode(
|
||||
jsonEncode({
|
||||
if (displayname != null) 'displayname': displayname,
|
||||
}),
|
||||
);
|
||||
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 ignore(json);
|
||||
}
|
||||
|
||||
/// Lists the public rooms on the server.
|
||||
/// Lists a server's published room directory.
|
||||
///
|
||||
/// This API returns paginated responses. The rooms are ordered by the number
|
||||
/// of joined members, with the largest rooms first.
|
||||
|
|
@ -2879,8 +2976,8 @@ class Api {
|
|||
/// The direction of pagination is specified solely by which token
|
||||
/// is supplied, rather than via an explicit flag.
|
||||
///
|
||||
/// [server] The server to fetch the public room lists from. Defaults to the
|
||||
/// local server. Case sensitive.
|
||||
/// [server] The server to fetch the published room directory from. Defaults
|
||||
/// to the local server. Case sensitive.
|
||||
Future<GetPublicRoomsResponse> getPublicRooms({
|
||||
int? limit,
|
||||
String? since,
|
||||
|
|
@ -2903,13 +3000,13 @@ class Api {
|
|||
return GetPublicRoomsResponse.fromJson(json as Map<String, Object?>);
|
||||
}
|
||||
|
||||
/// Lists the public rooms on the server, with optional filter.
|
||||
/// Lists a server's published room directory with an optional filter.
|
||||
///
|
||||
/// This API returns paginated responses. The rooms are ordered by the number
|
||||
/// of joined members, with the largest rooms first.
|
||||
///
|
||||
/// [server] The server to fetch the public room lists from. Defaults to the
|
||||
/// local server. Case sensitive.
|
||||
/// [server] The server to fetch the published room directory from. Defaults
|
||||
/// to the local server. Case sensitive.
|
||||
///
|
||||
/// [filter] Filter to apply to the results.
|
||||
///
|
||||
|
|
@ -4134,9 +4231,6 @@ class Api {
|
|||
///
|
||||
/// - The matrix user ID who invited them to the room
|
||||
///
|
||||
/// If a token is requested from the identity server, the homeserver will
|
||||
/// append a `m.room.third_party_invite` event to the room.
|
||||
///
|
||||
/// [roomId] The room identifier (not alias) to which to invite the user.
|
||||
///
|
||||
/// [body]
|
||||
|
|
@ -4739,14 +4833,23 @@ class Api {
|
|||
///
|
||||
/// [stateKey] The key of the state to look up. Defaults to an empty string. When
|
||||
/// an empty string, the trailing slash on this endpoint is optional.
|
||||
///
|
||||
/// [format] The format to use for the returned data. `content` (the default) will
|
||||
/// return only the content of the state event. `event` will return the entire
|
||||
/// event in the usual format suitable for clients, including fields like event
|
||||
/// ID, sender and timestamp.
|
||||
Future<Map<String, Object?>> getRoomStateWithKey(
|
||||
String roomId,
|
||||
String eventType,
|
||||
String stateKey,
|
||||
) async {
|
||||
String stateKey, {
|
||||
Format? format,
|
||||
}) async {
|
||||
final requestUri = Uri(
|
||||
path:
|
||||
'_matrix/client/v3/rooms/${Uri.encodeComponent(roomId)}/state/${Uri.encodeComponent(eventType)}/${Uri.encodeComponent(stateKey)}',
|
||||
queryParameters: {
|
||||
if (format != null) 'format': format.name,
|
||||
},
|
||||
);
|
||||
final request = Request('GET', baseUri!.resolveUri(requestUri));
|
||||
request.headers['authorization'] = 'Bearer ${bearerToken!}';
|
||||
|
|
@ -4886,11 +4989,26 @@ class Api {
|
|||
///
|
||||
/// [roomId] The ID of the room to upgrade.
|
||||
///
|
||||
/// [additionalCreators] When upgrading to a [room version](https://spec.matrix.org/unstable/rooms) which supports additional creators,
|
||||
/// the [user IDs](https://spec.matrix.org/unstable/appendices#user-identifiers) which should be considered room
|
||||
/// creators in addition to the user performing the upgrade.
|
||||
///
|
||||
/// If the room being upgraded has additional creators, they are *not* automatically
|
||||
/// copied to the new room. The full set of additional creators needs to be set to
|
||||
/// retain (or add/remove) more room creators.
|
||||
///
|
||||
/// When upgrading to a room version which doesn't support additional creators, this
|
||||
/// field is ignored and has no effect during the upgrade process.
|
||||
///
|
||||
/// [newVersion] The new version for the room.
|
||||
///
|
||||
/// returns `replacement_room`:
|
||||
/// The ID of the new room.
|
||||
Future<String> upgradeRoom(String roomId, String newVersion) async {
|
||||
Future<String> upgradeRoom(
|
||||
String roomId,
|
||||
String newVersion, {
|
||||
List<String>? additionalCreators,
|
||||
}) async {
|
||||
final requestUri = Uri(
|
||||
path: '_matrix/client/v3/rooms/${Uri.encodeComponent(roomId)}/upgrade',
|
||||
);
|
||||
|
|
@ -4899,6 +5017,8 @@ class Api {
|
|||
request.headers['content-type'] = 'application/json';
|
||||
request.bodyBytes = utf8.encode(
|
||||
jsonEncode({
|
||||
if (additionalCreators != null)
|
||||
'additional_creators': additionalCreators.map((v) => v).toList(),
|
||||
'new_version': newVersion,
|
||||
}),
|
||||
);
|
||||
|
|
@ -5043,12 +5163,32 @@ class Api {
|
|||
///
|
||||
/// By default, this is `0`, so the server will return immediately
|
||||
/// even if the response is empty.
|
||||
///
|
||||
/// [useStateAfter] Controls whether to receive state changes between the previous sync
|
||||
/// and the **start** of the timeline, or between the previous sync and
|
||||
/// the **end** of the timeline.
|
||||
///
|
||||
/// If this is set to `true`, servers MUST respond with the state
|
||||
/// between the previous sync and the **end** of the timeline in
|
||||
/// `state_after` and MUST omit `state`.
|
||||
///
|
||||
/// If `false`, servers MUST respond with the state between the previous
|
||||
/// sync and the **start** of the timeline in `state` and MUST omit
|
||||
/// `state_after`.
|
||||
///
|
||||
/// Even if this is set to `true`, clients MUST update their local state
|
||||
/// with events in `state` and `timeline` if `state_after` is missing in
|
||||
/// the response, for compatibility with servers that don't support this
|
||||
/// parameter.
|
||||
///
|
||||
/// By default, this is `false`.
|
||||
Future<SyncUpdate> sync({
|
||||
String? filter,
|
||||
String? since,
|
||||
bool? fullState,
|
||||
PresenceType? setPresence,
|
||||
int? timeout,
|
||||
bool? useStateAfter,
|
||||
}) async {
|
||||
final requestUri = Uri(
|
||||
path: '_matrix/client/v3/sync',
|
||||
|
|
@ -5058,6 +5198,7 @@ class Api {
|
|||
if (fullState != null) 'full_state': fullState.toString(),
|
||||
if (setPresence != null) 'set_presence': setPresence.name,
|
||||
if (timeout != null) 'timeout': timeout.toString(),
|
||||
if (useStateAfter != null) 'use_state_after': useStateAfter.toString(),
|
||||
},
|
||||
);
|
||||
final request = Request('GET', baseUri!.resolveUri(requestUri));
|
||||
|
|
@ -5507,10 +5648,17 @@ class Api {
|
|||
return ignore(json);
|
||||
}
|
||||
|
||||
/// Performs a search for users. The homeserver may
|
||||
/// determine which subset of users are searched, however the homeserver
|
||||
/// MUST at a minimum consider the users the requesting user shares a
|
||||
/// room with and those who reside in public rooms (known to the homeserver).
|
||||
/// Performs a search for users. The homeserver may determine which
|
||||
/// subset of users are searched. However, the homeserver MUST at a
|
||||
/// minimum consider users who are visible to the requester based
|
||||
/// on their membership in rooms known to the homeserver. This means:
|
||||
///
|
||||
/// - users that share a room with the requesting user
|
||||
/// - users who are joined to rooms known to the homeserver that have a
|
||||
/// `public` [join rule](#mroomjoin_rules)
|
||||
/// - users who are joined to rooms known to the homeserver that have a
|
||||
/// `world_readable` [history visibility](#room-history-visibility)
|
||||
///
|
||||
/// The search MUST consider local users to the homeserver, and SHOULD
|
||||
/// query remote users as part of the search.
|
||||
///
|
||||
|
|
@ -5663,9 +5811,9 @@ class Api {
|
|||
return CreateContentResponse.fromJson(json as Map<String, Object?>);
|
||||
}
|
||||
|
||||
/// {{% boxes/note %}}
|
||||
/// **NOTE:**
|
||||
/// Replaced by [`GET /_matrix/client/v1/media/config`](https://spec.matrix.org/unstable/client-server-api/#get_matrixclientv1mediaconfig).
|
||||
/// {{% /boxes/note %}}
|
||||
///
|
||||
///
|
||||
/// This endpoint allows clients to retrieve the configuration of the content
|
||||
/// repository, such as upload limitations.
|
||||
|
|
@ -5690,17 +5838,17 @@ class Api {
|
|||
return MediaConfig.fromJson(json as Map<String, Object?>);
|
||||
}
|
||||
|
||||
/// {{% boxes/note %}}
|
||||
/// **NOTE:**
|
||||
/// Replaced by [`GET /_matrix/client/v1/media/download/{serverName}/{mediaId}`](https://spec.matrix.org/unstable/client-server-api/#get_matrixclientv1mediadownloadservernamemediaid)
|
||||
/// (requires authentication).
|
||||
/// {{% /boxes/note %}}
|
||||
///
|
||||
/// {{% boxes/warning %}}
|
||||
/// {{% changed-in v="1.11" %}} This endpoint MAY return `404 M_NOT_FOUND`
|
||||
///
|
||||
/// **WARNING:**
|
||||
/// **[Changed in `v1.11`]** This endpoint MAY return `404 M_NOT_FOUND`
|
||||
/// for media which exists, but is after the server froze unauthenticated
|
||||
/// media access. See [Client Behaviour](https://spec.matrix.org/unstable/client-server-api/#content-repo-client-behaviour) for more
|
||||
/// information.
|
||||
/// {{% /boxes/warning %}}
|
||||
///
|
||||
///
|
||||
/// [serverName] The server name from the `mxc://` URI (the authority component).
|
||||
///
|
||||
|
|
@ -5752,21 +5900,21 @@ class Api {
|
|||
);
|
||||
}
|
||||
|
||||
/// {{% boxes/note %}}
|
||||
/// **NOTE:**
|
||||
/// Replaced by [`GET /_matrix/client/v1/media/download/{serverName}/{mediaId}/{fileName}`](https://spec.matrix.org/unstable/client-server-api/#get_matrixclientv1mediadownloadservernamemediaidfilename)
|
||||
/// (requires authentication).
|
||||
/// {{% /boxes/note %}}
|
||||
///
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// {{% boxes/warning %}}
|
||||
/// {{% changed-in v="1.11" %}} This endpoint MAY return `404 M_NOT_FOUND`
|
||||
/// **WARNING:**
|
||||
/// **[Changed in `v1.11`]** This endpoint MAY return `404 M_NOT_FOUND`
|
||||
/// for media which exists, but is after the server froze unauthenticated
|
||||
/// media access. See [Client Behaviour](https://spec.matrix.org/unstable/client-server-api/#content-repo-client-behaviour) for more
|
||||
/// information.
|
||||
/// {{% /boxes/warning %}}
|
||||
///
|
||||
///
|
||||
/// [serverName] The server name from the `mxc://` URI (the authority component).
|
||||
///
|
||||
|
|
@ -5821,9 +5969,9 @@ class Api {
|
|||
);
|
||||
}
|
||||
|
||||
/// {{% boxes/note %}}
|
||||
/// **NOTE:**
|
||||
/// Replaced by [`GET /_matrix/client/v1/media/preview_url`](https://spec.matrix.org/unstable/client-server-api/#get_matrixclientv1mediapreview_url).
|
||||
/// {{% /boxes/note %}}
|
||||
///
|
||||
///
|
||||
/// 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.
|
||||
|
|
@ -5858,20 +6006,20 @@ class Api {
|
|||
return PreviewForUrl.fromJson(json as Map<String, Object?>);
|
||||
}
|
||||
|
||||
/// {{% boxes/note %}}
|
||||
/// **NOTE:**
|
||||
/// Replaced by [`GET /_matrix/client/v1/media/thumbnail/{serverName}/{mediaId}`](https://spec.matrix.org/unstable/client-server-api/#get_matrixclientv1mediathumbnailservernamemediaid)
|
||||
/// (requires authentication).
|
||||
/// {{% /boxes/note %}}
|
||||
///
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// {{% boxes/warning %}}
|
||||
/// {{% changed-in v="1.11" %}} This endpoint MAY return `404 M_NOT_FOUND`
|
||||
/// **WARNING:**
|
||||
/// **[Changed in `v1.11`]** This endpoint MAY return `404 M_NOT_FOUND`
|
||||
/// for media which exists, but is after the server froze unauthenticated
|
||||
/// media access. See [Client Behaviour](https://spec.matrix.org/unstable/client-server-api/#content-repo-client-behaviour) for more
|
||||
/// information.
|
||||
/// {{% /boxes/warning %}}
|
||||
///
|
||||
///
|
||||
/// [serverName] The server name from the `mxc://` URI (the authority component).
|
||||
///
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -184,6 +184,17 @@ class MatrixApi extends Api {
|
|||
return;
|
||||
}
|
||||
|
||||
/// Variant of updateDevice operation that deletes the device displayname by
|
||||
/// setting `display_name: null`.
|
||||
Future<void> deleteDeviceDisplayName(String deviceId) async {
|
||||
await request(
|
||||
RequestType.PUT,
|
||||
'/client/v3/devices/${Uri.encodeComponent(deviceId)}',
|
||||
data: {'display_name': null},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/// This API provides credentials for the client to use when initiating
|
||||
/// calls.
|
||||
@override
|
||||
|
|
@ -233,3 +244,9 @@ class EventTooLarge implements Exception {
|
|||
int maxSize, actualSize;
|
||||
EventTooLarge(this.maxSize, this.actualSize);
|
||||
}
|
||||
|
||||
@Deprecated('Use PublishedRoomsChunk instead')
|
||||
typedef PublicRoomsChunk = PublishedRoomsChunk;
|
||||
|
||||
@Deprecated('Use SpaceRoomsChunk\$1 or SpaceRoomsChunk\$2 instead')
|
||||
typedef SpaceRoomsChunk = SpaceRoomsChunk$2;
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@ abstract class EventTypes {
|
|||
'org.matrix.call.asserted_identity';
|
||||
static const String Unknown = 'm.unknown';
|
||||
|
||||
/// An internal event type indicating that the last event in the room for
|
||||
/// a room list preview is currently being refreshed.
|
||||
static const String refreshingLastEvent = 'com.famedly.refreshing_last_event';
|
||||
|
||||
// To device event types
|
||||
static const String RoomKey = 'm.room_key';
|
||||
static const String ForwardedRoomKey = 'm.forwarded_room_key';
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
*/
|
||||
|
||||
import 'package:matrix/matrix_api_lite/utils/print_logs_native.dart'
|
||||
if (dart.library.html) 'print_logs_web.dart';
|
||||
if (dart.library.js_interop) 'print_logs_web.dart';
|
||||
|
||||
enum Level {
|
||||
wtf,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import 'dart:html';
|
||||
import 'dart:js_interop';
|
||||
|
||||
import 'package:web/web.dart';
|
||||
|
||||
import 'package:matrix/matrix_api_lite.dart';
|
||||
|
||||
|
|
@ -13,22 +15,22 @@ extension PrintLogs on LogEvent {
|
|||
}
|
||||
switch (level) {
|
||||
case Level.wtf:
|
||||
window.console.error('!!!CRITICAL!!! $logsStr');
|
||||
console.error('!!!CRITICAL!!! $logsStr'.toJS);
|
||||
break;
|
||||
case Level.error:
|
||||
window.console.error(logsStr);
|
||||
console.error(logsStr.toJS);
|
||||
break;
|
||||
case Level.warning:
|
||||
window.console.warn(logsStr);
|
||||
console.warn(logsStr.toJS);
|
||||
break;
|
||||
case Level.info:
|
||||
window.console.info(logsStr);
|
||||
console.info(logsStr.toJS);
|
||||
break;
|
||||
case Level.debug:
|
||||
window.console.debug(logsStr);
|
||||
console.debug(logsStr.toJS);
|
||||
break;
|
||||
case Level.verbose:
|
||||
window.console.log(logsStr);
|
||||
console.log(logsStr.toJS);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1611,14 +1611,23 @@ class Client extends MatrixApi {
|
|||
// We send an empty String to remove the avatar. Sending Null **should**
|
||||
// work but it doesn't with Synapse. See:
|
||||
// https://gitlab.com/famedly/company/frontend/famedlysdk/-/issues/254
|
||||
return setAvatarUrl(userID!, Uri.parse(''));
|
||||
await setProfileField(
|
||||
userID!,
|
||||
'avatar_url',
|
||||
{'avatar_url': Uri.parse('')},
|
||||
);
|
||||
return;
|
||||
}
|
||||
final uploadResp = await uploadContent(
|
||||
file.bytes,
|
||||
filename: file.name,
|
||||
contentType: file.mimeType,
|
||||
);
|
||||
await setAvatarUrl(userID!, uploadResp);
|
||||
await setProfileField(
|
||||
userID!,
|
||||
'avatar_url',
|
||||
{'avatar_url': uploadResp.toString()},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -2632,13 +2641,15 @@ class Client extends MatrixApi {
|
|||
final id = entry.key;
|
||||
final syncRoomUpdate = entry.value;
|
||||
|
||||
final room = await _updateRoomsByRoomUpdate(id, syncRoomUpdate);
|
||||
|
||||
// Is the timeline limited? Then all previous messages should be
|
||||
// removed from the database!
|
||||
if (syncRoomUpdate is JoinedRoomUpdate &&
|
||||
syncRoomUpdate.timeline?.limited == true) {
|
||||
await database.deleteTimelineForRoom(id);
|
||||
room.lastEvent = null;
|
||||
}
|
||||
final room = await _updateRoomsByRoomUpdate(id, syncRoomUpdate);
|
||||
|
||||
final timelineUpdateType = direction != null
|
||||
? (direction == Direction.b
|
||||
|
|
@ -2738,6 +2749,24 @@ class Client extends MatrixApi {
|
|||
Logs().d('Skip store LeftRoomUpdate for unknown room', id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (syncRoomUpdate is JoinedRoomUpdate &&
|
||||
(room.lastEvent?.type == EventTypes.refreshingLastEvent ||
|
||||
(syncRoomUpdate.timeline?.limited == true &&
|
||||
room.lastEvent == null))) {
|
||||
room.lastEvent = Event(
|
||||
originServerTs:
|
||||
syncRoomUpdate.timeline?.events?.firstOrNull?.originServerTs ??
|
||||
DateTime.now(),
|
||||
type: EventTypes.refreshingLastEvent,
|
||||
content: {'body': 'Refreshing last event...'},
|
||||
room: room,
|
||||
eventId: generateUniqueTransactionId(),
|
||||
senderId: userID!,
|
||||
);
|
||||
runInRoot(room.refreshLastEvent);
|
||||
}
|
||||
|
||||
await database.storeRoomUpdate(id, syncRoomUpdate, room.lastEvent, this);
|
||||
}
|
||||
}
|
||||
|
|
@ -3035,13 +3064,6 @@ class Client extends MatrixApi {
|
|||
}
|
||||
if (type != EventUpdateType.timeline) break;
|
||||
|
||||
// If last event is null or not a valid room preview event anyway,
|
||||
// just use this:
|
||||
if (room.lastEvent == null) {
|
||||
room.lastEvent = event;
|
||||
break;
|
||||
}
|
||||
|
||||
// Is this event redacting the last event?
|
||||
if (event.type == EventTypes.Redaction &&
|
||||
({
|
||||
|
|
@ -3758,10 +3780,30 @@ class Client extends MatrixApi {
|
|||
|
||||
/// Ignore another user. This will clear the local cached messages to
|
||||
/// hide all previous messages from this user.
|
||||
Future<void> ignoreUser(String userId) async {
|
||||
Future<void> ignoreUser(
|
||||
String userId, {
|
||||
/// Whether to also decline all invites and leave DM rooms with this user.
|
||||
bool leaveRooms = true,
|
||||
}) async {
|
||||
if (!userId.isValidMatrixId) {
|
||||
throw Exception('$userId is not a valid mxid!');
|
||||
}
|
||||
|
||||
if (leaveRooms) {
|
||||
for (final room in rooms) {
|
||||
final isInviteFromUser = room.membership == Membership.invite &&
|
||||
room.getState(EventTypes.RoomMember, userID!)?.senderId == userId;
|
||||
|
||||
if (room.directChatMatrixID == userId || isInviteFromUser) {
|
||||
try {
|
||||
await room.leave();
|
||||
} catch (e, s) {
|
||||
Logs().w('Unable to leave room with blocked user $userId', e, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await setAccountData(userID!, 'm.ignored_user_list', {
|
||||
'ignored_users': Map.fromEntries(
|
||||
(ignoredUsers..add(userId)).map((key) => MapEntry(key, {})),
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import 'dart:async';
|
||||
import 'dart:html';
|
||||
import 'dart:indexed_db';
|
||||
import 'dart:js_interop';
|
||||
|
||||
import 'package:web/web.dart';
|
||||
|
||||
import 'package:matrix/matrix_api_lite/utils/logs.dart';
|
||||
import 'package:matrix/src/database/zone_transaction_mixin.dart';
|
||||
|
||||
/// Key-Value store abstraction over IndexedDB so that the sdk database can use
|
||||
/// a single interface for all platforms. API is inspired by Hive.
|
||||
class BoxCollection with ZoneTransactionMixin {
|
||||
final Database _db;
|
||||
final IDBDatabase _db;
|
||||
final Set<String> boxNames;
|
||||
final String name;
|
||||
|
||||
|
|
@ -18,23 +20,45 @@ class BoxCollection with ZoneTransactionMixin {
|
|||
Set<String> boxNames, {
|
||||
Object? sqfliteDatabase,
|
||||
Object? sqfliteFactory,
|
||||
IdbFactory? idbFactory,
|
||||
IDBFactory? idbFactory,
|
||||
int version = 1,
|
||||
}) async {
|
||||
idbFactory ??= window.indexedDB!;
|
||||
final db = await idbFactory.open(
|
||||
name,
|
||||
version: version,
|
||||
onUpgradeNeeded: (VersionChangeEvent event) {
|
||||
final db = event.target.result;
|
||||
for (final name in boxNames) {
|
||||
if (db.objectStoreNames.contains(name)) continue;
|
||||
idbFactory ??= window.indexedDB;
|
||||
final dbOpenCompleter = Completer<BoxCollection>();
|
||||
final request = idbFactory.open(name, version);
|
||||
|
||||
db.createObjectStore(name, autoIncrement: true);
|
||||
}
|
||||
},
|
||||
);
|
||||
return BoxCollection(db, boxNames, name);
|
||||
request.onerror = (Event event) {
|
||||
Logs().e(
|
||||
'[IndexedDBBox] Error loading database - ${request.error?.toString()}',
|
||||
);
|
||||
dbOpenCompleter.completeError(
|
||||
'Error loading database - ${request.error?.toString()}',
|
||||
);
|
||||
}.toJS;
|
||||
|
||||
request.onupgradeneeded = (IDBVersionChangeEvent event) {
|
||||
final db = (event.target! as IDBOpenDBRequest).result as IDBDatabase;
|
||||
|
||||
db.onerror = (Event event) {
|
||||
Logs().e('[IndexedDBBox] [onupgradeneeded] Error loading database');
|
||||
dbOpenCompleter
|
||||
.completeError('Error loading database onupgradeneeded.');
|
||||
}.toJS;
|
||||
|
||||
for (final name in boxNames) {
|
||||
if (db.objectStoreNames.contains(name)) continue;
|
||||
db.createObjectStore(
|
||||
name,
|
||||
IDBObjectStoreParameters(autoIncrement: true),
|
||||
);
|
||||
}
|
||||
}.toJS;
|
||||
|
||||
request.onsuccess = (Event event) {
|
||||
final db = request.result as IDBDatabase;
|
||||
dbOpenCompleter.complete(BoxCollection(db, boxNames, name));
|
||||
}.toJS;
|
||||
return dbOpenCompleter.future;
|
||||
}
|
||||
|
||||
Box<V> openBox<V>(String name) {
|
||||
|
|
@ -44,7 +68,7 @@ class BoxCollection with ZoneTransactionMixin {
|
|||
return Box<V>(name, this);
|
||||
}
|
||||
|
||||
List<Future<void> Function(Transaction txn)>? _txnCache;
|
||||
List<Future<void> Function(IDBTransaction txn)>? _txnCache;
|
||||
|
||||
Future<void> transaction(
|
||||
Future<void> Function() action, {
|
||||
|
|
@ -52,15 +76,18 @@ class BoxCollection with ZoneTransactionMixin {
|
|||
bool readOnly = false,
|
||||
}) =>
|
||||
zoneTransaction(() async {
|
||||
boxNames ??= _db.objectStoreNames!.toList();
|
||||
final txnCache = _txnCache = [];
|
||||
await action();
|
||||
final cache =
|
||||
List<Future<void> Function(Transaction txn)>.from(txnCache);
|
||||
List<Future<void> Function(IDBTransaction txn)>.from(txnCache);
|
||||
_txnCache = null;
|
||||
if (cache.isEmpty) return;
|
||||
final txn =
|
||||
_db.transaction(boxNames, readOnly ? 'readonly' : 'readwrite');
|
||||
|
||||
final transactionCompleter = Completer<void>();
|
||||
final txn = _db.transaction(
|
||||
boxNames?.jsify() ?? _db.objectStoreNames,
|
||||
readOnly ? 'readonly' : 'readwrite',
|
||||
);
|
||||
for (final fun in cache) {
|
||||
// The IDB methods return a Future in Dart but must not be awaited in
|
||||
// order to have an actual transaction. They must only be performed and
|
||||
|
|
@ -69,16 +96,54 @@ class BoxCollection with ZoneTransactionMixin {
|
|||
// https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction
|
||||
unawaited(fun(txn));
|
||||
}
|
||||
await txn.completed;
|
||||
return;
|
||||
|
||||
txn.onerror = (Event event) {
|
||||
Logs().e(
|
||||
'[IndexedDBBox] [transaction] Error - ${txn.error?.toString()}',
|
||||
);
|
||||
transactionCompleter.completeError(
|
||||
'Transaction not completed due to an error - ${txn.error?.toString()}'
|
||||
.toJS,
|
||||
);
|
||||
}.toJS;
|
||||
|
||||
txn.oncomplete = (Event event) {
|
||||
transactionCompleter.complete();
|
||||
}.toJS;
|
||||
return transactionCompleter.future;
|
||||
});
|
||||
|
||||
Future<void> clear() async {
|
||||
final txn = _db.transaction(boxNames.toList(), 'readwrite');
|
||||
final transactionCompleter = Completer();
|
||||
final txn = _db.transaction(boxNames.toList().jsify()!, 'readwrite');
|
||||
for (final name in boxNames) {
|
||||
unawaited(txn.objectStore(name).clear());
|
||||
final objStoreClearCompleter = Completer();
|
||||
final request = txn.objectStore(name).clear();
|
||||
request.onerror = (Event event) {
|
||||
Logs().e(
|
||||
'[IndexedDBBox] [clear] Object store clear error - ${request.error?.toString()}',
|
||||
);
|
||||
objStoreClearCompleter.completeError(
|
||||
'Object store clear not completed due to an error - ${request.error?.toString()}'
|
||||
.toJS,
|
||||
);
|
||||
}.toJS;
|
||||
request.onsuccess = (Event event) {
|
||||
objStoreClearCompleter.complete();
|
||||
}.toJS;
|
||||
unawaited(objStoreClearCompleter.future);
|
||||
}
|
||||
await txn.completed;
|
||||
txn.onerror = (Event event) {
|
||||
Logs().e('[IndexedDBBox] [clear] Error - ${txn.error?.toString()}');
|
||||
transactionCompleter.completeError(
|
||||
'DB clear transaction not completed due to an error - ${txn.error?.toString()}'
|
||||
.toJS,
|
||||
);
|
||||
}.toJS;
|
||||
txn.oncomplete = (Event event) {
|
||||
transactionCompleter.complete();
|
||||
}.toJS;
|
||||
return transactionCompleter.future;
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
|
|
@ -87,13 +152,24 @@ class BoxCollection with ZoneTransactionMixin {
|
|||
return zoneTransaction(() async => _db.close());
|
||||
}
|
||||
|
||||
@Deprecated('use collection.deleteDatabase now')
|
||||
static Future<void> delete(String name, [dynamic factory]) =>
|
||||
(factory ?? window.indexedDB!).deleteDatabase(name);
|
||||
|
||||
Future<void> deleteDatabase(String name, [dynamic factory]) async {
|
||||
await close();
|
||||
await (factory ?? window.indexedDB).deleteDatabase(name);
|
||||
final deleteDatabaseCompleter = Completer();
|
||||
final request =
|
||||
((factory ?? window.indexedDB) as IDBFactory).deleteDatabase(name);
|
||||
request.onerror = (Event event) {
|
||||
Logs().e(
|
||||
'[IndexedDBBox] [deleteDatabase] Error - ${request.error?.toString()}',
|
||||
);
|
||||
deleteDatabaseCompleter.completeError(
|
||||
'Error deleting database - ${request.error?.toString()}'.toJS,
|
||||
);
|
||||
}.toJS;
|
||||
request.onsuccess = (Event event) {
|
||||
Logs().i('[IndexedDBBox] [deleteDatabase] Database deleted.');
|
||||
deleteDatabaseCompleter.complete();
|
||||
}.toJS;
|
||||
return deleteDatabaseCompleter.future;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,44 +187,109 @@ class Box<V> {
|
|||
|
||||
Box(this.name, this.boxCollection);
|
||||
|
||||
Future<List<String>> getAllKeys([Transaction? txn]) async {
|
||||
Future<List<String>> getAllKeys([IDBTransaction? txn]) async {
|
||||
if (_quickAccessCachedKeys != null) return _quickAccessCachedKeys!.toList();
|
||||
txn ??= boxCollection._db.transaction(name, 'readonly');
|
||||
txn ??= boxCollection._db.transaction(name.toJS, 'readonly');
|
||||
final store = txn.objectStore(name);
|
||||
final request = store.getAllKeys(null);
|
||||
await request.onSuccess.first;
|
||||
final keys = request.result.cast<String>();
|
||||
final getAllKeysCompleter = Completer();
|
||||
final request = store.getAllKeys();
|
||||
request.onerror = (Event event) {
|
||||
Logs().e(
|
||||
'[IndexedDBBox] [getAllKeys] Error - ${request.error?.toString()}',
|
||||
);
|
||||
getAllKeysCompleter.completeError(
|
||||
'[IndexedDBBox] [getAllKeys] Error - ${request.error?.toString()}'.toJS,
|
||||
);
|
||||
}.toJS;
|
||||
request.onsuccess = (Event event) {
|
||||
getAllKeysCompleter.complete();
|
||||
}.toJS;
|
||||
await getAllKeysCompleter.future;
|
||||
final keys = (request.result?.dartify() as List?)?.cast<String>() ?? [];
|
||||
_quickAccessCachedKeys = keys.toSet();
|
||||
return keys;
|
||||
}
|
||||
|
||||
Future<Map<String, V>> getAllValues([Transaction? txn]) async {
|
||||
txn ??= boxCollection._db.transaction(name, 'readonly');
|
||||
Future<Map<String, V>> getAllValues([IDBTransaction? txn]) async {
|
||||
txn ??= boxCollection._db.transaction(name.toJS, 'readonly');
|
||||
final store = txn.objectStore(name);
|
||||
final map = <String, V>{};
|
||||
final cursorStream = store.openCursor(autoAdvance: true);
|
||||
await for (final cursor in cursorStream) {
|
||||
map[cursor.key as String] = _fromValue(cursor.value) as V;
|
||||
}
|
||||
|
||||
/// NOTE: This is a workaround to get the keys as [IDBObjectStore.getAll()]
|
||||
/// only returns the values as a list.
|
||||
/// And using the [IDBObjectStore.openCursor()] method is not working as expected.
|
||||
final keys = await getAllKeys(txn);
|
||||
|
||||
final getAllValuesCompleter = Completer();
|
||||
final getAllValuesRequest = store.getAll();
|
||||
getAllValuesRequest.onerror = (Event event) {
|
||||
Logs().e(
|
||||
'[IndexedDBBox] [getAllValues] Error - ${getAllValuesRequest.error?.toString()}',
|
||||
);
|
||||
getAllValuesCompleter.completeError(
|
||||
'[IndexedDBBox] [getAllValues] Error - ${getAllValuesRequest.error?.toString()}'
|
||||
.toJS,
|
||||
);
|
||||
}.toJS;
|
||||
getAllValuesRequest.onsuccess = (Event event) {
|
||||
final values = getAllValuesRequest.result.dartify() as List;
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
map[keys[i]] = _fromValue(values[i]) as V;
|
||||
}
|
||||
getAllValuesCompleter.complete();
|
||||
}.toJS;
|
||||
await getAllValuesCompleter.future;
|
||||
return map;
|
||||
}
|
||||
|
||||
Future<V?> get(String key, [Transaction? txn]) async {
|
||||
Future<V?> get(String key, [IDBTransaction? txn]) async {
|
||||
if (_quickAccessCache.containsKey(key)) return _quickAccessCache[key];
|
||||
txn ??= boxCollection._db.transaction(name, 'readonly');
|
||||
txn ??= boxCollection._db.transaction(name.toJS, 'readonly');
|
||||
final store = txn.objectStore(name);
|
||||
_quickAccessCache[key] = await store.getObject(key).then(_fromValue);
|
||||
final getObjectRequest = store.get(key.toJS);
|
||||
final getObjectCompleter = Completer();
|
||||
getObjectRequest.onerror = (Event event) {
|
||||
Logs().e(
|
||||
'[IndexedDBBox] [get] Error - ${getObjectRequest.error?.toString()}',
|
||||
);
|
||||
getObjectCompleter.completeError(
|
||||
'[IndexedDBBox] [get] Error - ${getObjectRequest.error?.toString()}'
|
||||
.toJS,
|
||||
);
|
||||
}.toJS;
|
||||
getObjectRequest.onsuccess = (Event event) {
|
||||
getObjectCompleter.complete();
|
||||
}.toJS;
|
||||
await getObjectCompleter.future;
|
||||
_quickAccessCache[key] = _fromValue(getObjectRequest.result?.dartify());
|
||||
return _quickAccessCache[key];
|
||||
}
|
||||
|
||||
Future<List<V?>> getAll(List<String> keys, [Transaction? txn]) async {
|
||||
Future<List<V?>> getAll(List<String> keys, [IDBTransaction? txn]) async {
|
||||
if (keys.every((key) => _quickAccessCache.containsKey(key))) {
|
||||
return keys.map((key) => _quickAccessCache[key]).toList();
|
||||
}
|
||||
txn ??= boxCollection._db.transaction(name, 'readonly');
|
||||
txn ??= boxCollection._db.transaction(name.toJS, 'readonly');
|
||||
final store = txn.objectStore(name);
|
||||
final list = await Future.wait(
|
||||
keys.map((key) => store.getObject(key).then(_fromValue)),
|
||||
keys.map((key) async {
|
||||
final getObjectRequest = store.get(key.toJS);
|
||||
final getObjectCompleter = Completer();
|
||||
getObjectRequest.onerror = (Event event) {
|
||||
Logs().e(
|
||||
'[IndexedDBBox] [getAll] Error at key $key - ${getObjectRequest.error?.toString()}',
|
||||
);
|
||||
getObjectCompleter.completeError(
|
||||
'[IndexedDBBox] [getAll] Error at key $key - ${getObjectRequest.error?.toString()}'
|
||||
.toJS,
|
||||
);
|
||||
}.toJS;
|
||||
getObjectRequest.onsuccess = (Event event) {
|
||||
getObjectCompleter.complete();
|
||||
}.toJS;
|
||||
await getObjectCompleter.future;
|
||||
return _fromValue(getObjectRequest.result?.dartify());
|
||||
}),
|
||||
);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
_quickAccessCache[keys[i]] = list[i];
|
||||
|
|
@ -156,7 +297,7 @@ class Box<V> {
|
|||
return list;
|
||||
}
|
||||
|
||||
Future<void> put(String key, V val, [Transaction? txn]) async {
|
||||
Future<void> put(String key, V val, [IDBTransaction? txn]) async {
|
||||
if (boxCollection._txnCache != null) {
|
||||
boxCollection._txnCache!.add((txn) => put(key, val, txn));
|
||||
_quickAccessCache[key] = val;
|
||||
|
|
@ -164,15 +305,28 @@ class Box<V> {
|
|||
return;
|
||||
}
|
||||
|
||||
txn ??= boxCollection._db.transaction(name, 'readwrite');
|
||||
txn ??= boxCollection._db.transaction(name.toJS, 'readwrite');
|
||||
final store = txn.objectStore(name);
|
||||
await store.put(val as Object, key);
|
||||
final putRequest = store.put(val.jsify(), key.toJS);
|
||||
final putCompleter = Completer();
|
||||
putRequest.onerror = (Event event) {
|
||||
Logs().e(
|
||||
'[IndexedDBBox] [put] Error - ${putRequest.error?.toString()}',
|
||||
);
|
||||
putCompleter.completeError(
|
||||
'[IndexedDBBox] [put] Error - ${putRequest.error?.toString()}'.toJS,
|
||||
);
|
||||
}.toJS;
|
||||
putRequest.onsuccess = (Event event) {
|
||||
putCompleter.complete();
|
||||
}.toJS;
|
||||
await putCompleter.future;
|
||||
_quickAccessCache[key] = val;
|
||||
_quickAccessCachedKeys?.add(key);
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> delete(String key, [Transaction? txn]) async {
|
||||
Future<void> delete(String key, [IDBTransaction? txn]) async {
|
||||
if (boxCollection._txnCache != null) {
|
||||
boxCollection._txnCache!.add((txn) => delete(key, txn));
|
||||
_quickAccessCache[key] = null;
|
||||
|
|
@ -180,9 +334,23 @@ class Box<V> {
|
|||
return;
|
||||
}
|
||||
|
||||
txn ??= boxCollection._db.transaction(name, 'readwrite');
|
||||
txn ??= boxCollection._db.transaction(name.toJS, 'readwrite');
|
||||
final store = txn.objectStore(name);
|
||||
await store.delete(key);
|
||||
final deleteRequest = store.delete(key.toJS);
|
||||
final deleteCompleter = Completer();
|
||||
deleteRequest.onerror = (Event event) {
|
||||
Logs().e(
|
||||
'[IndexedDBBox] [delete] Error - ${deleteRequest.error?.toString()}',
|
||||
);
|
||||
deleteCompleter.completeError(
|
||||
'[IndexedDBBox] [delete] Error - ${deleteRequest.error?.toString()}'
|
||||
.toJS,
|
||||
);
|
||||
}.toJS;
|
||||
deleteRequest.onsuccess = (Event event) {
|
||||
deleteCompleter.complete();
|
||||
}.toJS;
|
||||
await deleteCompleter.future;
|
||||
|
||||
// Set to null instead remove() so that inside of transactions null is
|
||||
// returned.
|
||||
|
|
@ -191,7 +359,7 @@ class Box<V> {
|
|||
return;
|
||||
}
|
||||
|
||||
Future<void> deleteAll(List<String> keys, [Transaction? txn]) async {
|
||||
Future<void> deleteAll(List<String> keys, [IDBTransaction? txn]) async {
|
||||
if (boxCollection._txnCache != null) {
|
||||
boxCollection._txnCache!.add((txn) => deleteAll(keys, txn));
|
||||
for (final key in keys) {
|
||||
|
|
@ -201,10 +369,24 @@ class Box<V> {
|
|||
return;
|
||||
}
|
||||
|
||||
txn ??= boxCollection._db.transaction(name, 'readwrite');
|
||||
txn ??= boxCollection._db.transaction(name.toJS, 'readwrite');
|
||||
final store = txn.objectStore(name);
|
||||
for (final key in keys) {
|
||||
await store.delete(key);
|
||||
final deleteRequest = store.delete(key.toJS);
|
||||
final deleteCompleter = Completer();
|
||||
deleteRequest.onerror = (Event event) {
|
||||
Logs().e(
|
||||
'[IndexedDBBox] [deleteAll] Error at key $key - ${deleteRequest.error?.toString()}',
|
||||
);
|
||||
deleteCompleter.completeError(
|
||||
'[IndexedDBBox] [deleteAll] Error at key $key - ${deleteRequest.error?.toString()}'
|
||||
.toJS,
|
||||
);
|
||||
}.toJS;
|
||||
deleteRequest.onsuccess = (Event event) {
|
||||
deleteCompleter.complete();
|
||||
}.toJS;
|
||||
await deleteCompleter.future;
|
||||
_quickAccessCache[key] = null;
|
||||
_quickAccessCachedKeys?.remove(key);
|
||||
}
|
||||
|
|
@ -216,15 +398,28 @@ class Box<V> {
|
|||
_quickAccessCachedKeys = null;
|
||||
}
|
||||
|
||||
Future<void> clear([Transaction? txn]) async {
|
||||
Future<void> clear([IDBTransaction? txn]) async {
|
||||
if (boxCollection._txnCache != null) {
|
||||
boxCollection._txnCache!.add((txn) => clear(txn));
|
||||
} else {
|
||||
txn ??= boxCollection._db.transaction(name, 'readwrite');
|
||||
txn ??= boxCollection._db.transaction(name.toJS, 'readwrite');
|
||||
final store = txn.objectStore(name);
|
||||
await store.clear();
|
||||
final clearRequest = store.clear();
|
||||
final clearCompleter = Completer();
|
||||
clearRequest.onerror = (Event event) {
|
||||
Logs().e(
|
||||
'[IndexedDBBox] [clear] Error - ${clearRequest.error?.toString()}',
|
||||
);
|
||||
clearCompleter.completeError(
|
||||
'[IndexedDBBox] [clear] Error - ${clearRequest.error?.toString()}'
|
||||
.toJS,
|
||||
);
|
||||
}.toJS;
|
||||
clearRequest.onsuccess = (Event event) {
|
||||
clearCompleter.complete();
|
||||
}.toJS;
|
||||
await clearCompleter.future;
|
||||
}
|
||||
|
||||
clearQuickAccessCache();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ import 'package:matrix/src/utils/copy_map.dart';
|
|||
import 'package:matrix/src/utils/queued_to_device_event.dart';
|
||||
import 'package:matrix/src/utils/run_benchmarked.dart';
|
||||
|
||||
import 'package:matrix/src/database/indexeddb_box.dart'
|
||||
if (dart.library.io) 'package:matrix/src/database/sqflite_box.dart';
|
||||
import 'package:matrix/src/database/sqflite_box.dart'
|
||||
if (dart.library.js_interop) 'package:matrix/src/database/indexeddb_box.dart';
|
||||
|
||||
import 'package:matrix/src/database/database_file_storage_stub.dart'
|
||||
if (dart.library.io) 'package:matrix/src/database/database_file_storage_io.dart';
|
||||
|
|
@ -167,8 +167,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
|||
|
||||
Database? database;
|
||||
|
||||
/// Custom IdbFactory used to create the indexedDB. On IO platforms it would
|
||||
/// lead to an error to import "dart:indexed_db" so this is dynamically
|
||||
/// Custom [IDBFactory] used to create the indexedDB. On IO platforms it would
|
||||
/// lead to an error to import "package:web/web.dart" so this is dynamically
|
||||
/// typed.
|
||||
final dynamic idbFactory;
|
||||
|
||||
|
|
|
|||
|
|
@ -16,17 +16,20 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/src/utils/file_send_request_credentials.dart';
|
||||
import 'package:matrix/src/utils/html_to_text.dart';
|
||||
import 'package:matrix/src/utils/markdown.dart';
|
||||
import 'package:matrix/src/utils/multipart_request_progress.dart';
|
||||
|
||||
abstract class RelationshipTypes {
|
||||
static const String reply = 'm.in_reply_to';
|
||||
|
|
@ -747,6 +750,10 @@ class Event extends MatrixEvent {
|
|||
bool getThumbnail = false,
|
||||
Future<Uint8List> Function(Uri)? downloadCallback,
|
||||
bool fromLocalStoreOnly = false,
|
||||
|
||||
/// Callback which gets triggered on progress containing the amount of
|
||||
/// downloaded bytes.
|
||||
void Function(int)? onDownloadProgress,
|
||||
}) async {
|
||||
if (![EventTypes.Message, EventTypes.Sticker].contains(type)) {
|
||||
throw ("This event has the type '$type' and so it can't contain an attachment.");
|
||||
|
|
@ -1183,6 +1190,17 @@ class Event extends MatrixEvent {
|
|||
(fileSendingStatus) => fileSendingStatus.name == status,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the mentioned userIds and whether the event includes an @room
|
||||
/// mention. This is only determined by the `m.mention` object in the event
|
||||
/// content.
|
||||
({List<String> userIds, bool room}) get mentions {
|
||||
final mentionsMap = content.tryGetMap<String, Object?>('m.mentions');
|
||||
return (
|
||||
userIds: mentionsMap?.tryGetList<String>('user_ids') ?? [],
|
||||
room: mentionsMap?.tryGet<bool>('room') ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum FileSendingStatus {
|
||||
|
|
|
|||
|
|
@ -377,6 +377,73 @@ class Room {
|
|||
|
||||
Event? lastEvent;
|
||||
|
||||
/// Fetches the most recent event in the timeline from the server to have
|
||||
/// a valid preview after receiving a limited timeline from the sync. Will
|
||||
/// be triggered by the sync loop on demand. Multiple requests will be
|
||||
/// combined to the same request.
|
||||
Future<Event?> refreshLastEvent({
|
||||
timeout = const Duration(seconds: 30),
|
||||
}) async {
|
||||
final lastEvent = _refreshingLastEvent ??= _refreshLastEvent();
|
||||
_refreshingLastEvent = null;
|
||||
return lastEvent;
|
||||
}
|
||||
|
||||
Future<Event?>? _refreshingLastEvent;
|
||||
|
||||
Future<Event?> _refreshLastEvent({
|
||||
timeout = const Duration(seconds: 30),
|
||||
}) async {
|
||||
if (membership != Membership.join) return null;
|
||||
|
||||
final filter = StateFilter(types: client.roomPreviewLastEvents.toList());
|
||||
final result = await client
|
||||
.getRoomEvents(
|
||||
id,
|
||||
Direction.b,
|
||||
limit: 1,
|
||||
filter: jsonEncode(filter.toJson()),
|
||||
)
|
||||
.timeout(timeout);
|
||||
final matrixEvent = result.chunk.firstOrNull;
|
||||
if (matrixEvent == null) {
|
||||
if (lastEvent?.type == EventTypes.refreshingLastEvent) {
|
||||
lastEvent = null;
|
||||
}
|
||||
Logs().d('No last event found for room', id);
|
||||
return null;
|
||||
}
|
||||
var event = Event.fromMatrixEvent(
|
||||
matrixEvent,
|
||||
this,
|
||||
status: EventStatus.synced,
|
||||
);
|
||||
if (event.type == EventTypes.Encrypted) {
|
||||
final encryption = client.encryption;
|
||||
if (encryption != null) {
|
||||
event = await encryption.decryptRoomEvent(event);
|
||||
}
|
||||
}
|
||||
lastEvent = event;
|
||||
|
||||
Logs().d('Refreshed last event for room', id);
|
||||
|
||||
// Trigger sync handling so that lastEvent gets stored and room list gets
|
||||
// updated.
|
||||
await _handleFakeSync(
|
||||
SyncUpdate(
|
||||
nextBatch: '',
|
||||
rooms: RoomsUpdate(
|
||||
join: {
|
||||
id: JoinedRoomUpdate(timeline: TimelineUpdate(limited: false)),
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
void setEphemeral(BasicEvent ephemeral) {
|
||||
ephemerals[ephemeral.type] = ephemeral;
|
||||
if (ephemeral.type == 'm.typing') {
|
||||
|
|
@ -443,8 +510,16 @@ class Room {
|
|||
String get displayname => getLocalizedDisplayname();
|
||||
|
||||
/// When was the last event received.
|
||||
DateTime get latestEventReceivedTime =>
|
||||
lastEvent?.originServerTs ?? DateTime.now();
|
||||
DateTime get latestEventReceivedTime {
|
||||
final lastEventTime = lastEvent?.originServerTs;
|
||||
if (lastEventTime != null) return lastEventTime;
|
||||
|
||||
if (membership == Membership.invite) return DateTime.now();
|
||||
final createEvent = getState(EventTypes.RoomCreate);
|
||||
if (createEvent is MatrixEvent) return createEvent.originServerTs;
|
||||
|
||||
return DateTime(0);
|
||||
}
|
||||
|
||||
/// Call the Matrix API to change the name of this room. Returns the event ID of the
|
||||
/// new m.room.name event.
|
||||
|
|
@ -635,6 +710,7 @@ class Room {
|
|||
String? threadRootEventId,
|
||||
String? threadLastEventId,
|
||||
StringBuffer? commandStdout,
|
||||
bool addMentions = true,
|
||||
}) {
|
||||
if (parseCommands) {
|
||||
return client.parseAndRunCommand(
|
||||
|
|
@ -652,6 +728,41 @@ class Room {
|
|||
'msgtype': msgtype,
|
||||
'body': message,
|
||||
};
|
||||
|
||||
if (addMentions) {
|
||||
var potentialMentions = message
|
||||
.split('@')
|
||||
.map(
|
||||
(text) => text.startsWith('[')
|
||||
? '@${text.split(']').first}]'
|
||||
: '@${text.split(RegExp(r'\s+')).first}',
|
||||
)
|
||||
.toList()
|
||||
..removeAt(0);
|
||||
|
||||
final hasRoomMention = potentialMentions.remove('@room');
|
||||
|
||||
potentialMentions = potentialMentions
|
||||
.map(
|
||||
(mention) =>
|
||||
mention.isValidMatrixId ? mention : getMention(mention),
|
||||
)
|
||||
.nonNulls
|
||||
.toSet() // Deduplicate
|
||||
.toList()
|
||||
..remove(client.userID); // We should never mention ourself.
|
||||
|
||||
// https://spec.matrix.org/v1.7/client-server-api/#mentioning-the-replied-to-user
|
||||
if (inReplyTo != null) potentialMentions.add(inReplyTo.senderId);
|
||||
|
||||
if (hasRoomMention || potentialMentions.isNotEmpty) {
|
||||
event['m.mentions'] = {
|
||||
if (hasRoomMention) 'room': true,
|
||||
if (potentialMentions.isNotEmpty) 'user_ids': potentialMentions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (parseMarkdown) {
|
||||
final html = markdown(
|
||||
event['body'],
|
||||
|
|
@ -1996,7 +2107,10 @@ class Room {
|
|||
Future<String> setAvatar(MatrixFile? file) async {
|
||||
final uploadResp = file == null
|
||||
? null
|
||||
: await client.uploadContent(file.bytes, filename: file.name);
|
||||
: await client.uploadContent(
|
||||
file.bytes,
|
||||
filename: file.name,
|
||||
);
|
||||
return await client.setRoomStateWithKey(
|
||||
id,
|
||||
EventTypes.RoomAvatar,
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export 'native.dart' if (dart.library.js) 'js.dart';
|
||||
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:vodozemac/vodozemac.dart';
|
||||
|
||||
import 'package:matrix/encryption/utils/base64_unpadded.dart';
|
||||
import 'package:matrix/src/utils/crypto/crypto.dart';
|
||||
|
||||
|
|
@ -38,8 +40,8 @@ class EncryptedFile {
|
|||
Future<EncryptedFile> encryptFile(Uint8List input) async {
|
||||
final key = secureRandomBytes(32);
|
||||
final iv = secureRandomBytes(16);
|
||||
final data = await aesCtr.encrypt(input, key, iv);
|
||||
final hash = await sha256(data);
|
||||
final data = CryptoUtils.aesCtr(input: input, key: key, iv: iv);
|
||||
final hash = CryptoUtils.sha256(input: data);
|
||||
return EncryptedFile(
|
||||
data: data,
|
||||
k: base64Url.encode(key).replaceAll('=', ''),
|
||||
|
|
@ -51,12 +53,12 @@ Future<EncryptedFile> encryptFile(Uint8List input) async {
|
|||
/// you would likely want to use [NativeImplementations] and
|
||||
/// [Client.nativeImplementations] instead
|
||||
Future<Uint8List?> decryptFileImplementation(EncryptedFile input) async {
|
||||
if (base64.encode(await sha256(input.data)) !=
|
||||
if (base64.encode(CryptoUtils.sha256(input: input.data)) !=
|
||||
base64.normalize(input.sha256)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final key = base64decodeUnpadded(base64.normalize(input.k));
|
||||
final iv = base64decodeUnpadded(base64.normalize(input.iv));
|
||||
return await aesCtr.encrypt(input.data, key, iv);
|
||||
return CryptoUtils.aesCtr(input: input.data, key: key, iv: iv);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,164 +0,0 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2019, 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
|
||||
final libcrypto = () {
|
||||
if (Platform.isIOS) {
|
||||
return DynamicLibrary.process();
|
||||
} else if (Platform.isAndroid) {
|
||||
return DynamicLibrary.open('libcrypto.so');
|
||||
} else if (Platform.isWindows) {
|
||||
return DynamicLibrary.open('libcrypto.dll');
|
||||
} else if (Platform.isMacOS) {
|
||||
try {
|
||||
return DynamicLibrary.open('libcrypto.3.dylib');
|
||||
} catch (_) {
|
||||
return DynamicLibrary.open('libcrypto.1.1.dylib');
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return DynamicLibrary.open('libcrypto.so.3');
|
||||
} catch (_) {
|
||||
return DynamicLibrary.open('libcrypto.so.1.1');
|
||||
}
|
||||
}
|
||||
}();
|
||||
|
||||
final PKCS5_PBKDF2_HMAC = libcrypto.lookupFunction<
|
||||
IntPtr Function(
|
||||
Pointer<Uint8> pass,
|
||||
IntPtr passlen,
|
||||
Pointer<Uint8> salt,
|
||||
IntPtr saltlen,
|
||||
IntPtr iter,
|
||||
Pointer<NativeType> digest,
|
||||
IntPtr keylen,
|
||||
Pointer<Uint8> out,
|
||||
),
|
||||
int Function(
|
||||
Pointer<Uint8> pass,
|
||||
int passlen,
|
||||
Pointer<Uint8> salt,
|
||||
int saltlen,
|
||||
int iter,
|
||||
Pointer<NativeType> digest,
|
||||
int keylen,
|
||||
Pointer<Uint8> out,
|
||||
)>('PKCS5_PBKDF2_HMAC');
|
||||
|
||||
final EVP_sha1 = libcrypto.lookupFunction<Pointer<NativeType> Function(),
|
||||
Pointer<NativeType> Function()>('EVP_sha1');
|
||||
|
||||
final EVP_sha256 = libcrypto.lookupFunction<Pointer<NativeType> Function(),
|
||||
Pointer<NativeType> Function()>('EVP_sha256');
|
||||
|
||||
final EVP_sha512 = libcrypto.lookupFunction<Pointer<NativeType> Function(),
|
||||
Pointer<NativeType> Function()>('EVP_sha512');
|
||||
|
||||
final EVP_aes_128_ctr = libcrypto.lookupFunction<Pointer<NativeType> Function(),
|
||||
Pointer<NativeType> Function()>('EVP_aes_128_ctr');
|
||||
|
||||
final EVP_aes_256_ctr = libcrypto.lookupFunction<Pointer<NativeType> Function(),
|
||||
Pointer<NativeType> Function()>('EVP_aes_256_ctr');
|
||||
|
||||
final EVP_CIPHER_CTX_new = libcrypto.lookupFunction<
|
||||
Pointer<NativeType> Function(),
|
||||
Pointer<NativeType> Function()>('EVP_CIPHER_CTX_new');
|
||||
|
||||
final EVP_EncryptInit_ex = libcrypto.lookupFunction<
|
||||
Pointer<NativeType> Function(
|
||||
Pointer<NativeType> ctx,
|
||||
Pointer<NativeType> alg,
|
||||
Pointer<NativeType> some,
|
||||
Pointer<Uint8> key,
|
||||
Pointer<Uint8> iv,
|
||||
),
|
||||
Pointer<NativeType> Function(
|
||||
Pointer<NativeType> ctx,
|
||||
Pointer<NativeType> alg,
|
||||
Pointer<NativeType> some,
|
||||
Pointer<Uint8> key,
|
||||
Pointer<Uint8> iv,
|
||||
)>('EVP_EncryptInit_ex');
|
||||
|
||||
final EVP_EncryptUpdate = libcrypto.lookupFunction<
|
||||
Pointer<NativeType> Function(
|
||||
Pointer<NativeType> ctx,
|
||||
Pointer<Uint8> output,
|
||||
Pointer<IntPtr> outputLen,
|
||||
Pointer<Uint8> input,
|
||||
IntPtr inputLen,
|
||||
),
|
||||
Pointer<NativeType> Function(
|
||||
Pointer<NativeType> ctx,
|
||||
Pointer<Uint8> output,
|
||||
Pointer<IntPtr> outputLen,
|
||||
Pointer<Uint8> input,
|
||||
int inputLen,
|
||||
)>('EVP_EncryptUpdate');
|
||||
|
||||
final EVP_EncryptFinal_ex = libcrypto.lookupFunction<
|
||||
Pointer<NativeType> Function(
|
||||
Pointer<NativeType> ctx,
|
||||
Pointer<Uint8> data,
|
||||
Pointer<IntPtr> len,
|
||||
),
|
||||
Pointer<NativeType> Function(
|
||||
Pointer<NativeType> ctx,
|
||||
Pointer<Uint8> data,
|
||||
Pointer<IntPtr> len,
|
||||
)>('EVP_EncryptFinal_ex');
|
||||
|
||||
final EVP_CIPHER_CTX_free = libcrypto.lookupFunction<
|
||||
Pointer<NativeType> Function(Pointer<NativeType> ctx),
|
||||
Pointer<NativeType> Function(
|
||||
Pointer<NativeType> ctx,
|
||||
)>('EVP_CIPHER_CTX_free');
|
||||
|
||||
final EVP_Digest = libcrypto.lookupFunction<
|
||||
IntPtr Function(
|
||||
Pointer<Uint8> data,
|
||||
IntPtr len,
|
||||
Pointer<Uint8> hash,
|
||||
Pointer<IntPtr> hsize,
|
||||
Pointer<NativeType> alg,
|
||||
Pointer<NativeType> engine,
|
||||
),
|
||||
int Function(
|
||||
Pointer<Uint8> data,
|
||||
int len,
|
||||
Pointer<Uint8> hash,
|
||||
Pointer<IntPtr> hsize,
|
||||
Pointer<NativeType> alg,
|
||||
Pointer<NativeType> engine,
|
||||
)>('EVP_Digest');
|
||||
|
||||
final EVP_MD_size = () {
|
||||
// EVP_MD_size was renamed to EVP_MD_get_size in Openssl3.0.
|
||||
// There is an alias macro, but those don't exist in libraries.
|
||||
// Try loading the new name first, then fall back to the old one if not found.
|
||||
try {
|
||||
return libcrypto.lookupFunction<IntPtr Function(Pointer<NativeType> ctx),
|
||||
int Function(Pointer<NativeType> ctx)>('EVP_MD_get_size');
|
||||
} catch (e) {
|
||||
return libcrypto.lookupFunction<IntPtr Function(Pointer<NativeType> ctx),
|
||||
int Function(Pointer<NativeType> ctx)>('EVP_MD_size');
|
||||
}
|
||||
}();
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
// Copyright (c) 2020 Famedly GmbH
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:matrix/src/utils/crypto/subtle.dart' as subtle;
|
||||
import 'package:matrix/src/utils/crypto/subtle.dart';
|
||||
|
||||
abstract class Hash {
|
||||
Hash._(this.name);
|
||||
String name;
|
||||
|
||||
Future<Uint8List> call(Uint8List input) async =>
|
||||
Uint8List.view(await digest(name, input));
|
||||
}
|
||||
|
||||
final Hash sha1 = _Sha1();
|
||||
final Hash sha256 = _Sha256();
|
||||
final Hash sha512 = _Sha512();
|
||||
|
||||
class _Sha1 extends Hash {
|
||||
_Sha1() : super._('SHA-1');
|
||||
}
|
||||
|
||||
class _Sha256 extends Hash {
|
||||
_Sha256() : super._('SHA-256');
|
||||
}
|
||||
|
||||
class _Sha512 extends Hash {
|
||||
_Sha512() : super._('SHA-512');
|
||||
}
|
||||
|
||||
abstract class Cipher {
|
||||
Cipher._(this.name);
|
||||
String name;
|
||||
Object params(Uint8List iv);
|
||||
Future<Uint8List> encrypt(
|
||||
Uint8List input,
|
||||
Uint8List key,
|
||||
Uint8List iv,
|
||||
) async {
|
||||
final subtleKey = await importKey('raw', key, name, false, ['encrypt']);
|
||||
return (await subtle.encrypt(params(iv), subtleKey, input)).asUint8List();
|
||||
}
|
||||
}
|
||||
|
||||
final Cipher aesCtr = _AesCtr();
|
||||
|
||||
class _AesCtr extends Cipher {
|
||||
_AesCtr() : super._('AES-CTR');
|
||||
|
||||
@override
|
||||
Object params(Uint8List iv) =>
|
||||
AesCtrParams(name: name, counter: iv, length: 64);
|
||||
}
|
||||
|
||||
Future<Uint8List> pbkdf2(
|
||||
Uint8List passphrase,
|
||||
Uint8List salt,
|
||||
Hash hash,
|
||||
int iterations,
|
||||
int bits,
|
||||
) async {
|
||||
final raw =
|
||||
await importKey('raw', passphrase, 'PBKDF2', false, ['deriveBits']);
|
||||
final res = await deriveBits(
|
||||
Pbkdf2Params(
|
||||
name: 'PBKDF2',
|
||||
hash: hash.name,
|
||||
salt: salt,
|
||||
iterations: iterations,
|
||||
),
|
||||
raw,
|
||||
bits,
|
||||
);
|
||||
return Uint8List.view(res);
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
// ignore_for_file: deprecated_member_use
|
||||
// ignoring the elementAt deprecation because this would make the SDK
|
||||
// incompatible with older flutter versions than 3.19.0 or dart 3.3.0
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
||||
import 'package:matrix/src/utils/crypto/ffi.dart';
|
||||
|
||||
abstract class Hash {
|
||||
Hash._(this.ptr);
|
||||
Pointer<NativeType> ptr;
|
||||
|
||||
FutureOr<Uint8List> call(Uint8List data) {
|
||||
final outSize = EVP_MD_size(ptr);
|
||||
final mem = malloc.call<Uint8>(outSize + data.length);
|
||||
final dataMem = mem.elementAt(outSize);
|
||||
try {
|
||||
dataMem.asTypedList(data.length).setAll(0, data);
|
||||
EVP_Digest(dataMem, data.length, mem, nullptr, ptr, nullptr);
|
||||
return Uint8List.fromList(mem.asTypedList(outSize));
|
||||
} finally {
|
||||
malloc.free(mem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final Hash sha1 = _Sha1();
|
||||
final Hash sha256 = _Sha256();
|
||||
final Hash sha512 = _Sha512();
|
||||
|
||||
class _Sha1 extends Hash {
|
||||
_Sha1() : super._(EVP_sha1());
|
||||
}
|
||||
|
||||
class _Sha256 extends Hash {
|
||||
_Sha256() : super._(EVP_sha256());
|
||||
}
|
||||
|
||||
class _Sha512 extends Hash {
|
||||
_Sha512() : super._(EVP_sha512());
|
||||
}
|
||||
|
||||
abstract class Cipher {
|
||||
Cipher._();
|
||||
Pointer<NativeType> getAlg(int keysize);
|
||||
FutureOr<Uint8List> encrypt(Uint8List input, Uint8List key, Uint8List iv) {
|
||||
final alg = getAlg(key.length * 8);
|
||||
final mem = malloc
|
||||
.call<Uint8>(sizeOf<IntPtr>() + key.length + iv.length + input.length);
|
||||
final lenMem = mem.cast<IntPtr>();
|
||||
final keyMem = mem.elementAt(sizeOf<IntPtr>());
|
||||
final ivMem = keyMem.elementAt(key.length);
|
||||
final dataMem = ivMem.elementAt(iv.length);
|
||||
try {
|
||||
keyMem.asTypedList(key.length).setAll(0, key);
|
||||
ivMem.asTypedList(iv.length).setAll(0, iv);
|
||||
dataMem.asTypedList(input.length).setAll(0, input);
|
||||
final ctx = EVP_CIPHER_CTX_new();
|
||||
EVP_EncryptInit_ex(ctx, alg, nullptr, keyMem, ivMem);
|
||||
EVP_EncryptUpdate(ctx, dataMem, lenMem, dataMem, input.length);
|
||||
EVP_EncryptFinal_ex(ctx, dataMem.elementAt(lenMem.value), lenMem);
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
return Uint8List.fromList(dataMem.asTypedList(input.length));
|
||||
} finally {
|
||||
malloc.free(mem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final Cipher aesCtr = _AesCtr();
|
||||
|
||||
class _AesCtr extends Cipher {
|
||||
_AesCtr() : super._();
|
||||
|
||||
@override
|
||||
Pointer<NativeType> getAlg(int keysize) {
|
||||
switch (keysize) {
|
||||
case 128:
|
||||
return EVP_aes_128_ctr();
|
||||
case 256:
|
||||
return EVP_aes_256_ctr();
|
||||
default:
|
||||
throw ArgumentError('invalid key size');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<Uint8List> pbkdf2(
|
||||
Uint8List passphrase,
|
||||
Uint8List salt,
|
||||
Hash hash,
|
||||
int iterations,
|
||||
int bits,
|
||||
) {
|
||||
final outLen = bits ~/ 8;
|
||||
final mem = malloc.call<Uint8>(passphrase.length + salt.length + outLen);
|
||||
final saltMem = mem.elementAt(passphrase.length);
|
||||
final outMem = saltMem.elementAt(salt.length);
|
||||
try {
|
||||
mem.asTypedList(passphrase.length).setAll(0, passphrase);
|
||||
saltMem.asTypedList(salt.length).setAll(0, salt);
|
||||
PKCS5_PBKDF2_HMAC(
|
||||
mem,
|
||||
passphrase.length,
|
||||
saltMem,
|
||||
salt.length,
|
||||
iterations,
|
||||
hash.ptr,
|
||||
outLen,
|
||||
outMem,
|
||||
);
|
||||
return Uint8List.fromList(outMem.asTypedList(outLen));
|
||||
} finally {
|
||||
malloc.free(mem);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
// Copyright (c) 2020 Famedly GmbH
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:js_util';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:js/js.dart';
|
||||
|
||||
@JS()
|
||||
@anonymous
|
||||
class Pbkdf2Params {
|
||||
external factory Pbkdf2Params({
|
||||
String name,
|
||||
String hash,
|
||||
Uint8List salt,
|
||||
int iterations,
|
||||
});
|
||||
String? name;
|
||||
String? hash;
|
||||
Uint8List? salt;
|
||||
int? iterations;
|
||||
}
|
||||
|
||||
@JS()
|
||||
@anonymous
|
||||
class AesCtrParams {
|
||||
external factory AesCtrParams({
|
||||
String name,
|
||||
Uint8List counter,
|
||||
int length,
|
||||
});
|
||||
String? name;
|
||||
Uint8List? counter;
|
||||
int? length;
|
||||
}
|
||||
|
||||
@JS('crypto.subtle.encrypt')
|
||||
external dynamic _encrypt(dynamic algorithm, dynamic key, Uint8List data);
|
||||
|
||||
Future<ByteBuffer> encrypt(dynamic algorithm, dynamic key, Uint8List data) {
|
||||
return promiseToFuture(_encrypt(algorithm, key, data));
|
||||
}
|
||||
|
||||
@JS('crypto.subtle.decrypt')
|
||||
external dynamic _decrypt(dynamic algorithm, dynamic key, Uint8List data);
|
||||
|
||||
Future<ByteBuffer> decrypt(dynamic algorithm, dynamic key, Uint8List data) {
|
||||
return promiseToFuture(_decrypt(algorithm, key, data));
|
||||
}
|
||||
|
||||
@JS('crypto.subtle.importKey')
|
||||
external dynamic _importKey(
|
||||
String format,
|
||||
dynamic keyData,
|
||||
dynamic algorithm,
|
||||
bool extractable,
|
||||
List<String> keyUsages,
|
||||
);
|
||||
|
||||
Future<dynamic> importKey(
|
||||
String format,
|
||||
dynamic keyData,
|
||||
dynamic algorithm,
|
||||
bool extractable,
|
||||
List<String> keyUsages,
|
||||
) {
|
||||
return promiseToFuture(
|
||||
_importKey(format, keyData, algorithm, extractable, keyUsages),
|
||||
);
|
||||
}
|
||||
|
||||
@JS('crypto.subtle.exportKey')
|
||||
external dynamic _exportKey(String algorithm, dynamic key);
|
||||
|
||||
Future<dynamic> exportKey(String algorithm, dynamic key) {
|
||||
return promiseToFuture(_exportKey(algorithm, key));
|
||||
}
|
||||
|
||||
@JS('crypto.subtle.deriveKey')
|
||||
external dynamic _deriveKey(
|
||||
dynamic algorithm,
|
||||
dynamic baseKey,
|
||||
dynamic derivedKeyAlgorithm,
|
||||
bool extractable,
|
||||
List<String> keyUsages,
|
||||
);
|
||||
|
||||
Future<ByteBuffer> deriveKey(
|
||||
dynamic algorithm,
|
||||
dynamic baseKey,
|
||||
dynamic derivedKeyAlgorithm,
|
||||
bool extractable,
|
||||
List<String> keyUsages,
|
||||
) {
|
||||
return promiseToFuture(
|
||||
_deriveKey(
|
||||
algorithm,
|
||||
baseKey,
|
||||
derivedKeyAlgorithm,
|
||||
extractable,
|
||||
keyUsages,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@JS('crypto.subtle.deriveBits')
|
||||
external dynamic _deriveBits(dynamic algorithm, dynamic baseKey, int length);
|
||||
|
||||
Future<ByteBuffer> deriveBits(dynamic algorithm, dynamic baseKey, int length) {
|
||||
return promiseToFuture(_deriveBits(algorithm, baseKey, length));
|
||||
}
|
||||
|
||||
@JS('crypto.subtle.digest')
|
||||
external dynamic _digest(String algorithm, Uint8List data);
|
||||
|
||||
Future<ByteBuffer> digest(String algorithm, Uint8List data) {
|
||||
return promiseToFuture(_digest(algorithm, data));
|
||||
}
|
||||
|
|
@ -299,5 +299,6 @@ abstract class EventLocalizations {
|
|||
?.tryGet<String>('key') ??
|
||||
body,
|
||||
),
|
||||
EventTypes.refreshingLastEvent: (_, i18n, ___) => i18n.refreshingLastEvent,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -318,4 +318,7 @@ class MatrixDefaultLocalizations extends MatrixLocalizations {
|
|||
: '${duration.inMinutes.toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')} ';
|
||||
return '$senderName: ${durationString}Voice message';
|
||||
}
|
||||
|
||||
@override
|
||||
String get refreshingLastEvent => 'Refreshing last event...';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ abstract class MatrixLocalizations {
|
|||
|
||||
String get cancelledSend;
|
||||
|
||||
String get refreshingLastEvent;
|
||||
|
||||
String youInvitedBy(String senderName);
|
||||
|
||||
String invitedBy(String senderName);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
extension ToBytesWithProgress on http.ByteStream {
|
||||
/// Collects the data of this stream in a [Uint8List].
|
||||
Future<Uint8List> toBytesWithProgress(void Function(int)? onProgress) {
|
||||
var length = 0;
|
||||
final completer = Completer<Uint8List>();
|
||||
final sink = ByteConversionSink.withCallback(
|
||||
(bytes) => completer.complete(Uint8List.fromList(bytes)),
|
||||
);
|
||||
listen(
|
||||
(bytes) {
|
||||
sink.add(bytes);
|
||||
onProgress?.call(length += bytes.length);
|
||||
},
|
||||
onError: completer.completeError,
|
||||
onDone: sink.close,
|
||||
cancelOnError: true,
|
||||
);
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
|
|
@ -155,7 +155,10 @@ class NativeImplementationsIsolate extends NativeImplementations {
|
|||
bool retryInDummy = true,
|
||||
}) {
|
||||
return runInBackground<Uint8List?, EncryptedFile>(
|
||||
NativeImplementations.dummy.decryptFile,
|
||||
(EncryptedFile args) async {
|
||||
await vodozemacInit?.call();
|
||||
return NativeImplementations.dummy.decryptFile(args);
|
||||
},
|
||||
file,
|
||||
);
|
||||
}
|
||||
|
|
@ -180,7 +183,10 @@ class NativeImplementationsIsolate extends NativeImplementations {
|
|||
bool retryInDummy = true,
|
||||
}) {
|
||||
return runInBackground<Uint8List, KeyFromPassphraseArgs>(
|
||||
NativeImplementations.dummy.keyFromPassphrase,
|
||||
(KeyFromPassphraseArgs args) async {
|
||||
await vodozemacInit?.call();
|
||||
return NativeImplementations.dummy.keyFromPassphrase(args);
|
||||
},
|
||||
args,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:html';
|
||||
import 'dart:js_interop';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:web/web.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class NativeImplementationsWebWorker extends NativeImplementations {
|
||||
|
|
@ -23,8 +25,8 @@ class NativeImplementationsWebWorker extends NativeImplementations {
|
|||
Uri href, {
|
||||
this.timeout = const Duration(seconds: 30),
|
||||
this.onStackTrace = defaultStackTraceHandler,
|
||||
}) : worker = Worker(href.toString()) {
|
||||
worker.onMessage.listen(_handleIncomingMessage);
|
||||
}) : worker = Worker(href.toString().toJS) {
|
||||
worker.onmessage = _handleIncomingMessage.toJS;
|
||||
}
|
||||
|
||||
Future<T> operation<T, U>(WebWorkerOperations name, U argument) async {
|
||||
|
|
@ -32,27 +34,26 @@ class NativeImplementationsWebWorker extends NativeImplementations {
|
|||
final completer = Completer<T>();
|
||||
_completers[label] = completer;
|
||||
final message = WebWorkerData(label, name, argument);
|
||||
worker.postMessage(message.toJson());
|
||||
worker.postMessage(message.toJson().jsify());
|
||||
|
||||
return completer.future.timeout(timeout);
|
||||
}
|
||||
|
||||
Future<void> _handleIncomingMessage(MessageEvent event) async {
|
||||
final data = event.data;
|
||||
void _handleIncomingMessage(MessageEvent event) async {
|
||||
final data = event.data.dartify() as LinkedHashMap;
|
||||
// don't forget handling errors of our second thread...
|
||||
if (data['label'] == 'stacktrace') {
|
||||
final origin = event.data['origin'];
|
||||
final origin = data['origin'];
|
||||
final completer = _completers[origin];
|
||||
|
||||
final error = event.data['error']!;
|
||||
final error = data['error']!;
|
||||
|
||||
final stackTrace =
|
||||
await onStackTrace.call(event.data['stacktrace'] as String);
|
||||
final stackTrace = await onStackTrace.call(data['stacktrace'] as String);
|
||||
completer?.completeError(
|
||||
WebWorkerError(error: error, stackTrace: stackTrace),
|
||||
);
|
||||
} else {
|
||||
final response = WebWorkerData.fromJson(event.data);
|
||||
final response = WebWorkerData.fromJson(data);
|
||||
_completers[response.label]!.complete(response.data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
// ignore_for_file: avoid_print
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:html';
|
||||
import 'dart:indexed_db';
|
||||
import 'dart:js';
|
||||
import 'dart:collection';
|
||||
import 'dart:js_interop';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:js/js.dart';
|
||||
import 'package:js/js_util.dart';
|
||||
import 'package:web/web.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart' hide Event;
|
||||
import 'package:matrix/src/utils/web_worker/native_implementations_web_worker.dart';
|
||||
|
|
@ -32,63 +30,73 @@ import 'package:matrix/src/utils/web_worker/native_implementations_web_worker.da
|
|||
/// the web worker in your CI pipeline.
|
||||
///
|
||||
|
||||
DedicatedWorkerGlobalScope get _workerScope =>
|
||||
(globalContext as DedicatedWorkerGlobalScope).self
|
||||
as DedicatedWorkerGlobalScope;
|
||||
|
||||
@pragma('dart2js:tryInline')
|
||||
Future<void> startWebWorker() async {
|
||||
print('[native implementations worker]: Starting...');
|
||||
setProperty(
|
||||
context['self'] as Object,
|
||||
'onmessage',
|
||||
allowInterop(
|
||||
(MessageEvent event) async {
|
||||
final data = event.data;
|
||||
try {
|
||||
final operation = WebWorkerData.fromJson(data);
|
||||
switch (operation.name) {
|
||||
case WebWorkerOperations.shrinkImage:
|
||||
final result = MatrixImageFile.resizeImplementation(
|
||||
MatrixImageFileResizeArguments.fromJson(
|
||||
Map.from(operation.data as Map),
|
||||
),
|
||||
);
|
||||
sendResponse(operation.label as double, result?.toJson());
|
||||
break;
|
||||
case WebWorkerOperations.calcImageMetadata:
|
||||
final result = MatrixImageFile.calcMetadataImplementation(
|
||||
Uint8List.fromList(
|
||||
(operation.data as JsArray).whereType<int>().toList(),
|
||||
),
|
||||
);
|
||||
sendResponse(operation.label as double, result?.toJson());
|
||||
break;
|
||||
default:
|
||||
throw TypeError();
|
||||
}
|
||||
} on Event catch (e, s) {
|
||||
allowInterop(_replyError)
|
||||
.call((e.target as Request).error, s, data['label'] as double);
|
||||
} catch (e, s) {
|
||||
allowInterop(_replyError).call(e, s, data['label'] as double);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
Logs().i('[native implementations worker]: Starting...');
|
||||
_workerScope.onmessage = (MessageEvent event) {
|
||||
final data = event.data.dartify() as LinkedHashMap;
|
||||
try {
|
||||
final operation = WebWorkerData.fromJson(data);
|
||||
switch (operation.name) {
|
||||
case WebWorkerOperations.shrinkImage:
|
||||
final result = MatrixImageFile.resizeImplementation(
|
||||
MatrixImageFileResizeArguments.fromJson(
|
||||
Map.from(operation.data as Map),
|
||||
),
|
||||
);
|
||||
_sendResponse(
|
||||
operation.label as double,
|
||||
result?.toJson(),
|
||||
);
|
||||
break;
|
||||
case WebWorkerOperations.calcImageMetadata:
|
||||
final result = MatrixImageFile.calcMetadataImplementation(
|
||||
Uint8List.fromList(
|
||||
(operation.data as List).whereType<int>().toList(),
|
||||
),
|
||||
);
|
||||
_sendResponse(
|
||||
operation.label as double,
|
||||
result?.toJson(),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw TypeError();
|
||||
}
|
||||
} catch (e, s) {
|
||||
_replyError(e, s, data['label'] as double);
|
||||
}
|
||||
}.toJS;
|
||||
}
|
||||
|
||||
void sendResponse(double label, dynamic response) {
|
||||
void _sendResponse(
|
||||
double label,
|
||||
dynamic response,
|
||||
) {
|
||||
try {
|
||||
self.postMessage({
|
||||
'label': label,
|
||||
'data': response,
|
||||
});
|
||||
_workerScope.postMessage(
|
||||
{
|
||||
'label': label,
|
||||
'data': response,
|
||||
}.jsify(),
|
||||
);
|
||||
} catch (e, s) {
|
||||
print('[native implementations worker] Error responding: $e, $s');
|
||||
Logs().e('[native implementations worker] Error responding: $e, $s');
|
||||
}
|
||||
}
|
||||
|
||||
void _replyError(Object? error, StackTrace stackTrace, double origin) {
|
||||
void _replyError(
|
||||
Object? error,
|
||||
StackTrace stackTrace,
|
||||
double origin,
|
||||
) {
|
||||
if (error != null) {
|
||||
try {
|
||||
final jsError = jsify(error);
|
||||
final jsError = error.jsify();
|
||||
if (jsError != null) {
|
||||
error = jsError;
|
||||
}
|
||||
|
|
@ -97,24 +105,15 @@ void _replyError(Object? error, StackTrace stackTrace, double origin) {
|
|||
}
|
||||
}
|
||||
try {
|
||||
self.postMessage({
|
||||
'label': 'stacktrace',
|
||||
'origin': origin,
|
||||
'error': error,
|
||||
'stacktrace': stackTrace.toString(),
|
||||
});
|
||||
_workerScope.postMessage(
|
||||
{
|
||||
'label': 'stacktrace',
|
||||
'origin': origin,
|
||||
'error': error,
|
||||
'stacktrace': stackTrace.toString(),
|
||||
}.jsify(),
|
||||
);
|
||||
} catch (e, s) {
|
||||
print('[native implementations worker] Error responding: $e, $s');
|
||||
}
|
||||
}
|
||||
|
||||
/// represents the [WorkerGlobalScope] the worker currently runs in.
|
||||
@JS('self')
|
||||
external WorkerGlobalScope get self;
|
||||
|
||||
/// adding all missing WebWorker-only properties to the [WorkerGlobalScope]
|
||||
extension on WorkerGlobalScope {
|
||||
void postMessage(Object data) {
|
||||
callMethod(self, 'postMessage', [jsify(data)]);
|
||||
Logs().e('[native implementations worker] Error responding: $e, $s');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -197,7 +197,6 @@ class GroupCallSession {
|
|||
}
|
||||
return room.removeFamedlyCallMemberEvent(
|
||||
groupCallId,
|
||||
client.deviceID!,
|
||||
voip,
|
||||
application: application,
|
||||
scope: scope,
|
||||
|
|
|
|||
|
|
@ -121,12 +121,14 @@ extension FamedlyCallMemberEventsExtension on Room {
|
|||
await setFamedlyCallMemberEvent(
|
||||
newContent,
|
||||
callMembership.voip,
|
||||
callMembership.callId,
|
||||
application: callMembership.application,
|
||||
scope: callMembership.scope,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> removeFamedlyCallMemberEvent(
|
||||
String groupCallId,
|
||||
String deviceId,
|
||||
VoIP voip, {
|
||||
String? application = 'm.call',
|
||||
String? scope = 'm.room',
|
||||
|
|
@ -140,7 +142,7 @@ extension FamedlyCallMemberEventsExtension on Room {
|
|||
ownMemberships.removeWhere(
|
||||
(mem) =>
|
||||
mem.callId == groupCallId &&
|
||||
mem.deviceId == deviceId &&
|
||||
mem.deviceId == client.deviceID! &&
|
||||
mem.application == application &&
|
||||
mem.scope == scope,
|
||||
);
|
||||
|
|
@ -148,7 +150,13 @@ extension FamedlyCallMemberEventsExtension on Room {
|
|||
final newContent = {
|
||||
'memberships': List.from(ownMemberships.map((e) => e.toJson())),
|
||||
};
|
||||
await setFamedlyCallMemberEvent(newContent, voip);
|
||||
await setFamedlyCallMemberEvent(
|
||||
newContent,
|
||||
voip,
|
||||
groupCallId,
|
||||
application: application,
|
||||
scope: scope,
|
||||
);
|
||||
|
||||
_restartDelayedLeaveEventTimer?.cancel();
|
||||
if (_delayedLeaveEventId != null) {
|
||||
|
|
@ -163,7 +171,10 @@ extension FamedlyCallMemberEventsExtension on Room {
|
|||
Future<void> setFamedlyCallMemberEvent(
|
||||
Map<String, List> newContent,
|
||||
VoIP voip,
|
||||
) async {
|
||||
String groupCallId, {
|
||||
String? application = 'm.call',
|
||||
String? scope = 'm.room',
|
||||
}) async {
|
||||
if (canJoinGroupCall) {
|
||||
final stateKey = (roomVersion?.contains('msc3757') ?? false)
|
||||
? '${client.userID!}_${client.deviceID!}'
|
||||
|
|
@ -175,7 +186,7 @@ extension FamedlyCallMemberEventsExtension on Room {
|
|||
|
||||
/// can use delayed events and haven't used it yet
|
||||
if (useDelayedEvents && _delayedLeaveEventId == null) {
|
||||
// get existing ones
|
||||
// get existing ones and cancel them
|
||||
final List<ScheduledDelayedEvent> alreadyScheduledEvents = [];
|
||||
String? nextBatch;
|
||||
final sEvents = await client.getScheduledDelayedEvents();
|
||||
|
|
@ -200,14 +211,39 @@ extension FamedlyCallMemberEventsExtension on Room {
|
|||
);
|
||||
}
|
||||
|
||||
Map<String, List> newContent;
|
||||
if (roomVersion?.contains('msc3757') ?? false) {
|
||||
// scoped to deviceIds so clear the whole mems list
|
||||
newContent = {
|
||||
'memberships': [],
|
||||
};
|
||||
} else {
|
||||
// only clear our own deviceId
|
||||
final ownMemberships = getCallMembershipsForUser(
|
||||
client.userID!,
|
||||
client.deviceID!,
|
||||
voip,
|
||||
);
|
||||
|
||||
ownMemberships.removeWhere(
|
||||
(mem) =>
|
||||
mem.callId == groupCallId &&
|
||||
mem.deviceId == client.deviceID! &&
|
||||
mem.application == application &&
|
||||
mem.scope == scope,
|
||||
);
|
||||
|
||||
newContent = {
|
||||
'memberships': List.from(ownMemberships.map((e) => e.toJson())),
|
||||
};
|
||||
}
|
||||
|
||||
_delayedLeaveEventId = await client.setRoomStateWithKeyWithDelay(
|
||||
id,
|
||||
EventTypes.GroupCallMember,
|
||||
stateKey,
|
||||
voip.timeouts!.delayedEventApplyLeave.inMilliseconds,
|
||||
{
|
||||
'memberships': [],
|
||||
},
|
||||
newContent,
|
||||
);
|
||||
|
||||
_restartDelayedLeaveEventTimer = Timer.periodic(
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class WrappedMediaStream {
|
|||
|
||||
Future<void> dispose() async {
|
||||
// AOT it
|
||||
const isWeb = bool.fromEnvironment('dart.library.js_util');
|
||||
const isWeb = bool.fromEnvironment('dart.library.js_interop');
|
||||
|
||||
// libwebrtc does not provide a way to clone MediaStreams. So stopping the
|
||||
// local stream here would break calls with all other participants if anyone
|
||||
|
|
|
|||
|
|
@ -14,14 +14,10 @@ dependencies:
|
|||
blurhash_dart: ^1.1.0
|
||||
canonical_json: ^1.1.0
|
||||
collection: ^1.15.0
|
||||
crypto: ^3.0.0
|
||||
enhanced_enum: ^0.2.4
|
||||
ffi: ^2.0.0
|
||||
html: ^0.15.0
|
||||
html_unescape: ^2.0.0
|
||||
http: ">=0.13.0 <2.0.0"
|
||||
image: ^4.0.15
|
||||
js: ^0.6.3
|
||||
markdown: ^7.1.1
|
||||
mime: ">=1.0.0 <3.0.0"
|
||||
path: ^1.9.1
|
||||
|
|
@ -31,7 +27,8 @@ dependencies:
|
|||
sqflite_common: ^2.4.5
|
||||
sqlite3: ^2.1.0
|
||||
typed_data: ^1.3.2
|
||||
vodozemac: ^0.2.0
|
||||
vodozemac: ^0.3.0
|
||||
web: ^1.1.1
|
||||
webrtc_interface: ^1.2.0
|
||||
|
||||
dev_dependencies:
|
||||
|
|
@ -41,4 +38,4 @@ dev_dependencies:
|
|||
import_sorter: ^4.6.0
|
||||
lints: ^5.0.0
|
||||
sqflite_common_ffi: ^2.3.4+4 # sqflite_common_ffi aggressively requires newer dart versions
|
||||
test: ^1.25.13
|
||||
test: ^1.25.13
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
rm -rf rust
|
||||
git clone https://github.com/famedly/dart-vodozemac.git
|
||||
version=$(yq ".dependencies.vodozemac" < pubspec.yaml)
|
||||
version=$(expr "$version" : '\^*\(.*\)')
|
||||
git clone https://github.com/famedly/dart-vodozemac.git -b ${version}
|
||||
mv ./dart-vodozemac/rust ./
|
||||
rm -rf dart-vodozemac
|
||||
cd ./rust
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:matrix/src/database/indexeddb_box.dart'
|
||||
if (dart.library.io) 'package:matrix/src/database/sqflite_box.dart';
|
||||
import 'package:matrix/src/database/sqflite_box.dart'
|
||||
if (dart.library.js_interop) 'package:matrix/src/database/indexeddb_box.dart';
|
||||
|
||||
void main() {
|
||||
group('Box tests', () {
|
||||
|
|
@ -11,7 +11,7 @@ void main() {
|
|||
const data = {'name': 'Fluffy', 'age': 2};
|
||||
const data2 = {'name': 'Loki', 'age': 4};
|
||||
Database? db;
|
||||
const isWeb = bool.fromEnvironment('dart.library.js_util');
|
||||
const isWeb = bool.fromEnvironment('dart.library.js_interop');
|
||||
setUp(() async {
|
||||
if (!isWeb) {
|
||||
db = await databaseFactoryFfi.openDatabase(':memory:');
|
||||
|
|
|
|||
|
|
@ -201,26 +201,26 @@ void main() {
|
|||
matrix.getDirectChatFromUserId('@bob:example.com'),
|
||||
'!726s6s6q:example.com',
|
||||
);
|
||||
expect(matrix.rooms[2].directChatMatrixID, '@bob:example.com');
|
||||
expect(matrix.rooms[1].directChatMatrixID, '@bob:example.com');
|
||||
expect(matrix.directChats, matrix.accountData['m.direct']?.content);
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
expect(matrix.presences.length, 1);
|
||||
expect(matrix.rooms[2].ephemerals.length, 2);
|
||||
expect(matrix.rooms[2].typingUsers.length, 1);
|
||||
expect(matrix.rooms[2].typingUsers[0].id, '@alice:example.com');
|
||||
expect(matrix.rooms[2].roomAccountData.length, 3);
|
||||
expect(matrix.rooms[2].encrypted, true);
|
||||
expect(matrix.rooms[1].ephemerals.length, 2);
|
||||
expect(matrix.rooms[1].typingUsers.length, 1);
|
||||
expect(matrix.rooms[1].typingUsers[0].id, '@alice:example.com');
|
||||
expect(matrix.rooms[1].roomAccountData.length, 3);
|
||||
expect(matrix.rooms[1].encrypted, true);
|
||||
expect(
|
||||
matrix.rooms[2].encryptionAlgorithm,
|
||||
matrix.rooms[1].encryptionAlgorithm,
|
||||
Client.supportedGroupEncryptionAlgorithms.first,
|
||||
);
|
||||
expect(
|
||||
matrix
|
||||
.rooms[2].receiptState.global.otherUsers['@alice:example.com']?.ts,
|
||||
.rooms[1].receiptState.global.otherUsers['@alice:example.com']?.ts,
|
||||
1436451550453,
|
||||
);
|
||||
expect(
|
||||
matrix.rooms[2].receiptState.global.otherUsers['@alice:example.com']
|
||||
matrix.rooms[1].receiptState.global.otherUsers['@alice:example.com']
|
||||
?.eventId,
|
||||
'\$7365636s6r6432:example.com',
|
||||
);
|
||||
|
|
@ -231,7 +231,7 @@ void main() {
|
|||
expect(inviteRoom.states[EventTypes.RoomMember]?.length, 1);
|
||||
expect(matrix.rooms.length, 3);
|
||||
expect(
|
||||
matrix.rooms[2].canonicalAlias,
|
||||
matrix.rooms[1].canonicalAlias,
|
||||
"#famedlyContactDiscovery:${matrix.userID!.split(":")[1]}",
|
||||
);
|
||||
expect(
|
||||
|
|
@ -1488,8 +1488,10 @@ void main() {
|
|||
});
|
||||
test('upload', () async {
|
||||
final client = await getClient();
|
||||
final response =
|
||||
await client.uploadContent(Uint8List(0), filename: 'file.jpeg');
|
||||
final response = await client.uploadContent(
|
||||
Uint8List(0),
|
||||
filename: 'file.jpeg',
|
||||
);
|
||||
expect(response.toString(), 'mxc://example.com/AQwafuaFswefuhsfAFAgsw');
|
||||
expect(
|
||||
await client.database.getFile(response) != null,
|
||||
|
|
|
|||
|
|
@ -243,6 +243,9 @@ void main() {
|
|||
expect(sent, {
|
||||
'msgtype': 'm.text',
|
||||
'body': '> <@test:fakeServer.notExisting> reply\n\nreply',
|
||||
'm.mentions': {
|
||||
'user_ids': ['@test:fakeServer.notExisting'],
|
||||
},
|
||||
'format': 'org.matrix.custom.html',
|
||||
'formatted_body':
|
||||
'<mx-reply><blockquote><a href="https://matrix.to/#/!1234:fakeServer.notExisting/\$parent_event">In reply to</a> <a href="https://matrix.to/#/@test:fakeServer.notExisting">@test:fakeServer.notExisting</a><br>reply</blockquote></mx-reply>reply',
|
||||
|
|
|
|||
|
|
@ -48,462 +48,476 @@ class MockSSSS extends SSSS {
|
|||
}
|
||||
|
||||
void main() {
|
||||
group('SSSS', tags: 'olm', () {
|
||||
Logs().level = Level.error;
|
||||
group(
|
||||
'SSSS',
|
||||
tags: 'olm',
|
||||
() {
|
||||
Logs().level = Level.error;
|
||||
|
||||
late Client client;
|
||||
late Client client;
|
||||
|
||||
setUpAll(() async {
|
||||
await vod.init(
|
||||
wasmPath: './pkg/',
|
||||
libraryPath: './rust/target/debug/',
|
||||
);
|
||||
setUpAll(() async {
|
||||
await vod.init(
|
||||
wasmPath: './pkg/',
|
||||
libraryPath: './rust/target/debug/',
|
||||
);
|
||||
|
||||
client = await getClient();
|
||||
});
|
||||
|
||||
test('basic things', () async {
|
||||
expect(
|
||||
client.encryption!.ssss.defaultKeyId,
|
||||
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3',
|
||||
);
|
||||
});
|
||||
|
||||
test('encrypt / decrypt', () async {
|
||||
final key = Uint8List.fromList(secureRandomBytes(32));
|
||||
|
||||
final enc = await SSSS.encryptAes('secret foxies', key, 'name');
|
||||
final dec = await SSSS.decryptAes(enc, key, 'name');
|
||||
expect(dec, 'secret foxies');
|
||||
});
|
||||
|
||||
test('store', () async {
|
||||
final handle = client.encryption!.ssss.open();
|
||||
var failed = false;
|
||||
try {
|
||||
await handle.unlock(passphrase: 'invalid');
|
||||
} catch (_) {
|
||||
failed = true;
|
||||
}
|
||||
expect(failed, true);
|
||||
expect(handle.isUnlocked, false);
|
||||
failed = false;
|
||||
try {
|
||||
await handle.unlock(recoveryKey: 'invalid');
|
||||
} catch (_) {
|
||||
failed = true;
|
||||
}
|
||||
expect(failed, true);
|
||||
expect(handle.isUnlocked, false);
|
||||
await handle.unlock(passphrase: ssssPassphrase);
|
||||
await handle.unlock(recoveryKey: ssssKey);
|
||||
expect(handle.isUnlocked, true);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
|
||||
// OpenSSSS store waits for accountdata to be updated before returning
|
||||
// but we can't update that before the below endpoint is not hit.
|
||||
await handle.ssss
|
||||
.store('best animal', 'foxies', handle.keyId, handle.privateKey!);
|
||||
|
||||
final content = FakeMatrixApi
|
||||
.calledEndpoints[
|
||||
'/client/v3/user/%40test%3AfakeServer.notExisting/account_data/best%20animal']!
|
||||
.first;
|
||||
client.accountData['best animal'] = BasicEvent.fromJson({
|
||||
'type': 'best animal',
|
||||
'content': json.decode(content),
|
||||
client = await getClient();
|
||||
});
|
||||
expect(await handle.getStored('best animal'), 'foxies');
|
||||
});
|
||||
|
||||
test('encode / decode recovery key', () async {
|
||||
final key = Uint8List.fromList(secureRandomBytes(32));
|
||||
final encoded = SSSS.encodeRecoveryKey(key);
|
||||
var decoded = SSSS.decodeRecoveryKey(encoded);
|
||||
expect(key, decoded);
|
||||
test('basic things', () async {
|
||||
expect(
|
||||
client.encryption!.ssss.defaultKeyId,
|
||||
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3',
|
||||
);
|
||||
});
|
||||
|
||||
decoded = SSSS.decodeRecoveryKey('$encoded \n\t');
|
||||
expect(key, decoded);
|
||||
test('encrypt / decrypt', () async {
|
||||
final key = Uint8List.fromList(secureRandomBytes(32));
|
||||
|
||||
final handle = client.encryption!.ssss.open();
|
||||
await handle.unlock(recoveryKey: ssssKey);
|
||||
expect(handle.recoveryKey, ssssKey);
|
||||
});
|
||||
final enc = await SSSS.encryptAes('secret foxies', key, 'name');
|
||||
final dec = await SSSS.decryptAes(enc, key, 'name');
|
||||
expect(dec, 'secret foxies');
|
||||
});
|
||||
|
||||
test('cache', () async {
|
||||
await client.encryption!.ssss.clearCache();
|
||||
final handle =
|
||||
client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning);
|
||||
await handle.unlock(recoveryKey: ssssKey, postUnlock: false);
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningSelfSigning)) !=
|
||||
null,
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningUserSigning)) !=
|
||||
null,
|
||||
false,
|
||||
);
|
||||
await handle.getStored(EventTypes.CrossSigningSelfSigning);
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningSelfSigning)) !=
|
||||
null,
|
||||
true,
|
||||
);
|
||||
await handle.maybeCacheAll();
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningUserSigning)) !=
|
||||
null,
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
(await client.encryption!.ssss.getCached(EventTypes.MegolmBackup)) !=
|
||||
null,
|
||||
true,
|
||||
);
|
||||
});
|
||||
test('store', () async {
|
||||
final handle = client.encryption!.ssss.open();
|
||||
var failed = false;
|
||||
try {
|
||||
await handle.unlock(passphrase: 'invalid');
|
||||
} catch (_) {
|
||||
failed = true;
|
||||
}
|
||||
expect(failed, true);
|
||||
expect(handle.isUnlocked, false);
|
||||
failed = false;
|
||||
try {
|
||||
await handle.unlock(recoveryKey: 'invalid');
|
||||
} catch (_) {
|
||||
failed = true;
|
||||
}
|
||||
expect(failed, true);
|
||||
expect(handle.isUnlocked, false);
|
||||
await handle.unlock(passphrase: ssssPassphrase);
|
||||
await handle.unlock(recoveryKey: ssssKey);
|
||||
expect(handle.isUnlocked, true);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
|
||||
test('postUnlock', () async {
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.userDeviceKeys[client.userID!]!.masterKey!
|
||||
.setDirectVerified(false);
|
||||
final handle =
|
||||
client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning);
|
||||
await handle.unlock(recoveryKey: ssssKey);
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningSelfSigning)) !=
|
||||
null,
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningUserSigning)) !=
|
||||
null,
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
(await client.encryption!.ssss.getCached(EventTypes.MegolmBackup)) !=
|
||||
null,
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
client.userDeviceKeys[client.userID!]!.masterKey!.directVerified,
|
||||
true,
|
||||
);
|
||||
});
|
||||
// OpenSSSS store waits for accountdata to be updated before returning
|
||||
// but we can't update that before the below endpoint is not hit.
|
||||
await handle.ssss
|
||||
.store('best animal', 'foxies', handle.keyId, handle.privateKey!);
|
||||
|
||||
test('make share requests', () async {
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
|
||||
key.setDirectVerified(true);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.request('some.type', [key]);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
final content = FakeMatrixApi
|
||||
.calledEndpoints[
|
||||
'/client/v3/user/%40test%3AfakeServer.notExisting/account_data/best%20animal']!
|
||||
.first;
|
||||
client.accountData['best animal'] = BasicEvent.fromJson({
|
||||
'type': 'best animal',
|
||||
'content': json.decode(content),
|
||||
});
|
||||
expect(await handle.getStored('best animal'), 'foxies');
|
||||
});
|
||||
|
||||
test('answer to share requests', () async {
|
||||
var event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': EventTypes.CrossSigningSelfSigning,
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
|
||||
),
|
||||
true,
|
||||
);
|
||||
test('encode / decode recovery key', () async {
|
||||
final key = Uint8List.fromList(secureRandomBytes(32));
|
||||
final encoded = SSSS.encodeRecoveryKey(key);
|
||||
var decoded = SSSS.decodeRecoveryKey(encoded);
|
||||
expect(key, decoded);
|
||||
|
||||
// now test some fail scenarios
|
||||
decoded = SSSS.decodeRecoveryKey('$encoded \n\t');
|
||||
expect(key, decoded);
|
||||
|
||||
// not by us
|
||||
event = ToDeviceEvent(
|
||||
sender: '@someotheruser:example.org',
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': EventTypes.CrossSigningSelfSigning,
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
|
||||
),
|
||||
false,
|
||||
);
|
||||
final handle = client.encryption!.ssss.open();
|
||||
await handle.unlock(recoveryKey: ssssKey);
|
||||
expect(handle.recoveryKey, ssssKey);
|
||||
});
|
||||
|
||||
// secret not cached
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': 'm.unknown.secret',
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
|
||||
),
|
||||
false,
|
||||
);
|
||||
test('cache', () async {
|
||||
await client.encryption!.ssss.clearCache();
|
||||
final handle =
|
||||
client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning);
|
||||
await handle.unlock(recoveryKey: ssssKey, postUnlock: false);
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningSelfSigning)) !=
|
||||
null,
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningUserSigning)) !=
|
||||
null,
|
||||
false,
|
||||
);
|
||||
await handle.getStored(EventTypes.CrossSigningSelfSigning);
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningSelfSigning)) !=
|
||||
null,
|
||||
true,
|
||||
);
|
||||
await handle.maybeCacheAll();
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningUserSigning)) !=
|
||||
null,
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
(await client.encryption!.ssss.getCached(EventTypes.MegolmBackup)) !=
|
||||
null,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// is a cancelation
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request_cancellation',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': EventTypes.CrossSigningSelfSigning,
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
|
||||
),
|
||||
false,
|
||||
);
|
||||
test('postUnlock', () async {
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.userDeviceKeys[client.userID!]!.masterKey!
|
||||
.setDirectVerified(false);
|
||||
final handle =
|
||||
client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning);
|
||||
await handle.unlock(recoveryKey: ssssKey);
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningSelfSigning)) !=
|
||||
null,
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningUserSigning)) !=
|
||||
null,
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
(await client.encryption!.ssss.getCached(EventTypes.MegolmBackup)) !=
|
||||
null,
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
client.userDeviceKeys[client.userID!]!.masterKey!.directVerified,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// device not verified
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
|
||||
key.setDirectVerified(false);
|
||||
client.userDeviceKeys[client.userID!]!.masterKey!
|
||||
.setDirectVerified(false);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': EventTypes.CrossSigningSelfSigning,
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
|
||||
),
|
||||
false,
|
||||
);
|
||||
key.setDirectVerified(true);
|
||||
});
|
||||
test('make share requests', () async {
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
|
||||
key.setDirectVerified(true);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.request('some.type', [key]);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('receive share requests', () async {
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
|
||||
key.setDirectVerified(true);
|
||||
final handle =
|
||||
client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning);
|
||||
await handle.unlock(recoveryKey: ssssKey);
|
||||
test('answer to share requests', () async {
|
||||
var event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': EventTypes.CrossSigningSelfSigning,
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
|
||||
),
|
||||
true,
|
||||
);
|
||||
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request('best animal', [key]);
|
||||
var event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), 'foxies!');
|
||||
// now test some fail scenarios
|
||||
|
||||
// not by us
|
||||
event = ToDeviceEvent(
|
||||
sender: '@someotheruser:example.org',
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': EventTypes.CrossSigningSelfSigning,
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
|
||||
),
|
||||
false,
|
||||
);
|
||||
|
||||
// secret not cached
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': 'm.unknown.secret',
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
|
||||
),
|
||||
false,
|
||||
);
|
||||
|
||||
// is a cancelation
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request_cancellation',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': EventTypes.CrossSigningSelfSigning,
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
|
||||
),
|
||||
false,
|
||||
);
|
||||
|
||||
// device not verified
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
|
||||
key.setDirectVerified(false);
|
||||
client.userDeviceKeys[client.userID!]!.masterKey!
|
||||
.setDirectVerified(false);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': EventTypes.CrossSigningSelfSigning,
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
|
||||
),
|
||||
false,
|
||||
);
|
||||
key.setDirectVerified(true);
|
||||
});
|
||||
|
||||
test('receive share requests', () async {
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
|
||||
key.setDirectVerified(true);
|
||||
final handle =
|
||||
client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning);
|
||||
await handle.unlock(recoveryKey: ssssKey);
|
||||
|
||||
// test the different validators
|
||||
for (final type in [
|
||||
EventTypes.CrossSigningSelfSigning,
|
||||
EventTypes.CrossSigningUserSigning,
|
||||
EventTypes.MegolmBackup,
|
||||
]) {
|
||||
final secret = await handle.getStored(type);
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request(type, [key]);
|
||||
event = ToDeviceEvent(
|
||||
await client.encryption!.ssss.request('best animal', [key]);
|
||||
var event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id':
|
||||
client.encryption!.ssss.pendingShareRequests.keys.first,
|
||||
'secret': secret,
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached(type), secret);
|
||||
}
|
||||
expect(
|
||||
await client.encryption!.ssss.getCached('best animal'),
|
||||
'foxies!',
|
||||
);
|
||||
|
||||
// test different fail scenarios
|
||||
// test the different validators
|
||||
for (final type in [
|
||||
EventTypes.CrossSigningSelfSigning,
|
||||
EventTypes.CrossSigningUserSigning,
|
||||
EventTypes.MegolmBackup,
|
||||
]) {
|
||||
final secret = await handle.getStored(type);
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request(type, [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id':
|
||||
client.encryption!.ssss.pendingShareRequests.keys.first,
|
||||
'secret': secret,
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached(type), secret);
|
||||
}
|
||||
|
||||
// not encrypted
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), null);
|
||||
// test different fail scenarios
|
||||
|
||||
// unknown request id
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id': 'invalid',
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), null);
|
||||
// not encrypted
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id':
|
||||
client.encryption!.ssss.pendingShareRequests.keys.first,
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), null);
|
||||
|
||||
// not from a device we sent the request to
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': 'invalid',
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), null);
|
||||
// unknown request id
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id': 'invalid',
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), null);
|
||||
|
||||
// secret not a string
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
|
||||
'secret': 42,
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), null);
|
||||
// not from a device we sent the request to
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id':
|
||||
client.encryption!.ssss.pendingShareRequests.keys.first,
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': 'invalid',
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), null);
|
||||
|
||||
// validator doesn't check out
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request(EventTypes.MegolmBackup, [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
await client.encryption!.ssss.getCached(EventTypes.MegolmBackup),
|
||||
null,
|
||||
);
|
||||
});
|
||||
// secret not a string
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id':
|
||||
client.encryption!.ssss.pendingShareRequests.keys.first,
|
||||
'secret': 42,
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), null);
|
||||
|
||||
test('request all', () async {
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
|
||||
key.setDirectVerified(true);
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.maybeRequestAll([key]);
|
||||
expect(client.encryption!.ssss.pendingShareRequests.length, 3);
|
||||
});
|
||||
// validator doesn't check out
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request(EventTypes.MegolmBackup, [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id':
|
||||
client.encryption!.ssss.pendingShareRequests.keys.first,
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
await client.encryption!.ssss.getCached(EventTypes.MegolmBackup),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test('periodicallyRequestMissingCache', () async {
|
||||
client.userDeviceKeys[client.userID!]!.masterKey!.setDirectVerified(true);
|
||||
client.encryption!.ssss = MockSSSS(client.encryption!);
|
||||
(client.encryption!.ssss as MockSSSS).requestedSecrets = false;
|
||||
await client.encryption!.ssss.periodicallyRequestMissingCache();
|
||||
expect((client.encryption!.ssss as MockSSSS).requestedSecrets, true);
|
||||
// it should only retry once every 15 min
|
||||
(client.encryption!.ssss as MockSSSS).requestedSecrets = false;
|
||||
await client.encryption!.ssss.periodicallyRequestMissingCache();
|
||||
expect((client.encryption!.ssss as MockSSSS).requestedSecrets, false);
|
||||
});
|
||||
test('request all', () async {
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
|
||||
key.setDirectVerified(true);
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.maybeRequestAll([key]);
|
||||
expect(client.encryption!.ssss.pendingShareRequests.length, 3);
|
||||
});
|
||||
|
||||
test('createKey', () async {
|
||||
// with passphrase
|
||||
var newKey = await client.encryption!.ssss.createKey('test');
|
||||
expect(client.encryption!.ssss.isKeyValid(newKey.keyId), true);
|
||||
var testKey = client.encryption!.ssss.open(newKey.keyId);
|
||||
await testKey.unlock(passphrase: 'test');
|
||||
await testKey.setPrivateKey(newKey.privateKey!);
|
||||
test('periodicallyRequestMissingCache', () async {
|
||||
client.userDeviceKeys[client.userID!]!.masterKey!
|
||||
.setDirectVerified(true);
|
||||
client.encryption!.ssss = MockSSSS(client.encryption!);
|
||||
(client.encryption!.ssss as MockSSSS).requestedSecrets = false;
|
||||
await client.encryption!.ssss.periodicallyRequestMissingCache();
|
||||
expect((client.encryption!.ssss as MockSSSS).requestedSecrets, true);
|
||||
// it should only retry once every 15 min
|
||||
(client.encryption!.ssss as MockSSSS).requestedSecrets = false;
|
||||
await client.encryption!.ssss.periodicallyRequestMissingCache();
|
||||
expect((client.encryption!.ssss as MockSSSS).requestedSecrets, false);
|
||||
});
|
||||
|
||||
// without passphrase
|
||||
newKey = await client.encryption!.ssss.createKey();
|
||||
expect(client.encryption!.ssss.isKeyValid(newKey.keyId), true);
|
||||
testKey = client.encryption!.ssss.open(newKey.keyId);
|
||||
await testKey.setPrivateKey(newKey.privateKey!);
|
||||
});
|
||||
test('createKey', () async {
|
||||
// with passphrase
|
||||
var newKey = await client.encryption!.ssss.createKey('test');
|
||||
expect(client.encryption!.ssss.isKeyValid(newKey.keyId), true);
|
||||
var testKey = client.encryption!.ssss.open(newKey.keyId);
|
||||
await testKey.unlock(passphrase: 'test');
|
||||
await testKey.setPrivateKey(newKey.privateKey!);
|
||||
|
||||
test('dispose client', () async {
|
||||
await client.dispose(closeDatabase: true);
|
||||
});
|
||||
});
|
||||
// without passphrase
|
||||
newKey = await client.encryption!.ssss.createKey();
|
||||
expect(client.encryption!.ssss.isKeyValid(newKey.keyId), true);
|
||||
testKey = client.encryption!.ssss.open(newKey.keyId);
|
||||
await testKey.setPrivateKey(newKey.privateKey!);
|
||||
});
|
||||
|
||||
test('dispose client', () async {
|
||||
await client.dispose(closeDatabase: true);
|
||||
});
|
||||
},
|
||||
timeout: Timeout(const Duration(minutes: 2)),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2485,6 +2485,30 @@ void main() async {
|
|||
await room.client.dispose(closeDatabase: true);
|
||||
},
|
||||
);
|
||||
|
||||
test('downloadAndDecryptAttachment from server', () async {
|
||||
final client = await getClient();
|
||||
final event = Event(
|
||||
room: client.rooms.first,
|
||||
eventId: 'test',
|
||||
originServerTs: DateTime.now(),
|
||||
senderId: client.userID!,
|
||||
content: {
|
||||
'body': 'ascii.txt',
|
||||
'filename': 'ascii.txt',
|
||||
'info': {'mimetype': 'application/msword', 'size': 6},
|
||||
'msgtype': 'm.file',
|
||||
'url': 'mxc://example.org/abcd1234ascii',
|
||||
},
|
||||
type: EventTypes.Message,
|
||||
);
|
||||
final progressList = <int>[];
|
||||
await event.downloadAndDecryptAttachment(
|
||||
onDownloadProgress: progressList.add,
|
||||
);
|
||||
await client.dispose();
|
||||
expect(progressList, [112]);
|
||||
});
|
||||
test('downloadAndDecryptAttachment store', tags: 'olm', () async {
|
||||
final FILE_BUFF = Uint8List.fromList([0]);
|
||||
var serverHits = 0;
|
||||
|
|
@ -2951,5 +2975,28 @@ void main() async {
|
|||
Timeline(room: room, chunk: TimelineChunk(events: [targetEvent]));
|
||||
expect(await event.getReplyEvent(timeline), targetEvent);
|
||||
});
|
||||
test('getMentions', () {
|
||||
final event = Event.fromJson(
|
||||
{
|
||||
'content': {
|
||||
'msgtype': 'text',
|
||||
'body': 'Hello world @alice:matrix.org',
|
||||
'm.mentions': {
|
||||
'user_ids': ['@alice:matrix.org'],
|
||||
'room': false,
|
||||
},
|
||||
},
|
||||
'event_id': '\$143273582443PhrSn:example.org',
|
||||
'origin_server_ts': 1432735824653,
|
||||
'room_id': room.id,
|
||||
'sender': '@example:example.org',
|
||||
'type': 'm.room.message',
|
||||
'unsigned': {'age': 1234},
|
||||
},
|
||||
room,
|
||||
);
|
||||
expect(event.mentions.userIds, ['@alice:matrix.org']);
|
||||
expect(event.mentions.room, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,14 @@ import 'package:http/http.dart' as http;
|
|||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'fake_client.dart';
|
||||
|
||||
void main() {
|
||||
/// All Tests related to device keys
|
||||
group('Matrix File', tags: 'olm', () {
|
||||
setUpAll(() async {
|
||||
await getClient(); // To trigger vodozemac init
|
||||
});
|
||||
Logs().level = Level.error;
|
||||
test('Decrypt', () async {
|
||||
final text = 'hello world';
|
||||
|
|
|
|||
|
|
@ -883,6 +883,29 @@ void main() {
|
|||
expect(timeline.events.length, 17);
|
||||
});
|
||||
|
||||
test('Refresh last event', () async {
|
||||
expect(room.lastEvent?.eventId, '12');
|
||||
final lastEventUpdate =
|
||||
room.client.onSync.stream.firstWhere((u) => u.nextBatch.isEmpty);
|
||||
await room.client.handleSync(
|
||||
SyncUpdate(
|
||||
nextBatch: 'abcd',
|
||||
rooms: RoomsUpdate(
|
||||
join: {
|
||||
room.id: JoinedRoomUpdate(
|
||||
timeline: TimelineUpdate(
|
||||
events: [],
|
||||
limited: true,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
await lastEventUpdate;
|
||||
expect(room.lastEvent?.eventId, '3143273582443PhrSn:example.org');
|
||||
});
|
||||
|
||||
test('isFederated', () {
|
||||
expect(room.isFederated, true);
|
||||
room.setState(
|
||||
|
|
@ -1015,6 +1038,36 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
test('sendEvent with room mention', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
final resp = await room.sendTextEvent(
|
||||
'Hello world @room',
|
||||
txid: 'testtxid',
|
||||
addMentions: true,
|
||||
);
|
||||
expect(resp?.startsWith('\$event'), true);
|
||||
final entry = FakeMatrixApi.calledEndpoints.entries
|
||||
.firstWhere((p) => p.key.contains('/send/m.room.message/'));
|
||||
final content = json.decode(entry.value.first);
|
||||
expect(content['m.mentions'], {'room': true});
|
||||
});
|
||||
|
||||
test('sendEvent with user mention', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
final resp = await room.sendTextEvent(
|
||||
'Hello world @[Alice Margatroid]',
|
||||
addMentions: true,
|
||||
txid: 'testtxid',
|
||||
);
|
||||
expect(resp?.startsWith('\$event'), true);
|
||||
final entry = FakeMatrixApi.calledEndpoints.entries
|
||||
.firstWhere((p) => p.key.contains('/send/m.room.message/'));
|
||||
final content = json.decode(entry.value.first);
|
||||
expect(content['m.mentions'], {
|
||||
'user_ids': ['@alice:matrix.org'],
|
||||
});
|
||||
});
|
||||
|
||||
test('send edit', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
final dynamic resp = await room.sendTextEvent(
|
||||
|
|
@ -1066,6 +1119,9 @@ void main() {
|
|||
expect(content, {
|
||||
'body': '> <@alice:example.org> Blah\n\nHello world',
|
||||
'msgtype': 'm.text',
|
||||
'm.mentions': {
|
||||
'user_ids': ['@alice:example.org'],
|
||||
},
|
||||
'format': 'org.matrix.custom.html',
|
||||
'formatted_body':
|
||||
'<mx-reply><blockquote><a href="https://matrix.to/#/!localpart:server.abc/\$replyEvent">In reply to</a> <a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a><br>Blah</blockquote></mx-reply>Hello world',
|
||||
|
|
@ -1102,6 +1158,9 @@ void main() {
|
|||
'body':
|
||||
'> <@alice:example.org> <b>Blah</b>\n> beep\n\nHello world\nfox',
|
||||
'msgtype': 'm.text',
|
||||
'm.mentions': {
|
||||
'user_ids': ['@alice:example.org'],
|
||||
},
|
||||
'format': 'org.matrix.custom.html',
|
||||
'formatted_body':
|
||||
'<mx-reply><blockquote><a href="https://matrix.to/#/!localpart:server.abc/\$replyEvent">In reply to</a> <a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a><br><b>Blah</b><br>beep</blockquote></mx-reply>Hello world<br/>fox',
|
||||
|
|
@ -1139,6 +1198,9 @@ void main() {
|
|||
expect(content, {
|
||||
'body': '> <@alice:example.org> plaintext meow\n\nHello world',
|
||||
'msgtype': 'm.text',
|
||||
'm.mentions': {
|
||||
'user_ids': ['@alice:example.org'],
|
||||
},
|
||||
'format': 'org.matrix.custom.html',
|
||||
'formatted_body':
|
||||
'<mx-reply><blockquote><a href="https://matrix.to/#/!localpart:server.abc/\$replyEvent">In reply to</a> <a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a><br>meow</blockquote></mx-reply>Hello world',
|
||||
|
|
@ -1174,6 +1236,9 @@ void main() {
|
|||
expect(content, {
|
||||
'body': '> <@alice:example.org> Hey @\u{200b}room\n\nHello world',
|
||||
'msgtype': 'm.text',
|
||||
'm.mentions': {
|
||||
'user_ids': ['@alice:example.org'],
|
||||
},
|
||||
'format': 'org.matrix.custom.html',
|
||||
'formatted_body':
|
||||
'<mx-reply><blockquote><a href="https://matrix.to/#/!localpart:server.abc/\$replyEvent">In reply to</a> <a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a><br>Hey @room</blockquote></mx-reply>Hello world',
|
||||
|
|
@ -1191,6 +1256,9 @@ void main() {
|
|||
'content': {
|
||||
'body': '> <@alice:example.org> Hey\n\nHello world',
|
||||
'msgtype': 'm.text',
|
||||
'm.mentions': {
|
||||
'user_ids': ['@alice:example.org'],
|
||||
},
|
||||
'format': 'org.matrix.custom.html',
|
||||
'formatted_body':
|
||||
'<mx-reply><blockquote><a href="https://matrix.to/#/!localpart:server.abc/\$replyEvent">In reply to</a> <a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a><br>Hey</blockquote></mx-reply>Hello world',
|
||||
|
|
@ -1215,6 +1283,9 @@ void main() {
|
|||
expect(content, {
|
||||
'body': '> <@alice:example.org> Hello world\n\nFox',
|
||||
'msgtype': 'm.text',
|
||||
'm.mentions': {
|
||||
'user_ids': ['@alice:example.org'],
|
||||
},
|
||||
'format': 'org.matrix.custom.html',
|
||||
'formatted_body':
|
||||
'<mx-reply><blockquote><a href="https://matrix.to/#/!localpart:server.abc/\$replyEvent">In reply to</a> <a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a><br>Hello world</blockquote></mx-reply>Fox',
|
||||
|
|
@ -1273,7 +1344,7 @@ void main() {
|
|||
test('sendFileEvent', () async {
|
||||
final testFile = MatrixFile(bytes: Uint8List(0), name: 'file.jpeg');
|
||||
final resp = await room.sendFileEvent(testFile, txid: 'testtxid');
|
||||
expect(resp.toString(), '\$event10');
|
||||
expect(resp.toString(), '\$event12');
|
||||
});
|
||||
|
||||
test('pushRuleState', () async {
|
||||
|
|
|
|||
Loading…
Reference in New Issue