This commit is contained in:
OfficialDakari 2025-10-02 11:33:16 +05:00
commit 58c4cf19d0
44 changed files with 2695 additions and 1432 deletions

View File

@ -1,2 +1,2 @@
flutter_version=3.27.4
dart_version=3.6.2
flutter_version=3.35.4
dart_version=3.9.2

View File

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

View File

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

View File

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

View File

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

View File

@ -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
unawaited(
encryption.ssss
.maybeRequestAll(_verifiedDevices.whereType<DeviceKeys>().toList());
.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');
}

View File

@ -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':

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, {})),

View File

@ -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;
idbFactory ??= window.indexedDB;
final dbOpenCompleter = Completer<BoxCollection>();
final request = idbFactory.open(name, version);
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, autoIncrement: true);
}
},
db.createObjectStore(
name,
IDBObjectStoreParameters(autoIncrement: true),
);
return BoxCollection(db, boxNames, name);
}
}.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();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -299,5 +299,6 @@ abstract class EventLocalizations {
?.tryGet<String>('key') ??
body,
),
EventTypes.refreshingLastEvent: (_, i18n, ___) => i18n.refreshingLastEvent,
};
}

View File

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

View File

@ -62,6 +62,8 @@ abstract class MatrixLocalizations {
String get cancelledSend;
String get refreshingLastEvent;
String youInvitedBy(String senderName);
String invitedBy(String senderName);

View File

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

View File

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

View File

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

View File

@ -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,15 +30,15 @@ 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;
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) {
@ -50,45 +48,55 @@ Future<void> startWebWorker() async {
Map.from(operation.data as Map),
),
);
sendResponse(operation.label as double, result?.toJson());
_sendResponse(
operation.label as double,
result?.toJson(),
);
break;
case WebWorkerOperations.calcImageMetadata:
final result = MatrixImageFile.calcMetadataImplementation(
Uint8List.fromList(
(operation.data as JsArray).whereType<int>().toList(),
(operation.data as List).whereType<int>().toList(),
),
);
sendResponse(operation.label as double, result?.toJson());
_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);
_replyError(e, s, data['label'] as double);
}
},
),
);
}.toJS;
}
void sendResponse(double label, dynamic response) {
void _sendResponse(
double label,
dynamic response,
) {
try {
self.postMessage({
_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({
_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');
}
}

View File

@ -197,7 +197,6 @@ class GroupCallSession {
}
return room.removeFamedlyCallMemberEvent(
groupCallId,
client.deviceID!,
voip,
application: application,
scope: scope,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -48,7 +48,10 @@ class MockSSSS extends SSSS {
}
void main() {
group('SSSS', tags: 'olm', () {
group(
'SSSS',
tags: 'olm',
() {
Logs().level = Level.error;
late Client client;
@ -336,7 +339,8 @@ void main() {
sender: client.userID!,
type: 'm.secret.send',
content: {
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
'request_id':
client.encryption!.ssss.pendingShareRequests.keys.first,
'secret': 'foxies!',
},
encryptedContent: {
@ -344,7 +348,10 @@ void main() {
},
);
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(await client.encryption!.ssss.getCached('best animal'), 'foxies!');
expect(
await client.encryption!.ssss.getCached('best animal'),
'foxies!',
);
// test the different validators
for (final type in [
@ -382,7 +389,8 @@ void main() {
sender: client.userID!,
type: 'm.secret.send',
content: {
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
'request_id':
client.encryption!.ssss.pendingShareRequests.keys.first,
'secret': 'foxies!',
},
);
@ -415,7 +423,8 @@ void main() {
sender: client.userID!,
type: 'm.secret.send',
content: {
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
'request_id':
client.encryption!.ssss.pendingShareRequests.keys.first,
'secret': 'foxies!',
},
encryptedContent: {
@ -433,7 +442,8 @@ void main() {
sender: client.userID!,
type: 'm.secret.send',
content: {
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
'request_id':
client.encryption!.ssss.pendingShareRequests.keys.first,
'secret': 42,
},
encryptedContent: {
@ -451,7 +461,8 @@ void main() {
sender: client.userID!,
type: 'm.secret.send',
content: {
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
'request_id':
client.encryption!.ssss.pendingShareRequests.keys.first,
'secret': 'foxies!',
},
encryptedContent: {
@ -476,7 +487,8 @@ void main() {
});
test('periodicallyRequestMissingCache', () async {
client.userDeviceKeys[client.userID!]!.masterKey!.setDirectVerified(true);
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();
@ -505,5 +517,7 @@ void main() {
test('dispose client', () async {
await client.dispose(closeDatabase: true);
});
});
},
timeout: Timeout(const Duration(minutes: 2)),
);
}

View File

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

View File

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

View File

@ -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>&lt;b&gt;Blah&lt;&#47;b&gt;<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 {