Merge commit '84cc925b08e97098d00c54fff9c1244f91055de3' into yiffed
This commit is contained in:
commit
daccf50590
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -31,6 +31,7 @@ coverage_badge.svg
|
|||
.pub-cache/
|
||||
.pub/
|
||||
build/
|
||||
pubspec.lock
|
||||
|
||||
# Android related
|
||||
**/android/**/gradle-wrapper.jar
|
||||
|
|
|
@ -46,6 +46,30 @@ coverage_without_olm:
|
|||
- chmod +x ./test.sh
|
||||
- pub get
|
||||
- pub run test
|
||||
|
||||
e2ee_test:
|
||||
tags:
|
||||
- linux
|
||||
stage: coverage
|
||||
image: debian:testing
|
||||
dependencies: []
|
||||
script:
|
||||
- apt update
|
||||
- apt install -y curl gnupg2 git
|
||||
- curl https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
|
||||
- curl https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list > /etc/apt/sources.list.d/dart_stable.list
|
||||
- apt update
|
||||
- apt install -y dart chromium lcov libolm3 sqlite3 libsqlite3-dev
|
||||
- ln -s /usr/lib/dart/bin/pub /usr/bin/
|
||||
- useradd -m test
|
||||
- chown -R 'test:' '.'
|
||||
- chmod +x ./prepare.sh
|
||||
- chmod +x ./test_driver.sh
|
||||
- printf "abstract class TestUser {\n static const String homeserver = '$TEST_HOMESERVER';\n static const String username = '$TEST_USER1';\n static const String username2 = '$TEST_USER2';\n static const String password = '$TEST_USER_PASSWORD';\n}" > ./test_driver/test_config.dart
|
||||
- su -c ./prepare.sh test
|
||||
- su -c ./test_driver.sh test
|
||||
timeout: 16m
|
||||
resource_group: e2ee_test
|
||||
|
||||
code_analyze:
|
||||
tags:
|
||||
|
@ -57,7 +81,7 @@ code_analyze:
|
|||
- flutter format lib/ test/ test_driver/ --set-exit-if-changed
|
||||
- flutter analyze
|
||||
|
||||
build-api-doc:
|
||||
build_api_doc:
|
||||
tags:
|
||||
- docker
|
||||
stage: builddocs
|
||||
|
@ -68,9 +92,9 @@ build-api-doc:
|
|||
paths:
|
||||
- doc/api/
|
||||
only:
|
||||
- master
|
||||
- main
|
||||
|
||||
build-doc:
|
||||
build_doc:
|
||||
tags:
|
||||
- docker
|
||||
stage: builddocs
|
||||
|
@ -83,7 +107,7 @@ build-doc:
|
|||
paths:
|
||||
- doc-public
|
||||
only:
|
||||
- master
|
||||
- main
|
||||
|
||||
pages:
|
||||
tags:
|
||||
|
@ -95,10 +119,10 @@ pages:
|
|||
- mv doc-public ./home/doc
|
||||
- mv home public
|
||||
dependencies:
|
||||
- build-api-doc
|
||||
- build-doc
|
||||
- build_api_doc
|
||||
- build_doc
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
only:
|
||||
- master
|
||||
- main
|
|
@ -3,9 +3,10 @@ include: package:pedantic/analysis_options.yaml
|
|||
linter:
|
||||
rules:
|
||||
- camel_case_types
|
||||
- avoid_print
|
||||
|
||||
analyzer:
|
||||
errors:
|
||||
todo: ignore
|
||||
# exclude:
|
||||
# - path/to/excluded/files/**
|
||||
exclude:
|
||||
- example/main.dart
|
264
example/main.dart
Normal file
264
example/main.dart
Normal file
|
@ -0,0 +1,264 @@
|
|||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
runApp(FamedlySdkExampleApp());
|
||||
}
|
||||
|
||||
class FamedlySdkExampleApp extends StatelessWidget {
|
||||
static Client client = Client('Famedly SDK Example Client', debug: true);
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Famedly SDK Example App',
|
||||
home: LoginView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LoginView extends StatefulWidget {
|
||||
@override
|
||||
_LoginViewState createState() => _LoginViewState();
|
||||
}
|
||||
|
||||
class _LoginViewState extends State<LoginView> {
|
||||
final TextEditingController _homeserverController = TextEditingController();
|
||||
final TextEditingController _usernameController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
String _error;
|
||||
|
||||
void _loginAction() async {
|
||||
setState(() => _isLoading = true);
|
||||
setState(() => _error = null);
|
||||
try {
|
||||
if (await FamedlySdkExampleApp.client
|
||||
.checkServer(_homeserverController.text) ==
|
||||
false) {
|
||||
throw (Exception('Server not supported'));
|
||||
}
|
||||
if (await FamedlySdkExampleApp.client.login(
|
||||
_usernameController.text,
|
||||
_passwordController.text,
|
||||
) ==
|
||||
false) {
|
||||
throw (Exception('Username or password incorrect'));
|
||||
}
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (_) => ChatListView()),
|
||||
(route) => false,
|
||||
);
|
||||
} catch (e) {
|
||||
setState(() => _error = e.toString());
|
||||
}
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Login')),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
TextField(
|
||||
controller: _homeserverController,
|
||||
readOnly: _isLoading,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Homeserver',
|
||||
hintText: 'https://matrix.org',
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _usernameController,
|
||||
readOnly: _isLoading,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Username',
|
||||
hintText: '@username:domain',
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
readOnly: _isLoading,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
hintText: '****',
|
||||
errorText: _error,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
RaisedButton(
|
||||
child: _isLoading ? LinearProgressIndicator() : Text('Login'),
|
||||
onPressed: _isLoading ? null : _loginAction,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatListView extends StatefulWidget {
|
||||
@override
|
||||
_ChatListViewState createState() => _ChatListViewState();
|
||||
}
|
||||
|
||||
class _ChatListViewState extends State<ChatListView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Chats'),
|
||||
),
|
||||
body: StreamBuilder(
|
||||
stream: FamedlySdkExampleApp.client.onSync.stream,
|
||||
builder: (c, s) => ListView.builder(
|
||||
itemCount: FamedlySdkExampleApp.client.rooms.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
final room = FamedlySdkExampleApp.client.rooms[i];
|
||||
return ListTile(
|
||||
title: Text(room.displayname + ' (${room.notificationCount})'),
|
||||
subtitle: Text(room.lastMessage, maxLines: 1),
|
||||
leading: CircleAvatar(
|
||||
backgroundImage: NetworkImage(room.avatar.getThumbnail(
|
||||
FamedlySdkExampleApp.client,
|
||||
width: 64,
|
||||
height: 64,
|
||||
)),
|
||||
),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ChatView(room: room),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatView extends StatefulWidget {
|
||||
final Room room;
|
||||
|
||||
const ChatView({Key key, @required this.room}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ChatViewState createState() => _ChatViewState();
|
||||
}
|
||||
|
||||
class _ChatViewState extends State<ChatView> {
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
|
||||
void _sendAction() {
|
||||
print('Send Text');
|
||||
widget.room.sendTextEvent(_controller.text);
|
||||
_controller.clear();
|
||||
}
|
||||
|
||||
Timeline timeline;
|
||||
|
||||
Future<bool> getTimeline() async {
|
||||
timeline ??=
|
||||
await widget.room.getTimeline(onUpdate: () => setState(() => null));
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
timeline?.cancelSubscriptions();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: StreamBuilder<Object>(
|
||||
stream: widget.room.onUpdate.stream,
|
||||
builder: (context, snapshot) {
|
||||
return Text(widget.room.displayname);
|
||||
}),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FutureBuilder(
|
||||
future: getTimeline(),
|
||||
builder: (context, snapshot) => !snapshot.hasData
|
||||
? Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: ListView.builder(
|
||||
reverse: true,
|
||||
itemCount: timeline.events.length,
|
||||
itemBuilder: (BuildContext context, int i) => Opacity(
|
||||
opacity: timeline.events[i].status != 2 ? 0.5 : 1,
|
||||
child: ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
timeline.events[i].sender.calcDisplayname(),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
timeline.events[i].originServerTs
|
||||
.toIso8601String(),
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Text(timeline.events[i].body),
|
||||
leading: CircleAvatar(
|
||||
child: timeline.events[i].sender?.avatarUrl == null
|
||||
? Icon(Icons.person)
|
||||
: null,
|
||||
backgroundImage:
|
||||
timeline.events[i].sender?.avatarUrl != null
|
||||
? NetworkImage(
|
||||
timeline.events[i].sender?.avatarUrl
|
||||
?.getThumbnail(
|
||||
FamedlySdkExampleApp.client,
|
||||
width: 64,
|
||||
height: 64,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 60,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
labelText: 'Send a message ...',
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.send),
|
||||
onPressed: _sendAction,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
library encryption;
|
||||
|
||||
export './encryption/encryption.dart';
|
||||
export './encryption/key_manager.dart';
|
||||
export './encryption/ssss.dart';
|
||||
export './encryption/utils/key_verification.dart';
|
||||
export 'encryption/encryption.dart';
|
||||
export 'encryption/key_manager.dart';
|
||||
export 'encryption/ssss.dart';
|
||||
export 'encryption/utils/key_verification.dart';
|
||||
|
|
|
@ -16,12 +16,12 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:typed_data';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
|
||||
import '../famedlysdk.dart';
|
||||
import 'encryption.dart';
|
||||
|
||||
const SELF_SIGNING_KEY = 'm.cross_signing.self_signing';
|
||||
|
@ -167,7 +167,7 @@ class CrossSigning {
|
|||
}
|
||||
if (signedKeys.isNotEmpty) {
|
||||
// post our new keys!
|
||||
await client.api.uploadKeySignatures(signedKeys);
|
||||
await client.uploadKeySignatures(signedKeys);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,14 +17,18 @@
|
|||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
import 'key_manager.dart';
|
||||
import 'olm_manager.dart';
|
||||
import 'key_verification_manager.dart';
|
||||
|
||||
import '../famedlysdk.dart';
|
||||
import '../matrix_api.dart';
|
||||
import '../src/utils/run_in_root.dart';
|
||||
import '../src/utils/logs.dart';
|
||||
import 'cross_signing.dart';
|
||||
import 'key_manager.dart';
|
||||
import 'key_verification_manager.dart';
|
||||
import 'olm_manager.dart';
|
||||
import 'ssss.dart';
|
||||
|
||||
class Encryption {
|
||||
|
@ -61,10 +65,12 @@ class Encryption {
|
|||
|
||||
Future<void> init(String olmAccount) async {
|
||||
await olmManager.init(olmAccount);
|
||||
_backgroundTasksRunning = true;
|
||||
_backgroundTasks(); // start the background tasks
|
||||
}
|
||||
|
||||
void handleDeviceOneTimeKeysCount(Map<String, int> countJson) {
|
||||
olmManager.handleDeviceOneTimeKeysCount(countJson);
|
||||
runInRoot(() => olmManager.handleDeviceOneTimeKeysCount(countJson));
|
||||
}
|
||||
|
||||
void onSync() {
|
||||
|
@ -72,20 +78,29 @@ class Encryption {
|
|||
}
|
||||
|
||||
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
|
||||
if (['m.room_key', 'm.room_key_request', 'm.forwarded_room_key']
|
||||
.contains(event.type)) {
|
||||
// a new room key or thelike. We need to handle this asap, before other
|
||||
if (event.type == 'm.room_key') {
|
||||
// a new room key. We need to handle this asap, before other
|
||||
// events in /sync are handled
|
||||
await keyManager.handleToDeviceEvent(event);
|
||||
}
|
||||
if (['m.room_key_request', 'm.forwarded_room_key'].contains(event.type)) {
|
||||
// "just" room key request things. We don't need these asap, so we handle
|
||||
// them in the background
|
||||
unawaited(runInRoot(() => keyManager.handleToDeviceEvent(event)));
|
||||
}
|
||||
if (event.type.startsWith('m.key.verification.')) {
|
||||
// some key verification event. No need to handle it now, we can easily
|
||||
// do this in the background
|
||||
unawaited(keyVerificationManager.handleToDeviceEvent(event));
|
||||
unawaited(
|
||||
runInRoot(() => keyVerificationManager.handleToDeviceEvent(event)));
|
||||
}
|
||||
if (event.type.startsWith('m.secret.')) {
|
||||
// some ssss thing. We can do this in the background
|
||||
unawaited(ssss.handleToDeviceEvent(event));
|
||||
unawaited(runInRoot(() => ssss.handleToDeviceEvent(event)));
|
||||
}
|
||||
if (event.sender == client.userID) {
|
||||
// maybe we need to re-try SSSS secrets
|
||||
unawaited(runInRoot(() => ssss.periodicallyRequestMissingCache()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,7 +114,13 @@ class Encryption {
|
|||
update.content['content']['msgtype']
|
||||
.startsWith('m.key.verification.'))) {
|
||||
// "just" key verification, no need to do this in sync
|
||||
unawaited(keyVerificationManager.handleEventUpdate(update));
|
||||
unawaited(
|
||||
runInRoot(() => keyVerificationManager.handleEventUpdate(update)));
|
||||
}
|
||||
if (update.content['sender'] == client.userID &&
|
||||
!update.content['unsigned'].containsKey('transaction_id')) {
|
||||
// maybe we need to re-try SSSS secrets
|
||||
unawaited(runInRoot(() => ssss.periodicallyRequestMissingCache()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,17 +150,28 @@ class Encryption {
|
|||
final decryptResult = inboundGroupSession.inboundGroupSession
|
||||
.decrypt(event.content['ciphertext']);
|
||||
canRequestSession = false;
|
||||
final messageIndexKey = event.eventId +
|
||||
// we can't have the key be an int, else json-serializing will fail, thus we need it to be a string
|
||||
final messageIndexKey = 'key-' + decryptResult.message_index.toString();
|
||||
final messageIndexValue = event.eventId +
|
||||
'|' +
|
||||
event.originServerTs.millisecondsSinceEpoch.toString();
|
||||
var haveIndex = inboundGroupSession.indexes.containsKey(messageIndexKey);
|
||||
if (haveIndex &&
|
||||
inboundGroupSession.indexes[messageIndexKey] !=
|
||||
decryptResult.message_index) {
|
||||
inboundGroupSession.indexes[messageIndexKey] != messageIndexValue) {
|
||||
// TODO: maybe clear outbound session, if it is ours
|
||||
// TODO: Make it so that we can't re-request the session keys, this is just for debugging
|
||||
Logs.error('[Decrypt] Could not decrypt due to a corrupted session.');
|
||||
Logs.error('[Decrypt] Want session: $roomId $sessionId $senderKey');
|
||||
Logs.error(
|
||||
'[Decrypt] Have sessoin: ${inboundGroupSession.roomId} ${inboundGroupSession.sessionId} ${inboundGroupSession.senderKey}');
|
||||
Logs.error(
|
||||
'[Decrypt] Want indexes: $messageIndexKey $messageIndexValue');
|
||||
Logs.error(
|
||||
'[Decrypt] Have indexes: $messageIndexKey ${inboundGroupSession.indexes[messageIndexKey]}');
|
||||
canRequestSession = true;
|
||||
throw (DecryptError.CHANNEL_CORRUPTED);
|
||||
}
|
||||
inboundGroupSession.indexes[messageIndexKey] =
|
||||
decryptResult.message_index;
|
||||
inboundGroupSession.indexes[messageIndexKey] = messageIndexValue;
|
||||
if (!haveIndex) {
|
||||
// now we persist the udpated indexes into the database.
|
||||
// the entry should always exist. In the case it doesn't, the following
|
||||
|
@ -263,6 +295,9 @@ class Encryption {
|
|||
if (sess == null) {
|
||||
throw ('Unable to create new outbound group session');
|
||||
}
|
||||
// we clone the payload as we do not want to remove 'm.relates_to' from the
|
||||
// original payload passed into this function
|
||||
payload = Map<String, dynamic>.from(payload);
|
||||
final Map<String, dynamic> mRelatesTo = payload.remove('m.relates_to');
|
||||
final payloadContent = {
|
||||
'content': payload,
|
||||
|
@ -296,10 +331,41 @@ class Encryption {
|
|||
return await olmManager.encryptToDeviceMessage(deviceKeys, type, payload);
|
||||
}
|
||||
|
||||
Future<void> autovalidateMasterOwnKey() async {
|
||||
// check if we can set our own master key as verified, if it isn't yet
|
||||
if (client.database != null &&
|
||||
client.userDeviceKeys.containsKey(client.userID)) {
|
||||
final masterKey = client.userDeviceKeys[client.userID].masterKey;
|
||||
if (masterKey != null &&
|
||||
!masterKey.directVerified &&
|
||||
masterKey
|
||||
.hasValidSignatureChain(onlyValidateUserIds: {client.userID})) {
|
||||
await masterKey.setVerified(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this method is responsible for all background tasks, such as uploading online key backups
|
||||
bool _backgroundTasksRunning = true;
|
||||
void _backgroundTasks() {
|
||||
if (!_backgroundTasksRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
keyManager.backgroundTasks();
|
||||
|
||||
autovalidateMasterOwnKey();
|
||||
|
||||
if (_backgroundTasksRunning) {
|
||||
Timer(Duration(seconds: 10), _backgroundTasks);
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
keyManager.dispose();
|
||||
olmManager.dispose();
|
||||
keyVerificationManager.dispose();
|
||||
_backgroundTasksRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,14 +18,18 @@
|
|||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
|
||||
import './encryption.dart';
|
||||
import './utils/session_key.dart';
|
||||
import './utils/outbound_group_session.dart';
|
||||
import './utils/session_key.dart';
|
||||
import '../famedlysdk.dart';
|
||||
import '../matrix_api.dart';
|
||||
import '../src/database/database.dart';
|
||||
import '../src/utils/logs.dart';
|
||||
import '../src/utils/run_in_background.dart';
|
||||
import '../src/utils/run_in_root.dart';
|
||||
|
||||
const MEGOLM_KEY = 'm.megolm_backup.v1';
|
||||
|
||||
|
@ -43,7 +47,7 @@ class KeyManager {
|
|||
encryption.ssss.setValidator(MEGOLM_KEY, (String secret) async {
|
||||
final keyObj = olm.PkDecryption();
|
||||
try {
|
||||
final info = await client.api.getRoomKeysBackup();
|
||||
final info = await getRoomKeysBackupInfo(false);
|
||||
if (info.algorithm != RoomKeysAlgorithmType.v1Curve25519AesSha2) {
|
||||
return false;
|
||||
}
|
||||
|
@ -70,7 +74,16 @@ class KeyManager {
|
|||
|
||||
void setInboundGroupSession(String roomId, String sessionId, String senderKey,
|
||||
Map<String, dynamic> content,
|
||||
{bool forwarded = false}) {
|
||||
{bool forwarded = false,
|
||||
Map<String, String> senderClaimedKeys,
|
||||
bool uploaded = false}) {
|
||||
senderClaimedKeys ??= <String, String>{};
|
||||
if (!senderClaimedKeys.containsKey('ed25519')) {
|
||||
final device = client.getUserDeviceKeysByCurve25519Key(senderKey);
|
||||
if (device != null) {
|
||||
senderClaimedKeys['ed25519'] = device.ed25519Key;
|
||||
}
|
||||
}
|
||||
final oldSession =
|
||||
getInboundGroupSession(roomId, sessionId, senderKey, otherRooms: false);
|
||||
if (content['algorithm'] != 'm.megolm.v1.aes-sha2') {
|
||||
|
@ -84,17 +97,22 @@ class KeyManager {
|
|||
} else {
|
||||
inboundGroupSession.create(content['session_key']);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
inboundGroupSession.free();
|
||||
print(
|
||||
'[LibOlm] Could not create new InboundGroupSession: ' + e.toString());
|
||||
Logs.error(
|
||||
'[LibOlm] Could not create new InboundGroupSession: ' + e.toString(),
|
||||
s);
|
||||
return;
|
||||
}
|
||||
final newSession = SessionKey(
|
||||
content: content,
|
||||
inboundGroupSession: inboundGroupSession,
|
||||
indexes: {},
|
||||
roomId: roomId,
|
||||
sessionId: sessionId,
|
||||
key: client.userID,
|
||||
senderKey: senderKey,
|
||||
senderClaimedKeys: senderClaimedKeys,
|
||||
);
|
||||
final oldFirstIndex =
|
||||
oldSession?.inboundGroupSession?.first_known_index() ?? 0;
|
||||
|
@ -115,14 +133,23 @@ class KeyManager {
|
|||
_inboundGroupSessions[roomId] = <String, SessionKey>{};
|
||||
}
|
||||
_inboundGroupSessions[roomId][sessionId] = newSession;
|
||||
client.database?.storeInboundGroupSession(
|
||||
client.database
|
||||
?.storeInboundGroupSession(
|
||||
client.id,
|
||||
roomId,
|
||||
sessionId,
|
||||
inboundGroupSession.pickle(client.userID),
|
||||
json.encode(content),
|
||||
json.encode({}),
|
||||
);
|
||||
senderKey,
|
||||
json.encode(senderClaimedKeys),
|
||||
)
|
||||
?.then((_) {
|
||||
if (uploaded) {
|
||||
client.database
|
||||
.markInboundGroupSessionAsUploaded(client.id, roomId, sessionId);
|
||||
}
|
||||
});
|
||||
// TODO: somehow try to decrypt last message again
|
||||
final room = client.getRoomById(roomId);
|
||||
if (room != null) {
|
||||
|
@ -135,7 +162,11 @@ class KeyManager {
|
|||
{bool otherRooms = true}) {
|
||||
if (_inboundGroupSessions.containsKey(roomId) &&
|
||||
_inboundGroupSessions[roomId].containsKey(sessionId)) {
|
||||
return _inboundGroupSessions[roomId][sessionId];
|
||||
final sess = _inboundGroupSessions[roomId][sessionId];
|
||||
if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) {
|
||||
return null;
|
||||
}
|
||||
return sess;
|
||||
}
|
||||
if (!otherRooms) {
|
||||
return null;
|
||||
|
@ -143,7 +174,11 @@ class KeyManager {
|
|||
// search if this session id is *somehow* found in another room
|
||||
for (final val in _inboundGroupSessions.values) {
|
||||
if (val.containsKey(sessionId)) {
|
||||
return val[sessionId];
|
||||
final sess = val[sessionId];
|
||||
if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) {
|
||||
return null;
|
||||
}
|
||||
return sess;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
@ -157,7 +192,11 @@ class KeyManager {
|
|||
}
|
||||
if (_inboundGroupSessions.containsKey(roomId) &&
|
||||
_inboundGroupSessions[roomId].containsKey(sessionId)) {
|
||||
return _inboundGroupSessions[roomId][sessionId]; // nothing to do
|
||||
final sess = _inboundGroupSessions[roomId][sessionId];
|
||||
if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) {
|
||||
return null; // sender keys do not match....better not do anything
|
||||
}
|
||||
return sess; // nothing to do
|
||||
}
|
||||
final session = await client.database
|
||||
?.getDbInboundGroupSession(client.id, roomId, sessionId);
|
||||
|
@ -166,10 +205,12 @@ class KeyManager {
|
|||
final requestIdent = '$roomId|$sessionId|$senderKey';
|
||||
if (client.enableE2eeRecovery &&
|
||||
room != null &&
|
||||
!_requestedSessionIds.contains(requestIdent)) {
|
||||
!_requestedSessionIds.contains(requestIdent) &&
|
||||
!client.isUnknownSession) {
|
||||
// do e2ee recovery
|
||||
_requestedSessionIds.add(requestIdent);
|
||||
unawaited(request(room, sessionId, senderKey));
|
||||
unawaited(runInRoot(() =>
|
||||
request(room, sessionId, senderKey, askOnlyOwnDevices: true)));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -177,7 +218,8 @@ class KeyManager {
|
|||
_inboundGroupSessions[roomId] = <String, SessionKey>{};
|
||||
}
|
||||
final sess = SessionKey.fromDb(session, client.userID);
|
||||
if (!sess.isValid) {
|
||||
if (!sess.isValid ||
|
||||
(sess.senderKey.isNotEmpty && sess.senderKey != senderKey)) {
|
||||
return null;
|
||||
}
|
||||
_inboundGroupSessions[roomId][sessionId] = sess;
|
||||
|
@ -261,10 +303,11 @@ class KeyManager {
|
|||
final outboundGroupSession = olm.OutboundGroupSession();
|
||||
try {
|
||||
outboundGroupSession.create();
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
outboundGroupSession.free();
|
||||
print('[LibOlm] Unable to create new outboundGroupSession: ' +
|
||||
e.toString());
|
||||
Logs.error(
|
||||
'[LibOlm] Unable to create new outboundGroupSession: ' + e.toString(),
|
||||
s);
|
||||
return null;
|
||||
}
|
||||
final rawSession = <String, dynamic>{
|
||||
|
@ -283,14 +326,14 @@ class KeyManager {
|
|||
key: client.userID,
|
||||
);
|
||||
try {
|
||||
await client.sendToDevice(deviceKeys, 'm.room_key', rawSession);
|
||||
await client.sendToDeviceEncrypted(deviceKeys, 'm.room_key', rawSession);
|
||||
await storeOutboundGroupSession(roomId, sess);
|
||||
_outboundGroupSessions[roomId] = sess;
|
||||
} catch (e, s) {
|
||||
print(
|
||||
Logs.error(
|
||||
'[LibOlm] Unable to send the session key to the participating devices: ' +
|
||||
e.toString());
|
||||
print(s);
|
||||
e.toString(),
|
||||
s);
|
||||
sess.dispose();
|
||||
return null;
|
||||
}
|
||||
|
@ -327,6 +370,23 @@ class KeyManager {
|
|||
return (await encryption.ssss.getCached(MEGOLM_KEY)) != null;
|
||||
}
|
||||
|
||||
RoomKeysVersionResponse _roomKeysVersionCache;
|
||||
DateTime _roomKeysVersionCacheDate;
|
||||
Future<RoomKeysVersionResponse> getRoomKeysBackupInfo(
|
||||
[bool useCache = true]) async {
|
||||
if (_roomKeysVersionCache != null &&
|
||||
_roomKeysVersionCacheDate != null &&
|
||||
useCache &&
|
||||
DateTime.now()
|
||||
.subtract(Duration(minutes: 5))
|
||||
.isBefore(_roomKeysVersionCacheDate)) {
|
||||
return _roomKeysVersionCache;
|
||||
}
|
||||
_roomKeysVersionCache = await client.getRoomKeysBackup();
|
||||
_roomKeysVersionCacheDate = DateTime.now();
|
||||
return _roomKeysVersionCache;
|
||||
}
|
||||
|
||||
Future<void> loadFromResponse(RoomKeys keys) async {
|
||||
if (!(await isCached())) {
|
||||
return;
|
||||
|
@ -334,7 +394,7 @@ class KeyManager {
|
|||
final privateKey =
|
||||
base64.decode(await encryption.ssss.getCached(MEGOLM_KEY));
|
||||
final decryption = olm.PkDecryption();
|
||||
final info = await client.api.getRoomKeysBackup();
|
||||
final info = await getRoomKeysBackupInfo();
|
||||
String backupPubKey;
|
||||
try {
|
||||
backupPubKey = decryption.init_with_private_key(privateKey);
|
||||
|
@ -363,15 +423,20 @@ class KeyManager {
|
|||
try {
|
||||
decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'],
|
||||
sessionData['mac'], sessionData['ciphertext']));
|
||||
} catch (err) {
|
||||
print('[LibOlm] Error decrypting room key: ' + err.toString());
|
||||
} catch (e, s) {
|
||||
Logs.error(
|
||||
'[LibOlm] Error decrypting room key: ' + e.toString(), s);
|
||||
}
|
||||
if (decrypted != null) {
|
||||
decrypted['session_id'] = sessionId;
|
||||
decrypted['room_id'] = roomId;
|
||||
setInboundGroupSession(
|
||||
roomId, sessionId, decrypted['sender_key'], decrypted,
|
||||
forwarded: true);
|
||||
forwarded: true,
|
||||
senderClaimedKeys: decrypted['sender_claimed_keys'] != null
|
||||
? Map<String, String>.from(decrypted['sender_claimed_keys'])
|
||||
: null,
|
||||
uploaded: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -381,9 +446,9 @@ class KeyManager {
|
|||
}
|
||||
|
||||
Future<void> loadSingleKey(String roomId, String sessionId) async {
|
||||
final info = await client.api.getRoomKeysBackup();
|
||||
final info = await getRoomKeysBackupInfo();
|
||||
final ret =
|
||||
await client.api.getRoomKeysSingleKey(roomId, sessionId, info.version);
|
||||
await client.getRoomKeysSingleKey(roomId, sessionId, info.version);
|
||||
final keys = RoomKeys.fromJson({
|
||||
'rooms': {
|
||||
roomId: {
|
||||
|
@ -397,37 +462,53 @@ class KeyManager {
|
|||
}
|
||||
|
||||
/// Request a certain key from another device
|
||||
Future<void> request(Room room, String sessionId, String senderKey,
|
||||
{bool tryOnlineBackup = true}) async {
|
||||
if (tryOnlineBackup) {
|
||||
Future<void> request(
|
||||
Room room,
|
||||
String sessionId,
|
||||
String senderKey, {
|
||||
bool tryOnlineBackup = true,
|
||||
bool askOnlyOwnDevices = false,
|
||||
}) async {
|
||||
if (tryOnlineBackup && await isCached()) {
|
||||
// let's first check our online key backup store thingy...
|
||||
var hadPreviously =
|
||||
getInboundGroupSession(room.id, sessionId, senderKey) != null;
|
||||
try {
|
||||
await loadSingleKey(room.id, sessionId);
|
||||
} catch (err, stacktrace) {
|
||||
print('[KeyManager] Failed to access online key backup: ' +
|
||||
err.toString());
|
||||
print(stacktrace);
|
||||
if (err is MatrixException && err.errcode == 'M_NOT_FOUND') {
|
||||
Logs.info(
|
||||
'[KeyManager] Key not in online key backup, requesting it from other devices...');
|
||||
} else {
|
||||
Logs.error(
|
||||
'[KeyManager] Failed to access online key backup: ' +
|
||||
err.toString(),
|
||||
stacktrace);
|
||||
}
|
||||
}
|
||||
if (!hadPreviously &&
|
||||
getInboundGroupSession(room.id, sessionId, senderKey) != null) {
|
||||
return; // we managed to load the session from online backup, no need to care about it now
|
||||
}
|
||||
}
|
||||
// while we just send the to-device event to '*', we still need to save the
|
||||
// devices themself to know where to send the cancel to after receiving a reply
|
||||
final devices = await room.getUserDeviceKeys();
|
||||
final requestId = client.generateUniqueTransactionId();
|
||||
final request = KeyManagerKeyShareRequest(
|
||||
requestId: requestId,
|
||||
devices: devices,
|
||||
room: room,
|
||||
sessionId: sessionId,
|
||||
senderKey: senderKey,
|
||||
);
|
||||
await client.sendToDevice(
|
||||
[],
|
||||
try {
|
||||
// while we just send the to-device event to '*', we still need to save the
|
||||
// devices themself to know where to send the cancel to after receiving a reply
|
||||
final devices = await room.getUserDeviceKeys();
|
||||
if (askOnlyOwnDevices) {
|
||||
devices.removeWhere((d) => d.userId != client.userID);
|
||||
}
|
||||
final requestId = client.generateUniqueTransactionId();
|
||||
final request = KeyManagerKeyShareRequest(
|
||||
requestId: requestId,
|
||||
devices: devices,
|
||||
room: room,
|
||||
sessionId: sessionId,
|
||||
senderKey: senderKey,
|
||||
);
|
||||
final userList = await room.requestParticipants();
|
||||
await client.sendToDevicesOfUserIds(
|
||||
userList.map<String>((u) => u.id).toSet(),
|
||||
'm.room_key_request',
|
||||
{
|
||||
'action': 'request',
|
||||
|
@ -440,9 +521,87 @@ class KeyManager {
|
|||
'request_id': requestId,
|
||||
'requesting_device_id': client.deviceID,
|
||||
},
|
||||
encrypted: false,
|
||||
toUsers: await room.requestParticipants());
|
||||
outgoingShareRequests[request.requestId] = request;
|
||||
);
|
||||
outgoingShareRequests[request.requestId] = request;
|
||||
} catch (e, s) {
|
||||
Logs.error(
|
||||
'[Key Manager] Sending key verification request failed: ' +
|
||||
e.toString(),
|
||||
s);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isUploadingKeys = false;
|
||||
Future<void> backgroundTasks() async {
|
||||
if (_isUploadingKeys || client.database == null) {
|
||||
return;
|
||||
}
|
||||
_isUploadingKeys = true;
|
||||
try {
|
||||
if (!(await isCached())) {
|
||||
return; // we can't backup anyways
|
||||
}
|
||||
final dbSessions =
|
||||
await client.database.getInboundGroupSessionsToUpload().get();
|
||||
if (dbSessions.isEmpty) {
|
||||
return; // nothing to do
|
||||
}
|
||||
final privateKey =
|
||||
base64.decode(await encryption.ssss.getCached(MEGOLM_KEY));
|
||||
// decryption is needed to calculate the public key and thus see if the claimed information is in fact valid
|
||||
final decryption = olm.PkDecryption();
|
||||
final info = await getRoomKeysBackupInfo(false);
|
||||
String backupPubKey;
|
||||
try {
|
||||
backupPubKey = decryption.init_with_private_key(privateKey);
|
||||
|
||||
if (backupPubKey == null ||
|
||||
info.algorithm != RoomKeysAlgorithmType.v1Curve25519AesSha2 ||
|
||||
info.authData['public_key'] != backupPubKey) {
|
||||
return;
|
||||
}
|
||||
final args = _GenerateUploadKeysArgs(
|
||||
pubkey: backupPubKey,
|
||||
dbSessions: <_DbInboundGroupSessionBundle>[],
|
||||
userId: client.userID,
|
||||
);
|
||||
// we need to calculate verified beforehand, as else we pass a closure to an isolate
|
||||
// with 500 keys they do, however, noticably block the UI, which is why we give brief async suspentions in here
|
||||
// so that the event loop can progress
|
||||
var i = 0;
|
||||
for (final dbSession in dbSessions) {
|
||||
final device =
|
||||
client.getUserDeviceKeysByCurve25519Key(dbSession.senderKey);
|
||||
args.dbSessions.add(_DbInboundGroupSessionBundle(
|
||||
dbSession: dbSession,
|
||||
verified: device?.verified ?? false,
|
||||
));
|
||||
i++;
|
||||
if (i > 10) {
|
||||
await Future.delayed(Duration(milliseconds: 1));
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
final roomKeys =
|
||||
await runInBackground<RoomKeys, _GenerateUploadKeysArgs>(
|
||||
_generateUploadKeys, args);
|
||||
Logs.info('[Key Manager] Uploading ${dbSessions.length} room keys...');
|
||||
// upload the payload...
|
||||
await client.storeRoomKeys(info.version, roomKeys);
|
||||
// and now finally mark all the keys as uploaded
|
||||
// no need to optimze this, as we only run it so seldomly and almost never with many keys at once
|
||||
for (final dbSession in dbSessions) {
|
||||
await client.database.markInboundGroupSessionAsUploaded(
|
||||
client.id, dbSession.roomId, dbSession.sessionId);
|
||||
}
|
||||
} finally {
|
||||
decryption.free();
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logs.error('[Key Manager] Error uploading room keys: ' + e.toString(), s);
|
||||
} finally {
|
||||
_isUploadingKeys = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an incoming to_device event that is related to key sharing
|
||||
|
@ -453,27 +612,27 @@ class KeyManager {
|
|||
}
|
||||
if (event.content['action'] == 'request') {
|
||||
// we are *receiving* a request
|
||||
print('[KeyManager] Received key sharing request...');
|
||||
Logs.info('[KeyManager] Received key sharing request...');
|
||||
if (!event.content.containsKey('body')) {
|
||||
print('[KeyManager] No body, doing nothing');
|
||||
Logs.info('[KeyManager] No body, doing nothing');
|
||||
return; // no body
|
||||
}
|
||||
if (!client.userDeviceKeys.containsKey(event.sender) ||
|
||||
!client.userDeviceKeys[event.sender].deviceKeys
|
||||
.containsKey(event.content['requesting_device_id'])) {
|
||||
print('[KeyManager] Device not found, doing nothing');
|
||||
Logs.info('[KeyManager] Device not found, doing nothing');
|
||||
return; // device not found
|
||||
}
|
||||
final device = client.userDeviceKeys[event.sender]
|
||||
.deviceKeys[event.content['requesting_device_id']];
|
||||
if (device.userId == client.userID &&
|
||||
device.deviceId == client.deviceID) {
|
||||
print('[KeyManager] Request is by ourself, ignoring');
|
||||
Logs.info('[KeyManager] Request is by ourself, ignoring');
|
||||
return; // ignore requests by ourself
|
||||
}
|
||||
final room = client.getRoomById(event.content['body']['room_id']);
|
||||
if (room == null) {
|
||||
print('[KeyManager] Unknown room, ignoring');
|
||||
Logs.info('[KeyManager] Unknown room, ignoring');
|
||||
return; // unknown room
|
||||
}
|
||||
final sessionId = event.content['body']['session_id'];
|
||||
|
@ -481,7 +640,7 @@ class KeyManager {
|
|||
// okay, let's see if we have this session at all
|
||||
if ((await loadInboundGroupSession(room.id, sessionId, senderKey)) ==
|
||||
null) {
|
||||
print('[KeyManager] Unknown session, ignoring');
|
||||
Logs.info('[KeyManager] Unknown session, ignoring');
|
||||
return; // we don't have this session anyways
|
||||
}
|
||||
final request = KeyManagerKeyShareRequest(
|
||||
|
@ -492,7 +651,7 @@ class KeyManager {
|
|||
senderKey: senderKey,
|
||||
);
|
||||
if (incomingShareRequests.containsKey(request.requestId)) {
|
||||
print('[KeyManager] Already processed this request, ignoring');
|
||||
Logs.info('[KeyManager] Already processed this request, ignoring');
|
||||
return; // we don't want to process one and the same request multiple times
|
||||
}
|
||||
incomingShareRequests[request.requestId] = request;
|
||||
|
@ -501,11 +660,12 @@ class KeyManager {
|
|||
if (device.userId == client.userID &&
|
||||
device.verified &&
|
||||
!device.blocked) {
|
||||
print('[KeyManager] All checks out, forwarding key...');
|
||||
Logs.info('[KeyManager] All checks out, forwarding key...');
|
||||
// alright, we can forward the key
|
||||
await roomKeyRequest.forwardKey();
|
||||
} else {
|
||||
print('[KeyManager] Asking client, if the key should be forwarded');
|
||||
Logs.info(
|
||||
'[KeyManager] Asking client, if the key should be forwarded');
|
||||
client.onRoomKeyRequest
|
||||
.add(roomKeyRequest); // let the client handle this
|
||||
}
|
||||
|
@ -541,11 +701,20 @@ class KeyManager {
|
|||
if (device == null) {
|
||||
return; // someone we didn't send our request to replied....better ignore this
|
||||
}
|
||||
// we add the sender key to the forwarded key chain
|
||||
if (!(event.content['forwarding_curve25519_key_chain'] is List)) {
|
||||
event.content['forwarding_curve25519_key_chain'] = <String>[];
|
||||
}
|
||||
event.content['forwarding_curve25519_key_chain']
|
||||
.add(event.encryptedContent['sender_key']);
|
||||
// TODO: verify that the keys work to decrypt a message
|
||||
// alright, all checks out, let's go ahead and store this session
|
||||
setInboundGroupSession(
|
||||
request.room.id, request.sessionId, request.senderKey, event.content,
|
||||
forwarded: true);
|
||||
forwarded: true,
|
||||
senderClaimedKeys: {
|
||||
'ed25519': event.content['sender_claimed_ed25519_key'],
|
||||
});
|
||||
request.devices.removeWhere(
|
||||
(k) => k.userId == device.userId && k.deviceId == device.deviceId);
|
||||
outgoingShareRequests.remove(request.requestId);
|
||||
|
@ -553,15 +722,24 @@ class KeyManager {
|
|||
if (request.devices.isEmpty) {
|
||||
return; // no need to send any cancellation
|
||||
}
|
||||
// Send with send-to-device messaging
|
||||
final sendToDeviceMessage = {
|
||||
'action': 'request_cancellation',
|
||||
'request_id': request.requestId,
|
||||
'requesting_device_id': client.deviceID,
|
||||
};
|
||||
var data = <String, Map<String, Map<String, dynamic>>>{};
|
||||
for (final device in request.devices) {
|
||||
if (!data.containsKey(device.userId)) {
|
||||
data[device.userId] = {};
|
||||
}
|
||||
data[device.userId][device.deviceId] = sendToDeviceMessage;
|
||||
}
|
||||
await client.sendToDevice(
|
||||
request.devices,
|
||||
'm.room_key_request',
|
||||
{
|
||||
'action': 'request_cancellation',
|
||||
'request_id': request.requestId,
|
||||
'requesting_device_id': client.deviceID,
|
||||
},
|
||||
encrypted: false);
|
||||
'm.room_key_request',
|
||||
client.generateUniqueTransactionId(),
|
||||
data,
|
||||
);
|
||||
} else if (event.type == 'm.room_key') {
|
||||
if (event.encryptedContent == null) {
|
||||
return; // the event wasn't encrypted, this is a security risk;
|
||||
|
@ -635,32 +813,23 @@ class RoomKeyRequest extends ToDeviceEvent {
|
|||
var room = this.room;
|
||||
final session = await keyManager.loadInboundGroupSession(
|
||||
room.id, request.sessionId, request.senderKey);
|
||||
var forwardedKeys = <dynamic>[keyManager.encryption.identityKey];
|
||||
for (final key in session.forwardingCurve25519KeyChain) {
|
||||
forwardedKeys.add(key);
|
||||
}
|
||||
var message = session.content;
|
||||
message['forwarding_curve25519_key_chain'] = forwardedKeys;
|
||||
message['forwarding_curve25519_key_chain'] =
|
||||
List<String>.from(session.forwardingCurve25519KeyChain);
|
||||
|
||||
message['sender_key'] = request.senderKey;
|
||||
message['sender_key'] =
|
||||
(session.senderKey != null && session.senderKey.isNotEmpty)
|
||||
? session.senderKey
|
||||
: request.senderKey;
|
||||
message['sender_claimed_ed25519_key'] =
|
||||
forwardedKeys.isEmpty ? keyManager.encryption.fingerprintKey : null;
|
||||
if (message['sender_claimed_ed25519_key'] == null) {
|
||||
for (final value in keyManager.client.userDeviceKeys.values) {
|
||||
for (final key in value.deviceKeys.values) {
|
||||
if (key.curve25519Key == forwardedKeys.first) {
|
||||
message['sender_claimed_ed25519_key'] = key.ed25519Key;
|
||||
}
|
||||
}
|
||||
if (message['sender_claimed_ed25519_key'] != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
session.senderClaimedKeys['ed25519'] ??
|
||||
(session.forwardingCurve25519KeyChain.isEmpty
|
||||
? keyManager.encryption.fingerprintKey
|
||||
: null);
|
||||
message['session_key'] = session.inboundGroupSession
|
||||
.export_session(session.inboundGroupSession.first_known_index());
|
||||
// send the actual reply of the key back to the requester
|
||||
await keyManager.client.sendToDevice(
|
||||
await keyManager.client.sendToDeviceEncrypted(
|
||||
[requestingDevice],
|
||||
'm.forwarded_room_key',
|
||||
message,
|
||||
|
@ -668,3 +837,67 @@ class RoomKeyRequest extends ToDeviceEvent {
|
|||
keyManager.incomingShareRequests.remove(request.requestId);
|
||||
}
|
||||
}
|
||||
|
||||
RoomKeys _generateUploadKeys(_GenerateUploadKeysArgs args) {
|
||||
final enc = olm.PkEncryption();
|
||||
try {
|
||||
enc.set_recipient_key(args.pubkey);
|
||||
// first we generate the payload to upload all the session keys in this chunk
|
||||
final roomKeys = RoomKeys();
|
||||
for (final dbSession in args.dbSessions) {
|
||||
final sess = SessionKey.fromDb(dbSession.dbSession, args.userId);
|
||||
if (!sess.isValid) {
|
||||
continue;
|
||||
}
|
||||
// create the room if it doesn't exist
|
||||
if (!roomKeys.rooms.containsKey(sess.roomId)) {
|
||||
roomKeys.rooms[sess.roomId] = RoomKeysRoom();
|
||||
}
|
||||
// generate the encrypted content
|
||||
final payload = <String, dynamic>{
|
||||
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||
'forwarding_curve25519_key_chain': sess.forwardingCurve25519KeyChain,
|
||||
'sender_key': sess.senderKey,
|
||||
'sender_clencaimed_keys': sess.senderClaimedKeys,
|
||||
'session_key': sess.inboundGroupSession
|
||||
.export_session(sess.inboundGroupSession.first_known_index()),
|
||||
};
|
||||
// encrypt the content
|
||||
final encrypted = enc.encrypt(json.encode(payload));
|
||||
// fetch the device, if available...
|
||||
//final device = args.client.getUserDeviceKeysByCurve25519Key(sess.senderKey);
|
||||
// aaaand finally add the session key to our payload
|
||||
roomKeys.rooms[sess.roomId].sessions[sess.sessionId] = RoomKeysSingleKey(
|
||||
firstMessageIndex: sess.inboundGroupSession.first_known_index(),
|
||||
forwardedCount: sess.forwardingCurve25519KeyChain.length,
|
||||
isVerified: dbSession.verified, //device?.verified ?? false,
|
||||
sessionData: {
|
||||
'ephemeral': encrypted.ephemeral,
|
||||
'ciphertext': encrypted.ciphertext,
|
||||
'mac': encrypted.mac,
|
||||
},
|
||||
);
|
||||
}
|
||||
return roomKeys;
|
||||
} catch (e, s) {
|
||||
Logs.error('[Key Manager] Error generating payload ' + e.toString(), s);
|
||||
rethrow;
|
||||
} finally {
|
||||
enc.free();
|
||||
}
|
||||
}
|
||||
|
||||
class _DbInboundGroupSessionBundle {
|
||||
_DbInboundGroupSessionBundle({this.dbSession, this.verified});
|
||||
|
||||
DbInboundGroupSession dbSession;
|
||||
bool verified;
|
||||
}
|
||||
|
||||
class _GenerateUploadKeysArgs {
|
||||
_GenerateUploadKeysArgs({this.pubkey, this.dbSessions, this.userId});
|
||||
|
||||
String pubkey;
|
||||
List<_DbInboundGroupSessionBundle> dbSessions;
|
||||
String userId;
|
||||
}
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import './encryption.dart';
|
||||
import './utils/key_verification.dart';
|
||||
import '../famedlysdk.dart';
|
||||
import 'encryption.dart';
|
||||
import 'utils/key_verification.dart';
|
||||
|
||||
class KeyVerificationManager {
|
||||
final Encryption encryption;
|
||||
|
@ -67,6 +67,10 @@ class KeyVerificationManager {
|
|||
if (_requests.containsKey(transactionId)) {
|
||||
await _requests[transactionId].handlePayload(event.type, event.content);
|
||||
} else {
|
||||
if (!['m.key.verification.request', 'm.key.verification.start']
|
||||
.contains(event.type)) {
|
||||
return; // we can only start on these
|
||||
}
|
||||
final newKeyRequest =
|
||||
KeyVerification(encryption: encryption, userId: event.sender);
|
||||
await newKeyRequest.handlePayload(event.type, event.content);
|
||||
|
@ -111,6 +115,10 @@ class KeyVerificationManager {
|
|||
_requests.remove(transactionId);
|
||||
}
|
||||
} else if (event['sender'] != client.userID) {
|
||||
if (!['m.key.verification.request', 'm.key.verification.start']
|
||||
.contains(type)) {
|
||||
return; // we can only start on these
|
||||
}
|
||||
final room = client.getRoomById(update.roomID) ??
|
||||
Room(id: update.roomID, client: client);
|
||||
final newKeyRequest = KeyVerification(
|
||||
|
|
|
@ -18,13 +18,16 @@
|
|||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
import 'package:canonical_json/canonical_json.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
import './encryption.dart';
|
||||
import './utils/olm_session.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
|
||||
import '../encryption/utils/json_signature_check_extension.dart';
|
||||
import '../src/utils/logs.dart';
|
||||
import 'encryption.dart';
|
||||
import 'utils/olm_session.dart';
|
||||
|
||||
class OlmManager {
|
||||
final Encryption encryption;
|
||||
|
@ -73,7 +76,8 @@ class OlmManager {
|
|||
}
|
||||
}
|
||||
|
||||
/// Adds a signature to this json from this olm account.
|
||||
/// Adds a signature to this json from this olm account and returns the signed
|
||||
/// json.
|
||||
Map<String, dynamic> signJson(Map<String, dynamic> payload) {
|
||||
if (!enabled) throw ('Encryption is disabled');
|
||||
final Map<String, dynamic> unsigned = payload['unsigned'];
|
||||
|
@ -103,6 +107,7 @@ class OlmManager {
|
|||
}
|
||||
|
||||
/// Checks the signature of a signed json object.
|
||||
@deprecated
|
||||
bool checkJsonSignature(String key, Map<String, dynamic> signedJson,
|
||||
String userId, String deviceId) {
|
||||
if (!enabled) throw ('Encryption is disabled');
|
||||
|
@ -119,15 +124,17 @@ class OlmManager {
|
|||
try {
|
||||
olmutil.ed25519_verify(key, message, signature);
|
||||
isValid = true;
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
isValid = false;
|
||||
print('[LibOlm] Signature check failed: ' + e.toString());
|
||||
Logs.error('[LibOlm] Signature check failed: ' + e.toString(), s);
|
||||
} finally {
|
||||
olmutil.free();
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
|
||||
bool _uploadKeysLock = false;
|
||||
|
||||
/// Generates new one time keys, signs everything and upload it to the server.
|
||||
Future<bool> uploadKeys(
|
||||
{bool uploadDeviceKeys = false, int oldKeyCount = 0}) async {
|
||||
|
@ -135,62 +142,71 @@ class OlmManager {
|
|||
return true;
|
||||
}
|
||||
|
||||
// generate one-time keys
|
||||
// we generate 2/3rds of max, so that other keys people may still have can
|
||||
// still be used
|
||||
final oneTimeKeysCount =
|
||||
(_olmAccount.max_number_of_one_time_keys() * 2 / 3).floor() -
|
||||
oldKeyCount;
|
||||
_olmAccount.generate_one_time_keys(oneTimeKeysCount);
|
||||
final Map<String, dynamic> oneTimeKeys =
|
||||
json.decode(_olmAccount.one_time_keys());
|
||||
|
||||
// now sign all the one-time keys
|
||||
final signedOneTimeKeys = <String, dynamic>{};
|
||||
for (final entry in oneTimeKeys['curve25519'].entries) {
|
||||
final key = entry.key;
|
||||
final value = entry.value;
|
||||
signedOneTimeKeys['signed_curve25519:$key'] = <String, dynamic>{};
|
||||
signedOneTimeKeys['signed_curve25519:$key'] = signJson({
|
||||
'key': value,
|
||||
});
|
||||
if (_uploadKeysLock) {
|
||||
return false;
|
||||
}
|
||||
_uploadKeysLock = true;
|
||||
|
||||
// and now generate the payload to upload
|
||||
final keysContent = <String, dynamic>{
|
||||
if (uploadDeviceKeys)
|
||||
'device_keys': {
|
||||
'user_id': client.userID,
|
||||
'device_id': client.deviceID,
|
||||
'algorithms': [
|
||||
'm.olm.v1.curve25519-aes-sha2',
|
||||
'm.megolm.v1.aes-sha2'
|
||||
],
|
||||
'keys': <String, dynamic>{},
|
||||
},
|
||||
};
|
||||
if (uploadDeviceKeys) {
|
||||
final Map<String, dynamic> keys =
|
||||
json.decode(_olmAccount.identity_keys());
|
||||
for (final entry in keys.entries) {
|
||||
final algorithm = entry.key;
|
||||
try {
|
||||
// generate one-time keys
|
||||
// we generate 2/3rds of max, so that other keys people may still have can
|
||||
// still be used
|
||||
final oneTimeKeysCount =
|
||||
(_olmAccount.max_number_of_one_time_keys() * 2 / 3).floor() -
|
||||
oldKeyCount;
|
||||
_olmAccount.generate_one_time_keys(oneTimeKeysCount);
|
||||
final Map<String, dynamic> oneTimeKeys =
|
||||
json.decode(_olmAccount.one_time_keys());
|
||||
|
||||
// now sign all the one-time keys
|
||||
final signedOneTimeKeys = <String, dynamic>{};
|
||||
for (final entry in oneTimeKeys['curve25519'].entries) {
|
||||
final key = entry.key;
|
||||
final value = entry.value;
|
||||
keysContent['device_keys']['keys']['$algorithm:${client.deviceID}'] =
|
||||
value;
|
||||
signedOneTimeKeys['signed_curve25519:$key'] = <String, dynamic>{};
|
||||
signedOneTimeKeys['signed_curve25519:$key'] = signJson({
|
||||
'key': value,
|
||||
});
|
||||
}
|
||||
keysContent['device_keys'] =
|
||||
signJson(keysContent['device_keys'] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
final response = await client.api.uploadDeviceKeys(
|
||||
deviceKeys: uploadDeviceKeys
|
||||
? MatrixDeviceKeys.fromJson(keysContent['device_keys'])
|
||||
: null,
|
||||
oneTimeKeys: signedOneTimeKeys,
|
||||
);
|
||||
_olmAccount.mark_keys_as_published();
|
||||
await client.database?.updateClientKeys(pickledOlmAccount, client.id);
|
||||
return response['signed_curve25519'] == oneTimeKeysCount;
|
||||
// and now generate the payload to upload
|
||||
final keysContent = <String, dynamic>{
|
||||
if (uploadDeviceKeys)
|
||||
'device_keys': {
|
||||
'user_id': client.userID,
|
||||
'device_id': client.deviceID,
|
||||
'algorithms': [
|
||||
'm.olm.v1.curve25519-aes-sha2',
|
||||
'm.megolm.v1.aes-sha2'
|
||||
],
|
||||
'keys': <String, dynamic>{},
|
||||
},
|
||||
};
|
||||
if (uploadDeviceKeys) {
|
||||
final Map<String, dynamic> keys =
|
||||
json.decode(_olmAccount.identity_keys());
|
||||
for (final entry in keys.entries) {
|
||||
final algorithm = entry.key;
|
||||
final value = entry.value;
|
||||
keysContent['device_keys']['keys']['$algorithm:${client.deviceID}'] =
|
||||
value;
|
||||
}
|
||||
keysContent['device_keys'] =
|
||||
signJson(keysContent['device_keys'] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
final response = await client.uploadDeviceKeys(
|
||||
deviceKeys: uploadDeviceKeys
|
||||
? MatrixDeviceKeys.fromJson(keysContent['device_keys'])
|
||||
: null,
|
||||
oneTimeKeys: signedOneTimeKeys,
|
||||
);
|
||||
_olmAccount.mark_keys_as_published();
|
||||
await client.database?.updateClientKeys(pickledOlmAccount, client.id);
|
||||
return response['signed_curve25519'] == oneTimeKeysCount;
|
||||
} finally {
|
||||
_uploadKeysLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
void handleDeviceOneTimeKeysCount(Map<String, int> countJson) {
|
||||
|
@ -231,7 +247,7 @@ class OlmManager {
|
|||
return event;
|
||||
}
|
||||
if (event.content['algorithm'] != 'm.olm.v1.curve25519-aes-sha2') {
|
||||
throw ('Unknown algorithm: ${event.content}');
|
||||
throw ('Unknown algorithm: ${event.content['algorithm']}');
|
||||
}
|
||||
if (!event.content['ciphertext'].containsKey(identityKey)) {
|
||||
throw ("The message isn't sent for this device");
|
||||
|
@ -334,7 +350,7 @@ class OlmManager {
|
|||
return;
|
||||
}
|
||||
await startOutgoingOlmSessions([device]);
|
||||
await client.sendToDevice([device], 'm.dummy', {});
|
||||
await client.sendToDeviceEncrypted([device], 'm.dummy', {});
|
||||
}
|
||||
|
||||
Future<ToDeviceEvent> decryptToDeviceEvent(ToDeviceEvent event) async {
|
||||
|
@ -382,7 +398,7 @@ class OlmManager {
|
|||
}
|
||||
|
||||
final response =
|
||||
await client.api.requestOneTimeKeys(requestingKeysFrom, timeout: 10000);
|
||||
await client.requestOneTimeKeys(requestingKeysFrom, timeout: 10000);
|
||||
|
||||
for (var userKeysEntry in response.oneTimeKeys.entries) {
|
||||
final userId = userKeysEntry.key;
|
||||
|
@ -393,8 +409,7 @@ class OlmManager {
|
|||
final identityKey =
|
||||
client.userDeviceKeys[userId].deviceKeys[deviceId].curve25519Key;
|
||||
for (Map<String, dynamic> deviceKey in deviceKeysEntry.value.values) {
|
||||
if (!checkJsonSignature(
|
||||
fingerprintKey, deviceKey, userId, deviceId)) {
|
||||
if (!deviceKey.checkJsonSignature(fingerprintKey, userId, deviceId)) {
|
||||
continue;
|
||||
}
|
||||
var session = olm.Session();
|
||||
|
@ -408,10 +423,12 @@ class OlmManager {
|
|||
lastReceived:
|
||||
DateTime.now(), // we want to use a newly created session
|
||||
));
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
session.free();
|
||||
print('[LibOlm] Could not create new outbound olm session: ' +
|
||||
e.toString());
|
||||
Logs.error(
|
||||
'[LibOlm] Could not create new outbound olm session: ' +
|
||||
e.toString(),
|
||||
s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -483,8 +500,9 @@ class OlmManager {
|
|||
try {
|
||||
data[device.userId][device.deviceId] =
|
||||
await encryptToDeviceMessagePayload(device, type, payload);
|
||||
} catch (e) {
|
||||
print('[LibOlm] Error encrypting to-device event: ' + e.toString());
|
||||
} catch (e, s) {
|
||||
Logs.error(
|
||||
'[LibOlm] Error encrypting to-device event: ' + e.toString(), s);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,16 +16,19 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:typed_data';
|
||||
import 'dart:core';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:encrypt/encrypt.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:base58check/base58.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:encrypt/encrypt.dart';
|
||||
import 'package:password_hash/password_hash.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
|
||||
import '../famedlysdk.dart';
|
||||
import '../matrix_api.dart';
|
||||
import '../src/database/database.dart';
|
||||
import '../src/utils/logs.dart';
|
||||
import 'encryption.dart';
|
||||
|
||||
const CACHE_TYPES = <String>[
|
||||
|
@ -46,8 +49,15 @@ class SSSS {
|
|||
Client get client => encryption.client;
|
||||
final pendingShareRequests = <String, _ShareRequest>{};
|
||||
final _validators = <String, Future<bool> Function(String)>{};
|
||||
final Map<String, DbSSSSCache> _cache = <String, DbSSSSCache>{};
|
||||
SSSS(this.encryption);
|
||||
|
||||
// for testing
|
||||
Future<void> clearCache() async {
|
||||
await client.database?.clearSSSSCache(client.id);
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
static _DerivedKeys deriveKeys(Uint8List key, String name) {
|
||||
final zerosalt = Uint8List(8);
|
||||
final prk = Hmac(sha256, zerosalt).convert(key);
|
||||
|
@ -132,7 +142,7 @@ class SSSS {
|
|||
}
|
||||
final generator = PBKDF2(hashAlgorithm: sha512);
|
||||
return Uint8List.fromList(generator.generateKey(passphrase, info.salt,
|
||||
info.iterations, info.bits != null ? info.bits / 8 : 32));
|
||||
info.iterations, info.bits != null ? (info.bits / 8).ceil() : 32));
|
||||
}
|
||||
|
||||
void setValidator(String type, Future<bool> Function(String) validator) {
|
||||
|
@ -171,16 +181,22 @@ class SSSS {
|
|||
if (client.database == null) {
|
||||
return null;
|
||||
}
|
||||
// check if it is still valid
|
||||
final keys = keyIdsFromType(type);
|
||||
final isValid = (dbEntry) =>
|
||||
keys.contains(dbEntry.keyId) &&
|
||||
client.accountData[type].content['encrypted'][dbEntry.keyId]
|
||||
['ciphertext'] ==
|
||||
dbEntry.ciphertext;
|
||||
if (_cache.containsKey(type) && isValid(_cache[type])) {
|
||||
return _cache[type].content;
|
||||
}
|
||||
final ret = await client.database.getSSSSCache(client.id, type);
|
||||
if (ret == null) {
|
||||
return null;
|
||||
}
|
||||
// check if it is still valid
|
||||
final keys = keyIdsFromType(type);
|
||||
if (keys.contains(ret.keyId) &&
|
||||
client.accountData[type].content['encrypted'][ret.keyId]
|
||||
['ciphertext'] ==
|
||||
ret.ciphertext) {
|
||||
if (isValid(ret)) {
|
||||
_cache[type] = ret;
|
||||
return ret.content;
|
||||
}
|
||||
return null;
|
||||
|
@ -221,7 +237,7 @@ class SSSS {
|
|||
'mac': encrypted.mac,
|
||||
};
|
||||
// store the thing in your account data
|
||||
await client.api.setAccountData(client.userID, type, content);
|
||||
await client.setAccountData(client.userID, type, content);
|
||||
if (CACHE_TYPES.contains(type) && client.database != null) {
|
||||
// cache the thing
|
||||
await client.database
|
||||
|
@ -242,25 +258,34 @@ class SSSS {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> maybeRequestAll(List<DeviceKeys> devices) async {
|
||||
Future<void> maybeRequestAll([List<DeviceKeys> devices]) async {
|
||||
for (final type in CACHE_TYPES) {
|
||||
final secret = await getCached(type);
|
||||
if (secret == null) {
|
||||
await request(type, devices);
|
||||
if (keyIdsFromType(type) != null) {
|
||||
final secret = await getCached(type);
|
||||
if (secret == null) {
|
||||
await request(type, devices);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> request(String type, List<DeviceKeys> devices) async {
|
||||
Future<void> request(String type, [List<DeviceKeys> devices]) async {
|
||||
// only send to own, verified devices
|
||||
print('[SSSS] Requesting type ${type}...');
|
||||
Logs.info('[SSSS] Requesting type ${type}...');
|
||||
if (devices == null || devices.isEmpty) {
|
||||
if (!client.userDeviceKeys.containsKey(client.userID)) {
|
||||
Logs.warning('[SSSS] User does not have any devices');
|
||||
return;
|
||||
}
|
||||
devices = client.userDeviceKeys[client.userID].deviceKeys.values.toList();
|
||||
}
|
||||
devices.removeWhere((DeviceKeys d) =>
|
||||
d.userId != client.userID ||
|
||||
!d.verified ||
|
||||
d.blocked ||
|
||||
d.deviceId == client.deviceID);
|
||||
if (devices.isEmpty) {
|
||||
print('[SSSS] Warn: No devices');
|
||||
Logs.warning('[SSSS] No devices');
|
||||
return;
|
||||
}
|
||||
final requestId = client.generateUniqueTransactionId();
|
||||
|
@ -270,7 +295,7 @@ class SSSS {
|
|||
devices: devices,
|
||||
);
|
||||
pendingShareRequests[requestId] = request;
|
||||
await client.sendToDevice(devices, 'm.secret.request', {
|
||||
await client.sendToDeviceEncrypted(devices, 'm.secret.request', {
|
||||
'action': 'request',
|
||||
'requesting_device_id': client.deviceID,
|
||||
'request_id': requestId,
|
||||
|
@ -278,35 +303,57 @@ class SSSS {
|
|||
});
|
||||
}
|
||||
|
||||
DateTime _lastCacheRequest;
|
||||
bool _isPeriodicallyRequestingMissingCache = false;
|
||||
Future<void> periodicallyRequestMissingCache() async {
|
||||
if (_isPeriodicallyRequestingMissingCache ||
|
||||
(_lastCacheRequest != null &&
|
||||
DateTime.now()
|
||||
.subtract(Duration(minutes: 15))
|
||||
.isBefore(_lastCacheRequest)) ||
|
||||
client.isUnknownSession) {
|
||||
// we are already requesting right now or we attempted to within the last 15 min
|
||||
return;
|
||||
}
|
||||
_lastCacheRequest = DateTime.now();
|
||||
_isPeriodicallyRequestingMissingCache = true;
|
||||
try {
|
||||
await maybeRequestAll();
|
||||
} finally {
|
||||
_isPeriodicallyRequestingMissingCache = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
|
||||
if (event.type == 'm.secret.request') {
|
||||
// got a request to share a secret
|
||||
print('[SSSS] Received sharing request...');
|
||||
Logs.info('[SSSS] Received sharing request...');
|
||||
if (event.sender != client.userID ||
|
||||
!client.userDeviceKeys.containsKey(client.userID)) {
|
||||
print('[SSSS] Not sent by us');
|
||||
Logs.info('[SSSS] Not sent by us');
|
||||
return; // we aren't asking for it ourselves, so ignore
|
||||
}
|
||||
if (event.content['action'] != 'request') {
|
||||
print('[SSSS] it is actually a cancelation');
|
||||
Logs.info('[SSSS] it is actually a cancelation');
|
||||
return; // not actually requesting, so ignore
|
||||
}
|
||||
final device = client.userDeviceKeys[client.userID]
|
||||
.deviceKeys[event.content['requesting_device_id']];
|
||||
if (device == null || !device.verified || device.blocked) {
|
||||
print('[SSSS] Unknown / unverified devices, ignoring');
|
||||
Logs.info('[SSSS] Unknown / unverified devices, ignoring');
|
||||
return; // nope....unknown or untrusted device
|
||||
}
|
||||
// alright, all seems fine...let's check if we actually have the secret they are asking for
|
||||
final type = event.content['name'];
|
||||
final secret = await getCached(type);
|
||||
if (secret == null) {
|
||||
print('[SSSS] We don\'t have the secret for ${type} ourself, ignoring');
|
||||
Logs.info(
|
||||
'[SSSS] We don\'t have the secret for ${type} ourself, ignoring');
|
||||
return; // seems like we don't have this, either
|
||||
}
|
||||
// okay, all checks out...time to share this secret!
|
||||
print('[SSSS] Replying with secret for ${type}');
|
||||
await client.sendToDevice(
|
||||
Logs.info('[SSSS] Replying with secret for ${type}');
|
||||
await client.sendToDeviceEncrypted(
|
||||
[device],
|
||||
'm.secret.send',
|
||||
{
|
||||
|
@ -315,11 +362,11 @@ class SSSS {
|
|||
});
|
||||
} else if (event.type == 'm.secret.send') {
|
||||
// receiving a secret we asked for
|
||||
print('[SSSS] Received shared secret...');
|
||||
Logs.info('[SSSS] Received shared secret...');
|
||||
if (event.sender != client.userID ||
|
||||
!pendingShareRequests.containsKey(event.content['request_id']) ||
|
||||
event.encryptedContent == null) {
|
||||
print('[SSSS] Not by us or unknown request');
|
||||
Logs.info('[SSSS] Not by us or unknown request');
|
||||
return; // we have no idea what we just received
|
||||
}
|
||||
final request = pendingShareRequests[event.content['request_id']];
|
||||
|
@ -330,26 +377,26 @@ class SSSS {
|
|||
d.curve25519Key == event.encryptedContent['sender_key'],
|
||||
orElse: () => null);
|
||||
if (device == null) {
|
||||
print('[SSSS] Someone else replied?');
|
||||
Logs.info('[SSSS] Someone else replied?');
|
||||
return; // someone replied whom we didn't send the share request to
|
||||
}
|
||||
final secret = event.content['secret'];
|
||||
if (!(event.content['secret'] is String)) {
|
||||
print('[SSSS] Secret wasn\'t a string');
|
||||
Logs.info('[SSSS] Secret wasn\'t a string');
|
||||
return; // the secret wasn't a string....wut?
|
||||
}
|
||||
// let's validate if the secret is, well, valid
|
||||
if (_validators.containsKey(request.type) &&
|
||||
!(await _validators[request.type](secret))) {
|
||||
print('[SSSS] The received secret was invalid');
|
||||
Logs.info('[SSSS] The received secret was invalid');
|
||||
return; // didn't pass the validator
|
||||
}
|
||||
pendingShareRequests.remove(request.requestId);
|
||||
if (request.start.add(Duration(minutes: 15)).isBefore(DateTime.now())) {
|
||||
print('[SSSS] Request is too far in the past');
|
||||
Logs.info('[SSSS] Request is too far in the past');
|
||||
return; // our request is more than 15min in the past...better not trust it anymore
|
||||
}
|
||||
print('[SSSS] Secret for type ${request.type} is ok, storing it');
|
||||
Logs.info('[SSSS] Secret for type ${request.type} is ok, storing it');
|
||||
if (client.database != null) {
|
||||
final keyId = keyIdFromType(request.type);
|
||||
if (keyId != null) {
|
||||
|
|
29
lib/encryption/utils/json_signature_check_extension.dart
Normal file
29
lib/encryption/utils/json_signature_check_extension.dart
Normal file
|
@ -0,0 +1,29 @@
|
|||
import 'package:canonical_json/canonical_json.dart';
|
||||
import 'package:famedlysdk/src/utils/logs.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
extension JsonSignatureCheckExtension on Map<String, dynamic> {
|
||||
/// Checks the signature of a signed json object.
|
||||
bool checkJsonSignature(String key, String userId, String deviceId) {
|
||||
final Map<String, dynamic> signatures = this['signatures'];
|
||||
if (signatures == null || !signatures.containsKey(userId)) return false;
|
||||
remove('unsigned');
|
||||
remove('signatures');
|
||||
if (!signatures[userId].containsKey('ed25519:$deviceId')) return false;
|
||||
final String signature = signatures[userId]['ed25519:$deviceId'];
|
||||
final canonical = canonicalJson.encode(this);
|
||||
final message = String.fromCharCodes(canonical);
|
||||
var isValid = false;
|
||||
final olmutil = olm.Utility();
|
||||
try {
|
||||
olmutil.ed25519_verify(key, message, signature);
|
||||
isValid = true;
|
||||
} catch (e, s) {
|
||||
isValid = false;
|
||||
Logs.error('[LibOlm] Signature check failed: ' + e.toString(), s);
|
||||
} finally {
|
||||
olmutil.free();
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
}
|
|
@ -18,12 +18,14 @@
|
|||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:canonical_json/canonical_json.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
|
||||
import 'package:canonical_json/canonical_json.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
|
||||
import '../../famedlysdk.dart';
|
||||
import '../../matrix_api.dart';
|
||||
import '../../src/utils/logs.dart';
|
||||
import '../encryption.dart';
|
||||
|
||||
/*
|
||||
|
@ -150,7 +152,7 @@ class KeyVerification {
|
|||
}
|
||||
|
||||
void dispose() {
|
||||
print('[Key Verification] disposing object...');
|
||||
Logs.info('[Key Verification] disposing object...');
|
||||
method?.dispose();
|
||||
}
|
||||
|
||||
|
@ -202,7 +204,8 @@ class KeyVerification {
|
|||
await Future.delayed(Duration(milliseconds: 50));
|
||||
}
|
||||
_handlePayloadLock = true;
|
||||
print('[Key Verification] Received type ${type}: ' + payload.toString());
|
||||
Logs.info(
|
||||
'[Key Verification] Received type ${type}: ' + payload.toString());
|
||||
try {
|
||||
var thisLastStep = lastStep;
|
||||
switch (type) {
|
||||
|
@ -215,7 +218,10 @@ class KeyVerification {
|
|||
DateTime.fromMillisecondsSinceEpoch(payload['timestamp']);
|
||||
if (now.subtract(Duration(minutes: 10)).isAfter(verifyTime) ||
|
||||
now.add(Duration(minutes: 5)).isBefore(verifyTime)) {
|
||||
await cancel('m.timeout');
|
||||
// if the request is more than 20min in the past we just silently fail it
|
||||
// to not generate too many cancels
|
||||
await cancel('m.timeout',
|
||||
now.subtract(Duration(minutes: 20)).isAfter(verifyTime));
|
||||
return;
|
||||
}
|
||||
// verify it has a method we can use
|
||||
|
@ -280,6 +286,13 @@ class KeyVerification {
|
|||
}
|
||||
method = _makeVerificationMethod(payload['method'], this);
|
||||
if (lastStep == null) {
|
||||
// validate the start time
|
||||
if (room != null) {
|
||||
// we just silently ignore in-room-verification starts
|
||||
await cancel('m.unknown_method', true);
|
||||
return;
|
||||
}
|
||||
// validate the specific payload
|
||||
if (!method.validateStart(payload)) {
|
||||
await cancel('m.unknown_method');
|
||||
return;
|
||||
|
@ -287,7 +300,7 @@ class KeyVerification {
|
|||
startPaylaod = payload;
|
||||
setState(KeyVerificationState.askAccept);
|
||||
} else {
|
||||
print('handling start in method.....');
|
||||
Logs.info('handling start in method.....');
|
||||
await method.handlePayload(type, payload);
|
||||
}
|
||||
break;
|
||||
|
@ -301,18 +314,20 @@ class KeyVerification {
|
|||
setState(KeyVerificationState.error);
|
||||
break;
|
||||
default:
|
||||
await method.handlePayload(type, payload);
|
||||
if (method != null) {
|
||||
await method.handlePayload(type, payload);
|
||||
} else {
|
||||
await cancel('m.invalid_message');
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (lastStep == thisLastStep) {
|
||||
lastStep = type;
|
||||
}
|
||||
} catch (err, stacktrace) {
|
||||
print('[Key Verification] An error occured: ' + err.toString());
|
||||
print(stacktrace);
|
||||
if (deviceId != null) {
|
||||
await cancel('m.invalid_message');
|
||||
}
|
||||
Logs.error(
|
||||
'[Key Verification] An error occured: ' + err.toString(), stacktrace);
|
||||
await cancel('m.invalid_message');
|
||||
} finally {
|
||||
_handlePayloadLock = false;
|
||||
}
|
||||
|
@ -510,11 +525,13 @@ class KeyVerification {
|
|||
return false;
|
||||
}
|
||||
|
||||
Future<void> cancel([String code = 'm.unknown']) async {
|
||||
await send('m.key.verification.cancel', {
|
||||
'reason': code,
|
||||
'code': code,
|
||||
});
|
||||
Future<void> cancel([String code = 'm.unknown', bool quiet = false]) async {
|
||||
if (!quiet && (deviceId != null || room != null)) {
|
||||
await send('m.key.verification.cancel', {
|
||||
'reason': code,
|
||||
'code': code,
|
||||
});
|
||||
}
|
||||
canceled = true;
|
||||
canceledCode = code;
|
||||
setState(KeyVerificationState.error);
|
||||
|
@ -536,9 +553,10 @@ class KeyVerification {
|
|||
|
||||
Future<void> send(String type, Map<String, dynamic> payload) async {
|
||||
makePayload(payload);
|
||||
print('[Key Verification] Sending type ${type}: ' + payload.toString());
|
||||
Logs.info('[Key Verification] Sending type ${type}: ' + payload.toString());
|
||||
if (room != null) {
|
||||
print('[Key Verification] Sending to ${userId} in room ${room.id}');
|
||||
Logs.info(
|
||||
'[Key Verification] Sending to ${userId} in room ${room.id}...');
|
||||
if (['m.key.verification.request'].contains(type)) {
|
||||
payload['msgtype'] = type;
|
||||
payload['to'] = userId;
|
||||
|
@ -552,8 +570,9 @@ class KeyVerification {
|
|||
encryption.keyVerificationManager.addRequest(this);
|
||||
}
|
||||
} else {
|
||||
print('[Key Verification] Sending to ${userId} device ${deviceId}');
|
||||
await client.sendToDevice(
|
||||
Logs.info(
|
||||
'[Key Verification] Sending to ${userId} device ${deviceId}...');
|
||||
await client.sendToDeviceEncrypted(
|
||||
[client.userDeviceKeys[userId].deviceKeys[deviceId]], type, payload);
|
||||
}
|
||||
}
|
||||
|
@ -679,8 +698,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
break;
|
||||
}
|
||||
} catch (err, stacktrace) {
|
||||
print('[Key Verification SAS] An error occured: ' + err.toString());
|
||||
print(stacktrace);
|
||||
Logs.error('[Key Verification SAS] An error occured: ' + err.toString(),
|
||||
stacktrace);
|
||||
if (request.deviceId != null) {
|
||||
await request.cancel('m.invalid_message');
|
||||
}
|
||||
|
|
|
@ -17,7 +17,9 @@
|
|||
*/
|
||||
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import '../../src/database/database.dart' show DbOlmSessions;
|
||||
import '../../src/utils/logs.dart';
|
||||
|
||||
class OlmSession {
|
||||
String identityKey;
|
||||
|
@ -46,8 +48,8 @@ class OlmSession {
|
|||
lastReceived =
|
||||
dbEntry.lastReceived ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||
assert(sessionId == session.session_id());
|
||||
} catch (e) {
|
||||
print('[LibOlm] Could not unpickle olm session: ' + e.toString());
|
||||
} catch (e, s) {
|
||||
Logs.error('[LibOlm] Could not unpickle olm session: ' + e.toString(), s);
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,9 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import '../../src/database/database.dart' show DbOutboundGroupSession;
|
||||
import '../../src/utils/logs.dart';
|
||||
|
||||
class OutboundGroupSession {
|
||||
List<String> devices;
|
||||
|
@ -44,10 +46,11 @@ class OutboundGroupSession {
|
|||
devices = List<String>.from(json.decode(dbEntry.deviceIds));
|
||||
creationTime = dbEntry.creationTime;
|
||||
sentMessages = dbEntry.sentMessages;
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
dispose();
|
||||
print(
|
||||
'[LibOlm] Unable to unpickle outboundGroupSession: ' + e.toString());
|
||||
Logs.error(
|
||||
'[LibOlm] Unable to unpickle outboundGroupSession: ' + e.toString(),
|
||||
s);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,41 +19,87 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
|
||||
import '../../famedlysdk.dart';
|
||||
import '../../src/database/database.dart' show DbInboundGroupSession;
|
||||
import '../../src/utils/logs.dart';
|
||||
|
||||
class SessionKey {
|
||||
Map<String, dynamic> content;
|
||||
Map<String, int> indexes;
|
||||
Map<String, String> indexes;
|
||||
olm.InboundGroupSession inboundGroupSession;
|
||||
final String key;
|
||||
List<dynamic> get forwardingCurve25519KeyChain =>
|
||||
content['forwarding_curve25519_key_chain'] ?? [];
|
||||
String get senderClaimedEd25519Key =>
|
||||
content['sender_claimed_ed25519_key'] ?? '';
|
||||
String get senderKey => content['sender_key'] ?? '';
|
||||
List<String> get forwardingCurve25519KeyChain =>
|
||||
(content['forwarding_curve25519_key_chain'] != null
|
||||
? List<String>.from(content['forwarding_curve25519_key_chain'])
|
||||
: null) ??
|
||||
<String>[];
|
||||
Map<String, String> senderClaimedKeys;
|
||||
String senderKey;
|
||||
bool get isValid => inboundGroupSession != null;
|
||||
String roomId;
|
||||
String sessionId;
|
||||
|
||||
SessionKey({this.content, this.inboundGroupSession, this.key, this.indexes});
|
||||
SessionKey(
|
||||
{this.content,
|
||||
this.inboundGroupSession,
|
||||
this.key,
|
||||
this.indexes,
|
||||
this.roomId,
|
||||
this.sessionId,
|
||||
String senderKey,
|
||||
Map<String, String> senderClaimedKeys}) {
|
||||
_setSenderKey(senderKey);
|
||||
_setSenderClaimedKeys(senderClaimedKeys);
|
||||
}
|
||||
|
||||
SessionKey.fromDb(DbInboundGroupSession dbEntry, String key) : key = key {
|
||||
final parsedContent = Event.getMapFromPayload(dbEntry.content);
|
||||
final parsedIndexes = Event.getMapFromPayload(dbEntry.indexes);
|
||||
final parsedSenderClaimedKeys =
|
||||
Event.getMapFromPayload(dbEntry.senderClaimedKeys);
|
||||
content =
|
||||
parsedContent != null ? Map<String, dynamic>.from(parsedContent) : null;
|
||||
indexes = parsedIndexes != null
|
||||
? Map<String, int>.from(parsedIndexes)
|
||||
: <String, int>{};
|
||||
// we need to try...catch as the map used to be <String, int> and that will throw an error.
|
||||
try {
|
||||
indexes = parsedIndexes != null
|
||||
? Map<String, String>.from(parsedIndexes)
|
||||
: <String, String>{};
|
||||
} catch (e) {
|
||||
indexes = <String, String>{};
|
||||
}
|
||||
roomId = dbEntry.roomId;
|
||||
sessionId = dbEntry.sessionId;
|
||||
_setSenderKey(dbEntry.senderKey);
|
||||
_setSenderClaimedKeys(Map<String, String>.from(parsedSenderClaimedKeys));
|
||||
|
||||
inboundGroupSession = olm.InboundGroupSession();
|
||||
try {
|
||||
inboundGroupSession.unpickle(key, dbEntry.pickle);
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
dispose();
|
||||
print('[LibOlm] Unable to unpickle inboundGroupSession: ' + e.toString());
|
||||
Logs.error(
|
||||
'[LibOlm] Unable to unpickle inboundGroupSession: ' + e.toString(),
|
||||
s);
|
||||
}
|
||||
}
|
||||
|
||||
void _setSenderKey(String key) {
|
||||
senderKey = key ?? content['sender_key'] ?? '';
|
||||
}
|
||||
|
||||
void _setSenderClaimedKeys(Map<String, String> keys) {
|
||||
senderClaimedKeys = (keys != null && keys.isNotEmpty)
|
||||
? keys
|
||||
: (content['sender_claimed_keys'] is Map
|
||||
? Map<String, String>.from(content['sender_claimed_keys'])
|
||||
: (content['sender_claimed_ed25519_key'] is String
|
||||
? <String, String>{
|
||||
'ed25519': content['sender_claimed_ed25519_key']
|
||||
}
|
||||
: <String, String>{}));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
if (content != null) {
|
||||
|
|
|
@ -19,19 +19,20 @@
|
|||
library famedlysdk;
|
||||
|
||||
export 'matrix_api.dart';
|
||||
export 'package:famedlysdk/src/utils/room_update.dart';
|
||||
export 'package:famedlysdk/src/utils/event_update.dart';
|
||||
export 'package:famedlysdk/src/utils/device_keys_list.dart';
|
||||
export 'package:famedlysdk/src/utils/matrix_file.dart';
|
||||
export 'package:famedlysdk/src/utils/matrix_id_string_extension.dart';
|
||||
export 'package:famedlysdk/src/utils/uri_extension.dart';
|
||||
export 'package:famedlysdk/src/utils/matrix_localizations.dart';
|
||||
export 'package:famedlysdk/src/utils/receipt.dart';
|
||||
export 'package:famedlysdk/src/utils/states_map.dart';
|
||||
export 'package:famedlysdk/src/utils/to_device_event.dart';
|
||||
export 'package:famedlysdk/src/client.dart';
|
||||
export 'package:famedlysdk/src/event.dart';
|
||||
export 'package:famedlysdk/src/room.dart';
|
||||
export 'package:famedlysdk/src/timeline.dart';
|
||||
export 'package:famedlysdk/src/user.dart';
|
||||
export 'package:famedlysdk/src/database/database.dart' show Database;
|
||||
export 'src/utils/room_update.dart';
|
||||
export 'src/utils/event_update.dart';
|
||||
export 'src/utils/device_keys_list.dart';
|
||||
export 'src/utils/matrix_file.dart';
|
||||
export 'src/utils/matrix_id_string_extension.dart';
|
||||
export 'src/utils/uri_extension.dart';
|
||||
export 'src/utils/matrix_localizations.dart';
|
||||
export 'src/utils/receipt.dart';
|
||||
export 'src/utils/states_map.dart';
|
||||
export 'src/utils/sync_update_extension.dart';
|
||||
export 'src/utils/to_device_event.dart';
|
||||
export 'src/client.dart';
|
||||
export 'src/event.dart';
|
||||
export 'src/room.dart';
|
||||
export 'src/timeline.dart';
|
||||
export 'src/user.dart';
|
||||
export 'src/database/database.dart' show Database;
|
||||
|
|
|
@ -18,49 +18,49 @@
|
|||
|
||||
library matrix_api;
|
||||
|
||||
export 'package:famedlysdk/matrix_api/matrix_api.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/basic_event_with_sender.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/basic_event.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/device.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/basic_room_event.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/event_context.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/matrix_event.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/event_types.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/events_sync_update.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/filter.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/keys_query_response.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/login_response.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/login_types.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/matrix_exception.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/matrix_keys.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/message_types.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/presence_content.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/notifications_query_response.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/one_time_keys_claim_response.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/open_graph_data.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/open_id_credentials.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/presence.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/profile.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/public_rooms_response.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/push_rule_set.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/pusher.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/request_token_response.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/room_alias_informations.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/room_keys_info.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/room_keys_keys.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/room_summary.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/server_capabilities.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/stripped_state_event.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/supported_protocol.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/supported_versions.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/sync_update.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/tag.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/third_party_identifier.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/third_party_location.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/third_party_user.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/timeline_history_response.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/turn_server_credentials.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/upload_key_signatures_response.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/user_search_result.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/well_known_informations.dart';
|
||||
export 'package:famedlysdk/matrix_api/model/who_is_info.dart';
|
||||
export 'matrix_api/matrix_api.dart';
|
||||
export 'matrix_api/model/basic_event.dart';
|
||||
export 'matrix_api/model/basic_event_with_sender.dart';
|
||||
export 'matrix_api/model/basic_room_event.dart';
|
||||
export 'matrix_api/model/device.dart';
|
||||
export 'matrix_api/model/event_context.dart';
|
||||
export 'matrix_api/model/event_types.dart';
|
||||
export 'matrix_api/model/events_sync_update.dart';
|
||||
export 'matrix_api/model/filter.dart';
|
||||
export 'matrix_api/model/keys_query_response.dart';
|
||||
export 'matrix_api/model/login_response.dart';
|
||||
export 'matrix_api/model/login_types.dart';
|
||||
export 'matrix_api/model/matrix_event.dart';
|
||||
export 'matrix_api/model/matrix_exception.dart';
|
||||
export 'matrix_api/model/matrix_keys.dart';
|
||||
export 'matrix_api/model/message_types.dart';
|
||||
export 'matrix_api/model/notifications_query_response.dart';
|
||||
export 'matrix_api/model/one_time_keys_claim_response.dart';
|
||||
export 'matrix_api/model/open_graph_data.dart';
|
||||
export 'matrix_api/model/open_id_credentials.dart';
|
||||
export 'matrix_api/model/presence.dart';
|
||||
export 'matrix_api/model/presence_content.dart';
|
||||
export 'matrix_api/model/profile.dart';
|
||||
export 'matrix_api/model/public_rooms_response.dart';
|
||||
export 'matrix_api/model/push_rule_set.dart';
|
||||
export 'matrix_api/model/pusher.dart';
|
||||
export 'matrix_api/model/request_token_response.dart';
|
||||
export 'matrix_api/model/room_alias_informations.dart';
|
||||
export 'matrix_api/model/room_keys_info.dart';
|
||||
export 'matrix_api/model/room_keys_keys.dart';
|
||||
export 'matrix_api/model/room_summary.dart';
|
||||
export 'matrix_api/model/server_capabilities.dart';
|
||||
export 'matrix_api/model/stripped_state_event.dart';
|
||||
export 'matrix_api/model/supported_protocol.dart';
|
||||
export 'matrix_api/model/supported_versions.dart';
|
||||
export 'matrix_api/model/sync_update.dart';
|
||||
export 'matrix_api/model/tag.dart';
|
||||
export 'matrix_api/model/third_party_identifier.dart';
|
||||
export 'matrix_api/model/third_party_location.dart';
|
||||
export 'matrix_api/model/third_party_user.dart';
|
||||
export 'matrix_api/model/timeline_history_response.dart';
|
||||
export 'matrix_api/model/turn_server_credentials.dart';
|
||||
export 'matrix_api/model/upload_key_signatures_response.dart';
|
||||
export 'matrix_api/model/user_search_result.dart';
|
||||
export 'matrix_api/model/well_known_informations.dart';
|
||||
export 'matrix_api/model/who_is_info.dart';
|
||||
|
|
|
@ -19,19 +19,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:famedlysdk/matrix_api/model/filter.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/keys_query_response.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/login_types.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/notifications_query_response.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/open_graph_data.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/profile.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/request_token_response.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/server_capabilities.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/supported_versions.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/sync_update.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/third_party_location.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/timeline_history_response.dart';
|
||||
import 'package:famedlysdk/matrix_api/model/user_search_result.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:moor/moor.dart';
|
||||
|
@ -39,25 +26,38 @@ import 'package:moor/moor.dart';
|
|||
import 'model/device.dart';
|
||||
import 'model/event_context.dart';
|
||||
import 'model/events_sync_update.dart';
|
||||
import 'model/filter.dart';
|
||||
import 'model/keys_query_response.dart';
|
||||
import 'model/login_response.dart';
|
||||
import 'model/login_types.dart';
|
||||
import 'model/matrix_event.dart';
|
||||
import 'model/matrix_exception.dart';
|
||||
import 'model/matrix_keys.dart';
|
||||
import 'model/notifications_query_response.dart';
|
||||
import 'model/one_time_keys_claim_response.dart';
|
||||
import 'model/open_graph_data.dart';
|
||||
import 'model/open_id_credentials.dart';
|
||||
import 'model/presence_content.dart';
|
||||
import 'model/profile.dart';
|
||||
import 'model/public_rooms_response.dart';
|
||||
import 'model/push_rule_set.dart';
|
||||
import 'model/pusher.dart';
|
||||
import 'model/request_token_response.dart';
|
||||
import 'model/room_alias_informations.dart';
|
||||
import 'model/room_keys_info.dart';
|
||||
import 'model/room_keys_keys.dart';
|
||||
import 'model/server_capabilities.dart';
|
||||
import 'model/supported_protocol.dart';
|
||||
import 'model/supported_versions.dart';
|
||||
import 'model/sync_update.dart';
|
||||
import 'model/tag.dart';
|
||||
import 'model/third_party_identifier.dart';
|
||||
import 'model/third_party_location.dart';
|
||||
import 'model/third_party_user.dart';
|
||||
import 'model/timeline_history_response.dart';
|
||||
import 'model/turn_server_credentials.dart';
|
||||
import 'model/upload_key_signatures_response.dart';
|
||||
import 'model/user_search_result.dart';
|
||||
import 'model/well_known_informations.dart';
|
||||
import 'model/who_is_info.dart';
|
||||
|
||||
|
@ -88,9 +88,6 @@ class MatrixApi {
|
|||
/// timeout which is usually 30 seconds.
|
||||
int syncTimeoutSec;
|
||||
|
||||
/// Whether debug prints should be displayed.
|
||||
final bool debug;
|
||||
|
||||
http.Client httpClient = http.Client();
|
||||
|
||||
bool get _testMode =>
|
||||
|
@ -101,7 +98,6 @@ class MatrixApi {
|
|||
MatrixApi({
|
||||
this.homeserver,
|
||||
this.accessToken,
|
||||
this.debug = false,
|
||||
http.Client httpClient,
|
||||
this.syncTimeoutSec = 30,
|
||||
}) {
|
||||
|
@ -161,11 +157,6 @@ class MatrixApi {
|
|||
headers['Authorization'] = 'Bearer ${accessToken}';
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
print(
|
||||
'[REQUEST ${describeEnum(type)}] $action, Data: ${jsonEncode(data)}');
|
||||
}
|
||||
|
||||
http.Response resp;
|
||||
var jsonResp = <String, dynamic>{};
|
||||
try {
|
||||
|
@ -212,8 +203,6 @@ class MatrixApi {
|
|||
|
||||
throw exception;
|
||||
}
|
||||
|
||||
if (debug) print('[RESPONSE] ${jsonResp.toString()}');
|
||||
_timeoutFactor = 1;
|
||||
} on TimeoutException catch (_) {
|
||||
_timeoutFactor *= 2;
|
||||
|
@ -787,7 +776,7 @@ class MatrixApi {
|
|||
String stateKey = '',
|
||||
]) async {
|
||||
final response = await request(RequestType.PUT,
|
||||
'/client/r0/rooms/${Uri.encodeQueryComponent(roomId)}/state/${Uri.encodeQueryComponent(eventType)}/${Uri.encodeQueryComponent(stateKey)}',
|
||||
'/client/r0/rooms/${Uri.encodeComponent(roomId)}/state/${Uri.encodeComponent(eventType)}/${Uri.encodeComponent(stateKey)}',
|
||||
data: content);
|
||||
return response['event_id'];
|
||||
}
|
||||
|
@ -803,7 +792,7 @@ class MatrixApi {
|
|||
Map<String, dynamic> content,
|
||||
) async {
|
||||
final response = await request(RequestType.PUT,
|
||||
'/client/r0/rooms/${Uri.encodeQueryComponent(roomId)}/send/${Uri.encodeQueryComponent(eventType)}/${Uri.encodeQueryComponent(txnId)}',
|
||||
'/client/r0/rooms/${Uri.encodeComponent(roomId)}/send/${Uri.encodeComponent(eventType)}/${Uri.encodeComponent(txnId)}',
|
||||
data: content);
|
||||
return response['event_id'];
|
||||
}
|
||||
|
@ -818,7 +807,7 @@ class MatrixApi {
|
|||
String reason,
|
||||
}) async {
|
||||
final response = await request(RequestType.PUT,
|
||||
'/client/r0/rooms/${Uri.encodeQueryComponent(roomId)}/redact/${Uri.encodeQueryComponent(eventId)}/${Uri.encodeQueryComponent(txnId)}',
|
||||
'/client/r0/rooms/${Uri.encodeComponent(roomId)}/redact/${Uri.encodeComponent(eventId)}/${Uri.encodeComponent(txnId)}',
|
||||
data: {
|
||||
if (reason != null) 'reason': reason,
|
||||
});
|
||||
|
@ -1300,7 +1289,6 @@ class MatrixApi {
|
|||
streamedRequest.contentLength = await file.length;
|
||||
streamedRequest.sink.add(file);
|
||||
streamedRequest.sink.close();
|
||||
if (debug) print('[UPLOADING] $fileName');
|
||||
var streamedResponse = _testMode ? null : await streamedRequest.send();
|
||||
Map<String, dynamic> jsonResponse = json.decode(
|
||||
String.fromCharCodes(_testMode
|
||||
|
@ -1341,8 +1329,11 @@ class MatrixApi {
|
|||
|
||||
/// This endpoint is used to send send-to-device events to a set of client devices.
|
||||
/// https://matrix.org/docs/spec/client_server/r0.6.1#put-matrix-client-r0-sendtodevice-eventtype-txnid
|
||||
Future<void> sendToDevice(String eventType, String txnId,
|
||||
Map<String, Map<String, Map<String, dynamic>>> messages) async {
|
||||
Future<void> sendToDevice(
|
||||
String eventType,
|
||||
String txnId,
|
||||
Map<String, Map<String, Map<String, dynamic>>> messages,
|
||||
) async {
|
||||
await request(
|
||||
RequestType.PUT,
|
||||
'/client/r0/sendToDevice/${Uri.encodeComponent(eventType)}/${Uri.encodeComponent(txnId)}',
|
||||
|
@ -1734,7 +1725,7 @@ class MatrixApi {
|
|||
Future<Map<String, Tag>> requestRoomTags(String userId, String roomId) async {
|
||||
final response = await request(
|
||||
RequestType.GET,
|
||||
'/client/r0/user/${Uri.encodeQueryComponent(userId)}/rooms/${Uri.encodeQueryComponent(roomId)}/tags',
|
||||
'/client/r0/user/${Uri.encodeComponent(userId)}/rooms/${Uri.encodeComponent(roomId)}/tags',
|
||||
);
|
||||
return (response['tags'] as Map).map(
|
||||
(k, v) => MapEntry(k, Tag.fromJson(v)),
|
||||
|
@ -1750,7 +1741,7 @@ class MatrixApi {
|
|||
double order,
|
||||
}) async {
|
||||
await request(RequestType.PUT,
|
||||
'/client/r0/user/${Uri.encodeQueryComponent(userId)}/rooms/${Uri.encodeQueryComponent(roomId)}/tags/${Uri.encodeQueryComponent(tag)}',
|
||||
'/client/r0/user/${Uri.encodeComponent(userId)}/rooms/${Uri.encodeComponent(roomId)}/tags/${Uri.encodeComponent(tag)}',
|
||||
data: {
|
||||
if (order != null) 'order': order,
|
||||
});
|
||||
|
@ -1762,7 +1753,7 @@ class MatrixApi {
|
|||
Future<void> removeRoomTag(String userId, String roomId, String tag) async {
|
||||
await request(
|
||||
RequestType.DELETE,
|
||||
'/client/r0/user/${Uri.encodeQueryComponent(userId)}/rooms/${Uri.encodeQueryComponent(roomId)}/tags/${Uri.encodeQueryComponent(tag)}',
|
||||
'/client/r0/user/${Uri.encodeComponent(userId)}/rooms/${Uri.encodeComponent(roomId)}/tags/${Uri.encodeComponent(tag)}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -1777,7 +1768,7 @@ class MatrixApi {
|
|||
) async {
|
||||
await request(
|
||||
RequestType.PUT,
|
||||
'/client/r0/user/${Uri.encodeQueryComponent(userId)}/account_data/${Uri.encodeQueryComponent(type)}',
|
||||
'/client/r0/user/${Uri.encodeComponent(userId)}/account_data/${Uri.encodeComponent(type)}',
|
||||
data: content,
|
||||
);
|
||||
return;
|
||||
|
@ -1791,7 +1782,7 @@ class MatrixApi {
|
|||
) async {
|
||||
return await request(
|
||||
RequestType.GET,
|
||||
'/client/r0/user/${Uri.encodeQueryComponent(userId)}/account_data/${Uri.encodeQueryComponent(type)}',
|
||||
'/client/r0/user/${Uri.encodeComponent(userId)}/account_data/${Uri.encodeComponent(type)}',
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1806,7 +1797,7 @@ class MatrixApi {
|
|||
) async {
|
||||
await request(
|
||||
RequestType.PUT,
|
||||
'/client/r0/user/${Uri.encodeQueryComponent(userId)}/rooms/${Uri.encodeQueryComponent(roomId)}/account_data/${Uri.encodeQueryComponent(type)}',
|
||||
'/client/r0/user/${Uri.encodeComponent(userId)}/rooms/${Uri.encodeComponent(roomId)}/account_data/${Uri.encodeComponent(type)}',
|
||||
data: content,
|
||||
);
|
||||
return;
|
||||
|
@ -1821,7 +1812,7 @@ class MatrixApi {
|
|||
) async {
|
||||
return await request(
|
||||
RequestType.GET,
|
||||
'/client/r0/user/${Uri.encodeQueryComponent(userId)}/rooms/${Uri.encodeQueryComponent(roomId)}/account_data/${Uri.encodeQueryComponent(type)}',
|
||||
'/client/r0/user/${Uri.encodeComponent(userId)}/rooms/${Uri.encodeComponent(roomId)}/account_data/${Uri.encodeComponent(type)}',
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1830,7 +1821,7 @@ class MatrixApi {
|
|||
Future<WhoIsInfo> requestWhoIsInfo(String userId) async {
|
||||
final response = await request(
|
||||
RequestType.GET,
|
||||
'/client/r0/admin/whois/${Uri.encodeQueryComponent(userId)}',
|
||||
'/client/r0/admin/whois/${Uri.encodeComponent(userId)}',
|
||||
);
|
||||
return WhoIsInfo.fromJson(response);
|
||||
}
|
||||
|
@ -1845,7 +1836,7 @@ class MatrixApi {
|
|||
String filter,
|
||||
}) async {
|
||||
final response = await request(RequestType.GET,
|
||||
'/client/r0/rooms/${Uri.encodeQueryComponent(roomId)}/context/${Uri.encodeQueryComponent(eventId)}',
|
||||
'/client/r0/rooms/${Uri.encodeComponent(roomId)}/context/${Uri.encodeComponent(eventId)}',
|
||||
query: {
|
||||
if (filter != null) 'filter': filter,
|
||||
if (limit != null) 'limit': limit.toString(),
|
||||
|
@ -1862,7 +1853,7 @@ class MatrixApi {
|
|||
int score,
|
||||
) async {
|
||||
await request(RequestType.POST,
|
||||
'/client/r0/rooms/${Uri.encodeQueryComponent(roomId)}/report/${Uri.encodeQueryComponent(eventId)}',
|
||||
'/client/r0/rooms/${Uri.encodeComponent(roomId)}/report/${Uri.encodeComponent(eventId)}',
|
||||
data: {
|
||||
'reason': reason,
|
||||
'score': score,
|
||||
|
@ -2071,7 +2062,7 @@ class MatrixApi {
|
|||
return RoomKeysRoom.fromJson(ret);
|
||||
}
|
||||
|
||||
/// Deletes room ekys for a room
|
||||
/// Deletes room keys for a room
|
||||
/// https://matrix.org/docs/spec/client_server/unstable#delete-matrix-client-r0-room-keys-keys-roomid
|
||||
Future<RoomKeysUpdateResponse> deleteRoomKeysRoom(
|
||||
String roomId, String version) async {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:famedlysdk/matrix_api/model/basic_event.dart';
|
||||
import 'basic_event.dart';
|
||||
|
||||
class BasicRoomEvent extends BasicEvent {
|
||||
String roomId;
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
abstract class EventTypes {
|
||||
static const String Message = 'm.room.message';
|
||||
static const String Sticker = 'm.sticker';
|
||||
static const String Reaction = 'm.reaction';
|
||||
static const String Redaction = 'm.room.redaction';
|
||||
static const String RoomAliases = 'm.room.aliases';
|
||||
static const String RoomCanonicalAlias = 'm.room.canonical_alias';
|
||||
|
@ -35,9 +36,9 @@ abstract class EventTypes {
|
|||
static const String HistoryVisibility = 'm.room.history_visibility';
|
||||
static const String Encryption = 'm.room.encryption';
|
||||
static const String Encrypted = 'm.room.encrypted';
|
||||
static const String CallInvite = 'm.room.call.invite';
|
||||
static const String CallAnswer = 'm.room.call.answer';
|
||||
static const String CallCandidates = 'm.room.call.candidates';
|
||||
static const String CallHangup = 'm.room.call.hangup';
|
||||
static const String CallInvite = 'm.call.invite';
|
||||
static const String CallAnswer = 'm.call.answer';
|
||||
static const String CallCandidates = 'm.call.candidates';
|
||||
static const String CallHangup = 'm.call.hangup';
|
||||
static const String Unknown = 'm.unknown';
|
||||
}
|
||||
|
|
|
@ -26,7 +26,9 @@ class KeysQueryResponse {
|
|||
Map<String, MatrixCrossSigningKey> userSigningKeys;
|
||||
|
||||
KeysQueryResponse.fromJson(Map<String, dynamic> json) {
|
||||
failures = Map<String, dynamic>.from(json['failures']);
|
||||
failures = json['failures'] != null
|
||||
? Map<String, dynamic>.from(json['failures'])
|
||||
: null;
|
||||
deviceKeys = json['device_keys'] != null
|
||||
? (json['device_keys'] as Map).map(
|
||||
(k, v) => MapEntry(
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:famedlysdk/matrix_api/model/stripped_state_event.dart';
|
||||
import 'stripped_state_event.dart';
|
||||
|
||||
class MatrixEvent extends StrippedStateEvent {
|
||||
String eventId;
|
||||
|
|
|
@ -25,7 +25,6 @@ abstract class MessageTypes {
|
|||
static const String Audio = 'm.audio';
|
||||
static const String File = 'm.file';
|
||||
static const String Location = 'm.location';
|
||||
static const String Reply = 'm.relates_to';
|
||||
static const String Sticker = 'm.sticker';
|
||||
static const String BadEncrypted = 'm.bad.encrypted';
|
||||
static const String None = 'm.none';
|
||||
|
|
|
@ -22,6 +22,12 @@ class RoomKeysSingleKey {
|
|||
bool isVerified;
|
||||
Map<String, dynamic> sessionData;
|
||||
|
||||
RoomKeysSingleKey(
|
||||
{this.firstMessageIndex,
|
||||
this.forwardedCount,
|
||||
this.isVerified,
|
||||
this.sessionData});
|
||||
|
||||
RoomKeysSingleKey.fromJson(Map<String, dynamic> json) {
|
||||
firstMessageIndex = json['first_message_index'];
|
||||
forwardedCount = json['forwarded_count'];
|
||||
|
@ -42,6 +48,10 @@ class RoomKeysSingleKey {
|
|||
class RoomKeysRoom {
|
||||
Map<String, RoomKeysSingleKey> sessions;
|
||||
|
||||
RoomKeysRoom({this.sessions}) {
|
||||
sessions ??= <String, RoomKeysSingleKey>{};
|
||||
}
|
||||
|
||||
RoomKeysRoom.fromJson(Map<String, dynamic> json) {
|
||||
sessions = (json['sessions'] as Map)
|
||||
.map((k, v) => MapEntry(k, RoomKeysSingleKey.fromJson(v)));
|
||||
|
@ -57,6 +67,10 @@ class RoomKeysRoom {
|
|||
class RoomKeys {
|
||||
Map<String, RoomKeysRoom> rooms;
|
||||
|
||||
RoomKeys({this.rooms}) {
|
||||
rooms ??= <String, RoomKeysRoom>{};
|
||||
}
|
||||
|
||||
RoomKeys.fromJson(Map<String, dynamic> json) {
|
||||
rooms = (json['rooms'] as Map)
|
||||
.map((k, v) => MapEntry(k, RoomKeysRoom.fromJson(v)));
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:famedlysdk/matrix_api/model/basic_event_with_sender.dart';
|
||||
import 'basic_event_with_sender.dart';
|
||||
|
||||
class StrippedStateEvent extends BasicEventWithSender {
|
||||
String stateKey;
|
||||
|
|
|
@ -315,8 +315,8 @@ class DeviceListsUpdate {
|
|||
List<String> changed;
|
||||
List<String> left;
|
||||
DeviceListsUpdate.fromJson(Map<String, dynamic> json) {
|
||||
changed = List<String>.from(json['changed']);
|
||||
left = List<String>.from(json['left']);
|
||||
changed = List<String>.from(json['changed'] ?? []);
|
||||
left = List<String>.from(json['left'] ?? []);
|
||||
}
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
|
|
|
@ -20,7 +20,7 @@ class TurnServerCredentials {
|
|||
String username;
|
||||
String password;
|
||||
List<String> uris;
|
||||
int ttl;
|
||||
num ttl;
|
||||
|
||||
TurnServerCredentials.fromJson(Map<String, dynamic> json) {
|
||||
username = json['username'];
|
||||
|
|
|
@ -20,22 +20,20 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
import 'dart:core';
|
||||
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:famedlysdk/src/room.dart';
|
||||
import 'package:famedlysdk/src/utils/device_keys_list.dart';
|
||||
import 'package:famedlysdk/src/utils/matrix_file.dart';
|
||||
import 'package:famedlysdk/src/utils/to_device_event.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
|
||||
import '../encryption.dart';
|
||||
import '../famedlysdk.dart';
|
||||
import 'database/database.dart' show Database;
|
||||
import 'event.dart';
|
||||
import 'room.dart';
|
||||
import 'user.dart';
|
||||
import 'utils/device_keys_list.dart';
|
||||
import 'utils/event_update.dart';
|
||||
import 'utils/logs.dart';
|
||||
import 'utils/matrix_file.dart';
|
||||
import 'utils/room_update.dart';
|
||||
import 'utils/to_device_event.dart';
|
||||
|
||||
typedef RoomSorter = int Function(Room a, Room b);
|
||||
|
||||
|
@ -44,7 +42,7 @@ enum LoginState { logged, loggedOut }
|
|||
/// Represents a Matrix client to communicate with a
|
||||
/// [Matrix](https://matrix.org) homeserver and is the entry point for this
|
||||
/// SDK.
|
||||
class Client {
|
||||
class Client extends MatrixApi {
|
||||
int _id;
|
||||
int get id => _id;
|
||||
|
||||
|
@ -52,7 +50,8 @@ class Client {
|
|||
|
||||
bool enableE2eeRecovery;
|
||||
|
||||
MatrixApi api;
|
||||
@deprecated
|
||||
MatrixApi get api => this;
|
||||
|
||||
Encryption encryption;
|
||||
|
||||
|
@ -60,15 +59,18 @@ class Client {
|
|||
|
||||
Set<String> importantStateEvents;
|
||||
|
||||
Set<String> roomPreviewLastEvents;
|
||||
|
||||
int sendMessageTimeoutSeconds;
|
||||
|
||||
/// Create a client
|
||||
/// clientName = unique identifier of this client
|
||||
/// debug: Print debug output?
|
||||
/// database: The database instance to use
|
||||
/// enableE2eeRecovery: Enable additional logic to try to recover from bad e2ee sessions
|
||||
/// verificationMethods: A set of all the verification methods this client can handle. Includes:
|
||||
/// [clientName] = unique identifier of this client
|
||||
/// [database]: The database instance to use
|
||||
/// [enableE2eeRecovery]: Enable additional logic to try to recover from bad e2ee sessions
|
||||
/// [verificationMethods]: A set of all the verification methods this client can handle. Includes:
|
||||
/// KeyVerificationMethod.numbers: Compare numbers. Most basic, should be supported
|
||||
/// KeyVerificationMethod.emoji: Compare emojis
|
||||
/// importantStateEvents: A set of all the important state events to load when the client connects.
|
||||
/// [importantStateEvents]: A set of all the important state events to load when the client connects.
|
||||
/// To speed up performance only a set of state events is loaded on startup, those that are
|
||||
/// needed to display a room list. All the remaining state events are automatically post-loaded
|
||||
/// when opening the timeline of a room or manually by calling `room.postLoad()`.
|
||||
|
@ -81,16 +83,22 @@ class Client {
|
|||
/// - m.room.canonical_alias
|
||||
/// - m.room.tombstone
|
||||
/// - *some* m.room.member events, where needed
|
||||
Client(this.clientName,
|
||||
{this.debug = false,
|
||||
this.database,
|
||||
this.enableE2eeRecovery = false,
|
||||
this.verificationMethods,
|
||||
http.Client httpClient,
|
||||
this.importantStateEvents,
|
||||
this.pinUnreadRooms = false}) {
|
||||
/// [roomPreviewLastEvents]: The event types that should be used to calculate the last event
|
||||
/// in a room for the room list.
|
||||
Client(
|
||||
this.clientName, {
|
||||
this.database,
|
||||
this.enableE2eeRecovery = false,
|
||||
this.verificationMethods,
|
||||
http.Client httpClient,
|
||||
this.importantStateEvents,
|
||||
this.roomPreviewLastEvents,
|
||||
this.pinUnreadRooms = false,
|
||||
this.sendMessageTimeoutSeconds = 60,
|
||||
@deprecated bool debug,
|
||||
}) {
|
||||
verificationMethods ??= <KeyVerificationMethod>{};
|
||||
importantStateEvents ??= <String>{};
|
||||
importantStateEvents ??= {};
|
||||
importantStateEvents.addAll([
|
||||
EventTypes.RoomName,
|
||||
EventTypes.RoomAvatar,
|
||||
|
@ -100,17 +108,15 @@ class Client {
|
|||
EventTypes.RoomCanonicalAlias,
|
||||
EventTypes.RoomTombstone,
|
||||
]);
|
||||
api = MatrixApi(debug: debug, httpClient: httpClient);
|
||||
onLoginStateChanged.stream.listen((loginState) {
|
||||
if (debug) {
|
||||
print('[LoginState]: ${loginState.toString()}');
|
||||
}
|
||||
});
|
||||
roomPreviewLastEvents ??= {};
|
||||
roomPreviewLastEvents.addAll([
|
||||
EventTypes.Message,
|
||||
EventTypes.Encrypted,
|
||||
EventTypes.Sticker,
|
||||
]);
|
||||
this.httpClient = httpClient ?? http.Client();
|
||||
}
|
||||
|
||||
/// Whether debug prints should be displayed.
|
||||
final bool debug;
|
||||
|
||||
/// The required name for this client.
|
||||
final String clientName;
|
||||
|
||||
|
@ -130,7 +136,7 @@ class Client {
|
|||
String _deviceName;
|
||||
|
||||
/// Returns the current login state.
|
||||
bool isLogged() => api.accessToken != null;
|
||||
bool isLogged() => accessToken != null;
|
||||
|
||||
/// A list of all rooms the user is participating or invited.
|
||||
List<Room> get rooms => _rooms;
|
||||
|
@ -153,7 +159,7 @@ class Client {
|
|||
|
||||
/// Warning! This endpoint is for testing only!
|
||||
set rooms(List<Room> newList) {
|
||||
print('Warning! This endpoint is for testing only!');
|
||||
Logs.warning('Warning! This endpoint is for testing only!');
|
||||
_rooms = newList;
|
||||
}
|
||||
|
||||
|
@ -165,21 +171,6 @@ class Client {
|
|||
|
||||
int _transactionCounter = 0;
|
||||
|
||||
@Deprecated('Use [api.request()] instead')
|
||||
Future<Map<String, dynamic>> jsonRequest(
|
||||
{RequestType type,
|
||||
String action,
|
||||
dynamic data = '',
|
||||
int timeout,
|
||||
String contentType = 'application/json'}) =>
|
||||
api.request(
|
||||
type,
|
||||
action,
|
||||
data: data,
|
||||
timeout: timeout,
|
||||
contentType: contentType,
|
||||
);
|
||||
|
||||
String generateUniqueTransactionId() {
|
||||
_transactionCounter++;
|
||||
return '${clientName}-${_transactionCounter}-${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
@ -260,8 +251,20 @@ class Client {
|
|||
/// Throws FormatException, TimeoutException and MatrixException on error.
|
||||
Future<bool> checkServer(dynamic serverUrl) async {
|
||||
try {
|
||||
api.homeserver = (serverUrl is Uri) ? serverUrl : Uri.parse(serverUrl);
|
||||
final versions = await api.requestSupportedVersions();
|
||||
if (serverUrl is Uri) {
|
||||
homeserver = serverUrl;
|
||||
} else {
|
||||
// URLs allow to have whitespace surrounding them, see https://www.w3.org/TR/2011/WD-html5-20110525/urls.html
|
||||
// As we want to strip a trailing slash, though, we have to trim the url ourself
|
||||
// and thus can't let Uri.parse() deal with it.
|
||||
serverUrl = serverUrl.trim();
|
||||
// strip a trailing slash
|
||||
if (serverUrl.endsWith('/')) {
|
||||
serverUrl = serverUrl.substring(0, serverUrl.length - 1);
|
||||
}
|
||||
homeserver = Uri.parse(serverUrl);
|
||||
}
|
||||
final versions = await requestSupportedVersions();
|
||||
|
||||
for (var i = 0; i < versions.versions.length; i++) {
|
||||
if (versions.versions[i] == 'r0.5.0' ||
|
||||
|
@ -272,7 +275,7 @@ class Client {
|
|||
}
|
||||
}
|
||||
|
||||
final loginTypes = await api.requestLoginTypes();
|
||||
final loginTypes = await requestLoginTypes();
|
||||
if (loginTypes.flows.indexWhere((f) => f.type == 'm.login.password') ==
|
||||
-1) {
|
||||
return false;
|
||||
|
@ -280,7 +283,7 @@ class Client {
|
|||
|
||||
return true;
|
||||
} catch (_) {
|
||||
api.homeserver = null;
|
||||
homeserver = null;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
@ -288,16 +291,17 @@ class Client {
|
|||
/// Checks to see if a username is available, and valid, for the server.
|
||||
/// Returns the fully-qualified Matrix user ID (MXID) that has been registered.
|
||||
/// You have to call [checkServer] first to set a homeserver.
|
||||
Future<void> register({
|
||||
String kind,
|
||||
@override
|
||||
Future<LoginResponse> register({
|
||||
String username,
|
||||
String password,
|
||||
Map<String, dynamic> auth,
|
||||
String deviceId,
|
||||
String initialDeviceDisplayName,
|
||||
bool inhibitLogin,
|
||||
Map<String, dynamic> auth,
|
||||
String kind,
|
||||
}) async {
|
||||
final response = await api.register(
|
||||
final response = await super.register(
|
||||
username: username,
|
||||
password: password,
|
||||
auth: auth,
|
||||
|
@ -315,68 +319,78 @@ class Client {
|
|||
await connect(
|
||||
newToken: response.accessToken,
|
||||
newUserID: response.userId,
|
||||
newHomeserver: api.homeserver,
|
||||
newHomeserver: homeserver,
|
||||
newDeviceName: initialDeviceDisplayName ?? '',
|
||||
newDeviceID: response.deviceId);
|
||||
return;
|
||||
return response;
|
||||
}
|
||||
|
||||
/// Handles the login and allows the client to call all APIs which require
|
||||
/// authentication. Returns false if the login was not successful. Throws
|
||||
/// MatrixException if login was not successful.
|
||||
/// You have to call [checkServer] first to set a homeserver.
|
||||
Future<bool> login(
|
||||
String username,
|
||||
String password, {
|
||||
String initialDeviceDisplayName,
|
||||
@override
|
||||
Future<LoginResponse> login({
|
||||
String type = 'm.login.password',
|
||||
String userIdentifierType = 'm.id.user',
|
||||
String user,
|
||||
String medium,
|
||||
String address,
|
||||
String password,
|
||||
String token,
|
||||
String deviceId,
|
||||
String initialDeviceDisplayName,
|
||||
}) async {
|
||||
var data = <String, dynamic>{
|
||||
'type': 'm.login.password',
|
||||
'user': username,
|
||||
'identifier': {
|
||||
'type': 'm.id.user',
|
||||
'user': username,
|
||||
},
|
||||
'password': password,
|
||||
};
|
||||
if (deviceId != null) data['device_id'] = deviceId;
|
||||
if (initialDeviceDisplayName != null) {
|
||||
data['initial_device_display_name'] = initialDeviceDisplayName;
|
||||
}
|
||||
|
||||
final loginResp = await api.login(
|
||||
type: 'm.login.password',
|
||||
userIdentifierType: 'm.id.user',
|
||||
user: username,
|
||||
final loginResp = await super.login(
|
||||
type: type,
|
||||
userIdentifierType: userIdentifierType,
|
||||
user: user,
|
||||
password: password,
|
||||
deviceId: deviceId,
|
||||
initialDeviceDisplayName: initialDeviceDisplayName,
|
||||
medium: medium,
|
||||
address: address,
|
||||
token: token,
|
||||
);
|
||||
|
||||
// Connect if there is an access token in the response.
|
||||
if (loginResp.accessToken == null ||
|
||||
loginResp.deviceId == null ||
|
||||
loginResp.userId == null) {
|
||||
throw 'Registered but token, device ID or user ID is null.';
|
||||
throw Exception('Registered but token, device ID or user ID is null.');
|
||||
}
|
||||
await connect(
|
||||
newToken: loginResp.accessToken,
|
||||
newUserID: loginResp.userId,
|
||||
newHomeserver: api.homeserver,
|
||||
newHomeserver: homeserver,
|
||||
newDeviceName: initialDeviceDisplayName ?? '',
|
||||
newDeviceID: loginResp.deviceId,
|
||||
);
|
||||
return true;
|
||||
return loginResp;
|
||||
}
|
||||
|
||||
/// Sends a logout command to the homeserver and clears all local data,
|
||||
/// including all persistent data from the store.
|
||||
@override
|
||||
Future<void> logout() async {
|
||||
try {
|
||||
await api.logout();
|
||||
} catch (exception) {
|
||||
print(exception);
|
||||
await super.logout();
|
||||
} catch (e, s) {
|
||||
Logs.error(e, s);
|
||||
rethrow;
|
||||
} finally {
|
||||
await clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a logout command to the homeserver and clears all local data,
|
||||
/// including all persistent data from the store.
|
||||
@override
|
||||
Future<void> logoutAll() async {
|
||||
try {
|
||||
await super.logoutAll();
|
||||
} catch (e, s) {
|
||||
Logs.error(e, s);
|
||||
rethrow;
|
||||
} finally {
|
||||
await clear();
|
||||
|
@ -427,19 +441,19 @@ class Client {
|
|||
if (cache && _profileCache.containsKey(userId)) {
|
||||
return _profileCache[userId];
|
||||
}
|
||||
final profile = await api.requestProfile(userId);
|
||||
final profile = await requestProfile(userId);
|
||||
_profileCache[userId] = profile;
|
||||
return profile;
|
||||
}
|
||||
|
||||
Future<List<Room>> get archive async {
|
||||
var archiveList = <Room>[];
|
||||
final sync = await api.sync(
|
||||
final syncResp = await sync(
|
||||
filter: '{"room":{"include_leave":true,"timeline":{"limit":10}}}',
|
||||
timeout: 0,
|
||||
);
|
||||
if (sync.rooms.leave is Map<String, dynamic>) {
|
||||
for (var entry in sync.rooms.leave.entries) {
|
||||
if (syncResp.rooms.leave is Map<String, dynamic>) {
|
||||
for (var entry in syncResp.rooms.leave.entries) {
|
||||
final id = entry.key;
|
||||
final room = entry.value;
|
||||
var leftRoom = Room(
|
||||
|
@ -466,14 +480,10 @@ class Client {
|
|||
return archiveList;
|
||||
}
|
||||
|
||||
/// Changes the user's displayname.
|
||||
Future<void> setDisplayname(String displayname) =>
|
||||
api.setDisplayname(userID, displayname);
|
||||
|
||||
/// Uploads a new user avatar for this user.
|
||||
Future<void> setAvatar(MatrixFile file) async {
|
||||
final uploadResp = await api.upload(file.bytes, file.name);
|
||||
await api.setAvatarUrl(userID, Uri.parse(uploadResp));
|
||||
final uploadResp = await upload(file.bytes, file.name);
|
||||
await setAvatarUrl(userID, Uri.parse(uploadResp));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -517,7 +527,7 @@ class Client {
|
|||
StreamController.broadcast();
|
||||
|
||||
/// Synchronization erros are coming here.
|
||||
final StreamController<SyncError> onSyncError = StreamController.broadcast();
|
||||
final StreamController<SdkError> onSyncError = StreamController.broadcast();
|
||||
|
||||
/// Synchronization erros are coming here.
|
||||
final StreamController<ToDeviceEventDecryptionError> onOlmError =
|
||||
|
@ -556,10 +566,6 @@ class Client {
|
|||
final StreamController<KeyVerification> onKeyVerificationRequest =
|
||||
StreamController.broadcast();
|
||||
|
||||
/// Matrix synchronisation is done with https long polling. This needs a
|
||||
/// timeout which is usually 30 seconds.
|
||||
int syncTimeoutSec = 30;
|
||||
|
||||
/// How long should the app wait until it retrys the synchronisation after
|
||||
/// an error?
|
||||
int syncErrorTimeoutSec = 3;
|
||||
|
@ -581,7 +587,7 @@ class Client {
|
|||
/// "type": "m.login.password",
|
||||
/// "user": "test",
|
||||
/// "password": "1234",
|
||||
/// "initial_device_display_name": "Fluffy Matrix Client"
|
||||
/// "initial_device_display_name": "Matrix Client"
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
|
@ -610,8 +616,8 @@ class Client {
|
|||
final account = await database.getClient(clientName);
|
||||
if (account != null) {
|
||||
_id = account.clientId;
|
||||
api.homeserver = Uri.parse(account.homeserverUrl);
|
||||
api.accessToken = account.token;
|
||||
homeserver = Uri.parse(account.homeserverUrl);
|
||||
accessToken = account.token;
|
||||
_userID = account.userId;
|
||||
_deviceID = account.deviceId;
|
||||
_deviceName = account.deviceName;
|
||||
|
@ -619,15 +625,15 @@ class Client {
|
|||
olmAccount = account.olmAccount;
|
||||
}
|
||||
}
|
||||
api.accessToken = newToken ?? api.accessToken;
|
||||
api.homeserver = newHomeserver ?? api.homeserver;
|
||||
accessToken = newToken ?? accessToken;
|
||||
homeserver = newHomeserver ?? homeserver;
|
||||
_userID = newUserID ?? _userID;
|
||||
_deviceID = newDeviceID ?? _deviceID;
|
||||
_deviceName = newDeviceName ?? _deviceName;
|
||||
prevBatch = newPrevBatch ?? prevBatch;
|
||||
olmAccount = newOlmAccount ?? olmAccount;
|
||||
|
||||
if (api.accessToken == null || api.homeserver == null || _userID == null) {
|
||||
if (accessToken == null || homeserver == null || _userID == null) {
|
||||
// we aren't logged in
|
||||
encryption?.dispose();
|
||||
encryption = null;
|
||||
|
@ -635,15 +641,16 @@ class Client {
|
|||
return;
|
||||
}
|
||||
|
||||
encryption = Encryption(
|
||||
debug: debug, client: this, enableE2eeRecovery: enableE2eeRecovery);
|
||||
encryption?.dispose();
|
||||
encryption =
|
||||
Encryption(client: this, enableE2eeRecovery: enableE2eeRecovery);
|
||||
await encryption.init(olmAccount);
|
||||
|
||||
if (database != null) {
|
||||
if (id != null) {
|
||||
await database.updateClient(
|
||||
api.homeserver.toString(),
|
||||
api.accessToken,
|
||||
homeserver.toString(),
|
||||
accessToken,
|
||||
_userID,
|
||||
_deviceID,
|
||||
_deviceName,
|
||||
|
@ -654,8 +661,8 @@ class Client {
|
|||
} else {
|
||||
_id = await database.insertClient(
|
||||
clientName,
|
||||
api.homeserver.toString(),
|
||||
api.accessToken,
|
||||
homeserver.toString(),
|
||||
accessToken,
|
||||
_userID,
|
||||
_deviceID,
|
||||
_deviceName,
|
||||
|
@ -671,7 +678,11 @@ class Client {
|
|||
}
|
||||
|
||||
onLoginStateChanged.add(LoginState.logged);
|
||||
Logs.success(
|
||||
'Successfully connected as ${userID.localpart} with ${homeserver.toString()}',
|
||||
);
|
||||
|
||||
// Always do a _sync after login, even if backgroundSync is set to off
|
||||
return _sync();
|
||||
}
|
||||
|
||||
|
@ -683,42 +694,64 @@ class Client {
|
|||
/// Resets all settings and stops the synchronisation.
|
||||
void clear() {
|
||||
database?.clear(id);
|
||||
_id = api.accessToken =
|
||||
api.homeserver = _userID = _deviceID = _deviceName = prevBatch = null;
|
||||
_id = accessToken =
|
||||
homeserver = _userID = _deviceID = _deviceName = prevBatch = null;
|
||||
_rooms = [];
|
||||
encryption?.dispose();
|
||||
encryption = null;
|
||||
onLoginStateChanged.add(LoginState.loggedOut);
|
||||
}
|
||||
|
||||
Future<SyncUpdate> _syncRequest;
|
||||
Exception _lastSyncError;
|
||||
bool _backgroundSync = true;
|
||||
Future<void> _currentSync, _retryDelay = Future.value();
|
||||
bool get syncPending => _currentSync != null;
|
||||
|
||||
Future<void> _sync() async {
|
||||
if (isLogged() == false || _disposed) return;
|
||||
/// Controls the background sync (automatically looping forever if turned on).
|
||||
set backgroundSync(bool enabled) {
|
||||
_backgroundSync = enabled;
|
||||
if (_backgroundSync) {
|
||||
_sync();
|
||||
}
|
||||
}
|
||||
|
||||
/// Immediately start a sync and wait for completion.
|
||||
/// If there is an active sync already, wait for the active sync instead.
|
||||
Future<void> oneShotSync() {
|
||||
return _sync();
|
||||
}
|
||||
|
||||
Future<void> _sync() {
|
||||
if (_currentSync == null) {
|
||||
_currentSync = _innerSync();
|
||||
_currentSync.whenComplete(() {
|
||||
_currentSync = null;
|
||||
if (_backgroundSync && isLogged() && !_disposed) {
|
||||
_sync();
|
||||
}
|
||||
});
|
||||
}
|
||||
return _currentSync;
|
||||
}
|
||||
|
||||
Future<void> _innerSync() async {
|
||||
await _retryDelay;
|
||||
_retryDelay = Future.delayed(Duration(seconds: syncErrorTimeoutSec));
|
||||
if (!isLogged() || _disposed) return null;
|
||||
try {
|
||||
_syncRequest = api
|
||||
.sync(
|
||||
final syncResp = await sync(
|
||||
filter: syncFilters,
|
||||
since: prevBatch,
|
||||
timeout: prevBatch != null ? 30000 : null,
|
||||
)
|
||||
.catchError((e) {
|
||||
_lastSyncError = e;
|
||||
return null;
|
||||
});
|
||||
);
|
||||
if (_disposed) return;
|
||||
final hash = _syncRequest.hashCode;
|
||||
final syncResp = await _syncRequest;
|
||||
if (syncResp == null) throw _lastSyncError;
|
||||
if (hash != _syncRequest.hashCode) return;
|
||||
if (database != null) {
|
||||
await database.transaction(() async {
|
||||
_currentTransaction = database.transaction(() async {
|
||||
await handleSync(syncResp);
|
||||
if (prevBatch != syncResp.nextBatch) {
|
||||
await database.storePrevBatch(syncResp.nextBatch, id);
|
||||
}
|
||||
});
|
||||
await _currentTransaction;
|
||||
} else {
|
||||
await handleSync(syncResp);
|
||||
}
|
||||
|
@ -733,19 +766,19 @@ class Client {
|
|||
if (encryptionEnabled) {
|
||||
encryption.onSync();
|
||||
}
|
||||
if (hash == _syncRequest.hashCode) unawaited(_sync());
|
||||
} on MatrixException catch (exception) {
|
||||
onError.add(exception);
|
||||
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
|
||||
_retryDelay = Future.value();
|
||||
} on MatrixException catch (e) {
|
||||
onError.add(e);
|
||||
} catch (e, s) {
|
||||
if (isLogged() == false || _disposed) {
|
||||
return;
|
||||
}
|
||||
print('Error during processing events: ' + e.toString());
|
||||
print(s);
|
||||
onSyncError.add(SyncError(
|
||||
if (!isLogged() || _disposed) return;
|
||||
Logs.error('Error during processing events: ' + e.toString(), s);
|
||||
onSyncError.add(SdkError(
|
||||
exception: e is Exception ? e : Exception(e), stackTrace: s));
|
||||
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
|
||||
if (e is MatrixException &&
|
||||
e.errcode == MatrixError.M_UNKNOWN_TOKEN.toString().split('.').last) {
|
||||
Logs.warning('The user has been logged out!');
|
||||
clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -767,6 +800,7 @@ class Client {
|
|||
await _handleRooms(sync.rooms.leave, Membership.leave,
|
||||
sortAtTheEnd: sortAtTheEnd);
|
||||
}
|
||||
_sortRooms();
|
||||
}
|
||||
if (sync.presence != null) {
|
||||
for (final newPresence in sync.presence) {
|
||||
|
@ -821,10 +855,10 @@ class Client {
|
|||
try {
|
||||
toDeviceEvent = await encryption.decryptToDeviceEvent(toDeviceEvent);
|
||||
} catch (e, s) {
|
||||
print(
|
||||
'[LibOlm] Could not decrypt to device event from ${toDeviceEvent.sender} with content: ${toDeviceEvent.content}');
|
||||
print(e);
|
||||
print(s);
|
||||
Logs.error(
|
||||
'[LibOlm] Could not decrypt to device event from ${toDeviceEvent.sender} with content: ${toDeviceEvent.content}\n${e.toString()}',
|
||||
s);
|
||||
|
||||
onOlmError.add(
|
||||
ToDeviceEventDecryptionError(
|
||||
exception: e is Exception ? e : Exception(e),
|
||||
|
@ -1014,14 +1048,22 @@ class Client {
|
|||
}
|
||||
onEvent.add(update);
|
||||
|
||||
if (event['type'] == 'm.call.invite') {
|
||||
onCallInvite.add(Event.fromJson(event, room, sortOrder));
|
||||
} else if (event['type'] == 'm.call.hangup') {
|
||||
onCallHangup.add(Event.fromJson(event, room, sortOrder));
|
||||
} else if (event['type'] == 'm.call.answer') {
|
||||
onCallAnswer.add(Event.fromJson(event, room, sortOrder));
|
||||
} else if (event['type'] == 'm.call.candidates') {
|
||||
onCallCandidates.add(Event.fromJson(event, room, sortOrder));
|
||||
final rawUnencryptedEvent = update.content;
|
||||
|
||||
if (prevBatch != null && type == 'timeline') {
|
||||
if (rawUnencryptedEvent['type'] == EventTypes.CallInvite) {
|
||||
onCallInvite
|
||||
.add(Event.fromJson(rawUnencryptedEvent, room, sortOrder));
|
||||
} else if (rawUnencryptedEvent['type'] == EventTypes.CallHangup) {
|
||||
onCallHangup
|
||||
.add(Event.fromJson(rawUnencryptedEvent, room, sortOrder));
|
||||
} else if (rawUnencryptedEvent['type'] == EventTypes.CallAnswer) {
|
||||
onCallAnswer
|
||||
.add(Event.fromJson(rawUnencryptedEvent, room, sortOrder));
|
||||
} else if (rawUnencryptedEvent['type'] == EventTypes.CallCandidates) {
|
||||
onCallCandidates
|
||||
.add(Event.fromJson(rawUnencryptedEvent, room, sortOrder));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1084,50 +1126,53 @@ class Client {
|
|||
}
|
||||
if (rooms[j].onUpdate != null) rooms[j].onUpdate.add(rooms[j].id);
|
||||
}
|
||||
_sortRooms();
|
||||
}
|
||||
|
||||
void _updateRoomsByEventUpdate(EventUpdate eventUpdate) {
|
||||
if (eventUpdate.type == 'history') return;
|
||||
// Search the room in the rooms
|
||||
num j = 0;
|
||||
for (j = 0; j < rooms.length; j++) {
|
||||
if (rooms[j].id == eventUpdate.roomID) break;
|
||||
|
||||
final room = getRoomById(eventUpdate.roomID);
|
||||
if (room == null) return;
|
||||
|
||||
switch (eventUpdate.type) {
|
||||
case 'timeline':
|
||||
case 'state':
|
||||
case 'invite_state':
|
||||
var stateEvent =
|
||||
Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder);
|
||||
var prevState = room.getState(stateEvent.type, stateEvent.stateKey);
|
||||
if (prevState != null && prevState.sortOrder > stateEvent.sortOrder) {
|
||||
Logs.warning('''
|
||||
A new ${eventUpdate.type} event of the type ${stateEvent.type} has arrived with a previews
|
||||
sort order ${stateEvent.sortOrder} than the current ${stateEvent.type} event with a
|
||||
sort order of ${prevState.sortOrder}. This should never happen...''');
|
||||
return;
|
||||
}
|
||||
if (stateEvent.type == EventTypes.Redaction) {
|
||||
final String redacts = eventUpdate.content['redacts'];
|
||||
room.states.states.forEach(
|
||||
(String key, Map<String, Event> states) => states.forEach(
|
||||
(String key, Event state) {
|
||||
if (state.eventId == redacts) {
|
||||
state.setRedactionEvent(stateEvent);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
room.setState(stateEvent);
|
||||
}
|
||||
break;
|
||||
case 'account_data':
|
||||
room.roomAccountData[eventUpdate.eventType] =
|
||||
BasicRoomEvent.fromJson(eventUpdate.content);
|
||||
break;
|
||||
case 'ephemeral':
|
||||
room.ephemerals[eventUpdate.eventType] =
|
||||
BasicRoomEvent.fromJson(eventUpdate.content);
|
||||
break;
|
||||
}
|
||||
final found = (j < rooms.length && rooms[j].id == eventUpdate.roomID);
|
||||
if (!found) return;
|
||||
if (eventUpdate.type == 'timeline' ||
|
||||
eventUpdate.type == 'state' ||
|
||||
eventUpdate.type == 'invite_state') {
|
||||
var stateEvent =
|
||||
Event.fromJson(eventUpdate.content, rooms[j], eventUpdate.sortOrder);
|
||||
if (stateEvent.type == EventTypes.Redaction) {
|
||||
final String redacts = eventUpdate.content['redacts'];
|
||||
rooms[j].states.states.forEach(
|
||||
(String key, Map<String, Event> states) => states.forEach(
|
||||
(String key, Event state) {
|
||||
if (state.eventId == redacts) {
|
||||
state.setRedactionEvent(stateEvent);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
var prevState = rooms[j].getState(stateEvent.type, stateEvent.stateKey);
|
||||
if (prevState != null &&
|
||||
prevState.originServerTs.millisecondsSinceEpoch >
|
||||
stateEvent.originServerTs.millisecondsSinceEpoch) return;
|
||||
rooms[j].setState(stateEvent);
|
||||
}
|
||||
} else if (eventUpdate.type == 'account_data') {
|
||||
rooms[j].roomAccountData[eventUpdate.eventType] =
|
||||
BasicRoomEvent.fromJson(eventUpdate.content);
|
||||
} else if (eventUpdate.type == 'ephemeral') {
|
||||
rooms[j].ephemerals[eventUpdate.eventType] =
|
||||
BasicRoomEvent.fromJson(eventUpdate.content);
|
||||
}
|
||||
if (rooms[j].onUpdate != null) rooms[j].onUpdate.add(rooms[j].id);
|
||||
if (['timeline', 'account_data'].contains(eventUpdate.type)) _sortRooms();
|
||||
room.onUpdate.add(room.id);
|
||||
}
|
||||
|
||||
bool _sortLock = false;
|
||||
|
@ -1156,21 +1201,39 @@ class Client {
|
|||
Map<String, DeviceKeysList> get userDeviceKeys => _userDeviceKeys;
|
||||
Map<String, DeviceKeysList> _userDeviceKeys = {};
|
||||
|
||||
/// Gets user device keys by its curve25519 key. Returns null if it isn't found
|
||||
DeviceKeys getUserDeviceKeysByCurve25519Key(String senderKey) {
|
||||
for (final user in userDeviceKeys.values) {
|
||||
final device = user.deviceKeys.values
|
||||
.firstWhere((e) => e.curve25519Key == senderKey, orElse: () => null);
|
||||
if (device != null) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<Set<String>> _getUserIdsInEncryptedRooms() async {
|
||||
var userIds = <String>{};
|
||||
for (var i = 0; i < rooms.length; i++) {
|
||||
if (rooms[i].encrypted) {
|
||||
var userList = await rooms[i].requestParticipants();
|
||||
for (var user in userList) {
|
||||
if ([Membership.join, Membership.invite].contains(user.membership)) {
|
||||
userIds.add(user.id);
|
||||
try {
|
||||
var userList = await rooms[i].requestParticipants();
|
||||
for (var user in userList) {
|
||||
if ([Membership.join, Membership.invite]
|
||||
.contains(user.membership)) {
|
||||
userIds.add(user.id);
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logs.error('[E2EE] Failed to fetch participants: ' + e.toString(), s);
|
||||
}
|
||||
}
|
||||
}
|
||||
return userIds;
|
||||
}
|
||||
|
||||
final Map<String, DateTime> _keyQueryFailures = {};
|
||||
Future<void> _updateUserDeviceKeys() async {
|
||||
try {
|
||||
if (!isLogged()) return;
|
||||
|
@ -1189,15 +1252,19 @@ class Client {
|
|||
_userDeviceKeys[userId] = DeviceKeysList(userId, this);
|
||||
}
|
||||
var deviceKeysList = userDeviceKeys[userId];
|
||||
if (deviceKeysList.outdated) {
|
||||
if (deviceKeysList.outdated &&
|
||||
(!_keyQueryFailures.containsKey(userId.domain) ||
|
||||
DateTime.now()
|
||||
.subtract(Duration(minutes: 5))
|
||||
.isAfter(_keyQueryFailures[userId.domain]))) {
|
||||
outdatedLists[userId] = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (outdatedLists.isNotEmpty) {
|
||||
// Request the missing device key lists from the server.
|
||||
final response =
|
||||
await api.requestDeviceKeys(outdatedLists, timeout: 10000);
|
||||
if (!isLogged()) return;
|
||||
final response = await requestDeviceKeys(outdatedLists, timeout: 10000);
|
||||
|
||||
for (final rawDeviceKeyListEntry in response.deviceKeys.entries) {
|
||||
final userId = rawDeviceKeyListEntry.key;
|
||||
|
@ -1331,28 +1398,57 @@ class Client {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await database?.transaction(() async {
|
||||
for (final f in dbActions) {
|
||||
await f();
|
||||
|
||||
// now process all the failures
|
||||
if (response.failures != null) {
|
||||
for (final failureDomain in response.failures.keys) {
|
||||
_keyQueryFailures[failureDomain] = DateTime.now();
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
print('[LibOlm] Unable to update user device keys: ' + e.toString());
|
||||
}
|
||||
|
||||
if (dbActions.isNotEmpty) {
|
||||
await database?.transaction(() async {
|
||||
for (final f in dbActions) {
|
||||
await f();
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logs.error(
|
||||
'[LibOlm] Unable to update user device keys: ' + e.toString(), s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send an (unencrypted) to device [message] of a specific [eventType] to all
|
||||
/// devices of a set of [users].
|
||||
Future<void> sendToDevicesOfUserIds(
|
||||
Set<String> users,
|
||||
String eventType,
|
||||
Map<String, dynamic> message, {
|
||||
String messageId,
|
||||
}) async {
|
||||
// Send with send-to-device messaging
|
||||
var data = <String, Map<String, Map<String, dynamic>>>{};
|
||||
for (var user in users) {
|
||||
data[user] = {};
|
||||
data[user]['*'] = message;
|
||||
}
|
||||
await sendToDevice(
|
||||
eventType, messageId ?? generateUniqueTransactionId(), data);
|
||||
return;
|
||||
}
|
||||
|
||||
/// Sends an encrypted [message] of this [type] to these [deviceKeys]. To send
|
||||
/// the request to all devices of the current user, pass an empty list to [deviceKeys].
|
||||
Future<void> sendToDevice(
|
||||
Future<void> sendToDeviceEncrypted(
|
||||
List<DeviceKeys> deviceKeys,
|
||||
String type,
|
||||
String eventType,
|
||||
Map<String, dynamic> message, {
|
||||
bool encrypted = true,
|
||||
List<User> toUsers,
|
||||
String messageId,
|
||||
bool onlyVerified = false,
|
||||
}) async {
|
||||
if (encrypted && !encryptionEnabled) return;
|
||||
if (!encryptionEnabled) return;
|
||||
// Don't send this message to blocked devices, and if specified onlyVerified
|
||||
// then only send it to verified devices
|
||||
if (deviceKeys.isNotEmpty) {
|
||||
|
@ -1363,36 +1459,13 @@ class Client {
|
|||
if (deviceKeys.isEmpty) return;
|
||||
}
|
||||
|
||||
var sendToDeviceMessage = message;
|
||||
|
||||
// Send with send-to-device messaging
|
||||
var data = <String, Map<String, Map<String, dynamic>>>{};
|
||||
if (deviceKeys.isEmpty) {
|
||||
if (toUsers == null) {
|
||||
data[userID] = {};
|
||||
data[userID]['*'] = sendToDeviceMessage;
|
||||
} else {
|
||||
for (var user in toUsers) {
|
||||
data[user.id] = {};
|
||||
data[user.id]['*'] = sendToDeviceMessage;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (encrypted) {
|
||||
data =
|
||||
await encryption.encryptToDeviceMessage(deviceKeys, type, message);
|
||||
} else {
|
||||
for (final device in deviceKeys) {
|
||||
if (!data.containsKey(device.userId)) {
|
||||
data[device.userId] = {};
|
||||
}
|
||||
data[device.userId][device.deviceId] = sendToDeviceMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (encrypted) type = EventTypes.Encrypted;
|
||||
final messageID = generateUniqueTransactionId();
|
||||
await api.sendToDevice(type, messageID, data);
|
||||
data =
|
||||
await encryption.encryptToDeviceMessage(deviceKeys, eventType, message);
|
||||
eventType = EventTypes.Encrypted;
|
||||
await sendToDevice(
|
||||
eventType, messageId ?? generateUniqueTransactionId(), data);
|
||||
}
|
||||
|
||||
/// Whether all push notifications are muted using the [.m.rule.master]
|
||||
|
@ -1417,7 +1490,7 @@ class Client {
|
|||
}
|
||||
|
||||
Future<void> setMuteAllPushNotifications(bool muted) async {
|
||||
await api.enablePushRule(
|
||||
await enablePushRule(
|
||||
'global',
|
||||
PushRuleKind.override,
|
||||
'.m.rule.master',
|
||||
|
@ -1427,6 +1500,7 @@ class Client {
|
|||
}
|
||||
|
||||
/// Changes the password. You should either set oldPasswort or another authentication flow.
|
||||
@override
|
||||
Future<void> changePassword(String newPassword,
|
||||
{String oldPassword, Map<String, dynamic> auth}) async {
|
||||
try {
|
||||
|
@ -1437,7 +1511,7 @@ class Client {
|
|||
'password': oldPassword,
|
||||
};
|
||||
}
|
||||
await api.changePassword(newPassword, auth: auth);
|
||||
await super.changePassword(newPassword, auth: auth);
|
||||
} on MatrixException catch (matrixException) {
|
||||
if (!matrixException.requireAdditionalAuthentication) {
|
||||
rethrow;
|
||||
|
@ -1465,20 +1539,74 @@ class Client {
|
|||
}
|
||||
}
|
||||
|
||||
/// Clear all local cached messages and perform a new clean sync.
|
||||
Future<void> clearLocalCachedMessages() async {
|
||||
prevBatch = null;
|
||||
rooms.forEach((r) => r.prev_batch = null);
|
||||
await database?.clearCache(id);
|
||||
}
|
||||
|
||||
/// A list of mxids of users who are ignored.
|
||||
List<String> get ignoredUsers => (accountData
|
||||
.containsKey('m.ignored_user_list') &&
|
||||
accountData['m.ignored_user_list'].content['ignored_users'] is Map)
|
||||
? List<String>.from(
|
||||
accountData['m.ignored_user_list'].content['ignored_users'].keys)
|
||||
: [];
|
||||
|
||||
/// Ignore another user. This will clear the local cached messages to
|
||||
/// hide all previous messages from this user.
|
||||
Future<void> ignoreUser(String userId) async {
|
||||
if (!userId.isValidMatrixId) {
|
||||
throw Exception('$userId is not a valid mxid!');
|
||||
}
|
||||
await setAccountData(userID, 'm.ignored_user_list', {
|
||||
'ignored_users': Map.fromEntries(
|
||||
(ignoredUsers..add(userId)).map((key) => MapEntry(key, {}))),
|
||||
});
|
||||
await clearLocalCachedMessages();
|
||||
return;
|
||||
}
|
||||
|
||||
/// Unignore a user. This will clear the local cached messages and request
|
||||
/// them again from the server to avoid gaps in the timeline.
|
||||
Future<void> unignoreUser(String userId) async {
|
||||
if (!userId.isValidMatrixId) {
|
||||
throw Exception('$userId is not a valid mxid!');
|
||||
}
|
||||
if (!ignoredUsers.contains(userId)) {
|
||||
throw Exception('$userId is not in the ignore list!');
|
||||
}
|
||||
await setAccountData(userID, 'm.ignored_user_list', {
|
||||
'ignored_users': Map.fromEntries(
|
||||
(ignoredUsers..remove(userId)).map((key) => MapEntry(key, {}))),
|
||||
});
|
||||
await clearLocalCachedMessages();
|
||||
return;
|
||||
}
|
||||
|
||||
bool _disposed = false;
|
||||
Future _currentTransaction = Future.sync(() => {});
|
||||
|
||||
/// Stops the synchronization and closes the database. After this
|
||||
/// you can safely make this Client instance null.
|
||||
Future<void> dispose({bool closeDatabase = false}) async {
|
||||
_disposed = true;
|
||||
try {
|
||||
await _currentTransaction;
|
||||
} catch (_) {
|
||||
// No-OP
|
||||
}
|
||||
if (closeDatabase) await database?.close();
|
||||
database = null;
|
||||
encryption?.dispose();
|
||||
encryption = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
class SyncError {
|
||||
class SdkError {
|
||||
Exception exception;
|
||||
StackTrace stackTrace;
|
||||
SyncError({this.exception, this.stackTrace});
|
||||
SdkError({this.exception, this.stackTrace});
|
||||
}
|
||||
|
|
|
@ -1,14 +1,51 @@
|
|||
import 'package:moor/moor.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart' as sdk;
|
||||
import 'package:famedlysdk/matrix_api.dart' as api;
|
||||
import 'package:moor/moor.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import '../../famedlysdk.dart' as sdk;
|
||||
import '../../matrix_api.dart' as api;
|
||||
import '../../matrix_api.dart';
|
||||
import '../client.dart';
|
||||
import '../room.dart';
|
||||
import '../utils/logs.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
extension MigratorExtension on Migrator {
|
||||
Future<void> createIndexIfNotExists(Index index) async {
|
||||
try {
|
||||
await createIndex(index);
|
||||
} catch (err) {
|
||||
if (!err.toString().toLowerCase().contains('already exists')) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createTableIfNotExists(TableInfo<Table, DataClass> table) async {
|
||||
try {
|
||||
await createTable(table);
|
||||
} catch (err) {
|
||||
if (!err.toString().toLowerCase().contains('already exists')) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addColumnIfNotExists(
|
||||
TableInfo<Table, DataClass> table, GeneratedColumn column) async {
|
||||
try {
|
||||
await addColumn(table, column);
|
||||
} catch (err) {
|
||||
if (!err.toString().toLowerCase().contains('duplicate column name')) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UseMoor(
|
||||
include: {'database.moor'},
|
||||
)
|
||||
|
@ -18,56 +55,87 @@ class Database extends _$Database {
|
|||
Database.connect(DatabaseConnection connection) : super.connect(connection);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 5;
|
||||
int get schemaVersion => 6;
|
||||
|
||||
int get maxFileSize => 1 * 1024 * 1024;
|
||||
|
||||
/// Update errors are coming here.
|
||||
final StreamController<SdkError> onError = StreamController.broadcast();
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
onCreate: (Migrator m) {
|
||||
return m.createAll();
|
||||
onCreate: (Migrator m) async {
|
||||
try {
|
||||
await m.createAll();
|
||||
} catch (e, s) {
|
||||
Logs.error(e, s);
|
||||
onError.add(SdkError(exception: e, stackTrace: s));
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
onUpgrade: (Migrator m, int from, int to) async {
|
||||
// this appears to be only called once, so multiple consecutive upgrades have to be handled appropriately in here
|
||||
if (from == 1) {
|
||||
await m.createIndex(userDeviceKeysIndex);
|
||||
await m.createIndex(userDeviceKeysKeyIndex);
|
||||
await m.createIndex(olmSessionsIndex);
|
||||
await m.createIndex(outboundGroupSessionsIndex);
|
||||
await m.createIndex(inboundGroupSessionsIndex);
|
||||
await m.createIndex(roomsIndex);
|
||||
await m.createIndex(eventsIndex);
|
||||
await m.createIndex(roomStatesIndex);
|
||||
await m.createIndex(accountDataIndex);
|
||||
await m.createIndex(roomAccountDataIndex);
|
||||
await m.createIndex(presencesIndex);
|
||||
from++;
|
||||
}
|
||||
if (from == 2) {
|
||||
await m.deleteTable('outbound_group_sessions');
|
||||
await m.createTable(outboundGroupSessions);
|
||||
from++;
|
||||
}
|
||||
if (from == 3) {
|
||||
await m.createTable(userCrossSigningKeys);
|
||||
await m.createTable(ssssCache);
|
||||
// mark all keys as outdated so that the cross signing keys will be fetched
|
||||
await m.issueCustomQuery(
|
||||
'UPDATE user_device_keys SET outdated = true');
|
||||
from++;
|
||||
}
|
||||
if (from == 4) {
|
||||
await m.addColumn(olmSessions, olmSessions.lastReceived);
|
||||
from++;
|
||||
try {
|
||||
// this appears to be only called once, so multiple consecutive upgrades have to be handled appropriately in here
|
||||
if (from == 1) {
|
||||
await m.createIndexIfNotExists(userDeviceKeysIndex);
|
||||
await m.createIndexIfNotExists(userDeviceKeysKeyIndex);
|
||||
await m.createIndexIfNotExists(olmSessionsIndex);
|
||||
await m.createIndexIfNotExists(outboundGroupSessionsIndex);
|
||||
await m.createIndexIfNotExists(inboundGroupSessionsIndex);
|
||||
await m.createIndexIfNotExists(roomsIndex);
|
||||
await m.createIndexIfNotExists(eventsIndex);
|
||||
await m.createIndexIfNotExists(roomStatesIndex);
|
||||
await m.createIndexIfNotExists(accountDataIndex);
|
||||
await m.createIndexIfNotExists(roomAccountDataIndex);
|
||||
await m.createIndexIfNotExists(presencesIndex);
|
||||
from++;
|
||||
}
|
||||
if (from == 2) {
|
||||
await m.deleteTable('outbound_group_sessions');
|
||||
await m.createTable(outboundGroupSessions);
|
||||
from++;
|
||||
}
|
||||
if (from == 3) {
|
||||
await m.createTableIfNotExists(userCrossSigningKeys);
|
||||
await m.createTableIfNotExists(ssssCache);
|
||||
// mark all keys as outdated so that the cross signing keys will be fetched
|
||||
await customStatement(
|
||||
'UPDATE user_device_keys SET outdated = true');
|
||||
from++;
|
||||
}
|
||||
if (from == 4) {
|
||||
await m.addColumnIfNotExists(
|
||||
olmSessions, olmSessions.lastReceived);
|
||||
from++;
|
||||
}
|
||||
if (from == 5) {
|
||||
await m.addColumnIfNotExists(
|
||||
inboundGroupSessions, inboundGroupSessions.uploaded);
|
||||
await m.addColumnIfNotExists(
|
||||
inboundGroupSessions, inboundGroupSessions.senderKey);
|
||||
await m.addColumnIfNotExists(
|
||||
inboundGroupSessions, inboundGroupSessions.senderClaimedKeys);
|
||||
from++;
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logs.error(e, s);
|
||||
onError.add(SdkError(exception: e, stackTrace: s));
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
beforeOpen: (_) async {
|
||||
if (executor.dialect == SqlDialect.sqlite) {
|
||||
final ret = await customSelect('PRAGMA journal_mode=WAL').get();
|
||||
if (ret.isNotEmpty) {
|
||||
print('[Moor] Switched database to mode ' +
|
||||
ret.first.data['journal_mode'].toString());
|
||||
try {
|
||||
if (executor.dialect == SqlDialect.sqlite) {
|
||||
final ret = await customSelect('PRAGMA journal_mode=WAL').get();
|
||||
if (ret.isNotEmpty) {
|
||||
Logs.info('[Moor] Switched database to mode ' +
|
||||
ret.first.data['journal_mode'].toString());
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logs.error(e, s);
|
||||
onError.add(SdkError(exception: e, stackTrace: s));
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -75,6 +143,7 @@ class Database extends _$Database {
|
|||
Future<DbClient> getClient(String name) async {
|
||||
final res = await dbGetClient(name).get();
|
||||
if (res.isEmpty) return null;
|
||||
await markPendingEventsAsError(res.first.clientId);
|
||||
return res.first;
|
||||
}
|
||||
|
||||
|
@ -112,8 +181,9 @@ class Database extends _$Database {
|
|||
var session = olm.Session();
|
||||
session.unpickle(userId, row.pickle);
|
||||
res[row.identityKey].add(session);
|
||||
} catch (e) {
|
||||
print('[LibOlm] Could not unpickle olm session: ' + e.toString());
|
||||
} catch (e, s) {
|
||||
Logs.error(
|
||||
'[LibOlm] Could not unpickle olm session: ' + e.toString(), s);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
|
@ -329,7 +399,7 @@ class Database extends _$Database {
|
|||
// Is the timeline limited? Then all previous messages should be
|
||||
// removed from the database!
|
||||
if (roomUpdate.limitedTimeline) {
|
||||
await removeRoomEvents(clientId, roomUpdate.id);
|
||||
await removeSuccessfulRoomEvents(clientId, roomUpdate.id);
|
||||
await updateRoomSortOrder(0.0, 0.0, clientId, roomUpdate.id);
|
||||
await setRoomPrevBatch(roomUpdate.prev_batch, clientId, roomUpdate.id);
|
||||
}
|
||||
|
@ -357,14 +427,50 @@ class Database extends _$Database {
|
|||
if (type == 'timeline' || type == 'history') {
|
||||
// calculate the status
|
||||
var status = 2;
|
||||
if (eventContent['unsigned'] is Map<String, dynamic> &&
|
||||
eventContent['unsigned'][MessageSendingStatusKey] is num) {
|
||||
status = eventContent['unsigned'][MessageSendingStatusKey];
|
||||
}
|
||||
if (eventContent['status'] is num) status = eventContent['status'];
|
||||
if ((status == 1 || status == -1) &&
|
||||
var storeNewEvent = !((status == 1 || status == -1) &&
|
||||
eventContent['unsigned'] is Map<String, dynamic> &&
|
||||
eventContent['unsigned']['transaction_id'] is String) {
|
||||
// status changed and we have an old transaction id --> update event id and stuffs
|
||||
await updateEventStatus(status, eventContent['event_id'], clientId,
|
||||
eventContent['unsigned']['transaction_id'], chatId);
|
||||
} else {
|
||||
eventContent['unsigned']['transaction_id'] is String);
|
||||
if (!storeNewEvent) {
|
||||
final allOldEvents =
|
||||
await getEvent(clientId, eventContent['event_id'], chatId).get();
|
||||
if (allOldEvents.isNotEmpty) {
|
||||
// we were likely unable to change transaction_id -> event_id.....because the event ID already exists!
|
||||
// So, we try to fetch the old event
|
||||
// the transaction id event will automatically be deleted further down
|
||||
final oldEvent = allOldEvents.first;
|
||||
// do we update the status? We should allow 0 -> -1 updates and status increases
|
||||
if (status > oldEvent.status ||
|
||||
(oldEvent.status == 0 && status == -1)) {
|
||||
// update the status
|
||||
await updateEventStatusOnly(
|
||||
status, clientId, eventContent['event_id'], chatId);
|
||||
}
|
||||
} else {
|
||||
// status changed and we have an old transaction id --> update event id and stuffs
|
||||
try {
|
||||
final updated = await updateEventStatus(
|
||||
status,
|
||||
eventContent['event_id'],
|
||||
clientId,
|
||||
eventContent['unsigned']['transaction_id'],
|
||||
chatId);
|
||||
if (updated == 0) {
|
||||
storeNewEvent = true;
|
||||
}
|
||||
} catch (err) {
|
||||
// we could not update the transaction id to the event id....so it already exists
|
||||
// as we just tried to fetch the event previously this is a race condition if the event comes down sync in the mean time
|
||||
// that means that the status we already have in the database is likely more accurate
|
||||
// than our status. So, we just ignore this error
|
||||
}
|
||||
}
|
||||
}
|
||||
if (storeNewEvent) {
|
||||
DbEvent oldEvent;
|
||||
if (type == 'history') {
|
||||
final allOldEvents =
|
||||
|
@ -394,6 +500,7 @@ class Database extends _$Database {
|
|||
|
||||
// is there a transaction id? Then delete the event with this id.
|
||||
if (status != -1 &&
|
||||
status != 0 &&
|
||||
eventUpdate.content['unsigned'] is Map &&
|
||||
eventUpdate.content['unsigned']['transaction_id'] is String) {
|
||||
await removeEvent(clientId,
|
||||
|
|
|
@ -2062,19 +2062,26 @@ class DbInboundGroupSession extends DataClass
|
|||
final String pickle;
|
||||
final String content;
|
||||
final String indexes;
|
||||
final bool uploaded;
|
||||
final String senderKey;
|
||||
final String senderClaimedKeys;
|
||||
DbInboundGroupSession(
|
||||
{@required this.clientId,
|
||||
@required this.roomId,
|
||||
@required this.sessionId,
|
||||
@required this.pickle,
|
||||
this.content,
|
||||
this.indexes});
|
||||
this.indexes,
|
||||
this.uploaded,
|
||||
this.senderKey,
|
||||
this.senderClaimedKeys});
|
||||
factory DbInboundGroupSession.fromData(
|
||||
Map<String, dynamic> data, GeneratedDatabase db,
|
||||
{String prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
final intType = db.typeSystem.forDartType<int>();
|
||||
final stringType = db.typeSystem.forDartType<String>();
|
||||
final boolType = db.typeSystem.forDartType<bool>();
|
||||
return DbInboundGroupSession(
|
||||
clientId:
|
||||
intType.mapFromDatabaseResponse(data['${effectivePrefix}client_id']),
|
||||
|
@ -2088,6 +2095,12 @@ class DbInboundGroupSession extends DataClass
|
|||
stringType.mapFromDatabaseResponse(data['${effectivePrefix}content']),
|
||||
indexes:
|
||||
stringType.mapFromDatabaseResponse(data['${effectivePrefix}indexes']),
|
||||
uploaded:
|
||||
boolType.mapFromDatabaseResponse(data['${effectivePrefix}uploaded']),
|
||||
senderKey: stringType
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}sender_key']),
|
||||
senderClaimedKeys: stringType.mapFromDatabaseResponse(
|
||||
data['${effectivePrefix}sender_claimed_keys']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
|
@ -2111,6 +2124,15 @@ class DbInboundGroupSession extends DataClass
|
|||
if (!nullToAbsent || indexes != null) {
|
||||
map['indexes'] = Variable<String>(indexes);
|
||||
}
|
||||
if (!nullToAbsent || uploaded != null) {
|
||||
map['uploaded'] = Variable<bool>(uploaded);
|
||||
}
|
||||
if (!nullToAbsent || senderKey != null) {
|
||||
map['sender_key'] = Variable<String>(senderKey);
|
||||
}
|
||||
if (!nullToAbsent || senderClaimedKeys != null) {
|
||||
map['sender_claimed_keys'] = Variable<String>(senderClaimedKeys);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
|
@ -2124,6 +2146,10 @@ class DbInboundGroupSession extends DataClass
|
|||
pickle: serializer.fromJson<String>(json['pickle']),
|
||||
content: serializer.fromJson<String>(json['content']),
|
||||
indexes: serializer.fromJson<String>(json['indexes']),
|
||||
uploaded: serializer.fromJson<bool>(json['uploaded']),
|
||||
senderKey: serializer.fromJson<String>(json['sender_key']),
|
||||
senderClaimedKeys:
|
||||
serializer.fromJson<String>(json['sender_claimed_keys']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
|
@ -2136,6 +2162,9 @@ class DbInboundGroupSession extends DataClass
|
|||
'pickle': serializer.toJson<String>(pickle),
|
||||
'content': serializer.toJson<String>(content),
|
||||
'indexes': serializer.toJson<String>(indexes),
|
||||
'uploaded': serializer.toJson<bool>(uploaded),
|
||||
'sender_key': serializer.toJson<String>(senderKey),
|
||||
'sender_claimed_keys': serializer.toJson<String>(senderClaimedKeys),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -2145,7 +2174,10 @@ class DbInboundGroupSession extends DataClass
|
|||
String sessionId,
|
||||
String pickle,
|
||||
String content,
|
||||
String indexes}) =>
|
||||
String indexes,
|
||||
bool uploaded,
|
||||
String senderKey,
|
||||
String senderClaimedKeys}) =>
|
||||
DbInboundGroupSession(
|
||||
clientId: clientId ?? this.clientId,
|
||||
roomId: roomId ?? this.roomId,
|
||||
|
@ -2153,6 +2185,9 @@ class DbInboundGroupSession extends DataClass
|
|||
pickle: pickle ?? this.pickle,
|
||||
content: content ?? this.content,
|
||||
indexes: indexes ?? this.indexes,
|
||||
uploaded: uploaded ?? this.uploaded,
|
||||
senderKey: senderKey ?? this.senderKey,
|
||||
senderClaimedKeys: senderClaimedKeys ?? this.senderClaimedKeys,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
|
@ -2162,7 +2197,10 @@ class DbInboundGroupSession extends DataClass
|
|||
..write('sessionId: $sessionId, ')
|
||||
..write('pickle: $pickle, ')
|
||||
..write('content: $content, ')
|
||||
..write('indexes: $indexes')
|
||||
..write('indexes: $indexes, ')
|
||||
..write('uploaded: $uploaded, ')
|
||||
..write('senderKey: $senderKey, ')
|
||||
..write('senderClaimedKeys: $senderClaimedKeys')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
@ -2174,8 +2212,16 @@ class DbInboundGroupSession extends DataClass
|
|||
roomId.hashCode,
|
||||
$mrjc(
|
||||
sessionId.hashCode,
|
||||
$mrjc(pickle.hashCode,
|
||||
$mrjc(content.hashCode, indexes.hashCode))))));
|
||||
$mrjc(
|
||||
pickle.hashCode,
|
||||
$mrjc(
|
||||
content.hashCode,
|
||||
$mrjc(
|
||||
indexes.hashCode,
|
||||
$mrjc(
|
||||
uploaded.hashCode,
|
||||
$mrjc(senderKey.hashCode,
|
||||
senderClaimedKeys.hashCode)))))))));
|
||||
@override
|
||||
bool operator ==(dynamic other) =>
|
||||
identical(this, other) ||
|
||||
|
@ -2185,7 +2231,10 @@ class DbInboundGroupSession extends DataClass
|
|||
other.sessionId == this.sessionId &&
|
||||
other.pickle == this.pickle &&
|
||||
other.content == this.content &&
|
||||
other.indexes == this.indexes);
|
||||
other.indexes == this.indexes &&
|
||||
other.uploaded == this.uploaded &&
|
||||
other.senderKey == this.senderKey &&
|
||||
other.senderClaimedKeys == this.senderClaimedKeys);
|
||||
}
|
||||
|
||||
class InboundGroupSessionsCompanion
|
||||
|
@ -2196,6 +2245,9 @@ class InboundGroupSessionsCompanion
|
|||
final Value<String> pickle;
|
||||
final Value<String> content;
|
||||
final Value<String> indexes;
|
||||
final Value<bool> uploaded;
|
||||
final Value<String> senderKey;
|
||||
final Value<String> senderClaimedKeys;
|
||||
const InboundGroupSessionsCompanion({
|
||||
this.clientId = const Value.absent(),
|
||||
this.roomId = const Value.absent(),
|
||||
|
@ -2203,6 +2255,9 @@ class InboundGroupSessionsCompanion
|
|||
this.pickle = const Value.absent(),
|
||||
this.content = const Value.absent(),
|
||||
this.indexes = const Value.absent(),
|
||||
this.uploaded = const Value.absent(),
|
||||
this.senderKey = const Value.absent(),
|
||||
this.senderClaimedKeys = const Value.absent(),
|
||||
});
|
||||
InboundGroupSessionsCompanion.insert({
|
||||
@required int clientId,
|
||||
|
@ -2211,6 +2266,9 @@ class InboundGroupSessionsCompanion
|
|||
@required String pickle,
|
||||
this.content = const Value.absent(),
|
||||
this.indexes = const Value.absent(),
|
||||
this.uploaded = const Value.absent(),
|
||||
this.senderKey = const Value.absent(),
|
||||
this.senderClaimedKeys = const Value.absent(),
|
||||
}) : clientId = Value(clientId),
|
||||
roomId = Value(roomId),
|
||||
sessionId = Value(sessionId),
|
||||
|
@ -2222,6 +2280,9 @@ class InboundGroupSessionsCompanion
|
|||
Expression<String> pickle,
|
||||
Expression<String> content,
|
||||
Expression<String> indexes,
|
||||
Expression<bool> uploaded,
|
||||
Expression<String> senderKey,
|
||||
Expression<String> senderClaimedKeys,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (clientId != null) 'client_id': clientId,
|
||||
|
@ -2230,6 +2291,9 @@ class InboundGroupSessionsCompanion
|
|||
if (pickle != null) 'pickle': pickle,
|
||||
if (content != null) 'content': content,
|
||||
if (indexes != null) 'indexes': indexes,
|
||||
if (uploaded != null) 'uploaded': uploaded,
|
||||
if (senderKey != null) 'sender_key': senderKey,
|
||||
if (senderClaimedKeys != null) 'sender_claimed_keys': senderClaimedKeys,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2239,7 +2303,10 @@ class InboundGroupSessionsCompanion
|
|||
Value<String> sessionId,
|
||||
Value<String> pickle,
|
||||
Value<String> content,
|
||||
Value<String> indexes}) {
|
||||
Value<String> indexes,
|
||||
Value<bool> uploaded,
|
||||
Value<String> senderKey,
|
||||
Value<String> senderClaimedKeys}) {
|
||||
return InboundGroupSessionsCompanion(
|
||||
clientId: clientId ?? this.clientId,
|
||||
roomId: roomId ?? this.roomId,
|
||||
|
@ -2247,6 +2314,9 @@ class InboundGroupSessionsCompanion
|
|||
pickle: pickle ?? this.pickle,
|
||||
content: content ?? this.content,
|
||||
indexes: indexes ?? this.indexes,
|
||||
uploaded: uploaded ?? this.uploaded,
|
||||
senderKey: senderKey ?? this.senderKey,
|
||||
senderClaimedKeys: senderClaimedKeys ?? this.senderClaimedKeys,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2271,6 +2341,15 @@ class InboundGroupSessionsCompanion
|
|||
if (indexes.present) {
|
||||
map['indexes'] = Variable<String>(indexes.value);
|
||||
}
|
||||
if (uploaded.present) {
|
||||
map['uploaded'] = Variable<bool>(uploaded.value);
|
||||
}
|
||||
if (senderKey.present) {
|
||||
map['sender_key'] = Variable<String>(senderKey.value);
|
||||
}
|
||||
if (senderClaimedKeys.present) {
|
||||
map['sender_claimed_keys'] = Variable<String>(senderClaimedKeys.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
@ -2328,9 +2407,45 @@ class InboundGroupSessions extends Table
|
|||
$customConstraints: '');
|
||||
}
|
||||
|
||||
final VerificationMeta _uploadedMeta = const VerificationMeta('uploaded');
|
||||
GeneratedBoolColumn _uploaded;
|
||||
GeneratedBoolColumn get uploaded => _uploaded ??= _constructUploaded();
|
||||
GeneratedBoolColumn _constructUploaded() {
|
||||
return GeneratedBoolColumn('uploaded', $tableName, true,
|
||||
$customConstraints: 'DEFAULT false',
|
||||
defaultValue: const CustomExpression<bool>('false'));
|
||||
}
|
||||
|
||||
final VerificationMeta _senderKeyMeta = const VerificationMeta('senderKey');
|
||||
GeneratedTextColumn _senderKey;
|
||||
GeneratedTextColumn get senderKey => _senderKey ??= _constructSenderKey();
|
||||
GeneratedTextColumn _constructSenderKey() {
|
||||
return GeneratedTextColumn('sender_key', $tableName, true,
|
||||
$customConstraints: '');
|
||||
}
|
||||
|
||||
final VerificationMeta _senderClaimedKeysMeta =
|
||||
const VerificationMeta('senderClaimedKeys');
|
||||
GeneratedTextColumn _senderClaimedKeys;
|
||||
GeneratedTextColumn get senderClaimedKeys =>
|
||||
_senderClaimedKeys ??= _constructSenderClaimedKeys();
|
||||
GeneratedTextColumn _constructSenderClaimedKeys() {
|
||||
return GeneratedTextColumn('sender_claimed_keys', $tableName, true,
|
||||
$customConstraints: '');
|
||||
}
|
||||
|
||||
@override
|
||||
List<GeneratedColumn> get $columns =>
|
||||
[clientId, roomId, sessionId, pickle, content, indexes];
|
||||
List<GeneratedColumn> get $columns => [
|
||||
clientId,
|
||||
roomId,
|
||||
sessionId,
|
||||
pickle,
|
||||
content,
|
||||
indexes,
|
||||
uploaded,
|
||||
senderKey,
|
||||
senderClaimedKeys
|
||||
];
|
||||
@override
|
||||
InboundGroupSessions get asDslTable => this;
|
||||
@override
|
||||
|
@ -2375,6 +2490,20 @@ class InboundGroupSessions extends Table
|
|||
context.handle(_indexesMeta,
|
||||
indexes.isAcceptableOrUnknown(data['indexes'], _indexesMeta));
|
||||
}
|
||||
if (data.containsKey('uploaded')) {
|
||||
context.handle(_uploadedMeta,
|
||||
uploaded.isAcceptableOrUnknown(data['uploaded'], _uploadedMeta));
|
||||
}
|
||||
if (data.containsKey('sender_key')) {
|
||||
context.handle(_senderKeyMeta,
|
||||
senderKey.isAcceptableOrUnknown(data['sender_key'], _senderKeyMeta));
|
||||
}
|
||||
if (data.containsKey('sender_claimed_keys')) {
|
||||
context.handle(
|
||||
_senderClaimedKeysMeta,
|
||||
senderClaimedKeys.isAcceptableOrUnknown(
|
||||
data['sender_claimed_keys'], _senderClaimedKeysMeta));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
|
@ -5669,6 +5798,9 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
pickle: row.readString('pickle'),
|
||||
content: row.readString('content'),
|
||||
indexes: row.readString('indexes'),
|
||||
uploaded: row.readBool('uploaded'),
|
||||
senderKey: row.readString('sender_key'),
|
||||
senderClaimedKeys: row.readString('sender_claimed_keys'),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -5701,17 +5833,26 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
readsFrom: {inboundGroupSessions}).map(_rowToDbInboundGroupSession);
|
||||
}
|
||||
|
||||
Future<int> storeInboundGroupSession(int client_id, String room_id,
|
||||
String session_id, String pickle, String content, String indexes) {
|
||||
Future<int> storeInboundGroupSession(
|
||||
int client_id,
|
||||
String room_id,
|
||||
String session_id,
|
||||
String pickle,
|
||||
String content,
|
||||
String indexes,
|
||||
String sender_key,
|
||||
String sender_claimed_keys) {
|
||||
return customInsert(
|
||||
'INSERT OR REPLACE INTO inbound_group_sessions (client_id, room_id, session_id, pickle, content, indexes) VALUES (:client_id, :room_id, :session_id, :pickle, :content, :indexes)',
|
||||
'INSERT OR REPLACE INTO inbound_group_sessions (client_id, room_id, session_id, pickle, content, indexes, sender_key, sender_claimed_keys) VALUES (:client_id, :room_id, :session_id, :pickle, :content, :indexes, :sender_key, :sender_claimed_keys)',
|
||||
variables: [
|
||||
Variable.withInt(client_id),
|
||||
Variable.withString(room_id),
|
||||
Variable.withString(session_id),
|
||||
Variable.withString(pickle),
|
||||
Variable.withString(content),
|
||||
Variable.withString(indexes)
|
||||
Variable.withString(indexes),
|
||||
Variable.withString(sender_key),
|
||||
Variable.withString(sender_claimed_keys)
|
||||
],
|
||||
updates: {inboundGroupSessions},
|
||||
);
|
||||
|
@ -5732,6 +5873,27 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
);
|
||||
}
|
||||
|
||||
Selectable<DbInboundGroupSession> getInboundGroupSessionsToUpload() {
|
||||
return customSelect(
|
||||
'SELECT * FROM inbound_group_sessions WHERE uploaded = false LIMIT 500',
|
||||
variables: [],
|
||||
readsFrom: {inboundGroupSessions}).map(_rowToDbInboundGroupSession);
|
||||
}
|
||||
|
||||
Future<int> markInboundGroupSessionAsUploaded(
|
||||
int client_id, String room_id, String session_id) {
|
||||
return customUpdate(
|
||||
'UPDATE inbound_group_sessions SET uploaded = true WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id',
|
||||
variables: [
|
||||
Variable.withInt(client_id),
|
||||
Variable.withString(room_id),
|
||||
Variable.withString(session_id)
|
||||
],
|
||||
updates: {inboundGroupSessions},
|
||||
updateKind: UpdateKind.update,
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> storeUserDeviceKeysInfo(
|
||||
int client_id, String user_id, bool outdated) {
|
||||
return customInsert(
|
||||
|
@ -6033,6 +6195,21 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
);
|
||||
}
|
||||
|
||||
Future<int> updateEventStatusOnly(
|
||||
int status, int client_id, String event_id, String room_id) {
|
||||
return customUpdate(
|
||||
'UPDATE events SET status = :status WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id',
|
||||
variables: [
|
||||
Variable.withInt(status),
|
||||
Variable.withInt(client_id),
|
||||
Variable.withString(event_id),
|
||||
Variable.withString(room_id)
|
||||
],
|
||||
updates: {events},
|
||||
updateKind: UpdateKind.update,
|
||||
);
|
||||
}
|
||||
|
||||
DbRoomState _rowToDbRoomState(QueryRow row) {
|
||||
return DbRoomState(
|
||||
clientId: row.readInt('client_id'),
|
||||
|
@ -6302,9 +6479,9 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
);
|
||||
}
|
||||
|
||||
Future<int> removeRoomEvents(int client_id, String room_id) {
|
||||
Future<int> removeSuccessfulRoomEvents(int client_id, String room_id) {
|
||||
return customUpdate(
|
||||
'DELETE FROM events WHERE client_id = :client_id AND room_id = :room_id',
|
||||
'DELETE FROM events WHERE client_id = :client_id AND room_id = :room_id AND status <> -1 AND status <> 0',
|
||||
variables: [Variable.withInt(client_id), Variable.withString(room_id)],
|
||||
updates: {events},
|
||||
updateKind: UpdateKind.delete,
|
||||
|
@ -6337,6 +6514,15 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
readsFrom: {files}).map(_rowToDbFile);
|
||||
}
|
||||
|
||||
Future<int> markPendingEventsAsError(int client_id) {
|
||||
return customUpdate(
|
||||
'UPDATE events SET status = -1 WHERE client_id = :client_id AND status = 0',
|
||||
variables: [Variable.withInt(client_id)],
|
||||
updates: {events},
|
||||
updateKind: UpdateKind.update,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>();
|
||||
@override
|
||||
|
|
|
@ -71,6 +71,9 @@ CREATE TABLE inbound_group_sessions (
|
|||
pickle TEXT NOT NULL,
|
||||
content TEXT,
|
||||
indexes TEXT,
|
||||
uploaded BOOLEAN DEFAULT false,
|
||||
sender_key TEXT,
|
||||
sender_claimed_keys TEXT,
|
||||
UNIQUE(client_id, room_id, session_id)
|
||||
) AS DbInboundGroupSession;
|
||||
CREATE INDEX inbound_group_sessions_index ON inbound_group_sessions(client_id);
|
||||
|
@ -186,8 +189,10 @@ removeOutboundGroupSession: DELETE FROM outbound_group_sessions WHERE client_id
|
|||
dbGetInboundGroupSessionKey: SELECT * FROM inbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id;
|
||||
dbGetInboundGroupSessionKeys: SELECT * FROM inbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id;
|
||||
getAllInboundGroupSessions: SELECT * FROM inbound_group_sessions WHERE client_id = :client_id;
|
||||
storeInboundGroupSession: INSERT OR REPLACE INTO inbound_group_sessions (client_id, room_id, session_id, pickle, content, indexes) VALUES (:client_id, :room_id, :session_id, :pickle, :content, :indexes);
|
||||
storeInboundGroupSession: INSERT OR REPLACE INTO inbound_group_sessions (client_id, room_id, session_id, pickle, content, indexes, sender_key, sender_claimed_keys) VALUES (:client_id, :room_id, :session_id, :pickle, :content, :indexes, :sender_key, :sender_claimed_keys);
|
||||
updateInboundGroupSessionIndexes: UPDATE inbound_group_sessions SET indexes = :indexes WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id;
|
||||
getInboundGroupSessionsToUpload: SELECT * FROM inbound_group_sessions WHERE uploaded = false LIMIT 500;
|
||||
markInboundGroupSessionAsUploaded: UPDATE inbound_group_sessions SET uploaded = true WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id;
|
||||
storeUserDeviceKeysInfo: INSERT OR REPLACE INTO user_device_keys (client_id, user_id, outdated) VALUES (:client_id, :user_id, :outdated);
|
||||
setVerifiedUserDeviceKey: UPDATE user_device_keys_key SET verified = :verified WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;
|
||||
setBlockedUserDeviceKey: UPDATE user_device_keys_key SET blocked = :blocked WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;
|
||||
|
@ -208,6 +213,7 @@ getAllAccountData: SELECT * FROM account_data WHERE client_id = :client_id;
|
|||
storeAccountData: INSERT OR REPLACE INTO account_data (client_id, type, content) VALUES (:client_id, :type, :content);
|
||||
updateEvent: UPDATE events SET unsigned = :unsigned, content = :content, prev_content = :prev_content WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id;
|
||||
updateEventStatus: UPDATE events SET status = :status, event_id = :new_event_id WHERE client_id = :client_id AND event_id = :old_event_id AND room_id = :room_id;
|
||||
updateEventStatusOnly: UPDATE events SET status = :status WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id;
|
||||
getImportantRoomStates: SELECT * FROM room_states WHERE client_id = :client_id AND type IN :events;
|
||||
getAllRoomStates: SELECT * FROM room_states WHERE client_id = :client_id;
|
||||
getUnimportantRoomStatesForRoom: SELECT * FROM room_states WHERE client_id = :client_id AND room_id = :room_id AND type NOT IN :events;
|
||||
|
@ -224,6 +230,7 @@ getRoom: SELECT * FROM rooms WHERE client_id = :client_id AND room_id = :room_id
|
|||
getEvent: SELECT * FROM events WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id;
|
||||
removeEvent: DELETE FROM events WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id;
|
||||
removeRoom: DELETE FROM rooms WHERE client_id = :client_id AND room_id = :room_id;
|
||||
removeRoomEvents: DELETE FROM events WHERE client_id = :client_id AND room_id = :room_id;
|
||||
removeSuccessfulRoomEvents: DELETE FROM events WHERE client_id = :client_id AND room_id = :room_id AND status <> -1 AND status <> 0;
|
||||
storeFile: INSERT OR REPLACE INTO files (mxc_uri, bytes, saved_at) VALUES (:mxc_uri, :bytes, :time);
|
||||
dbGetFile: SELECT * FROM files WHERE mxc_uri = :mxc_uri;
|
||||
markPendingEventsAsError: UPDATE events SET status = -1 WHERE client_id = :client_id AND status = 0;
|
||||
|
|
|
@ -18,15 +18,23 @@
|
|||
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
import 'package:famedlysdk/src/utils/receipt.dart';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
||||
|
||||
import '../encryption.dart';
|
||||
import '../famedlysdk.dart';
|
||||
import '../matrix_api.dart';
|
||||
import './room.dart';
|
||||
import 'database/database.dart' show DbRoomState, DbEvent;
|
||||
import 'room.dart';
|
||||
import 'utils/matrix_localizations.dart';
|
||||
import './database/database.dart' show DbRoomState, DbEvent;
|
||||
import 'utils/receipt.dart';
|
||||
|
||||
abstract class RelationshipTypes {
|
||||
static const String Reply = 'm.in_reply_to';
|
||||
static const String Edit = 'm.replace';
|
||||
static const String Reaction = 'm.annotation';
|
||||
}
|
||||
|
||||
/// All data exchanged over Matrix is expressed as an "event". Typically each client action (e.g. sending a message) correlates with exactly one event.
|
||||
class Event extends MatrixEvent {
|
||||
|
@ -90,12 +98,20 @@ class Event extends MatrixEvent {
|
|||
this.senderId = senderId;
|
||||
this.unsigned = unsigned;
|
||||
// synapse unfortunatley isn't following the spec and tosses the prev_content
|
||||
// into the unsigned block
|
||||
this.prevContent = prevContent != null && prevContent.isNotEmpty
|
||||
? prevContent
|
||||
: (unsigned != null && unsigned['prev_content'] is Map
|
||||
? unsigned['prev_content']
|
||||
: null);
|
||||
// into the unsigned block.
|
||||
// Currently we are facing a very strange bug in web which is impossible to debug.
|
||||
// It may be because of this line so we put this in try-catch until we can fix it.
|
||||
try {
|
||||
this.prevContent = (prevContent != null && prevContent.isNotEmpty)
|
||||
? prevContent
|
||||
: (unsigned != null &&
|
||||
unsigned.containsKey('prev_content') &&
|
||||
unsigned['prev_content'] is Map)
|
||||
? unsigned['prev_content']
|
||||
: null;
|
||||
} catch (_) {
|
||||
// A strange bug in dart web makes this crash
|
||||
}
|
||||
this.stateKey = stateKey;
|
||||
this.originServerTs = originServerTs;
|
||||
}
|
||||
|
@ -140,7 +156,9 @@ class Event extends MatrixEvent {
|
|||
final unsigned = Event.getMapFromPayload(jsonPayload['unsigned']);
|
||||
final prevContent = Event.getMapFromPayload(jsonPayload['prev_content']);
|
||||
return Event(
|
||||
status: jsonPayload['status'] ?? defaultStatus,
|
||||
status: jsonPayload['status'] ??
|
||||
unsigned[MessageSendingStatusKey] ??
|
||||
defaultStatus,
|
||||
stateKey: jsonPayload['state_key'],
|
||||
prevContent: prevContent,
|
||||
content: content,
|
||||
|
@ -212,9 +230,8 @@ class Event extends MatrixEvent {
|
|||
unsigned: unsigned,
|
||||
room: room);
|
||||
|
||||
String get messageType => (content['m.relates_to'] is Map &&
|
||||
content['m.relates_to']['m.in_reply_to'] != null)
|
||||
? MessageTypes.Reply
|
||||
String get messageType => type == EventTypes.Sticker
|
||||
? MessageTypes.Sticker
|
||||
: content['msgtype'] ?? MessageTypes.Text;
|
||||
|
||||
void setRedactionEvent(Event redactedBecause) {
|
||||
|
@ -312,12 +329,13 @@ class Event extends MatrixEvent {
|
|||
/// Try to send this event again. Only works with events of status -1.
|
||||
Future<String> sendAgain({String txid}) async {
|
||||
if (status != -1) return null;
|
||||
await remove();
|
||||
final eventID = await room.sendEvent(
|
||||
// we do not remove the event here. It will automatically be updated
|
||||
// in the `sendEvent` method to transition -1 -> 0 -> 1 -> 2
|
||||
final newEventId = await room.sendEvent(
|
||||
content,
|
||||
txid: txid ?? unsigned['transaction_id'],
|
||||
txid: txid ?? unsigned['transaction_id'] ?? eventId,
|
||||
);
|
||||
return eventID;
|
||||
return newEventId;
|
||||
}
|
||||
|
||||
/// Whether the client is allowed to redact this event.
|
||||
|
@ -327,20 +345,10 @@ class Event extends MatrixEvent {
|
|||
Future<dynamic> redact({String reason, String txid}) =>
|
||||
room.redactEvent(eventId, reason: reason, txid: txid);
|
||||
|
||||
/// Whether this event is in reply to another event.
|
||||
bool get isReply =>
|
||||
content['m.relates_to'] is Map<String, dynamic> &&
|
||||
content['m.relates_to']['m.in_reply_to'] is Map<String, dynamic> &&
|
||||
content['m.relates_to']['m.in_reply_to']['event_id'] is String &&
|
||||
(content['m.relates_to']['m.in_reply_to']['event_id'] as String)
|
||||
.isNotEmpty;
|
||||
|
||||
/// Searches for the reply event in the given timeline.
|
||||
Future<Event> getReplyEvent(Timeline timeline) async {
|
||||
if (!isReply) return null;
|
||||
final String replyEventId =
|
||||
content['m.relates_to']['m.in_reply_to']['event_id'];
|
||||
return await timeline.getEventById(replyEventId);
|
||||
if (relationshipType != RelationshipTypes.Reply) return null;
|
||||
return await timeline.getEventById(relationshipEventId);
|
||||
}
|
||||
|
||||
/// If this event is encrypted and the decryption was not successful because
|
||||
|
@ -367,7 +375,8 @@ class Event extends MatrixEvent {
|
|||
/// contain an attachment, this throws an error. Set [getThumbnail] to
|
||||
/// true to download the thumbnail instead.
|
||||
Future<MatrixFile> downloadAndDecryptAttachment(
|
||||
{bool getThumbnail = false}) async {
|
||||
{bool getThumbnail = false,
|
||||
Future<Uint8List> Function(String) downloadCallback}) async {
|
||||
if (![EventTypes.Message, EventTypes.Sticker].contains(type)) {
|
||||
throw ("This event has the type '$type' and so it can't contain an attachment.");
|
||||
}
|
||||
|
@ -397,7 +406,7 @@ class Event extends MatrixEvent {
|
|||
// Is this file storeable?
|
||||
final infoMap =
|
||||
getThumbnail ? content['info']['thumbnail_info'] : content['info'];
|
||||
final storeable = room.client.database != null &&
|
||||
var storeable = room.client.database != null &&
|
||||
infoMap is Map<String, dynamic> &&
|
||||
infoMap['size'] is int &&
|
||||
infoMap['size'] <= room.client.database.maxFileSize;
|
||||
|
@ -408,8 +417,13 @@ class Event extends MatrixEvent {
|
|||
|
||||
// Download the file
|
||||
if (uint8list == null) {
|
||||
downloadCallback ??= (String url) async {
|
||||
return (await http.get(url)).bodyBytes;
|
||||
};
|
||||
uint8list =
|
||||
(await http.get(mxContent.getDownloadLink(room.client))).bodyBytes;
|
||||
await downloadCallback(mxContent.getDownloadLink(room.client));
|
||||
storeable = storeable &&
|
||||
uint8list.lengthInBytes < room.client.database.maxFileSize;
|
||||
if (storeable) {
|
||||
await room.client.database
|
||||
.storeFile(mxContent.toString(), uint8list, DateTime.now());
|
||||
|
@ -480,9 +494,8 @@ class Event extends MatrixEvent {
|
|||
final targetName = stateKeyUser.calcDisplayname();
|
||||
// Has the membership changed?
|
||||
final newMembership = content['membership'] ?? '';
|
||||
final oldMembership = unsigned['prev_content'] is Map<String, dynamic>
|
||||
? unsigned['prev_content']['membership'] ?? ''
|
||||
: '';
|
||||
final oldMembership =
|
||||
prevContent != null ? prevContent['membership'] ?? '' : '';
|
||||
if (newMembership != oldMembership) {
|
||||
if (oldMembership == 'invite' && newMembership == 'join') {
|
||||
text = i18n.acceptedTheInvitation(targetName);
|
||||
|
@ -517,15 +530,12 @@ class Event extends MatrixEvent {
|
|||
}
|
||||
} else if (newMembership == 'join') {
|
||||
final newAvatar = content['avatar_url'] ?? '';
|
||||
final oldAvatar = unsigned['prev_content'] is Map<String, dynamic>
|
||||
? unsigned['prev_content']['avatar_url'] ?? ''
|
||||
: '';
|
||||
final oldAvatar =
|
||||
prevContent != null ? prevContent['avatar_url'] ?? '' : '';
|
||||
|
||||
final newDisplayname = content['displayname'] ?? '';
|
||||
final oldDisplayname =
|
||||
unsigned['prev_content'] is Map<String, dynamic>
|
||||
? unsigned['prev_content']['displayname'] ?? ''
|
||||
: '';
|
||||
prevContent != null ? prevContent['displayname'] ?? '' : '';
|
||||
|
||||
// Has the user avatar changed?
|
||||
if (newAvatar != oldAvatar) {
|
||||
|
@ -583,6 +593,18 @@ class Event extends MatrixEvent {
|
|||
localizedBody += '. ' + i18n.needPantalaimonWarning;
|
||||
}
|
||||
break;
|
||||
case EventTypes.CallAnswer:
|
||||
localizedBody = i18n.answeredTheCall(senderName);
|
||||
break;
|
||||
case EventTypes.CallHangup:
|
||||
localizedBody = i18n.endedTheCall(senderName);
|
||||
break;
|
||||
case EventTypes.CallInvite:
|
||||
localizedBody = i18n.startedACall(senderName);
|
||||
break;
|
||||
case EventTypes.CallCandidates:
|
||||
localizedBody = i18n.sentCallInformations(senderName);
|
||||
break;
|
||||
case EventTypes.Encrypted:
|
||||
case EventTypes.Message:
|
||||
switch (messageType) {
|
||||
|
@ -631,7 +653,6 @@ class Event extends MatrixEvent {
|
|||
case MessageTypes.Text:
|
||||
case MessageTypes.Notice:
|
||||
case MessageTypes.None:
|
||||
case MessageTypes.Reply:
|
||||
localizedBody = body;
|
||||
break;
|
||||
}
|
||||
|
@ -660,9 +681,130 @@ class Event extends MatrixEvent {
|
|||
|
||||
static const Set<String> textOnlyMessageTypes = {
|
||||
MessageTypes.Text,
|
||||
MessageTypes.Reply,
|
||||
MessageTypes.Notice,
|
||||
MessageTypes.Emote,
|
||||
MessageTypes.None,
|
||||
};
|
||||
|
||||
/// returns if this event matches the passed event or transaction id
|
||||
bool matchesEventOrTransactionId(String search) {
|
||||
if (search == null) {
|
||||
return false;
|
||||
}
|
||||
if (eventId == search) {
|
||||
return true;
|
||||
}
|
||||
return unsigned != null && unsigned['transaction_id'] == search;
|
||||
}
|
||||
|
||||
/// Get the relationship type of an event. `null` if there is none
|
||||
String get relationshipType {
|
||||
if (content == null || !(content['m.relates_to'] is Map)) {
|
||||
return null;
|
||||
}
|
||||
if (content['m.relates_to'].containsKey('rel_type')) {
|
||||
return content['m.relates_to']['rel_type'];
|
||||
}
|
||||
if (content['m.relates_to'].containsKey('m.in_reply_to')) {
|
||||
return RelationshipTypes.Reply;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get the event ID that this relationship will reference. `null` if there is none
|
||||
String get relationshipEventId {
|
||||
if (content == null || !(content['m.relates_to'] is Map)) {
|
||||
return null;
|
||||
}
|
||||
if (content['m.relates_to'].containsKey('event_id')) {
|
||||
return content['m.relates_to']['event_id'];
|
||||
}
|
||||
if (content['m.relates_to']['m.in_reply_to'] is Map &&
|
||||
content['m.relates_to']['m.in_reply_to'].containsKey('event_id')) {
|
||||
return content['m.relates_to']['m.in_reply_to']['event_id'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get wether this event has aggregated events from a certain [type]
|
||||
/// To be able to do that you need to pass a [timeline]
|
||||
bool hasAggregatedEvents(Timeline timeline, String type) =>
|
||||
timeline.aggregatedEvents.containsKey(eventId) &&
|
||||
timeline.aggregatedEvents[eventId].containsKey(type);
|
||||
|
||||
/// Get all the aggregated event objects for a given [type]. To be able to do this
|
||||
/// you have to pass a [timeline]
|
||||
Set<Event> aggregatedEvents(Timeline timeline, String type) =>
|
||||
hasAggregatedEvents(timeline, type)
|
||||
? timeline.aggregatedEvents[eventId][type]
|
||||
: <Event>{};
|
||||
|
||||
/// Fetches the event to be rendered, taking into account all the edits and the like.
|
||||
/// It needs a [timeline] for that.
|
||||
Event getDisplayEvent(Timeline timeline) {
|
||||
if (hasAggregatedEvents(timeline, RelationshipTypes.Edit)) {
|
||||
// alright, we have an edit
|
||||
final allEditEvents = aggregatedEvents(timeline, RelationshipTypes.Edit)
|
||||
// we only allow edits made by the original author themself
|
||||
.where((e) => e.senderId == senderId && e.type == EventTypes.Message)
|
||||
.toList();
|
||||
// we need to check again if it isn't empty, as we potentially removed all
|
||||
// aggregated edits
|
||||
if (allEditEvents.isNotEmpty) {
|
||||
allEditEvents.sort((a, b) => a.sortOrder - b.sortOrder > 0 ? 1 : -1);
|
||||
var rawEvent = allEditEvents.last.toJson();
|
||||
// update the content of the new event to render
|
||||
if (rawEvent['content']['m.new_content'] is Map) {
|
||||
rawEvent['content'] = rawEvent['content']['m.new_content'];
|
||||
}
|
||||
return Event.fromJson(rawEvent, room);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// returns if a message is a rich message
|
||||
bool get isRichMessage =>
|
||||
content['format'] == 'org.matrix.custom.html' &&
|
||||
content['formatted_body'] is String;
|
||||
|
||||
// regexes to fetch the number of emotes, including emoji, and if the message consists of only those
|
||||
// to match an emoji we can use the following regex:
|
||||
// (?:\x{00a9}|\x{00ae}|[\x{2000}-\x{3300}]|\x{d83c}[\x{d000}-\x{dfff}]|\x{d83d}[\x{d000}-\x{dfff}]|\x{d83e}[\x{d000}-\x{dfff}])[\x{fe00}-\x{fe0f}]?
|
||||
// we need to replace \x{0000} with \u0000, the comment is left in the other format to be able to paste into regex101.com
|
||||
// to see if there is a custom emote, we use the following regex: <img[^>]+data-mx-(?:emote|emoticon)(?==|>|\s)[^>]*>
|
||||
// now we combind the two to have four regexes:
|
||||
// 1. are there only emoji, or whitespace
|
||||
// 2. are there only emoji, emotes, or whitespace
|
||||
// 3. count number of emoji
|
||||
// 4- count number of emoji or emotes
|
||||
static final RegExp _onlyEmojiRegex = RegExp(
|
||||
r'^((?:\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])[\ufe00-\ufe0f]?|\s)*$',
|
||||
caseSensitive: false,
|
||||
multiLine: false);
|
||||
static final RegExp _onlyEmojiEmoteRegex = RegExp(
|
||||
r'^((?:\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])[\ufe00-\ufe0f]?|<img[^>]+data-mx-(?:emote|emoticon)(?==|>|\s)[^>]*>|\s)*$',
|
||||
caseSensitive: false,
|
||||
multiLine: false);
|
||||
static final RegExp _countEmojiRegex = RegExp(
|
||||
r'((?:\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])[\ufe00-\ufe0f]?)',
|
||||
caseSensitive: false,
|
||||
multiLine: false);
|
||||
static final RegExp _countEmojiEmoteRegex = RegExp(
|
||||
r'((?:\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])[\ufe00-\ufe0f]?|<img[^>]+data-mx-(?:emote|emoticon)(?==|>|\s)[^>]*>)',
|
||||
caseSensitive: false,
|
||||
multiLine: false);
|
||||
|
||||
/// Returns if a given event only has emotes, emojis or whitespace as content.
|
||||
/// This is useful to determine if stand-alone emotes should be displayed bigger.
|
||||
bool get onlyEmotes => isRichMessage
|
||||
? _onlyEmojiEmoteRegex.hasMatch(content['formatted_body'])
|
||||
: _onlyEmojiRegex.hasMatch(content['body'] ?? '');
|
||||
|
||||
/// Gets the number of emotes in a given message. This is useful to determine if
|
||||
/// emotes should be displayed bigger. WARNING: This does **not** test if there are
|
||||
/// only emotes. Use `event.onlyEmotes` for that!
|
||||
int get numberEmotes => isRichMessage
|
||||
? _countEmojiEmoteRegex.allMatches(content['formatted_body']).length
|
||||
: _countEmojiRegex.allMatches(content['body'] ?? '').length;
|
||||
}
|
||||
|
|
|
@ -18,27 +18,30 @@
|
|||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/src/client.dart';
|
||||
import 'package:famedlysdk/src/event.dart';
|
||||
import 'package:famedlysdk/src/utils/event_update.dart';
|
||||
import 'package:famedlysdk/src/utils/room_update.dart';
|
||||
import 'package:famedlysdk/src/utils/matrix_file.dart';
|
||||
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
||||
import 'package:html_unescape/html_unescape.dart';
|
||||
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
||||
|
||||
import './user.dart';
|
||||
import '../famedlysdk.dart';
|
||||
import '../matrix_api.dart';
|
||||
import 'client.dart';
|
||||
import 'database/database.dart' show DbRoom;
|
||||
import 'event.dart';
|
||||
import 'timeline.dart';
|
||||
import 'user.dart';
|
||||
import 'utils/event_update.dart';
|
||||
import 'utils/logs.dart';
|
||||
import 'utils/markdown.dart';
|
||||
import 'utils/matrix_file.dart';
|
||||
import 'utils/matrix_localizations.dart';
|
||||
import 'utils/room_update.dart';
|
||||
import 'utils/states_map.dart';
|
||||
import './utils/markdown.dart';
|
||||
import './database/database.dart' show DbRoom;
|
||||
|
||||
enum PushRuleState { notify, mentions_only, dont_notify }
|
||||
enum JoinRules { public, knock, invite, private }
|
||||
enum GuestAccess { can_join, forbidden }
|
||||
enum HistoryVisibility { invited, joined, shared, world_readable }
|
||||
const String MessageSendingStatusKey =
|
||||
'com.famedly.famedlysdk.message_sending_status';
|
||||
|
||||
/// Represents a Matrix room.
|
||||
class Room {
|
||||
|
@ -104,7 +107,9 @@ class Room {
|
|||
/// Flag if the room is partial, meaning not all state events have been loaded yet
|
||||
bool partial = true;
|
||||
|
||||
/// Load all the missing state events for the room from the database. If the room has already been loaded, this does nothing.
|
||||
/// Post-loads the room.
|
||||
/// This load all the missing state events for the room from the database
|
||||
/// If the room has already been loaded, this does nothing.
|
||||
Future<void> postLoad() async {
|
||||
if (!partial || client.database == null) {
|
||||
return;
|
||||
|
@ -132,8 +137,8 @@ class Room {
|
|||
if (state.type == EventTypes.Encrypted && client.encryptionEnabled) {
|
||||
try {
|
||||
state = client.encryption.decryptRoomEventSync(id, state);
|
||||
} catch (e) {
|
||||
print('[LibOlm] Could not decrypt room state: ' + e.toString());
|
||||
} catch (e, s) {
|
||||
Logs.error('[LibOlm] Could not decrypt room state: ' + e.toString(), s);
|
||||
}
|
||||
}
|
||||
if (!(state.stateKey is String) &&
|
||||
|
@ -259,7 +264,13 @@ class Room {
|
|||
// perfect, it is only used for the room preview in the room list and sorting
|
||||
// said room list, so it should be good enough.
|
||||
var lastTime = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
var lastEvent = getState(EventTypes.Message);
|
||||
final lastEvents = <Event>[
|
||||
for (var type in client.roomPreviewLastEvents) getState(type)
|
||||
]..removeWhere((e) => e == null);
|
||||
|
||||
var lastEvent = lastEvents.isEmpty
|
||||
? null
|
||||
: lastEvents.reduce((a, b) => a.sortOrder > b.sortOrder ? a : b);
|
||||
if (lastEvent == null) {
|
||||
states.forEach((final String key, final entry) {
|
||||
if (!entry.containsKey('')) return;
|
||||
|
@ -369,21 +380,21 @@ class Room {
|
|||
|
||||
/// Call the Matrix API to change the name of this room. Returns the event ID of the
|
||||
/// new m.room.name event.
|
||||
Future<String> setName(String newName) => client.api.sendState(
|
||||
Future<String> setName(String newName) => client.sendState(
|
||||
id,
|
||||
EventTypes.RoomName,
|
||||
{'name': newName},
|
||||
);
|
||||
|
||||
/// Call the Matrix API to change the topic of this room.
|
||||
Future<String> setDescription(String newName) => client.api.sendState(
|
||||
Future<String> setDescription(String newName) => client.sendState(
|
||||
id,
|
||||
EventTypes.RoomTopic,
|
||||
{'topic': newName},
|
||||
);
|
||||
|
||||
/// Add a tag to the room.
|
||||
Future<void> addTag(String tag, {double order}) => client.api.addRoomTag(
|
||||
Future<void> addTag(String tag, {double order}) => client.addRoomTag(
|
||||
client.userID,
|
||||
id,
|
||||
tag,
|
||||
|
@ -391,7 +402,7 @@ class Room {
|
|||
);
|
||||
|
||||
/// Removes a tag from the room.
|
||||
Future<void> removeTag(String tag) => client.api.removeRoomTag(
|
||||
Future<void> removeTag(String tag) => client.removeRoomTag(
|
||||
client.userID,
|
||||
id,
|
||||
tag,
|
||||
|
@ -418,7 +429,7 @@ class Room {
|
|||
|
||||
/// Call the Matrix API to change the pinned events of this room.
|
||||
Future<String> setPinnedEvents(List<String> pinnedEventIds) =>
|
||||
client.api.sendState(
|
||||
client.sendState(
|
||||
id,
|
||||
EventTypes.RoomPinnedEvents,
|
||||
{'pinned': pinnedEventIds},
|
||||
|
@ -432,9 +443,10 @@ class Room {
|
|||
name = name.replaceAll(RegExp(r'[^\w-]'), '');
|
||||
return name.toLowerCase();
|
||||
};
|
||||
final allMxcs = <String>{}; // for easy dedupint
|
||||
final addEmotePack = (String packName, Map<String, dynamic> content,
|
||||
[String packNameOverride]) {
|
||||
if (!(content['short'] is Map)) {
|
||||
if (!(content['emoticons'] is Map) && !(content['short'] is Map)) {
|
||||
return;
|
||||
}
|
||||
if (content['pack'] is Map && content['pack']['name'] is String) {
|
||||
|
@ -447,34 +459,37 @@ class Room {
|
|||
if (!packs.containsKey(packName)) {
|
||||
packs[packName] = <String, String>{};
|
||||
}
|
||||
content['short'].forEach((key, value) {
|
||||
if (key is String && value is String && value.startsWith('mxc://')) {
|
||||
packs[packName][key] = value;
|
||||
}
|
||||
});
|
||||
};
|
||||
// first add all the room emotes
|
||||
final allRoomEmotes = states.states['im.ponies.room_emotes'];
|
||||
if (allRoomEmotes != null) {
|
||||
for (final entry in allRoomEmotes.entries) {
|
||||
final stateKey = entry.key;
|
||||
final event = entry.value;
|
||||
addEmotePack(stateKey.isEmpty ? 'room' : stateKey, event.content);
|
||||
if (content['emoticons'] is Map) {
|
||||
content['emoticons'].forEach((key, value) {
|
||||
if (key is String &&
|
||||
value is Map &&
|
||||
value['url'] is String &&
|
||||
value['url'].startsWith('mxc://')) {
|
||||
if (allMxcs.add(value['url'])) {
|
||||
packs[packName][key] = value['url'];
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
content['short'].forEach((key, value) {
|
||||
if (key is String && value is String && value.startsWith('mxc://')) {
|
||||
if (allMxcs.add(value)) {
|
||||
packs[packName][key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// next add all the user emotes
|
||||
};
|
||||
// first add all the user emotes
|
||||
final userEmotes = client.accountData['im.ponies.user_emotes'];
|
||||
if (userEmotes != null) {
|
||||
addEmotePack('user', userEmotes.content);
|
||||
}
|
||||
// finally add all the external emote rooms
|
||||
// next add all the external emote rooms
|
||||
final emoteRooms = client.accountData['im.ponies.emote_rooms'];
|
||||
if (emoteRooms != null && emoteRooms.content['rooms'] is Map) {
|
||||
for (final roomEntry in emoteRooms.content['rooms'].entries) {
|
||||
final roomId = roomEntry.key;
|
||||
if (roomId == id) {
|
||||
continue;
|
||||
}
|
||||
final room = client.getRoomById(roomId);
|
||||
if (room != null && roomEntry.value is Map) {
|
||||
for (final stateKeyEntry in roomEntry.value.entries) {
|
||||
|
@ -492,6 +507,15 @@ class Room {
|
|||
}
|
||||
}
|
||||
}
|
||||
// finally add all the room emotes
|
||||
final allRoomEmotes = states.states['im.ponies.room_emotes'];
|
||||
if (allRoomEmotes != null) {
|
||||
for (final entry in allRoomEmotes.entries) {
|
||||
final stateKey = entry.key;
|
||||
final event = entry.value;
|
||||
addEmotePack(stateKey.isEmpty ? 'room' : stateKey, event.content);
|
||||
}
|
||||
}
|
||||
return packs;
|
||||
}
|
||||
|
||||
|
@ -500,6 +524,7 @@ class Room {
|
|||
Future<String> sendTextEvent(String message,
|
||||
{String txid,
|
||||
Event inReplyTo,
|
||||
String editEventId,
|
||||
bool parseMarkdown = true,
|
||||
Map<String, Map<String, String>> emotePacks}) {
|
||||
final event = <String, dynamic>{
|
||||
|
@ -518,7 +543,31 @@ class Room {
|
|||
event['formatted_body'] = html;
|
||||
}
|
||||
}
|
||||
return sendEvent(event, txid: txid, inReplyTo: inReplyTo);
|
||||
return sendEvent(event,
|
||||
txid: txid, inReplyTo: inReplyTo, editEventId: editEventId);
|
||||
}
|
||||
|
||||
/// Sends a reaction to an event with an [eventId] and the content [key] into a room.
|
||||
/// Returns the event ID generated by the server for this reaction.
|
||||
Future<String> sendReaction(String eventId, String key, {String txid}) {
|
||||
return sendEvent({
|
||||
'm.relates_to': {
|
||||
'rel_type': RelationshipTypes.Reaction,
|
||||
'event_id': eventId,
|
||||
'key': key,
|
||||
},
|
||||
}, type: EventTypes.Reaction, txid: txid);
|
||||
}
|
||||
|
||||
/// Sends the location with description [body] and geo URI [geoUri] into a room.
|
||||
/// Returns the event ID generated by the server for this message.
|
||||
Future<String> sendLocation(String body, String geoUri, {String txid}) {
|
||||
final event = <String, dynamic>{
|
||||
'msgtype': 'm.location',
|
||||
'body': body,
|
||||
'geo_uri': geoUri,
|
||||
};
|
||||
return sendEvent(event, txid: txid);
|
||||
}
|
||||
|
||||
/// Sends a [file] to this room after uploading it. Returns the mxc uri of
|
||||
|
@ -529,6 +578,7 @@ class Room {
|
|||
MatrixFile file, {
|
||||
String txid,
|
||||
Event inReplyTo,
|
||||
String editEventId,
|
||||
bool waitUntilSent = false,
|
||||
MatrixImageFile thumbnail,
|
||||
}) async {
|
||||
|
@ -545,13 +595,13 @@ class Room {
|
|||
uploadThumbnail = encryptedThumbnail.toMatrixFile();
|
||||
}
|
||||
}
|
||||
final uploadResp = await client.api.upload(
|
||||
final uploadResp = await client.upload(
|
||||
uploadFile.bytes,
|
||||
uploadFile.name,
|
||||
contentType: uploadFile.mimeType,
|
||||
);
|
||||
final thumbnailUploadResp = uploadThumbnail != null
|
||||
? await client.api.upload(
|
||||
? await client.upload(
|
||||
uploadThumbnail.bytes,
|
||||
uploadThumbnail.name,
|
||||
contentType: uploadThumbnail.mimeType,
|
||||
|
@ -605,6 +655,7 @@ class Room {
|
|||
content,
|
||||
txid: txid,
|
||||
inReplyTo: inReplyTo,
|
||||
editEventId: editEventId,
|
||||
);
|
||||
if (waitUntilSent) {
|
||||
await sendResponse;
|
||||
|
@ -612,13 +663,35 @@ class Room {
|
|||
return uploadResp;
|
||||
}
|
||||
|
||||
Future<String> _sendContent(
|
||||
String type,
|
||||
Map<String, dynamic> content, {
|
||||
String txid,
|
||||
}) async {
|
||||
txid ??= client.generateUniqueTransactionId();
|
||||
final mustEncrypt = encrypted && client.encryptionEnabled;
|
||||
final sendMessageContent = mustEncrypt
|
||||
? await client.encryption
|
||||
.encryptGroupMessagePayload(id, content, type: type)
|
||||
: content;
|
||||
return await client.sendMessage(
|
||||
id,
|
||||
mustEncrypt ? EventTypes.Encrypted : type,
|
||||
txid,
|
||||
sendMessageContent,
|
||||
);
|
||||
}
|
||||
|
||||
/// Sends an event to this room with this json as a content. Returns the
|
||||
/// event ID generated from the server.
|
||||
Future<String> sendEvent(Map<String, dynamic> content,
|
||||
{String type, String txid, Event inReplyTo}) async {
|
||||
Future<String> sendEvent(
|
||||
Map<String, dynamic> content, {
|
||||
String type,
|
||||
String txid,
|
||||
Event inReplyTo,
|
||||
String editEventId,
|
||||
}) async {
|
||||
type = type ?? EventTypes.Message;
|
||||
final sendType =
|
||||
(encrypted && client.encryptionEnabled) ? EventTypes.Encrypted : type;
|
||||
|
||||
// Create new transaction id
|
||||
String messageID;
|
||||
|
@ -645,60 +718,72 @@ class Room {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
final sortOrder = newSortOrder;
|
||||
// Display a *sending* event and store it.
|
||||
var eventUpdate = EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: id,
|
||||
eventType: type,
|
||||
sortOrder: sortOrder,
|
||||
content: {
|
||||
'type': type,
|
||||
'event_id': messageID,
|
||||
'sender': client.userID,
|
||||
'status': 0,
|
||||
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
|
||||
'content': content
|
||||
},
|
||||
);
|
||||
client.onEvent.add(eventUpdate);
|
||||
await client.database?.transaction(() async {
|
||||
await client.database.storeEventUpdate(client.id, eventUpdate);
|
||||
await updateSortOrder();
|
||||
});
|
||||
if (editEventId != null) {
|
||||
final newContent = Map<String, dynamic>.from(content);
|
||||
content['m.new_content'] = newContent;
|
||||
content['m.relates_to'] = {
|
||||
'event_id': editEventId,
|
||||
'rel_type': RelationshipTypes.Edit,
|
||||
};
|
||||
if (content['body'] is String) {
|
||||
content['body'] = '* ' + content['body'];
|
||||
}
|
||||
if (content['formatted_body'] is String) {
|
||||
content['formatted_body'] = '* ' + content['formatted_body'];
|
||||
}
|
||||
}
|
||||
final sentDate = DateTime.now();
|
||||
final syncUpdate = SyncUpdate()
|
||||
..rooms = (RoomsUpdate()
|
||||
..join = (<String, JoinedRoomUpdate>{}..[id] = (JoinedRoomUpdate()
|
||||
..timeline = (TimelineUpdate()
|
||||
..events = [
|
||||
MatrixEvent()
|
||||
..content = content
|
||||
..type = type
|
||||
..eventId = messageID
|
||||
..senderId = client.userID
|
||||
..originServerTs = sentDate
|
||||
..unsigned = {
|
||||
MessageSendingStatusKey: 0,
|
||||
'transaction_id': messageID,
|
||||
},
|
||||
]))));
|
||||
await _handleFakeSync(syncUpdate);
|
||||
|
||||
// Send the text and on success, store and display a *sent* event.
|
||||
try {
|
||||
final sendMessageContent = encrypted && client.encryptionEnabled
|
||||
? await client.encryption
|
||||
.encryptGroupMessagePayload(id, content, type: type)
|
||||
: content;
|
||||
final res = await client.api.sendMessage(
|
||||
id,
|
||||
sendType,
|
||||
messageID,
|
||||
sendMessageContent,
|
||||
);
|
||||
eventUpdate.content['status'] = 1;
|
||||
eventUpdate.content['unsigned'] = {'transaction_id': messageID};
|
||||
eventUpdate.content['event_id'] = res;
|
||||
client.onEvent.add(eventUpdate);
|
||||
await client.database?.transaction(() async {
|
||||
await client.database.storeEventUpdate(client.id, eventUpdate);
|
||||
});
|
||||
return res;
|
||||
} catch (exception) {
|
||||
print('[Client] Error while sending: ' + exception.toString());
|
||||
// On error, set status to -1
|
||||
eventUpdate.content['status'] = -1;
|
||||
eventUpdate.content['unsigned'] = {'transaction_id': messageID};
|
||||
client.onEvent.add(eventUpdate);
|
||||
await client.database?.transaction(() async {
|
||||
await client.database.storeEventUpdate(client.id, eventUpdate);
|
||||
});
|
||||
String res;
|
||||
while (res == null) {
|
||||
try {
|
||||
res = await _sendContent(
|
||||
type,
|
||||
content,
|
||||
txid: messageID,
|
||||
);
|
||||
} catch (e, s) {
|
||||
if ((DateTime.now().millisecondsSinceEpoch -
|
||||
sentDate.millisecondsSinceEpoch) <
|
||||
(1000 * client.sendMessageTimeoutSeconds)) {
|
||||
Logs.warning('[Client] Problem while sending message because of "' +
|
||||
e.toString() +
|
||||
'". Try again in 1 seconds...');
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
} else {
|
||||
Logs.warning(
|
||||
'[Client] Problem while sending message: ' + e.toString(), s);
|
||||
syncUpdate.rooms.join.values.first.timeline.events.first
|
||||
.unsigned[MessageSendingStatusKey] = -1;
|
||||
await _handleFakeSync(syncUpdate);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
syncUpdate.rooms.join.values.first.timeline.events.first
|
||||
.unsigned[MessageSendingStatusKey] = 1;
|
||||
syncUpdate.rooms.join.values.first.timeline.events.first.eventId = res;
|
||||
await _handleFakeSync(syncUpdate);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/// Call the Matrix API to join this room if the user is not already a member.
|
||||
|
@ -706,7 +791,7 @@ class Room {
|
|||
/// automatically be set.
|
||||
Future<void> join() async {
|
||||
try {
|
||||
await client.api.joinRoom(id);
|
||||
await client.joinRoom(id);
|
||||
final invitation = getState(EventTypes.RoomMember, client.userID);
|
||||
if (invitation != null &&
|
||||
invitation.content['is_direct'] is bool &&
|
||||
|
@ -732,25 +817,25 @@ class Room {
|
|||
/// chat, this will be removed too.
|
||||
Future<void> leave() async {
|
||||
if (directChatMatrixID != '') await removeFromDirectChat();
|
||||
await client.api.leaveRoom(id);
|
||||
await client.leaveRoom(id);
|
||||
return;
|
||||
}
|
||||
|
||||
/// Call the Matrix API to forget this room if you already left it.
|
||||
Future<void> forget() async {
|
||||
await client.database?.forgetRoom(client.id, id);
|
||||
await client.api.forgetRoom(id);
|
||||
await client.forgetRoom(id);
|
||||
return;
|
||||
}
|
||||
|
||||
/// Call the Matrix API to kick a user from this room.
|
||||
Future<void> kick(String userID) => client.api.kickFromRoom(id, userID);
|
||||
Future<void> kick(String userID) => client.kickFromRoom(id, userID);
|
||||
|
||||
/// Call the Matrix API to ban a user from this room.
|
||||
Future<void> ban(String userID) => client.api.banFromRoom(id, userID);
|
||||
Future<void> ban(String userID) => client.banFromRoom(id, userID);
|
||||
|
||||
/// Call the Matrix API to unban a banned user from this room.
|
||||
Future<void> unban(String userID) => client.api.unbanInRoom(id, userID);
|
||||
Future<void> unban(String userID) => client.unbanInRoom(id, userID);
|
||||
|
||||
/// Set the power level of the user with the [userID] to the value [power].
|
||||
/// Returns the event ID of the new state event. If there is no known
|
||||
|
@ -762,7 +847,7 @@ class Room {
|
|||
if (powerMap['users'] == null) powerMap['users'] = {};
|
||||
powerMap['users'][userID] = power;
|
||||
|
||||
return await client.api.sendState(
|
||||
return await client.sendState(
|
||||
id,
|
||||
EventTypes.RoomPowerLevels,
|
||||
powerMap,
|
||||
|
@ -770,14 +855,14 @@ class Room {
|
|||
}
|
||||
|
||||
/// Call the Matrix API to invite a user to this room.
|
||||
Future<void> invite(String userID) => client.api.inviteToRoom(id, userID);
|
||||
Future<void> invite(String userID) => client.inviteToRoom(id, userID);
|
||||
|
||||
/// Request more previous events from the server. [historyCount] defines how much events should
|
||||
/// be received maximum. When the request is answered, [onHistoryReceived] will be triggered **before**
|
||||
/// the historical events will be published in the onEvent stream.
|
||||
Future<void> requestHistory(
|
||||
{int historyCount = DefaultHistoryCount, onHistoryReceived}) async {
|
||||
final resp = await client.api.requestMessages(
|
||||
final resp = await client.requestMessages(
|
||||
id,
|
||||
prev_batch,
|
||||
Direction.b,
|
||||
|
@ -828,7 +913,7 @@ class Room {
|
|||
directChats[userID] = [id];
|
||||
}
|
||||
|
||||
await client.api.setAccountData(
|
||||
await client.setAccountData(
|
||||
client.userID,
|
||||
'm.direct',
|
||||
directChats,
|
||||
|
@ -846,7 +931,7 @@ class Room {
|
|||
return;
|
||||
} // Nothing to do here
|
||||
|
||||
await client.api.setRoomAccountData(
|
||||
await client.setRoomAccountData(
|
||||
client.userID,
|
||||
id,
|
||||
'm.direct',
|
||||
|
@ -859,7 +944,7 @@ class Room {
|
|||
Future<void> sendReadReceipt(String eventID) async {
|
||||
notificationCount = 0;
|
||||
await client.database?.resetNotificationCount(client.id, id);
|
||||
await client.api.sendReadMarker(
|
||||
await client.sendReadMarker(
|
||||
id,
|
||||
eventID,
|
||||
readReceiptLocationEventId: eventID,
|
||||
|
@ -981,6 +1066,8 @@ class Room {
|
|||
return userList;
|
||||
}
|
||||
|
||||
bool _requestedParticipants = false;
|
||||
|
||||
/// Request the full list of participants from the server. The local list
|
||||
/// from the store is not complete if the client uses lazy loading.
|
||||
Future<List<User>> requestParticipants() async {
|
||||
|
@ -991,13 +1078,16 @@ class Room {
|
|||
setState(user);
|
||||
}
|
||||
}
|
||||
if (participantListComplete) return getParticipants();
|
||||
final matrixEvents = await client.api.requestMembers(id);
|
||||
if (_requestedParticipants || participantListComplete) {
|
||||
return getParticipants();
|
||||
}
|
||||
final matrixEvents = await client.requestMembers(id);
|
||||
final users =
|
||||
matrixEvents.map((e) => Event.fromMatrixEvent(e, this).asUser).toList();
|
||||
for (final user in users) {
|
||||
setState(user); // at *least* cache this in-memory
|
||||
}
|
||||
_requestedParticipants = true;
|
||||
users.removeWhere(
|
||||
(u) => [Membership.leave, Membership.ban].contains(u.membership));
|
||||
return users;
|
||||
|
@ -1055,7 +1145,7 @@ class Room {
|
|||
if (mxID == null || !_requestingMatrixIds.add(mxID)) return null;
|
||||
Map<String, dynamic> resp;
|
||||
try {
|
||||
resp = await client.api.requestStateContent(
|
||||
resp = await client.requestStateContent(
|
||||
id,
|
||||
EventTypes.RoomMember,
|
||||
mxID,
|
||||
|
@ -1068,10 +1158,10 @@ class Room {
|
|||
}
|
||||
if (resp == null && requestProfile) {
|
||||
try {
|
||||
final profile = await client.api.requestProfile(mxID);
|
||||
final profile = await client.requestProfile(mxID);
|
||||
resp = {
|
||||
'displayname': profile.displayname,
|
||||
'avatar_url': profile.avatarUrl,
|
||||
'avatar_url': profile.avatarUrl.toString(),
|
||||
};
|
||||
} catch (exception) {
|
||||
_requestingMatrixIds.remove(mxID);
|
||||
|
@ -1110,7 +1200,7 @@ class Room {
|
|||
|
||||
/// Searches for the event on the server. Returns null if not found.
|
||||
Future<Event> getEventById(String eventID) async {
|
||||
final matrixEvent = await client.api.requestEvent(id, eventID);
|
||||
final matrixEvent = await client.requestEvent(id, eventID);
|
||||
return Event.fromMatrixEvent(matrixEvent, this);
|
||||
}
|
||||
|
||||
|
@ -1144,8 +1234,8 @@ class Room {
|
|||
/// Uploads a new user avatar for this room. Returns the event ID of the new
|
||||
/// m.room.avatar event.
|
||||
Future<String> setAvatar(MatrixFile file) async {
|
||||
final uploadResp = await client.api.upload(file.bytes, file.name);
|
||||
return await client.api.sendState(
|
||||
final uploadResp = await client.upload(file.bytes, file.name);
|
||||
return await client.sendState(
|
||||
id,
|
||||
EventTypes.RoomAvatar,
|
||||
{'url': uploadResp},
|
||||
|
@ -1242,23 +1332,23 @@ class Room {
|
|||
// All push notifications should be sent to the user
|
||||
case PushRuleState.notify:
|
||||
if (pushRuleState == PushRuleState.dont_notify) {
|
||||
await client.api.deletePushRule('global', PushRuleKind.override, id);
|
||||
await client.deletePushRule('global', PushRuleKind.override, id);
|
||||
} else if (pushRuleState == PushRuleState.mentions_only) {
|
||||
await client.api.deletePushRule('global', PushRuleKind.room, id);
|
||||
await client.deletePushRule('global', PushRuleKind.room, id);
|
||||
}
|
||||
break;
|
||||
// Only when someone mentions the user, a push notification should be sent
|
||||
case PushRuleState.mentions_only:
|
||||
if (pushRuleState == PushRuleState.dont_notify) {
|
||||
await client.api.deletePushRule('global', PushRuleKind.override, id);
|
||||
await client.api.setPushRule(
|
||||
await client.deletePushRule('global', PushRuleKind.override, id);
|
||||
await client.setPushRule(
|
||||
'global',
|
||||
PushRuleKind.room,
|
||||
id,
|
||||
[PushRuleAction.dont_notify],
|
||||
);
|
||||
} else if (pushRuleState == PushRuleState.notify) {
|
||||
await client.api.setPushRule(
|
||||
await client.setPushRule(
|
||||
'global',
|
||||
PushRuleKind.room,
|
||||
id,
|
||||
|
@ -1269,9 +1359,9 @@ class Room {
|
|||
// No push notification should be ever sent for this room.
|
||||
case PushRuleState.dont_notify:
|
||||
if (pushRuleState == PushRuleState.mentions_only) {
|
||||
await client.api.deletePushRule('global', PushRuleKind.room, id);
|
||||
await client.deletePushRule('global', PushRuleKind.room, id);
|
||||
}
|
||||
await client.api.setPushRule(
|
||||
await client.setPushRule(
|
||||
'global',
|
||||
PushRuleKind.override,
|
||||
id,
|
||||
|
@ -1297,7 +1387,7 @@ class Room {
|
|||
}
|
||||
var data = <String, dynamic>{};
|
||||
if (reason != null) data['reason'] = reason;
|
||||
return await client.api.redact(
|
||||
return await client.redact(
|
||||
id,
|
||||
eventId,
|
||||
messageID,
|
||||
|
@ -1310,7 +1400,7 @@ class Room {
|
|||
'typing': isTyping,
|
||||
};
|
||||
if (timeout != null) data['timeout'] = timeout;
|
||||
return client.api.sendTypingNotification(client.userID, id, isTyping);
|
||||
return client.sendTypingNotification(client.userID, id, isTyping);
|
||||
}
|
||||
|
||||
/// This is sent by the caller when they wish to establish a call.
|
||||
|
@ -1324,16 +1414,16 @@ class Room {
|
|||
{String type = 'offer', int version = 0, String txid}) async {
|
||||
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
return await client.api.sendMessage(
|
||||
id,
|
||||
final content = {
|
||||
'call_id': callId,
|
||||
'lifetime': lifetime,
|
||||
'offer': {'sdp': sdp, 'type': type},
|
||||
'version': version,
|
||||
};
|
||||
return await _sendContent(
|
||||
EventTypes.CallInvite,
|
||||
txid,
|
||||
{
|
||||
'call_id': callId,
|
||||
'lifetime': lifetime,
|
||||
'offer': {'sdp': sdp, 'type': type},
|
||||
'version': version,
|
||||
},
|
||||
content,
|
||||
txid: txid,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1362,15 +1452,15 @@ class Room {
|
|||
String txid,
|
||||
}) async {
|
||||
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
|
||||
return await client.api.sendMessage(
|
||||
id,
|
||||
final content = {
|
||||
'call_id': callId,
|
||||
'candidates': candidates,
|
||||
'version': version,
|
||||
};
|
||||
return await _sendContent(
|
||||
EventTypes.CallCandidates,
|
||||
txid,
|
||||
{
|
||||
'call_id': callId,
|
||||
'candidates': candidates,
|
||||
'version': version,
|
||||
},
|
||||
content,
|
||||
txid: txid,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1382,15 +1472,15 @@ class Room {
|
|||
Future<String> answerCall(String callId, String sdp,
|
||||
{String type = 'answer', int version = 0, String txid}) async {
|
||||
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
|
||||
return await client.api.sendMessage(
|
||||
id,
|
||||
final content = {
|
||||
'call_id': callId,
|
||||
'answer': {'sdp': sdp, 'type': type},
|
||||
'version': version,
|
||||
};
|
||||
return await _sendContent(
|
||||
EventTypes.CallAnswer,
|
||||
txid,
|
||||
{
|
||||
'call_id': callId,
|
||||
'answer': {'sdp': sdp, 'type': type},
|
||||
'version': version,
|
||||
},
|
||||
content,
|
||||
txid: txid,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1400,14 +1490,15 @@ class Room {
|
|||
Future<String> hangupCall(String callId,
|
||||
{int version = 0, String txid}) async {
|
||||
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
|
||||
return await client.api.sendMessage(
|
||||
id,
|
||||
|
||||
final content = {
|
||||
'call_id': callId,
|
||||
'version': version,
|
||||
};
|
||||
return await _sendContent(
|
||||
EventTypes.CallHangup,
|
||||
txid,
|
||||
{
|
||||
'call_id': callId,
|
||||
'version': version,
|
||||
},
|
||||
content,
|
||||
txid: txid,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1436,7 +1527,7 @@ class Room {
|
|||
|
||||
/// Changes the join rules. You should check first if the user is able to change it.
|
||||
Future<void> setJoinRules(JoinRules joinRules) async {
|
||||
await client.api.sendState(
|
||||
await client.sendState(
|
||||
id,
|
||||
EventTypes.RoomJoinRules,
|
||||
{
|
||||
|
@ -1461,7 +1552,7 @@ class Room {
|
|||
|
||||
/// Changes the guest access. You should check first if the user is able to change it.
|
||||
Future<void> setGuestAccess(GuestAccess guestAccess) async {
|
||||
await client.api.sendState(
|
||||
await client.sendState(
|
||||
id,
|
||||
EventTypes.GuestAccess,
|
||||
{
|
||||
|
@ -1487,7 +1578,7 @@ class Room {
|
|||
|
||||
/// Changes the history visibility. You should check first if the user is able to change it.
|
||||
Future<void> setHistoryVisibility(HistoryVisibility historyVisibility) async {
|
||||
await client.api.sendState(
|
||||
await client.sendState(
|
||||
id,
|
||||
EventTypes.HistoryVisibility,
|
||||
{
|
||||
|
@ -1514,7 +1605,7 @@ class Room {
|
|||
Future<void> enableEncryption({int algorithmIndex = 0}) async {
|
||||
if (encrypted) throw ('Encryption is already enabled!');
|
||||
final algorithm = Client.supportedGroupEncryptionAlgorithms[algorithmIndex];
|
||||
await client.api.sendState(
|
||||
await client.sendState(
|
||||
id,
|
||||
EventTypes.Encryption,
|
||||
{
|
||||
|
@ -1545,4 +1636,15 @@ class Room {
|
|||
}
|
||||
await client.encryption.keyManager.request(this, sessionId, senderKey);
|
||||
}
|
||||
|
||||
Future<void> _handleFakeSync(SyncUpdate syncUpdate,
|
||||
{bool sortAtTheEnd = false}) async {
|
||||
if (client.database != null) {
|
||||
await client.database.transaction(() async {
|
||||
await client.handleSync(syncUpdate, sortAtTheEnd: sortAtTheEnd);
|
||||
});
|
||||
} else {
|
||||
await client.handleSync(syncUpdate, sortAtTheEnd: sortAtTheEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,11 +18,11 @@
|
|||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
|
||||
import '../matrix_api.dart';
|
||||
import 'event.dart';
|
||||
import 'room.dart';
|
||||
import 'utils/event_update.dart';
|
||||
import 'utils/logs.dart';
|
||||
import 'utils/room_update.dart';
|
||||
|
||||
typedef onTimelineUpdateCallback = void Function();
|
||||
|
@ -35,6 +35,9 @@ class Timeline {
|
|||
final Room room;
|
||||
List<Event> events = [];
|
||||
|
||||
/// Map of event ID to map of type to set of aggregated events
|
||||
Map<String, Map<String, Set<Event>>> aggregatedEvents = {};
|
||||
|
||||
final onTimelineUpdateCallback onUpdate;
|
||||
final onTimelineInsertCallback onInsert;
|
||||
|
||||
|
@ -66,7 +69,10 @@ class Timeline {
|
|||
await room.requestHistory(
|
||||
historyCount: historyCount,
|
||||
onHistoryReceived: () {
|
||||
if (room.prev_batch.isEmpty || room.prev_batch == null) events = [];
|
||||
if (room.prev_batch.isEmpty || room.prev_batch == null) {
|
||||
events.clear();
|
||||
aggregatedEvents.clear();
|
||||
}
|
||||
},
|
||||
);
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
@ -82,9 +88,18 @@ class Timeline {
|
|||
// to be received via the onEvent stream, it is unneeded to call sortAndUpdate
|
||||
roomSub ??= room.client.onRoomUpdate.stream
|
||||
.where((r) => r.id == room.id && r.limitedTimeline == true)
|
||||
.listen((r) => events.clear());
|
||||
.listen((r) {
|
||||
events.clear();
|
||||
aggregatedEvents.clear();
|
||||
});
|
||||
sessionIdReceivedSub ??=
|
||||
room.onSessionKeyReceived.stream.listen(_sessionKeyReceived);
|
||||
|
||||
// we want to populate our aggregated events
|
||||
for (final e in events) {
|
||||
addAggregatedEvent(e);
|
||||
}
|
||||
_sort();
|
||||
}
|
||||
|
||||
/// Don't forget to call this before you dismiss this object!
|
||||
|
@ -122,33 +137,101 @@ class Timeline {
|
|||
}
|
||||
|
||||
int _findEvent({String event_id, String unsigned_txid}) {
|
||||
// we want to find any existing event where either the passed event_id or the passed unsigned_txid
|
||||
// matches either the event_id or transaction_id of the existing event.
|
||||
// For that we create two sets, searchNeedle, what we search, and searchHaystack, where we check if there is a match.
|
||||
// Now, after having these two sets, if the intersect between them is non-empty, we know that we have at least one match in one pair,
|
||||
// thus meaning we found our element.
|
||||
final searchNeedle = <String>{};
|
||||
if (event_id != null) {
|
||||
searchNeedle.add(event_id);
|
||||
}
|
||||
if (unsigned_txid != null) {
|
||||
searchNeedle.add(unsigned_txid);
|
||||
}
|
||||
int i;
|
||||
for (i = 0; i < events.length; i++) {
|
||||
if (events[i].eventId == event_id ||
|
||||
(unsigned_txid != null && events[i].eventId == unsigned_txid)) break;
|
||||
final searchHaystack = <String>{};
|
||||
if (events[i].eventId != null) {
|
||||
searchHaystack.add(events[i].eventId);
|
||||
}
|
||||
if (events[i].unsigned != null &&
|
||||
events[i].unsigned['transaction_id'] != null) {
|
||||
searchHaystack.add(events[i].unsigned['transaction_id']);
|
||||
}
|
||||
if (searchNeedle.intersection(searchHaystack).isNotEmpty) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
void _removeEventFromSet(Set<Event> eventSet, Event event) {
|
||||
eventSet.removeWhere((e) =>
|
||||
e.matchesEventOrTransactionId(event.eventId) ||
|
||||
(event.unsigned != null &&
|
||||
e.matchesEventOrTransactionId(event.unsigned['transaction_id'])));
|
||||
}
|
||||
|
||||
void addAggregatedEvent(Event event) {
|
||||
// we want to add an event to the aggregation tree
|
||||
if (event.relationshipType == null || event.relationshipEventId == null) {
|
||||
return; // nothing to do
|
||||
}
|
||||
if (!aggregatedEvents.containsKey(event.relationshipEventId)) {
|
||||
aggregatedEvents[event.relationshipEventId] = <String, Set<Event>>{};
|
||||
}
|
||||
if (!aggregatedEvents[event.relationshipEventId]
|
||||
.containsKey(event.relationshipType)) {
|
||||
aggregatedEvents[event.relationshipEventId]
|
||||
[event.relationshipType] = <Event>{};
|
||||
}
|
||||
// remove a potential old event
|
||||
_removeEventFromSet(
|
||||
aggregatedEvents[event.relationshipEventId][event.relationshipType],
|
||||
event);
|
||||
// add the new one
|
||||
aggregatedEvents[event.relationshipEventId][event.relationshipType]
|
||||
.add(event);
|
||||
}
|
||||
|
||||
void removeAggregatedEvent(Event event) {
|
||||
aggregatedEvents.remove(event.eventId);
|
||||
if (event.unsigned != null) {
|
||||
aggregatedEvents.remove(event.unsigned['transaction_id']);
|
||||
}
|
||||
for (final types in aggregatedEvents.values) {
|
||||
for (final events in types.values) {
|
||||
_removeEventFromSet(events, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleEventUpdate(EventUpdate eventUpdate) async {
|
||||
try {
|
||||
if (eventUpdate.roomID != room.id) return;
|
||||
|
||||
if (eventUpdate.type == 'timeline' || eventUpdate.type == 'history') {
|
||||
var status = eventUpdate.content['status'] ??
|
||||
(eventUpdate.content['unsigned'] is Map<String, dynamic>
|
||||
? eventUpdate.content['unsigned'][MessageSendingStatusKey]
|
||||
: null) ??
|
||||
2;
|
||||
// Redaction events are handled as modification for existing events.
|
||||
if (eventUpdate.eventType == EventTypes.Redaction) {
|
||||
final eventId = _findEvent(event_id: eventUpdate.content['redacts']);
|
||||
if (eventId != null) {
|
||||
if (eventId < events.length) {
|
||||
removeAggregatedEvent(events[eventId]);
|
||||
events[eventId].setRedactionEvent(Event.fromJson(
|
||||
eventUpdate.content, room, eventUpdate.sortOrder));
|
||||
}
|
||||
} else if (eventUpdate.content['status'] == -2) {
|
||||
} else if (status == -2) {
|
||||
var i = _findEvent(event_id: eventUpdate.content['event_id']);
|
||||
if (i < events.length) events.removeAt(i);
|
||||
}
|
||||
// Is this event already in the timeline?
|
||||
else if (eventUpdate.content['unsigned'] is Map &&
|
||||
eventUpdate.content['unsigned']['transaction_id'] is String) {
|
||||
if (i < events.length) {
|
||||
removeAggregatedEvent(events[i]);
|
||||
events.removeAt(i);
|
||||
}
|
||||
} else {
|
||||
var i = _findEvent(
|
||||
event_id: eventUpdate.content['event_id'],
|
||||
unsigned_txid: eventUpdate.content['unsigned'] is Map
|
||||
|
@ -156,55 +239,51 @@ class Timeline {
|
|||
: null);
|
||||
|
||||
if (i < events.length) {
|
||||
final tempSortOrder = events[i].sortOrder;
|
||||
// if the old status is larger than the new one, we also want to preserve the old status
|
||||
final oldStatus = events[i].status;
|
||||
events[i] = Event.fromJson(
|
||||
eventUpdate.content, room, eventUpdate.sortOrder);
|
||||
events[i].sortOrder = tempSortOrder;
|
||||
// do we preserve the status? we should allow 0 -> -1 updates and status increases
|
||||
if (status < oldStatus && !(status == -1 && oldStatus == 0)) {
|
||||
events[i].status = oldStatus;
|
||||
}
|
||||
addAggregatedEvent(events[i]);
|
||||
} else {
|
||||
var newEvent = Event.fromJson(
|
||||
eventUpdate.content, room, eventUpdate.sortOrder);
|
||||
|
||||
if (eventUpdate.type == 'history' &&
|
||||
events.indexWhere(
|
||||
(e) => e.eventId == eventUpdate.content['event_id']) !=
|
||||
-1) return;
|
||||
|
||||
events.insert(0, newEvent);
|
||||
addAggregatedEvent(newEvent);
|
||||
if (onInsert != null) onInsert(0);
|
||||
}
|
||||
} else {
|
||||
Event newEvent;
|
||||
var senderUser = room
|
||||
.getState(
|
||||
EventTypes.RoomMember, eventUpdate.content['sender'])
|
||||
?.asUser ??
|
||||
await room.client.database?.getUser(
|
||||
room.client.id, eventUpdate.content['sender'], room);
|
||||
if (senderUser != null) {
|
||||
eventUpdate.content['displayname'] = senderUser.displayName;
|
||||
eventUpdate.content['avatar_url'] = senderUser.avatarUrl.toString();
|
||||
}
|
||||
|
||||
newEvent =
|
||||
Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder);
|
||||
|
||||
if (eventUpdate.type == 'history' &&
|
||||
events.indexWhere(
|
||||
(e) => e.eventId == eventUpdate.content['event_id']) !=
|
||||
-1) return;
|
||||
|
||||
events.insert(0, newEvent);
|
||||
if (onInsert != null) onInsert(0);
|
||||
}
|
||||
}
|
||||
sortAndUpdate();
|
||||
} catch (e) {
|
||||
if (room.client.debug) {
|
||||
print('[WARNING] (_handleEventUpdate) ${e.toString()}');
|
||||
}
|
||||
_sort();
|
||||
if (onUpdate != null) onUpdate();
|
||||
} catch (e, s) {
|
||||
Logs.warning('Handle event update failed: ${e.toString()}', s);
|
||||
}
|
||||
}
|
||||
|
||||
bool sortLock = false;
|
||||
bool _sortLock = false;
|
||||
|
||||
void sort() {
|
||||
if (sortLock || events.length < 2) return;
|
||||
sortLock = true;
|
||||
events?.sort((a, b) => b.sortOrder - a.sortOrder > 0 ? 1 : -1);
|
||||
sortLock = false;
|
||||
}
|
||||
|
||||
void sortAndUpdate() async {
|
||||
sort();
|
||||
if (onUpdate != null) onUpdate();
|
||||
void _sort() {
|
||||
if (_sortLock || events.length < 2) return;
|
||||
_sortLock = true;
|
||||
events?.sort((a, b) {
|
||||
if (b.status == -1 && a.status != -1) {
|
||||
return 1;
|
||||
}
|
||||
if (a.status == -1 && b.status != -1) {
|
||||
return -1;
|
||||
}
|
||||
return b.sortOrder - a.sortOrder > 0 ? 1 : -1;
|
||||
});
|
||||
_sortLock = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,10 +16,10 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:famedlysdk/src/room.dart';
|
||||
import 'package:famedlysdk/src/event.dart';
|
||||
import '../famedlysdk.dart';
|
||||
import '../matrix_api.dart';
|
||||
import 'event.dart';
|
||||
import 'room.dart';
|
||||
|
||||
/// Represents a Matrix User which may be a participant in a Matrix Room.
|
||||
class User extends Event {
|
||||
|
@ -146,7 +146,7 @@ class User extends Event {
|
|||
if (roomID != null) return roomID;
|
||||
|
||||
// Start a new direct chat
|
||||
final newRoomID = await room.client.api.createRoom(
|
||||
final newRoomID = await room.client.createRoom(
|
||||
invite: [id],
|
||||
isDirect: true,
|
||||
preset: CreateRoomPreset.trusted_private_chat,
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:canonical_json/canonical_json.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
|
||||
import '../../encryption.dart';
|
||||
import '../../matrix_api.dart';
|
||||
import '../client.dart';
|
||||
import '../user.dart';
|
||||
import '../room.dart';
|
||||
import '../database/database.dart'
|
||||
show DbUserDeviceKey, DbUserDeviceKeysKey, DbUserCrossSigningKey;
|
||||
import '../event.dart';
|
||||
import '../room.dart';
|
||||
import '../user.dart';
|
||||
|
||||
enum UserVerifiedStatus { verified, unknown, unknownDevice }
|
||||
|
||||
|
@ -157,14 +157,20 @@ abstract class SignableKey extends MatrixSignableKey {
|
|||
return valid;
|
||||
}
|
||||
|
||||
bool hasValidSignatureChain({bool verifiedOnly = true, Set<String> visited}) {
|
||||
bool hasValidSignatureChain(
|
||||
{bool verifiedOnly = true,
|
||||
Set<String> visited,
|
||||
Set<String> onlyValidateUserIds}) {
|
||||
if (!client.encryptionEnabled) {
|
||||
return false;
|
||||
}
|
||||
visited ??= <String>{};
|
||||
onlyValidateUserIds ??= <String>{};
|
||||
final setKey = '${userId};${identifier}';
|
||||
if (visited.contains(setKey)) {
|
||||
return false; // prevent recursion
|
||||
if (visited.contains(setKey) ||
|
||||
(onlyValidateUserIds.isNotEmpty &&
|
||||
!onlyValidateUserIds.contains(userId))) {
|
||||
return false; // prevent recursion & validate hasValidSignatureChain
|
||||
}
|
||||
visited.add(setKey);
|
||||
for (final signatureEntries in signatures.entries) {
|
||||
|
@ -189,6 +195,13 @@ abstract class SignableKey extends MatrixSignableKey {
|
|||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (onlyValidateUserIds.isNotEmpty &&
|
||||
!onlyValidateUserIds.contains(key.userId)) {
|
||||
// we don't want to verify keys from this user
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key.blocked) {
|
||||
continue; // we can't be bothered about this keys signatures
|
||||
}
|
||||
|
@ -228,7 +241,9 @@ abstract class SignableKey extends MatrixSignableKey {
|
|||
}
|
||||
// or else we just recurse into that key and chack if it works out
|
||||
final haveChain = key.hasValidSignatureChain(
|
||||
verifiedOnly: verifiedOnly, visited: visited);
|
||||
verifiedOnly: verifiedOnly,
|
||||
visited: visited,
|
||||
onlyValidateUserIds: onlyValidateUserIds);
|
||||
if (haveChain) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
import '../../famedlysdk.dart';
|
||||
import '../../matrix_api.dart';
|
||||
import 'logs.dart';
|
||||
|
||||
/// Represents a new event (e.g. a message in a room) or an update for an
|
||||
/// already known event.
|
||||
|
@ -57,8 +58,8 @@ class EventUpdate {
|
|||
content: decrpytedEvent.toJson(),
|
||||
sortOrder: sortOrder,
|
||||
);
|
||||
} catch (e) {
|
||||
print('[LibOlm] Could not decrypt megolm event: ' + e.toString());
|
||||
} catch (e, s) {
|
||||
Logs.error('[LibOlm] Could not decrypt megolm event: ' + e.toString(), s);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
52
lib/src/utils/logs.dart
Normal file
52
lib/src/utils/logs.dart
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 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 'package:ansicolor/ansicolor.dart';
|
||||
|
||||
abstract class Logs {
|
||||
static final AnsiPen _infoPen = AnsiPen()..blue();
|
||||
static final AnsiPen _warningPen = AnsiPen()..yellow();
|
||||
static final AnsiPen _successPen = AnsiPen()..green();
|
||||
static final AnsiPen _errorPen = AnsiPen()..red();
|
||||
|
||||
static const String _prefixText = '[Famedly Matrix SDK] ';
|
||||
|
||||
// ignore: avoid_print
|
||||
static void info(dynamic info) => print(
|
||||
_prefixText + _infoPen(info.toString()),
|
||||
);
|
||||
|
||||
// ignore: avoid_print
|
||||
static void success(dynamic obj, [dynamic stackTrace]) => print(
|
||||
_prefixText + _successPen(obj.toString()),
|
||||
);
|
||||
|
||||
// ignore: avoid_print
|
||||
static void warning(dynamic warning, [dynamic stackTrace]) => print(
|
||||
_prefixText +
|
||||
_warningPen(warning.toString()) +
|
||||
(stackTrace != null ? '\n${stackTrace.toString()}' : ''),
|
||||
);
|
||||
|
||||
// ignore: avoid_print
|
||||
static void error(dynamic obj, [dynamic stackTrace]) => print(
|
||||
_prefixText +
|
||||
_errorPen(obj.toString()) +
|
||||
(stackTrace != null ? '\n${stackTrace.toString()}' : ''),
|
||||
);
|
||||
}
|
|
@ -65,6 +65,7 @@ class EmoteSyntax extends InlineSyntax {
|
|||
return true;
|
||||
}
|
||||
final element = Element.empty('img');
|
||||
element.attributes['data-mx-emoticon'] = '';
|
||||
element.attributes['src'] = htmlEscape.convert(mxc);
|
||||
element.attributes['alt'] = htmlEscape.convert(emote);
|
||||
element.attributes['title'] = htmlEscape.convert(emote);
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
/// Workaround until [File] in dart:io and dart:html is unified
|
||||
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
import '../../matrix_api/model/message_types.dart';
|
||||
|
||||
class MatrixFile {
|
||||
Uint8List bytes;
|
||||
String name;
|
||||
|
@ -16,13 +19,25 @@ class MatrixFile {
|
|||
}
|
||||
|
||||
MatrixFile({this.bytes, this.name, this.mimeType}) {
|
||||
mimeType ??= lookupMimeType(name, headerBytes: bytes);
|
||||
mimeType ??=
|
||||
lookupMimeType(name, headerBytes: bytes) ?? 'application/octet-stream';
|
||||
name = name.split('/').last.toLowerCase();
|
||||
}
|
||||
|
||||
int get size => bytes.length;
|
||||
|
||||
String get msgType => 'm.file';
|
||||
String get msgType {
|
||||
if (mimeType.toLowerCase().startsWith('image/')) {
|
||||
return MessageTypes.Image;
|
||||
}
|
||||
if (mimeType.toLowerCase().startsWith('video/')) {
|
||||
return MessageTypes.Video;
|
||||
}
|
||||
if (mimeType.toLowerCase().startsWith('audio/')) {
|
||||
return MessageTypes.Audio;
|
||||
}
|
||||
return MessageTypes.File;
|
||||
}
|
||||
|
||||
Map<String, dynamic> get info => ({
|
||||
'mimetype': mimeType,
|
||||
|
|
|
@ -3,14 +3,29 @@ extension MatrixIdExtension on String {
|
|||
|
||||
static const int MAX_LENGTH = 255;
|
||||
|
||||
List<String> _getParts() {
|
||||
final s = substring(1);
|
||||
final ix = s.indexOf(':');
|
||||
if (ix == -1) {
|
||||
return [substring(1)];
|
||||
}
|
||||
return [s.substring(0, ix), s.substring(ix + 1)];
|
||||
}
|
||||
|
||||
bool get isValidMatrixId {
|
||||
if (isEmpty ?? true) return false;
|
||||
if (length > MAX_LENGTH) return false;
|
||||
if (!VALID_SIGILS.contains(substring(0, 1))) {
|
||||
return false;
|
||||
}
|
||||
final parts = substring(1).split(':');
|
||||
if (parts.length != 2 || parts[0].isEmpty || parts[1].isEmpty) {
|
||||
// event IDs do not have to have a domain
|
||||
if (substring(0, 1) == '\$') {
|
||||
return true;
|
||||
}
|
||||
// all other matrix IDs have to have a domain
|
||||
final parts = _getParts();
|
||||
// the localpart can be an empty string, e.g. for aliases
|
||||
if (parts.length != 2 || parts[1].isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
@ -18,10 +33,9 @@ extension MatrixIdExtension on String {
|
|||
|
||||
String get sigil => isValidMatrixId ? substring(0, 1) : null;
|
||||
|
||||
String get localpart =>
|
||||
isValidMatrixId ? substring(1).split(':').first : null;
|
||||
String get localpart => isValidMatrixId ? _getParts().first : null;
|
||||
|
||||
String get domain => isValidMatrixId ? substring(1).split(':')[1] : null;
|
||||
String get domain => isValidMatrixId ? _getParts().last : null;
|
||||
|
||||
bool equals(String other) => toLowerCase() == other?.toLowerCase();
|
||||
}
|
||||
|
|
|
@ -109,6 +109,14 @@ abstract class MatrixLocalizations {
|
|||
String couldNotDecryptMessage(String errorText);
|
||||
|
||||
String unknownEvent(String typeKey);
|
||||
|
||||
String startedACall(String senderName);
|
||||
|
||||
String endedTheCall(String senderName);
|
||||
|
||||
String answeredTheCall(String senderName);
|
||||
|
||||
String sentCallInformations(String senderName);
|
||||
}
|
||||
|
||||
extension HistoryVisibilityDisplayString on HistoryVisibility {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import '../../matrix_api.dart';
|
||||
|
||||
/// Represents a new room or an update for an
|
||||
/// already known room.
|
||||
|
|
30
lib/src/utils/run_in_background.dart
Normal file
30
lib/src/utils/run_in_background.dart
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 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 'package:isolate/isolate.dart';
|
||||
import 'dart:async';
|
||||
|
||||
Future<T> runInBackground<T, U>(
|
||||
FutureOr<T> Function(U arg) function, U arg) async {
|
||||
final isolate = await IsolateRunner.spawn();
|
||||
try {
|
||||
return await isolate.run(function, arg);
|
||||
} finally {
|
||||
await isolate.close();
|
||||
}
|
||||
}
|
32
lib/src/utils/run_in_root.dart
Normal file
32
lib/src/utils/run_in_root.dart
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 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:async';
|
||||
|
||||
import 'logs.dart';
|
||||
|
||||
Future<T> runInRoot<T>(FutureOr<T> Function() fn) async {
|
||||
return await Zone.root.run(() async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e, s) {
|
||||
Logs.error('Error thrown in root zone: ' + e.toString(), s);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
|
||||
import '../../famedlysdk.dart';
|
||||
import '../../matrix_api.dart';
|
||||
|
||||
/// Matrix room states are addressed by a tuple of the [type] and an
|
||||
|
|
44
lib/src/utils/sync_update_extension.dart
Normal file
44
lib/src/utils/sync_update_extension.dart
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 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 '../../matrix_api.dart';
|
||||
|
||||
/// This extension adds easy-to-use filters for the sync update, meant to be used on the `client.onSync` stream, e.g.
|
||||
/// `client.onSync.stream.where((s) => s.hasRoomUpdate)`. Multiple filters can easily be
|
||||
/// combind with boolean logic: `client.onSync.stream.where((s) => s.hasRoomUpdate || s.hasPresenceUpdate)`
|
||||
extension SyncUpdateFilters on SyncUpdate {
|
||||
/// Returns true if this sync updat has a room update
|
||||
/// That means there is account data, if there is a room in one of the `join`, `leave` or `invite` blocks of the sync or if there is a to_device event.
|
||||
bool get hasRoomUpdate {
|
||||
// if we have an account data change we need to re-render, as `m.direct` might have changed
|
||||
if (accountData?.isNotEmpty ?? false) {
|
||||
return true;
|
||||
}
|
||||
// check for a to_device event
|
||||
if (toDevice?.isNotEmpty ?? false) {
|
||||
return true;
|
||||
}
|
||||
// return if there are rooms to update
|
||||
return (rooms?.join?.isNotEmpty ?? false) ||
|
||||
(rooms?.invite?.isNotEmpty ?? false) ||
|
||||
(rooms?.leave?.isNotEmpty ?? false);
|
||||
}
|
||||
|
||||
/// Returns if this sync update has presence updates
|
||||
bool get hasPresenceUpdate => presence != null && presence.isNotEmpty;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import '../../matrix_api.dart';
|
||||
|
||||
class ToDeviceEvent extends BasicEventWithSender {
|
||||
Map<String, dynamic> encryptedContent;
|
||||
|
|
|
@ -16,14 +16,15 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:famedlysdk/src/client.dart';
|
||||
import 'dart:core';
|
||||
|
||||
import '../client.dart';
|
||||
|
||||
extension MxcUriExtension on Uri {
|
||||
/// Returns a download Link to this content.
|
||||
String getDownloadLink(Client matrix) => isScheme('mxc')
|
||||
? matrix.api.homeserver != null
|
||||
? '${matrix.api.homeserver.toString()}/_matrix/media/r0/download/$host$path'
|
||||
? matrix.homeserver != null
|
||||
? '${matrix.homeserver.toString()}/_matrix/media/r0/download/$host$path'
|
||||
: ''
|
||||
: toString();
|
||||
|
||||
|
@ -36,8 +37,8 @@ extension MxcUriExtension on Uri {
|
|||
final methodStr = method.toString().split('.').last;
|
||||
width = width.round();
|
||||
height = height.round();
|
||||
return matrix.api.homeserver != null
|
||||
? '${matrix.api.homeserver.toString()}/_matrix/media/r0/thumbnail/$host$path?width=$width&height=$height&method=$methodStr'
|
||||
return matrix.homeserver != null
|
||||
? '${matrix.homeserver.toString()}/_matrix/media/r0/thumbnail/$host$path?width=$width&height=$height&method=$methodStr'
|
||||
: '';
|
||||
}
|
||||
}
|
||||
|
|
1
olm
Submodule
1
olm
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit efd17631b16d1271a029e0af8f7d8e5ae795cc5d
|
663
pubspec.lock
663
pubspec.lock
|
@ -1,663 +0,0 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.39.8"
|
||||
analyzer_plugin_fork:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer_plugin_fork
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
asn1lib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: asn1lib
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.4"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
base58check:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: base58check
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.9"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.2.0"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.3.2"
|
||||
built_value:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "7.1.0"
|
||||
canonical_json:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: canonical_json
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_util
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.4"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.14.12"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
coverage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.13.9"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.16.1"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.6"
|
||||
encrypt:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: encrypt
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.10.11"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.14.0+3"
|
||||
html_unescape:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: html_unescape
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1+3"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.1"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.4"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.1+1"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
lcov:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lcov
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.7.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.11.4"
|
||||
markdown:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: markdown
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.6"
|
||||
matrix_file_e2ee:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: matrix_file_e2ee
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.8"
|
||||
mime:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: mime
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.6+3"
|
||||
moor:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: moor
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
moor_ffi:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: moor_ffi
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
moor_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: moor_generator
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
multi_server_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: multi_server_socket
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
node_interop:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_interop
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
node_io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_io
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_preamble
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.8"
|
||||
olm:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: olm
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.3"
|
||||
password_hash:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: password_hash
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
pedantic:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: pedantic
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointycastle
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.4"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.5"
|
||||
quiver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quiver
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
random_string:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: random_string
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
recase:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: recase
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.7.5"
|
||||
shelf_packages_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_packages_handler
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
shelf_static:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_static
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.8"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.3"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.5"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_map_stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
source_maps:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.10.9"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
sqlparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlparser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.8.1"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.3"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_transform
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
test:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.14.3"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.15"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.4"
|
||||
test_coverage:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test_coverage
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.1"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timing
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.1+2"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.6"
|
||||
unorm_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: unorm_dart
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.4"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.7+15"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webkit_inspection_protocol
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.4"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
sdks:
|
||||
dart: ">=2.7.0 <3.0.0"
|
|
@ -21,10 +21,12 @@ dependencies:
|
|||
password_hash: ^2.0.0
|
||||
olm: ^1.2.1
|
||||
matrix_file_e2ee: ^1.0.4
|
||||
ansicolor: ^1.0.2
|
||||
isolate: ^2.0.3
|
||||
|
||||
dev_dependencies:
|
||||
test: ^1.0.0
|
||||
test_coverage: ^0.4.1
|
||||
test_coverage: ^0.4.3
|
||||
moor_generator: ^3.0.0
|
||||
build_runner: ^1.5.2
|
||||
pedantic: ^1.9.0
|
||||
|
|
4
test.sh
4
test.sh
|
@ -1,6 +1,6 @@
|
|||
#!/bin/sh -e
|
||||
pub run test -p vm
|
||||
pub run test_coverage
|
||||
# pub run test -p vm
|
||||
pub run test_coverage --print-test-output
|
||||
pub global activate remove_from_coverage
|
||||
pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '\.g\.dart$'
|
||||
genhtml -o coverage coverage/lcov.info || true
|
||||
|
|
|
@ -23,6 +23,7 @@ import 'package:famedlysdk/famedlysdk.dart';
|
|||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:famedlysdk/src/client.dart';
|
||||
import 'package:famedlysdk/src/utils/event_update.dart';
|
||||
import 'package:famedlysdk/src/utils/logs.dart';
|
||||
import 'package:famedlysdk/src/utils/room_update.dart';
|
||||
import 'package:famedlysdk/src/utils/matrix_file.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
@ -45,10 +46,10 @@ void main() {
|
|||
const fingerprintKey = 'gjL//fyaFHADt9KBADGag8g7F8Up78B/K1zXeiEPLJo';
|
||||
|
||||
/// All Tests related to the Login
|
||||
group('FluffyMatrix', () {
|
||||
group('Client', () {
|
||||
/// Check if all Elements get created
|
||||
|
||||
matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
||||
matrix = Client('testclient', httpClient: FakeMatrixApi());
|
||||
|
||||
roomUpdateListFuture = matrix.onRoomUpdate.stream.toList();
|
||||
eventUpdateListFuture = matrix.onEvent.stream.toList();
|
||||
|
@ -59,9 +60,9 @@ void main() {
|
|||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
Logs.success('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
test('Login', () async {
|
||||
var presenceCounter = 0;
|
||||
|
@ -73,7 +74,7 @@ void main() {
|
|||
accountDataCounter++;
|
||||
});
|
||||
|
||||
expect(matrix.api.homeserver, null);
|
||||
expect(matrix.homeserver, null);
|
||||
|
||||
try {
|
||||
await matrix.checkServer('https://fakeserver.wrongaddress');
|
||||
|
@ -81,17 +82,9 @@ void main() {
|
|||
expect(exception != null, true);
|
||||
}
|
||||
await matrix.checkServer('https://fakeserver.notexisting');
|
||||
expect(
|
||||
matrix.api.homeserver.toString(), 'https://fakeserver.notexisting');
|
||||
expect(matrix.homeserver.toString(), 'https://fakeserver.notexisting');
|
||||
|
||||
final resp = await matrix.api.login(
|
||||
type: 'm.login.password',
|
||||
user: 'test',
|
||||
password: '1234',
|
||||
initialDeviceDisplayName: 'Fluffy Matrix Client',
|
||||
);
|
||||
|
||||
final available = await matrix.api.usernameAvailable('testuser');
|
||||
final available = await matrix.usernameAvailable('testuser');
|
||||
expect(available, true);
|
||||
|
||||
var loginStateFuture = matrix.onLoginStateChanged.stream.first;
|
||||
|
@ -99,21 +92,16 @@ void main() {
|
|||
var syncFuture = matrix.onSync.stream.first;
|
||||
|
||||
matrix.connect(
|
||||
newToken: resp.accessToken,
|
||||
newUserID: resp.userId,
|
||||
newHomeserver: matrix.api.homeserver,
|
||||
newToken: 'abcd',
|
||||
newUserID: '@test:fakeServer.notExisting',
|
||||
newHomeserver: matrix.homeserver,
|
||||
newDeviceName: 'Text Matrix Client',
|
||||
newDeviceID: resp.deviceId,
|
||||
newDeviceID: 'GHTYAJCE',
|
||||
newOlmAccount: pickledOlmAccount,
|
||||
);
|
||||
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
|
||||
expect(matrix.api.accessToken == resp.accessToken, true);
|
||||
expect(matrix.deviceName == 'Text Matrix Client', true);
|
||||
expect(matrix.deviceID == resp.deviceId, true);
|
||||
expect(matrix.userID == resp.userId, true);
|
||||
|
||||
var loginState = await loginStateFuture;
|
||||
var firstSync = await firstSyncFuture;
|
||||
var sync = await syncFuture;
|
||||
|
@ -207,14 +195,11 @@ void main() {
|
|||
});
|
||||
|
||||
test('Logout', () async {
|
||||
await matrix.api.logout();
|
||||
|
||||
var loginStateFuture = matrix.onLoginStateChanged.stream.first;
|
||||
await matrix.logout();
|
||||
|
||||
matrix.clear();
|
||||
|
||||
expect(matrix.api.accessToken == null, true);
|
||||
expect(matrix.api.homeserver == null, true);
|
||||
expect(matrix.accessToken == null, true);
|
||||
expect(matrix.homeserver == null, true);
|
||||
expect(matrix.userID == null, true);
|
||||
expect(matrix.deviceID == null, true);
|
||||
expect(matrix.deviceName == null, true);
|
||||
|
@ -322,17 +307,17 @@ void main() {
|
|||
});
|
||||
|
||||
test('Login', () async {
|
||||
matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
||||
matrix = Client('testclient', httpClient: FakeMatrixApi());
|
||||
|
||||
roomUpdateListFuture = matrix.onRoomUpdate.stream.toList();
|
||||
eventUpdateListFuture = matrix.onEvent.stream.toList();
|
||||
final checkResp =
|
||||
await matrix.checkServer('https://fakeServer.notExisting');
|
||||
|
||||
final loginResp = await matrix.login('test', '1234');
|
||||
final loginResp = await matrix.login(user: 'test', password: '1234');
|
||||
|
||||
expect(checkResp, true);
|
||||
expect(loginResp, true);
|
||||
expect(loginResp != null, true);
|
||||
});
|
||||
|
||||
test('setAvatar', () async {
|
||||
|
@ -385,8 +370,8 @@ void main() {
|
|||
}
|
||||
}
|
||||
}, matrix);
|
||||
test('sendToDevice', () async {
|
||||
await matrix.sendToDevice(
|
||||
test('sendToDeviceEncrypted', () async {
|
||||
await matrix.sendToDeviceEncrypted(
|
||||
[deviceKeys],
|
||||
'm.message',
|
||||
{
|
||||
|
@ -395,8 +380,7 @@ void main() {
|
|||
});
|
||||
});
|
||||
test('Test the fake store api', () async {
|
||||
var client1 =
|
||||
Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
||||
var client1 = Client('testclient', httpClient: FakeMatrixApi());
|
||||
client1.database = getDatabase();
|
||||
|
||||
client1.connect(
|
||||
|
@ -413,17 +397,16 @@ void main() {
|
|||
expect(client1.isLogged(), true);
|
||||
expect(client1.rooms.length, 2);
|
||||
|
||||
var client2 =
|
||||
Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
||||
var client2 = Client('testclient', httpClient: FakeMatrixApi());
|
||||
client2.database = client1.database;
|
||||
|
||||
client2.connect();
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
|
||||
expect(client2.isLogged(), true);
|
||||
expect(client2.api.accessToken, client1.api.accessToken);
|
||||
expect(client2.accessToken, client1.accessToken);
|
||||
expect(client2.userID, client1.userID);
|
||||
expect(client2.api.homeserver, client1.api.homeserver);
|
||||
expect(client2.homeserver, client1.homeserver);
|
||||
expect(client2.deviceID, client1.deviceID);
|
||||
expect(client2.deviceName, client1.deviceName);
|
||||
if (client2.encryptionEnabled) {
|
||||
|
@ -438,6 +421,20 @@ void main() {
|
|||
test('changePassword', () async {
|
||||
await matrix.changePassword('1234', oldPassword: '123456');
|
||||
});
|
||||
test('ignoredUsers', () async {
|
||||
expect(matrix.ignoredUsers, []);
|
||||
matrix.accountData['m.ignored_user_list'] =
|
||||
BasicEvent(type: 'm.ignored_user_list', content: {
|
||||
'ignored_users': {
|
||||
'@charley:stupid.abc': {},
|
||||
},
|
||||
});
|
||||
expect(matrix.ignoredUsers, ['@charley:stupid.abc']);
|
||||
});
|
||||
test('ignoredUsers', () async {
|
||||
await matrix.ignoreUser('@charley2:stupid.abc');
|
||||
await matrix.unignoreUser('@charley:stupid.abc');
|
||||
});
|
||||
|
||||
test('dispose', () async {
|
||||
await matrix.dispose(closeDatabase: true);
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/src/utils/logs.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
|
@ -74,9 +75,9 @@ void main() {
|
|||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
Logs.error('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
Logs.success('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/src/utils/logs.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
|
@ -33,9 +34,9 @@ void main() {
|
|||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
Logs.success('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/src/utils/logs.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
|
@ -30,9 +31,9 @@ void main() {
|
|||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
Logs.success('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/src/utils/logs.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
|
@ -35,15 +36,14 @@ void main() {
|
|||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
Logs.success('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
Client client;
|
||||
var otherClient =
|
||||
Client('othertestclient', debug: true, httpClient: FakeMatrixApi());
|
||||
var otherClient = Client('othertestclient', httpClient: FakeMatrixApi());
|
||||
DeviceKeys device;
|
||||
Map<String, dynamic> payload;
|
||||
|
||||
|
@ -54,7 +54,7 @@ void main() {
|
|||
otherClient.connect(
|
||||
newToken: 'abc',
|
||||
newUserID: '@othertest:fakeServer.notExisting',
|
||||
newHomeserver: otherClient.api.homeserver,
|
||||
newHomeserver: otherClient.homeserver,
|
||||
newDeviceName: 'Text Matrix Client',
|
||||
newDeviceID: 'FOXDEVICE',
|
||||
newOlmAccount: otherPickledOlmAccount,
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/src/utils/logs.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
|
@ -30,9 +31,9 @@ void main() {
|
|||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
Logs.success('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
|
@ -59,7 +60,7 @@ void main() {
|
|||
'session_key': sessionKey,
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': validSessionId,
|
||||
'sender_key': validSenderKey,
|
||||
});
|
||||
await client.encryption.keyManager.handleToDeviceEvent(event);
|
||||
expect(
|
||||
|
@ -184,6 +185,11 @@ void main() {
|
|||
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
||||
null,
|
||||
true);
|
||||
expect(
|
||||
client.encryption.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, 'invalid') !=
|
||||
null,
|
||||
false);
|
||||
|
||||
expect(
|
||||
client.encryption.keyManager
|
||||
|
@ -195,6 +201,11 @@ void main() {
|
|||
.getInboundGroupSession('otherroom', sessionId, senderKey) !=
|
||||
null,
|
||||
true);
|
||||
expect(
|
||||
client.encryption.keyManager
|
||||
.getInboundGroupSession('otherroom', sessionId, 'invalid') !=
|
||||
null,
|
||||
false);
|
||||
expect(
|
||||
client.encryption.keyManager
|
||||
.getInboundGroupSession('otherroom', 'invalid', senderKey) !=
|
||||
|
@ -214,6 +225,20 @@ void main() {
|
|||
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
||||
null,
|
||||
true);
|
||||
|
||||
client.encryption.keyManager.clearInboundGroupSessions();
|
||||
expect(
|
||||
client.encryption.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
||||
null,
|
||||
false);
|
||||
await client.encryption.keyManager
|
||||
.loadInboundGroupSession(roomId, sessionId, 'invalid');
|
||||
expect(
|
||||
client.encryption.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, 'invalid') !=
|
||||
null,
|
||||
false);
|
||||
});
|
||||
|
||||
test('setInboundGroupSession', () async {
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
import 'dart:convert';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/src/utils/logs.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
|
@ -45,14 +46,14 @@ void main() {
|
|||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
Logs.success('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
|
||||
final validSenderKey = '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI';
|
||||
final validSenderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg';
|
||||
test('Create Request', () async {
|
||||
var matrix = await getClient();
|
||||
final requestRoom = matrix.getRoomById('!726s6s6q:example.com');
|
||||
|
@ -106,7 +107,7 @@ void main() {
|
|||
'requesting_device_id': 'OTHERDEVICE',
|
||||
});
|
||||
await matrix.encryption.keyManager.handleToDeviceEvent(event);
|
||||
print(FakeMatrixApi.calledEndpoints.keys.toString());
|
||||
Logs.info(FakeMatrixApi.calledEndpoints.keys.toString());
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
|
|
|
@ -20,6 +20,7 @@ import 'dart:convert';
|
|||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
import 'package:famedlysdk/src/utils/logs.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
|
@ -31,7 +32,7 @@ class MockSSSS extends SSSS {
|
|||
|
||||
bool requestedSecrets = false;
|
||||
@override
|
||||
Future<void> maybeRequestAll(List<DeviceKeys> devices) async {
|
||||
Future<void> maybeRequestAll([List<DeviceKeys> devices]) async {
|
||||
requestedSecrets = true;
|
||||
final handle = open();
|
||||
handle.unlock(recoveryKey: SSSS_KEY);
|
||||
|
@ -67,9 +68,9 @@ void main() {
|
|||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
Logs.success('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
|
@ -82,14 +83,13 @@ void main() {
|
|||
|
||||
test('setupClient', () async {
|
||||
client1 = await getClient();
|
||||
client2 =
|
||||
Client('othertestclient', debug: true, httpClient: FakeMatrixApi());
|
||||
client2 = Client('othertestclient', httpClient: FakeMatrixApi());
|
||||
client2.database = client1.database;
|
||||
await client2.checkServer('https://fakeServer.notExisting');
|
||||
client2.connect(
|
||||
newToken: 'abc',
|
||||
newUserID: '@othertest:fakeServer.notExisting',
|
||||
newHomeserver: client2.api.homeserver,
|
||||
newHomeserver: client2.homeserver,
|
||||
newDeviceName: 'Text Matrix Client',
|
||||
newDeviceID: 'FOXDEVICE',
|
||||
newOlmAccount: otherPickledOlmAccount,
|
||||
|
@ -207,7 +207,7 @@ void main() {
|
|||
|
||||
test('ask SSSS start', () async {
|
||||
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(true);
|
||||
await client1.database.clearSSSSCache(client1.id);
|
||||
await client1.encryption.ssss.clearCache();
|
||||
final req1 =
|
||||
await client1.userDeviceKeys[client2.userID].startVerification();
|
||||
expect(req1.state, KeyVerificationState.askSSSS);
|
||||
|
@ -288,7 +288,7 @@ void main() {
|
|||
|
||||
// alright, they match
|
||||
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(true);
|
||||
await client1.database.clearSSSSCache(client1.id);
|
||||
await client1.encryption.ssss.clearCache();
|
||||
|
||||
// send mac
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
|
@ -312,7 +312,7 @@ void main() {
|
|||
|
||||
client1.encryption.ssss = MockSSSS(client1.encryption);
|
||||
(client1.encryption.ssss as MockSSSS).requestedSecrets = false;
|
||||
await client1.database.clearSSSSCache(client1.id);
|
||||
await client1.encryption.ssss.clearCache();
|
||||
await req1.maybeRequestSSSSSecrets();
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
expect((client1.encryption.ssss as MockSSSS).requestedSecrets, true);
|
||||
|
|
|
@ -18,8 +18,10 @@
|
|||
|
||||
import 'dart:convert';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/src/utils/logs.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
import 'package:famedlysdk/encryption/utils/json_signature_check_extension.dart';
|
||||
|
||||
import '../fake_client.dart';
|
||||
import '../fake_matrix_api.dart';
|
||||
|
@ -32,9 +34,9 @@ void main() {
|
|||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
Logs.success('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
|
@ -50,13 +52,9 @@ void main() {
|
|||
};
|
||||
final signedPayload = client.encryption.olmManager.signJson(payload);
|
||||
expect(
|
||||
client.encryption.olmManager.checkJsonSignature(client.fingerprintKey,
|
||||
signedPayload, client.userID, client.deviceID),
|
||||
signedPayload.checkJsonSignature(
|
||||
client.fingerprintKey, client.userID, client.deviceID),
|
||||
true);
|
||||
expect(
|
||||
client.encryption.olmManager.checkJsonSignature(
|
||||
client.fingerprintKey, payload, client.userID, client.deviceID),
|
||||
false);
|
||||
});
|
||||
|
||||
test('uploadKeys', () async {
|
||||
|
|
|
@ -16,11 +16,16 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/src/utils/logs.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import '../fake_client.dart';
|
||||
import '../fake_matrix_api.dart';
|
||||
|
||||
void main() {
|
||||
group('Online Key Backup', () {
|
||||
|
@ -30,9 +35,9 @@ void main() {
|
|||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
Logs.success('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
|
@ -66,6 +71,49 @@ void main() {
|
|||
true);
|
||||
});
|
||||
|
||||
test('upload key', () async {
|
||||
final session = olm.OutboundGroupSession();
|
||||
session.create();
|
||||
final inbound = olm.InboundGroupSession();
|
||||
inbound.create(session.session_key());
|
||||
final senderKey = client.identityKey;
|
||||
final roomId = '!someroom:example.org';
|
||||
final sessionId = inbound.session_id();
|
||||
// set a payload...
|
||||
var sessionPayload = <String, dynamic>{
|
||||
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||
'room_id': roomId,
|
||||
'forwarding_curve25519_key_chain': [client.identityKey],
|
||||
'session_id': sessionId,
|
||||
'session_key': inbound.export_session(1),
|
||||
'sender_key': senderKey,
|
||||
'sender_claimed_ed25519_key': client.fingerprintKey,
|
||||
};
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
client.encryption.keyManager.setInboundGroupSession(
|
||||
roomId, sessionId, senderKey, sessionPayload,
|
||||
forwarded: true);
|
||||
var dbSessions =
|
||||
await client.database.getInboundGroupSessionsToUpload().get();
|
||||
expect(dbSessions.isNotEmpty, true);
|
||||
await client.encryption.keyManager.backgroundTasks();
|
||||
final payload = FakeMatrixApi
|
||||
.calledEndpoints['/client/unstable/room_keys/keys?version=5'].first;
|
||||
dbSessions =
|
||||
await client.database.getInboundGroupSessionsToUpload().get();
|
||||
expect(dbSessions.isEmpty, true);
|
||||
|
||||
final onlineKeys = RoomKeys.fromJson(json.decode(payload));
|
||||
client.encryption.keyManager.clearInboundGroupSessions();
|
||||
var ret = client.encryption.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey);
|
||||
expect(ret, null);
|
||||
await client.encryption.keyManager.loadFromResponse(onlineKeys);
|
||||
ret = client.encryption.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey);
|
||||
expect(ret != null, true);
|
||||
});
|
||||
|
||||
test('dispose client', () async {
|
||||
await client.dispose(closeDatabase: true);
|
||||
});
|
||||
|
|
|
@ -22,6 +22,7 @@ import 'dart:convert';
|
|||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
import 'package:famedlysdk/src/utils/logs.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:encrypt/encrypt.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
@ -29,6 +30,19 @@ import 'package:olm/olm.dart' as olm;
|
|||
import '../fake_client.dart';
|
||||
import '../fake_matrix_api.dart';
|
||||
|
||||
class MockSSSS extends SSSS {
|
||||
MockSSSS(Encryption encryption) : super(encryption);
|
||||
|
||||
bool requestedSecrets = false;
|
||||
@override
|
||||
Future<void> maybeRequestAll([List<DeviceKeys> devices]) async {
|
||||
requestedSecrets = true;
|
||||
final handle = open();
|
||||
handle.unlock(recoveryKey: SSSS_KEY);
|
||||
await handle.maybeCacheAll();
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('SSSS', () {
|
||||
var olmEnabled = true;
|
||||
|
@ -37,9 +51,9 @@ void main() {
|
|||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
Logs.success('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
|
@ -89,7 +103,7 @@ void main() {
|
|||
// account_data for this test
|
||||
final content = FakeMatrixApi
|
||||
.calledEndpoints[
|
||||
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best+animal']
|
||||
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best%20animal']
|
||||
.first;
|
||||
client.accountData['best animal'] = BasicEvent.fromJson({
|
||||
'type': 'best animal',
|
||||
|
@ -247,7 +261,7 @@ void main() {
|
|||
client.encryption.ssss.open('m.cross_signing.self_signing');
|
||||
handle.unlock(recoveryKey: SSSS_KEY);
|
||||
|
||||
await client.database.clearSSSSCache(client.id);
|
||||
await client.encryption.ssss.clearCache();
|
||||
client.encryption.ssss.pendingShareRequests.clear();
|
||||
await client.encryption.ssss.request('best animal', [key]);
|
||||
var event = ToDeviceEvent(
|
||||
|
@ -271,7 +285,7 @@ void main() {
|
|||
'm.megolm_backup.v1'
|
||||
]) {
|
||||
final secret = await handle.getStored(type);
|
||||
await client.database.clearSSSSCache(client.id);
|
||||
await client.encryption.ssss.clearCache();
|
||||
client.encryption.ssss.pendingShareRequests.clear();
|
||||
await client.encryption.ssss.request(type, [key]);
|
||||
event = ToDeviceEvent(
|
||||
|
@ -293,7 +307,7 @@ void main() {
|
|||
// test different fail scenarios
|
||||
|
||||
// not encrypted
|
||||
await client.database.clearSSSSCache(client.id);
|
||||
await client.encryption.ssss.clearCache();
|
||||
client.encryption.ssss.pendingShareRequests.clear();
|
||||
await client.encryption.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
|
@ -308,7 +322,7 @@ void main() {
|
|||
expect(await client.encryption.ssss.getCached('best animal'), null);
|
||||
|
||||
// unknown request id
|
||||
await client.database.clearSSSSCache(client.id);
|
||||
await client.encryption.ssss.clearCache();
|
||||
client.encryption.ssss.pendingShareRequests.clear();
|
||||
await client.encryption.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
|
@ -326,7 +340,7 @@ void main() {
|
|||
expect(await client.encryption.ssss.getCached('best animal'), null);
|
||||
|
||||
// not from a device we sent the request to
|
||||
await client.database.clearSSSSCache(client.id);
|
||||
await client.encryption.ssss.clearCache();
|
||||
client.encryption.ssss.pendingShareRequests.clear();
|
||||
await client.encryption.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
|
@ -344,7 +358,7 @@ void main() {
|
|||
expect(await client.encryption.ssss.getCached('best animal'), null);
|
||||
|
||||
// secret not a string
|
||||
await client.database.clearSSSSCache(client.id);
|
||||
await client.encryption.ssss.clearCache();
|
||||
client.encryption.ssss.pendingShareRequests.clear();
|
||||
await client.encryption.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
|
@ -362,7 +376,7 @@ void main() {
|
|||
expect(await client.encryption.ssss.getCached('best animal'), null);
|
||||
|
||||
// validator doesn't check out
|
||||
await client.database.clearSSSSCache(client.id);
|
||||
await client.encryption.ssss.clearCache();
|
||||
client.encryption.ssss.pendingShareRequests.clear();
|
||||
await client.encryption.ssss.request('m.megolm_backup.v1', [key]);
|
||||
event = ToDeviceEvent(
|
||||
|
@ -385,12 +399,24 @@ void main() {
|
|||
final key =
|
||||
client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE'];
|
||||
key.setDirectVerified(true);
|
||||
await client.database.clearSSSSCache(client.id);
|
||||
await client.encryption.ssss.clearCache();
|
||||
client.encryption.ssss.pendingShareRequests.clear();
|
||||
await client.encryption.ssss.maybeRequestAll([key]);
|
||||
expect(client.encryption.ssss.pendingShareRequests.length, 3);
|
||||
});
|
||||
|
||||
test('periodicallyRequestMissingCache', () async {
|
||||
client.userDeviceKeys[client.userID].masterKey.setDirectVerified(true);
|
||||
client.encryption.ssss = MockSSSS(client.encryption);
|
||||
(client.encryption.ssss as MockSSSS).requestedSecrets = false;
|
||||
await client.encryption.ssss.periodicallyRequestMissingCache();
|
||||
expect((client.encryption.ssss as MockSSSS).requestedSecrets, true);
|
||||
// it should only retry once every 15 min
|
||||
(client.encryption.ssss as MockSSSS).requestedSecrets = false;
|
||||
await client.encryption.ssss.periodicallyRequestMissingCache();
|
||||
expect((client.encryption.ssss as MockSSSS).requestedSecrets, false);
|
||||
});
|
||||
|
||||
test('dispose client', () async {
|
||||
await client.dispose(closeDatabase: true);
|
||||
});
|
||||
|
|
|
@ -17,19 +17,33 @@
|
|||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
import 'package:famedlysdk/src/event.dart';
|
||||
import 'package:famedlysdk/src/utils/logs.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import 'fake_client.dart';
|
||||
import 'fake_matrix_api.dart';
|
||||
import 'fake_matrix_localizations.dart';
|
||||
|
||||
void main() {
|
||||
/// All Tests related to the Event
|
||||
group('Event', () {
|
||||
var olmEnabled = true;
|
||||
try {
|
||||
olm.init();
|
||||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
Logs.success('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final id = '!4fsdfjisjf:server.abc';
|
||||
final senderID = '@alice:server.abc';
|
||||
|
@ -50,7 +64,7 @@ void main() {
|
|||
'status': 2,
|
||||
'content': contentJson,
|
||||
};
|
||||
var client = Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
||||
var client = Client('testclient', httpClient: FakeMatrixApi());
|
||||
var event = Event.fromJson(
|
||||
jsonObj, Room(id: '!localpart:server.abc', client: client));
|
||||
|
||||
|
@ -67,7 +81,7 @@ void main() {
|
|||
expect(event.formattedText, formatted_body);
|
||||
expect(event.body, body);
|
||||
expect(event.type, EventTypes.Message);
|
||||
expect(event.isReply, true);
|
||||
expect(event.relationshipType, RelationshipTypes.Reply);
|
||||
jsonObj['state_key'] = '';
|
||||
var state = Event.fromJson(jsonObj, null);
|
||||
expect(state.eventId, id);
|
||||
|
@ -153,6 +167,11 @@ void main() {
|
|||
event = Event.fromJson(jsonObj, null);
|
||||
expect(event.messageType, MessageTypes.Location);
|
||||
|
||||
jsonObj['type'] = 'm.sticker';
|
||||
jsonObj['content']['msgtype'] = null;
|
||||
event = Event.fromJson(jsonObj, null);
|
||||
expect(event.messageType, MessageTypes.Sticker);
|
||||
|
||||
jsonObj['type'] = 'm.room.message';
|
||||
jsonObj['content']['msgtype'] = 'm.text';
|
||||
jsonObj['content']['m.relates_to'] = {};
|
||||
|
@ -160,7 +179,43 @@ void main() {
|
|||
'event_id': '1234',
|
||||
};
|
||||
event = Event.fromJson(jsonObj, null);
|
||||
expect(event.messageType, MessageTypes.Reply);
|
||||
expect(event.messageType, MessageTypes.Text);
|
||||
expect(event.relationshipType, RelationshipTypes.Reply);
|
||||
expect(event.relationshipEventId, '1234');
|
||||
});
|
||||
|
||||
test('relationship types', () async {
|
||||
Event event;
|
||||
|
||||
jsonObj['content'] = <String, dynamic>{
|
||||
'msgtype': 'm.text',
|
||||
'text': 'beep',
|
||||
};
|
||||
event = Event.fromJson(jsonObj, null);
|
||||
expect(event.relationshipType, null);
|
||||
expect(event.relationshipEventId, null);
|
||||
|
||||
jsonObj['content']['m.relates_to'] = <String, dynamic>{
|
||||
'rel_type': 'm.replace',
|
||||
'event_id': 'abc',
|
||||
};
|
||||
event = Event.fromJson(jsonObj, null);
|
||||
expect(event.relationshipType, RelationshipTypes.Edit);
|
||||
expect(event.relationshipEventId, 'abc');
|
||||
|
||||
jsonObj['content']['m.relates_to']['rel_type'] = 'm.annotation';
|
||||
event = Event.fromJson(jsonObj, null);
|
||||
expect(event.relationshipType, RelationshipTypes.Reaction);
|
||||
expect(event.relationshipEventId, 'abc');
|
||||
|
||||
jsonObj['content']['m.relates_to'] = {
|
||||
'm.in_reply_to': {
|
||||
'event_id': 'def',
|
||||
},
|
||||
};
|
||||
event = Event.fromJson(jsonObj, null);
|
||||
expect(event.relationshipType, RelationshipTypes.Reply);
|
||||
expect(event.relationshipEventId, 'def');
|
||||
});
|
||||
|
||||
test('redact', () async {
|
||||
|
@ -175,8 +230,7 @@ void main() {
|
|||
];
|
||||
for (final testType in testTypes) {
|
||||
redactJsonObj['type'] = testType;
|
||||
final room =
|
||||
Room(id: '1234', client: Client('testclient', debug: true));
|
||||
final room = Room(id: '1234', client: Client('testclient'));
|
||||
final redactionEventJson = {
|
||||
'content': {'reason': 'Spamming'},
|
||||
'event_id': '143273582443PhrSn:example.org',
|
||||
|
@ -200,7 +254,7 @@ void main() {
|
|||
|
||||
test('remove', () async {
|
||||
var event = Event.fromJson(
|
||||
jsonObj, Room(id: '1234', client: Client('testclient', debug: true)));
|
||||
jsonObj, Room(id: '1234', client: Client('testclient')));
|
||||
final removed1 = await event.remove();
|
||||
event.status = 0;
|
||||
final removed2 = await event.remove();
|
||||
|
@ -209,10 +263,9 @@ void main() {
|
|||
});
|
||||
|
||||
test('sendAgain', () async {
|
||||
var matrix =
|
||||
Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
||||
var matrix = Client('testclient', httpClient: FakeMatrixApi());
|
||||
await matrix.checkServer('https://fakeServer.notExisting');
|
||||
await matrix.login('test', '1234');
|
||||
await matrix.login(user: 'test', password: '1234');
|
||||
|
||||
var event = Event.fromJson(
|
||||
jsonObj, Room(id: '!1234:example.com', client: matrix));
|
||||
|
@ -226,10 +279,9 @@ void main() {
|
|||
});
|
||||
|
||||
test('requestKey', () async {
|
||||
var matrix =
|
||||
Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
||||
var matrix = Client('testclient', httpClient: FakeMatrixApi());
|
||||
await matrix.checkServer('https://fakeServer.notExisting');
|
||||
await matrix.login('test', '1234');
|
||||
await matrix.login(user: 'test', password: '1234');
|
||||
|
||||
var event = Event.fromJson(
|
||||
jsonObj, Room(id: '!1234:example.com', client: matrix));
|
||||
|
@ -274,8 +326,7 @@ void main() {
|
|||
expect(event.canRedact, true);
|
||||
});
|
||||
test('getLocalizedBody', () async {
|
||||
final matrix =
|
||||
Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
||||
final matrix = Client('testclient', httpClient: FakeMatrixApi());
|
||||
final room = Room(id: '!1234:example.com', client: matrix);
|
||||
var event = Event.fromJson({
|
||||
'content': {
|
||||
|
@ -790,5 +841,444 @@ void main() {
|
|||
}, room);
|
||||
expect(event.getLocalizedBody(FakeMatrixLocalizations()), null);
|
||||
});
|
||||
|
||||
test('aggregations', () {
|
||||
var event = Event.fromJson({
|
||||
'content': {
|
||||
'body': 'blah',
|
||||
'msgtype': 'm.text',
|
||||
},
|
||||
'event_id': '\$source',
|
||||
}, null);
|
||||
var edit1 = Event.fromJson({
|
||||
'content': {
|
||||
'body': 'blah',
|
||||
'msgtype': 'm.text',
|
||||
'm.relates_to': {
|
||||
'event_id': '\$source',
|
||||
'rel_type': RelationshipTypes.Edit,
|
||||
},
|
||||
},
|
||||
'event_id': '\$edit1',
|
||||
}, null);
|
||||
var edit2 = Event.fromJson({
|
||||
'content': {
|
||||
'body': 'blah',
|
||||
'msgtype': 'm.text',
|
||||
'm.relates_to': {
|
||||
'event_id': '\$source',
|
||||
'rel_type': RelationshipTypes.Edit,
|
||||
},
|
||||
},
|
||||
'event_id': '\$edit2',
|
||||
}, null);
|
||||
var room = Room(client: client);
|
||||
var timeline = Timeline(events: <Event>[event, edit1, edit2], room: room);
|
||||
expect(event.hasAggregatedEvents(timeline, RelationshipTypes.Edit), true);
|
||||
expect(event.aggregatedEvents(timeline, RelationshipTypes.Edit),
|
||||
{edit1, edit2});
|
||||
expect(event.aggregatedEvents(timeline, RelationshipTypes.Reaction),
|
||||
<Event>{});
|
||||
expect(event.hasAggregatedEvents(timeline, RelationshipTypes.Reaction),
|
||||
false);
|
||||
|
||||
timeline.removeAggregatedEvent(edit2);
|
||||
expect(event.aggregatedEvents(timeline, RelationshipTypes.Edit), {edit1});
|
||||
timeline.addAggregatedEvent(edit2);
|
||||
expect(event.aggregatedEvents(timeline, RelationshipTypes.Edit),
|
||||
{edit1, edit2});
|
||||
|
||||
timeline.removeAggregatedEvent(event);
|
||||
expect(
|
||||
event.aggregatedEvents(timeline, RelationshipTypes.Edit), <Event>{});
|
||||
});
|
||||
test('getDisplayEvent', () {
|
||||
var event = Event.fromJson({
|
||||
'type': EventTypes.Message,
|
||||
'content': {
|
||||
'body': 'blah',
|
||||
'msgtype': 'm.text',
|
||||
},
|
||||
'event_id': '\$source',
|
||||
'sender': '@alice:example.org',
|
||||
}, null);
|
||||
event.sortOrder = 0;
|
||||
var edit1 = Event.fromJson({
|
||||
'type': EventTypes.Message,
|
||||
'content': {
|
||||
'body': '* edit 1',
|
||||
'msgtype': 'm.text',
|
||||
'm.new_content': {
|
||||
'body': 'edit 1',
|
||||
'msgtype': 'm.text',
|
||||
},
|
||||
'm.relates_to': {
|
||||
'event_id': '\$source',
|
||||
'rel_type': RelationshipTypes.Edit,
|
||||
},
|
||||
},
|
||||
'event_id': '\$edit1',
|
||||
'sender': '@alice:example.org',
|
||||
}, null);
|
||||
edit1.sortOrder = 1;
|
||||
var edit2 = Event.fromJson({
|
||||
'type': EventTypes.Message,
|
||||
'content': {
|
||||
'body': '* edit 2',
|
||||
'msgtype': 'm.text',
|
||||
'm.new_content': {
|
||||
'body': 'edit 2',
|
||||
'msgtype': 'm.text',
|
||||
},
|
||||
'm.relates_to': {
|
||||
'event_id': '\$source',
|
||||
'rel_type': RelationshipTypes.Edit,
|
||||
},
|
||||
},
|
||||
'event_id': '\$edit2',
|
||||
'sender': '@alice:example.org',
|
||||
}, null);
|
||||
edit2.sortOrder = 2;
|
||||
var edit3 = Event.fromJson({
|
||||
'type': EventTypes.Message,
|
||||
'content': {
|
||||
'body': '* edit 3',
|
||||
'msgtype': 'm.text',
|
||||
'm.new_content': {
|
||||
'body': 'edit 3',
|
||||
'msgtype': 'm.text',
|
||||
},
|
||||
'm.relates_to': {
|
||||
'event_id': '\$source',
|
||||
'rel_type': RelationshipTypes.Edit,
|
||||
},
|
||||
},
|
||||
'event_id': '\$edit3',
|
||||
'sender': '@bob:example.org',
|
||||
}, null);
|
||||
edit3.sortOrder = 3;
|
||||
var room = Room(client: client);
|
||||
// no edits
|
||||
var displayEvent =
|
||||
event.getDisplayEvent(Timeline(events: <Event>[event], room: room));
|
||||
expect(displayEvent.body, 'blah');
|
||||
// one edit
|
||||
displayEvent = event
|
||||
.getDisplayEvent(Timeline(events: <Event>[event, edit1], room: room));
|
||||
expect(displayEvent.body, 'edit 1');
|
||||
// two edits
|
||||
displayEvent = event.getDisplayEvent(
|
||||
Timeline(events: <Event>[event, edit1, edit2], room: room));
|
||||
expect(displayEvent.body, 'edit 2');
|
||||
// foreign edit
|
||||
displayEvent = event
|
||||
.getDisplayEvent(Timeline(events: <Event>[event, edit3], room: room));
|
||||
expect(displayEvent.body, 'blah');
|
||||
// mixed foreign and non-foreign
|
||||
displayEvent = event.getDisplayEvent(
|
||||
Timeline(events: <Event>[event, edit1, edit2, edit3], room: room));
|
||||
expect(displayEvent.body, 'edit 2');
|
||||
});
|
||||
test('downloadAndDecryptAttachment', () async {
|
||||
final FILE_BUFF = Uint8List.fromList([0]);
|
||||
final THUMBNAIL_BUFF = Uint8List.fromList([2]);
|
||||
final downloadCallback = (String url) async {
|
||||
return {
|
||||
'https://fakeserver.notexisting/_matrix/media/r0/download/example.org/file':
|
||||
FILE_BUFF,
|
||||
'https://fakeserver.notexisting/_matrix/media/r0/download/example.org/thumb':
|
||||
THUMBNAIL_BUFF,
|
||||
}[url];
|
||||
};
|
||||
await client.checkServer('https://fakeServer.notExisting');
|
||||
final room = Room(id: '!localpart:server.abc', client: client);
|
||||
var event = Event.fromJson({
|
||||
'type': EventTypes.Message,
|
||||
'content': {
|
||||
'body': 'image',
|
||||
'msgtype': 'm.image',
|
||||
'url': 'mxc://example.org/file',
|
||||
},
|
||||
'event_id': '\$edit2',
|
||||
'sender': '@alice:example.org',
|
||||
}, room);
|
||||
var buffer = await event.downloadAndDecryptAttachment(
|
||||
downloadCallback: downloadCallback);
|
||||
expect(buffer.bytes, FILE_BUFF);
|
||||
|
||||
event = Event.fromJson({
|
||||
'type': EventTypes.Message,
|
||||
'content': {
|
||||
'body': 'image',
|
||||
'msgtype': 'm.image',
|
||||
'url': 'mxc://example.org/file',
|
||||
'info': {
|
||||
'thumbnail_url': 'mxc://example.org/thumb',
|
||||
},
|
||||
},
|
||||
'event_id': '\$edit2',
|
||||
'sender': '@alice:example.org',
|
||||
}, room);
|
||||
buffer = await event.downloadAndDecryptAttachment(
|
||||
downloadCallback: downloadCallback);
|
||||
expect(buffer.bytes, FILE_BUFF);
|
||||
|
||||
buffer = await event.downloadAndDecryptAttachment(
|
||||
getThumbnail: true, downloadCallback: downloadCallback);
|
||||
expect(buffer.bytes, THUMBNAIL_BUFF);
|
||||
});
|
||||
test('downloadAndDecryptAttachment encrypted', () async {
|
||||
if (!olmEnabled) return;
|
||||
|
||||
final FILE_BUFF_ENC = Uint8List.fromList([0x3B, 0x6B, 0xB2, 0x8C, 0xAF]);
|
||||
final FILE_BUFF_DEC = Uint8List.fromList([0x74, 0x65, 0x73, 0x74, 0x0A]);
|
||||
final THUMB_BUFF_ENC =
|
||||
Uint8List.fromList([0x55, 0xD7, 0xEB, 0x72, 0x05, 0x13]);
|
||||
final THUMB_BUFF_DEC =
|
||||
Uint8List.fromList([0x74, 0x68, 0x75, 0x6D, 0x62, 0x0A]);
|
||||
final downloadCallback = (String url) async {
|
||||
return {
|
||||
'https://fakeserver.notexisting/_matrix/media/r0/download/example.com/file':
|
||||
FILE_BUFF_ENC,
|
||||
'https://fakeserver.notexisting/_matrix/media/r0/download/example.com/thumb':
|
||||
THUMB_BUFF_ENC,
|
||||
}[url];
|
||||
};
|
||||
final room = Room(id: '!localpart:server.abc', client: await getClient());
|
||||
var event = Event.fromJson({
|
||||
'type': EventTypes.Message,
|
||||
'content': {
|
||||
'body': 'image',
|
||||
'msgtype': 'm.image',
|
||||
'file': {
|
||||
'v': 'v2',
|
||||
'key': {
|
||||
'alg': 'A256CTR',
|
||||
'ext': true,
|
||||
'k': '7aPRNIDPeUAUqD6SPR3vVX5W9liyMG98NexVJ9udnCc',
|
||||
'key_ops': ['encrypt', 'decrypt'],
|
||||
'kty': 'oct'
|
||||
},
|
||||
'iv': 'Wdsf+tnOHIoAAAAAAAAAAA',
|
||||
'hashes': {'sha256': 'WgC7fw2alBC5t+xDx+PFlZxfFJXtIstQCg+j0WDaXxE'},
|
||||
'url': 'mxc://example.com/file',
|
||||
'mimetype': 'text/plain'
|
||||
},
|
||||
},
|
||||
'event_id': '\$edit2',
|
||||
'sender': '@alice:example.org',
|
||||
}, room);
|
||||
var buffer = await event.downloadAndDecryptAttachment(
|
||||
downloadCallback: downloadCallback);
|
||||
expect(buffer.bytes, FILE_BUFF_DEC);
|
||||
|
||||
event = Event.fromJson({
|
||||
'type': EventTypes.Message,
|
||||
'content': {
|
||||
'body': 'image',
|
||||
'msgtype': 'm.image',
|
||||
'file': {
|
||||
'v': 'v2',
|
||||
'key': {
|
||||
'alg': 'A256CTR',
|
||||
'ext': true,
|
||||
'k': '7aPRNIDPeUAUqD6SPR3vVX5W9liyMG98NexVJ9udnCc',
|
||||
'key_ops': ['encrypt', 'decrypt'],
|
||||
'kty': 'oct'
|
||||
},
|
||||
'iv': 'Wdsf+tnOHIoAAAAAAAAAAA',
|
||||
'hashes': {'sha256': 'WgC7fw2alBC5t+xDx+PFlZxfFJXtIstQCg+j0WDaXxE'},
|
||||
'url': 'mxc://example.com/file',
|
||||
'mimetype': 'text/plain'
|
||||
},
|
||||
'info': {
|
||||
'thumbnail_file': {
|
||||
'v': 'v2',
|
||||
'key': {
|
||||
'alg': 'A256CTR',
|
||||
'ext': true,
|
||||
'k': 'TmF-rZYetZbxpL5yjDPE21UALQJcpEE6X-nvUDD5rA0',
|
||||
'key_ops': ['encrypt', 'decrypt'],
|
||||
'kty': 'oct'
|
||||
},
|
||||
'iv': '41ZqNRZSLFUAAAAAAAAAAA',
|
||||
'hashes': {
|
||||
'sha256': 'zccOwXiOTAYhGXyk0Fra7CRreBF6itjiCKdd+ov8mO4'
|
||||
},
|
||||
'url': 'mxc://example.com/thumb',
|
||||
'mimetype': 'text/plain'
|
||||
}
|
||||
},
|
||||
},
|
||||
'event_id': '\$edit2',
|
||||
'sender': '@alice:example.org',
|
||||
}, room);
|
||||
buffer = await event.downloadAndDecryptAttachment(
|
||||
downloadCallback: downloadCallback);
|
||||
expect(buffer.bytes, FILE_BUFF_DEC);
|
||||
|
||||
buffer = await event.downloadAndDecryptAttachment(
|
||||
getThumbnail: true, downloadCallback: downloadCallback);
|
||||
expect(buffer.bytes, THUMB_BUFF_DEC);
|
||||
|
||||
await room.client.dispose(closeDatabase: true);
|
||||
});
|
||||
test('downloadAndDecryptAttachment store', () async {
|
||||
final FILE_BUFF = Uint8List.fromList([0]);
|
||||
var serverHits = 0;
|
||||
final downloadCallback = (String url) async {
|
||||
serverHits++;
|
||||
return {
|
||||
'https://fakeserver.notexisting/_matrix/media/r0/download/example.org/newfile':
|
||||
FILE_BUFF,
|
||||
}[url];
|
||||
};
|
||||
await client.checkServer('https://fakeServer.notExisting');
|
||||
final room = Room(id: '!localpart:server.abc', client: await getClient());
|
||||
var event = Event.fromJson({
|
||||
'type': EventTypes.Message,
|
||||
'content': {
|
||||
'body': 'image',
|
||||
'msgtype': 'm.image',
|
||||
'url': 'mxc://example.org/newfile',
|
||||
'info': {
|
||||
'size': 5,
|
||||
},
|
||||
},
|
||||
'event_id': '\$edit2',
|
||||
'sender': '@alice:example.org',
|
||||
}, room);
|
||||
var buffer = await event.downloadAndDecryptAttachment(
|
||||
downloadCallback: downloadCallback);
|
||||
expect(buffer.bytes, FILE_BUFF);
|
||||
expect(serverHits, 1);
|
||||
buffer = await event.downloadAndDecryptAttachment(
|
||||
downloadCallback: downloadCallback);
|
||||
expect(buffer.bytes, FILE_BUFF);
|
||||
expect(serverHits, 1);
|
||||
|
||||
await room.client.dispose(closeDatabase: true);
|
||||
});
|
||||
test('emote detection', () async {
|
||||
var event = Event.fromJson({
|
||||
'type': EventTypes.Message,
|
||||
'content': {
|
||||
'msgtype': 'm.text',
|
||||
'body': 'normal message',
|
||||
},
|
||||
'event_id': '\$edit2',
|
||||
'sender': '@alice:example.org',
|
||||
}, null);
|
||||
expect(event.onlyEmotes, false);
|
||||
expect(event.numberEmotes, 0);
|
||||
event = Event.fromJson({
|
||||
'type': EventTypes.Message,
|
||||
'content': {
|
||||
'msgtype': 'm.text',
|
||||
'body': 'normal message\n\nvery normal',
|
||||
},
|
||||
'event_id': '\$edit2',
|
||||
'sender': '@alice:example.org',
|
||||
}, null);
|
||||
expect(event.onlyEmotes, false);
|
||||
expect(event.numberEmotes, 0);
|
||||
event = Event.fromJson({
|
||||
'type': EventTypes.Message,
|
||||
'content': {
|
||||
'msgtype': 'm.text',
|
||||
'body': 'normal message with emoji 🦊',
|
||||
},
|
||||
'event_id': '\$edit2',
|
||||
'sender': '@alice:example.org',
|
||||
}, null);
|
||||
expect(event.onlyEmotes, false);
|
||||
expect(event.numberEmotes, 1);
|
||||
event = Event.fromJson({
|
||||
'type': EventTypes.Message,
|
||||
'content': {
|
||||
'msgtype': 'm.text',
|
||||
'body': '🦊',
|
||||
},
|
||||
'event_id': '\$edit2',
|
||||
'sender': '@alice:example.org',
|
||||
}, null);
|
||||
expect(event.onlyEmotes, true);
|
||||
expect(event.numberEmotes, 1);
|
||||
event = Event.fromJson({
|
||||
'type': EventTypes.Message,
|
||||
'content': {
|
||||
'msgtype': 'm.text',
|
||||
'body': '🦊🦊 🦊\n🦊🦊',
|
||||
},
|
||||
'event_id': '\$edit2',
|
||||
'sender': '@alice:example.org',
|
||||
}, null);
|
||||
expect(event.onlyEmotes, true);
|
||||
expect(event.numberEmotes, 5);
|
||||
event = Event.fromJson({
|
||||
'type': EventTypes.Message,
|
||||
'content': {
|
||||
'msgtype': 'm.text',
|
||||
'body': 'rich message',
|
||||
'format': 'org.matrix.custom.html',
|
||||
'formatted_body': 'rich message'
|
||||
},
|
||||
'event_id': '\$edit2',
|
||||
'sender': '@alice:example.org',
|
||||
}, null);
|
||||
expect(event.onlyEmotes, false);
|
||||
expect(event.numberEmotes, 0);
|
||||
event = Event.fromJson({
|
||||
'type': EventTypes.Message,
|
||||
'content': {
|
||||
'msgtype': 'm.text',
|
||||
'body': '🦊',
|
||||
'format': 'org.matrix.custom.html',
|
||||
'formatted_body': '🦊'
|
||||
},
|
||||
'event_id': '\$edit2',
|
||||
'sender': '@alice:example.org',
|
||||
}, null);
|
||||
expect(event.onlyEmotes, true);
|
||||
expect(event.numberEmotes, 1);
|
||||
event = Event.fromJson({
|
||||
'type': EventTypes.Message,
|
||||
'content': {
|
||||
'msgtype': 'm.text',
|
||||
'body': ':blah:',
|
||||
'format': 'org.matrix.custom.html',
|
||||
'formatted_body': '<img data-mx-emoticon src="mxc://blah/blubb">'
|
||||
},
|
||||
'event_id': '\$edit2',
|
||||
'sender': '@alice:example.org',
|
||||
}, null);
|
||||
expect(event.onlyEmotes, true);
|
||||
expect(event.numberEmotes, 1);
|
||||
event = Event.fromJson({
|
||||
'type': EventTypes.Message,
|
||||
'content': {
|
||||
'msgtype': 'm.text',
|
||||
'body': '🦊 :blah:',
|
||||
'format': 'org.matrix.custom.html',
|
||||
'formatted_body': '🦊 <img data-mx-emoticon src="mxc://blah/blubb">'
|
||||
},
|
||||
'event_id': '\$edit2',
|
||||
'sender': '@alice:example.org',
|
||||
}, null);
|
||||
expect(event.onlyEmotes, true);
|
||||
expect(event.numberEmotes, 2);
|
||||
// with variant selector
|
||||
event = Event.fromJson({
|
||||
'type': EventTypes.Message,
|
||||
'content': {
|
||||
'msgtype': 'm.text',
|
||||
'body': '❤️',
|
||||
},
|
||||
'event_id': '\$edit2',
|
||||
'sender': '@alice:example.org',
|
||||
}, null);
|
||||
expect(event.onlyEmotes, true);
|
||||
expect(event.numberEmotes, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -29,21 +29,15 @@ const pickledOlmAccount =
|
|||
'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw';
|
||||
|
||||
Future<Client> getClient() async {
|
||||
final client = Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
||||
final client = Client('testclient', httpClient: FakeMatrixApi());
|
||||
client.database = getDatabase();
|
||||
await client.checkServer('https://fakeServer.notExisting');
|
||||
final resp = await client.api.login(
|
||||
type: 'm.login.password',
|
||||
user: 'test',
|
||||
password: '1234',
|
||||
initialDeviceDisplayName: 'Fluffy Matrix Client',
|
||||
);
|
||||
client.connect(
|
||||
newToken: resp.accessToken,
|
||||
newUserID: resp.userId,
|
||||
newHomeserver: client.api.homeserver,
|
||||
newToken: 'abcd',
|
||||
newUserID: '@test:fakeServer.notExisting',
|
||||
newHomeserver: client.homeserver,
|
||||
newDeviceName: 'Text Matrix Client',
|
||||
newDeviceID: resp.deviceId,
|
||||
newDeviceID: 'GHTYAJCE',
|
||||
newOlmAccount: pickledOlmAccount,
|
||||
);
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
|
|
|
@ -80,8 +80,12 @@ class FakeMatrixApi extends MockClient {
|
|||
res = {'displayname': ''};
|
||||
} else if (method == 'PUT' &&
|
||||
action.contains(
|
||||
'/client/r0/rooms/%211234%3AfakeServer.notExisting/send/')) {
|
||||
'/client/r0/rooms/!1234%3AfakeServer.notExisting/send/')) {
|
||||
res = {'event_id': '\$event${FakeMatrixApi.eventCounter++}'};
|
||||
} else if (action.contains('/client/r0/sync')) {
|
||||
res = {
|
||||
'next_batch': DateTime.now().millisecondsSinceEpoch.toString
|
||||
};
|
||||
} else {
|
||||
res = {
|
||||
'errcode': 'M_UNRECOGNIZED',
|
||||
|
@ -748,7 +752,7 @@ class FakeMatrixApi extends MockClient {
|
|||
'app_url': 'https://custom.app.example.org'
|
||||
}
|
||||
},
|
||||
'/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags':
|
||||
'/client/r0/user/%40alice%3Aexample.com/rooms/!localpart%3Aexample.com/tags':
|
||||
(var req) => {
|
||||
'tags': {
|
||||
'm.favourite': {'order': 0.1},
|
||||
|
@ -1978,25 +1982,27 @@ class FakeMatrixApi extends MockClient {
|
|||
'/client/unstable/room_keys/version': (var reqI) => {'version': '5'},
|
||||
},
|
||||
'PUT': {
|
||||
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/m.ignored_user_list':
|
||||
(var req) => {},
|
||||
'/client/r0/presence/${Uri.encodeComponent('@alice:example.com')}/status':
|
||||
(var req) => {},
|
||||
'/client/r0/pushrules/global/content/nocake/enabled': (var req) => {},
|
||||
'/client/r0/pushrules/global/content/nocake/actions': (var req) => {},
|
||||
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.history_visibility':
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.history_visibility':
|
||||
(var req) => {},
|
||||
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.join_rules':
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.join_rules':
|
||||
(var req) => {},
|
||||
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.guest_access':
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.guest_access':
|
||||
(var req) => {},
|
||||
'/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.call.invite/1234':
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/send/m.call.invite/1234':
|
||||
(var req) => {},
|
||||
'/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.call.answer/1234':
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/send/m.call.answer/1234':
|
||||
(var req) => {},
|
||||
'/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.call.candidates/1234':
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/send/m.call.candidates/1234':
|
||||
(var req) => {},
|
||||
'/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.call.hangup/1234':
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/send/m.call.hangup/1234':
|
||||
(var req) => {},
|
||||
'/client/r0/rooms/%211234%3Aexample.com/redact/1143273582443PhrSn%3Aexample.org/1234':
|
||||
'/client/r0/rooms/!1234%3Aexample.com/redact/1143273582443PhrSn%3Aexample.org/1234':
|
||||
(var req) => {'event_id': '1234'},
|
||||
'/client/r0/pushrules/global/room/!localpart%3Aserver.abc': (var req) =>
|
||||
{},
|
||||
|
@ -2006,23 +2012,31 @@ class FakeMatrixApi extends MockClient {
|
|||
(var req) => {},
|
||||
'/client/r0/devices/QBUAZIFURK': (var req) => {},
|
||||
'/client/r0/directory/room/%23testalias%3Aexample.com': (var reqI) => {},
|
||||
'/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.message/testtxid':
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/send/m.room.message/testtxid':
|
||||
(var reqI) => {
|
||||
'event_id': '\$event${FakeMatrixApi.eventCounter++}',
|
||||
},
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/send/m.reaction/testtxid':
|
||||
(var reqI) => {
|
||||
'event_id': '\$event${FakeMatrixApi.eventCounter++}',
|
||||
},
|
||||
'/client/r0/rooms/!localpart%3Aexample.com/typing/%40alice%3Aexample.com':
|
||||
(var req) => {},
|
||||
'/client/r0/rooms/%211234%3Aexample.com/send/m.room.message/1234':
|
||||
'/client/r0/rooms/!1234%3Aexample.com/send/m.room.message/1234':
|
||||
(var reqI) => {
|
||||
'event_id': '\$event${FakeMatrixApi.eventCounter++}',
|
||||
},
|
||||
'/client/r0/user/%40test%3AfakeServer.notExisting/rooms/%21localpart%3Aserver.abc/tags/m.favourite':
|
||||
'/client/r0/rooms/!1234%3Aexample.com/send/m.room.message/newresend':
|
||||
(var reqI) => {
|
||||
'event_id': '\$event${FakeMatrixApi.eventCounter++}',
|
||||
},
|
||||
'/client/r0/user/%40test%3AfakeServer.notExisting/rooms/!localpart%3Aserver.abc/tags/m.favourite':
|
||||
(var req) => {},
|
||||
'/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags/testtag':
|
||||
'/client/r0/user/%40alice%3Aexample.com/rooms/!localpart%3Aexample.com/tags/testtag':
|
||||
(var req) => {},
|
||||
'/client/r0/user/%40alice%3Aexample.com/account_data/test.account.data':
|
||||
(var req) => {},
|
||||
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best+animal':
|
||||
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best%20animal':
|
||||
(var req) => {},
|
||||
'/client/r0/user/%40alice%3Aexample.com/rooms/1234/account_data/test.account.data':
|
||||
(var req) => {},
|
||||
|
@ -2034,27 +2048,27 @@ class FakeMatrixApi extends MockClient {
|
|||
'/client/r0/profile/%40alice%3Aexample.com/avatar_url': (var reqI) => {},
|
||||
'/client/r0/profile/%40test%3AfakeServer.notExisting/avatar_url':
|
||||
(var reqI) => {},
|
||||
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.encryption':
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.encryption':
|
||||
(var reqI) => {'event_id': 'YUwRidLecu:example.com'},
|
||||
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.avatar':
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.avatar':
|
||||
(var reqI) => {'event_id': 'YUwRidLecu:example.com'},
|
||||
'/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.message/1234':
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/send/m.room.message/1234':
|
||||
(var reqI) => {'event_id': 'YUwRidLecu:example.com'},
|
||||
'/client/r0/rooms/%21localpart%3Aserver.abc/redact/1234/1234':
|
||||
(var reqI) => {'event_id': 'YUwRidLecu:example.com'},
|
||||
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.name':
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/redact/1234/1234': (var reqI) =>
|
||||
{'event_id': 'YUwRidLecu:example.com'},
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.name':
|
||||
(var reqI) => {
|
||||
'event_id': '42',
|
||||
},
|
||||
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.topic':
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.topic':
|
||||
(var reqI) => {
|
||||
'event_id': '42',
|
||||
},
|
||||
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.pinned_events':
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.pinned_events':
|
||||
(var reqI) => {
|
||||
'event_id': '42',
|
||||
},
|
||||
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.power_levels':
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.power_levels':
|
||||
(var reqI) => {
|
||||
'event_id': '42',
|
||||
},
|
||||
|
@ -2083,9 +2097,9 @@ class FakeMatrixApi extends MockClient {
|
|||
'/client/r0/pushrules/global/content/nocake': (var req) => {},
|
||||
'/client/r0/pushrules/global/override/!localpart%3Aserver.abc':
|
||||
(var req) => {},
|
||||
'/client/r0/user/%40test%3AfakeServer.notExisting/rooms/%21localpart%3Aserver.abc/tags/m.favourite':
|
||||
'/client/r0/user/%40test%3AfakeServer.notExisting/rooms/!localpart%3Aserver.abc/tags/m.favourite':
|
||||
(var req) => {},
|
||||
'/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags/testtag':
|
||||
'/client/r0/user/%40alice%3Aexample.com/rooms/!localpart%3Aexample.com/tags/testtag':
|
||||
(var req) => {},
|
||||
'/client/unstable/room_keys/version/5': (var req) => {},
|
||||
'/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}/${Uri.encodeComponent('ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU')}?version=5':
|
||||
|
|
|
@ -306,4 +306,28 @@ class FakeMatrixLocalizations extends MatrixLocalizations {
|
|||
@override
|
||||
// TODO: implement you
|
||||
String get you => null;
|
||||
|
||||
@override
|
||||
String answeredTheCall(String senderName) {
|
||||
// TODO: implement answeredTheCall
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
String endedTheCall(String senderName) {
|
||||
// TODO: implement endedTheCall
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
String sentCallInformations(String senderName) {
|
||||
// TODO: implement sentCallInformations
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
String startedACall(String senderName) {
|
||||
// TODO: implement startedACall
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,11 +54,11 @@ void main() {
|
|||
});
|
||||
test('emotes', () {
|
||||
expect(markdown(':fox:', emotePacks),
|
||||
'<img src="mxc://roomfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
|
||||
'<img data-mx-emoticon="" src="mxc://roomfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
|
||||
expect(markdown(':user~fox:', emotePacks),
|
||||
'<img src="mxc://userfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
|
||||
'<img data-mx-emoticon="" src="mxc://userfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
|
||||
expect(markdown(':raccoon:', emotePacks),
|
||||
'<img src="mxc://raccoon" alt=":raccoon:" title=":raccoon:" height="32" vertical-align="middle" />');
|
||||
'<img data-mx-emoticon="" src="mxc://raccoon" alt=":raccoon:" title=":raccoon:" height="32" vertical-align="middle" />');
|
||||
expect(markdown(':invalid:', emotePacks), ':invalid:');
|
||||
expect(markdown(':room~invalid:', emotePacks), ':room~invalid:');
|
||||
});
|
||||
|
|
|
@ -33,7 +33,6 @@ void main() {
|
|||
group('Matrix API', () {
|
||||
final matrixApi = MatrixApi(
|
||||
httpClient: FakeMatrixApi(),
|
||||
debug: true,
|
||||
);
|
||||
test('MatrixException test', () async {
|
||||
final exception = MatrixException.fromJson({
|
||||
|
@ -1377,7 +1376,7 @@ void main() {
|
|||
'@alice:example.com', '!localpart:example.com');
|
||||
expect(
|
||||
FakeMatrixApi.api['GET'][
|
||||
'/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags']({}),
|
||||
'/client/r0/user/%40alice%3Aexample.com/rooms/!localpart%3Aexample.com/tags']({}),
|
||||
{'tags': response.map((k, v) => MapEntry(k, v.toJson()))},
|
||||
);
|
||||
|
||||
|
|
184
test/matrix_database_test.dart
Normal file
184
test/matrix_database_test.dart
Normal file
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 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 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'fake_database.dart';
|
||||
|
||||
void main() {
|
||||
group('Databse', () {
|
||||
final database = getDatabase();
|
||||
var clientId = -1;
|
||||
var room = Room(id: '!room:blubb');
|
||||
test('setupDatabase', () async {
|
||||
clientId = await database.insertClient(
|
||||
'testclient',
|
||||
'https://example.org',
|
||||
'blubb',
|
||||
'@test:example.org',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
});
|
||||
test('storeEventUpdate', () async {
|
||||
// store a simple update
|
||||
var update = EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: room.id,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'origin_server_ts': 100,
|
||||
'content': {'blah': 'blubb'},
|
||||
'event_id': '\$event-1',
|
||||
'sender': '@blah:blubb',
|
||||
},
|
||||
sortOrder: 0.0,
|
||||
);
|
||||
await database.storeEventUpdate(clientId, update);
|
||||
var event = await database.getEventById(clientId, '\$event-1', room);
|
||||
expect(event.eventId, '\$event-1');
|
||||
|
||||
// insert a transaction id
|
||||
update = EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: room.id,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'origin_server_ts': 100,
|
||||
'content': {'blah': 'blubb'},
|
||||
'event_id': 'transaction-1',
|
||||
'sender': '@blah:blubb',
|
||||
'status': 0,
|
||||
},
|
||||
sortOrder: 0.0,
|
||||
);
|
||||
await database.storeEventUpdate(clientId, update);
|
||||
event = await database.getEventById(clientId, 'transaction-1', room);
|
||||
expect(event.eventId, 'transaction-1');
|
||||
update = EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: room.id,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'origin_server_ts': 100,
|
||||
'content': {'blah': 'blubb'},
|
||||
'event_id': '\$event-2',
|
||||
'sender': '@blah:blubb',
|
||||
'unsigned': {
|
||||
'transaction_id': 'transaction-1',
|
||||
},
|
||||
'status': 1,
|
||||
},
|
||||
sortOrder: 0.0,
|
||||
);
|
||||
await database.storeEventUpdate(clientId, update);
|
||||
event = await database.getEventById(clientId, 'transaction-1', room);
|
||||
expect(event, null);
|
||||
event = await database.getEventById(clientId, '\$event-2', room);
|
||||
|
||||
// insert a transaction id if the event id for it already exists
|
||||
update = EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: room.id,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'origin_server_ts': 100,
|
||||
'content': {'blah': 'blubb'},
|
||||
'event_id': '\$event-3',
|
||||
'sender': '@blah:blubb',
|
||||
'status': 0,
|
||||
},
|
||||
sortOrder: 0.0,
|
||||
);
|
||||
await database.storeEventUpdate(clientId, update);
|
||||
event = await database.getEventById(clientId, '\$event-3', room);
|
||||
expect(event.eventId, '\$event-3');
|
||||
update = EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: room.id,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'origin_server_ts': 100,
|
||||
'content': {'blah': 'blubb'},
|
||||
'event_id': '\$event-3',
|
||||
'sender': '@blah:blubb',
|
||||
'status': 1,
|
||||
'unsigned': {
|
||||
'transaction_id': 'transaction-2',
|
||||
},
|
||||
},
|
||||
sortOrder: 0.0,
|
||||
);
|
||||
await database.storeEventUpdate(clientId, update);
|
||||
event = await database.getEventById(clientId, '\$event-3', room);
|
||||
expect(event.eventId, '\$event-3');
|
||||
expect(event.status, 1);
|
||||
event = await database.getEventById(clientId, 'transaction-2', room);
|
||||
expect(event, null);
|
||||
|
||||
// insert transaction id and not update status
|
||||
update = EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: room.id,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'origin_server_ts': 100,
|
||||
'content': {'blah': 'blubb'},
|
||||
'event_id': '\$event-4',
|
||||
'sender': '@blah:blubb',
|
||||
'status': 2,
|
||||
},
|
||||
sortOrder: 0.0,
|
||||
);
|
||||
await database.storeEventUpdate(clientId, update);
|
||||
event = await database.getEventById(clientId, '\$event-4', room);
|
||||
expect(event.eventId, '\$event-4');
|
||||
update = EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: room.id,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'origin_server_ts': 100,
|
||||
'content': {'blah': 'blubb'},
|
||||
'event_id': '\$event-4',
|
||||
'sender': '@blah:blubb',
|
||||
'status': 1,
|
||||
'unsigned': {
|
||||
'transaction_id': 'transaction-3',
|
||||
},
|
||||
},
|
||||
sortOrder: 0.0,
|
||||
);
|
||||
await database.storeEventUpdate(clientId, update);
|
||||
event = await database.getEventById(clientId, '\$event-4', room);
|
||||
expect(event.eventId, '\$event-4');
|
||||
expect(event.status, 2);
|
||||
event = await database.getEventById(clientId, 'transaction-3', room);
|
||||
expect(event, null);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -205,4 +205,24 @@ class MatrixDefaultLocalizations extends MatrixLocalizations {
|
|||
|
||||
@override
|
||||
String get you => 'You';
|
||||
|
||||
@override
|
||||
String answeredTheCall(String senderName) {
|
||||
return 'answeredTheCall';
|
||||
}
|
||||
|
||||
@override
|
||||
String endedTheCall(String senderName) {
|
||||
return 'endedTheCall';
|
||||
}
|
||||
|
||||
@override
|
||||
String sentCallInformations(String senderName) {
|
||||
return 'sentCallInformations';
|
||||
}
|
||||
|
||||
@override
|
||||
String startedACall(String senderName) {
|
||||
return 'startedACall';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,9 +29,10 @@ void main() {
|
|||
expect('!test:example.com'.isValidMatrixId, true);
|
||||
expect('+test:example.com'.isValidMatrixId, true);
|
||||
expect('\$test:example.com'.isValidMatrixId, true);
|
||||
expect('\$testevent'.isValidMatrixId, true);
|
||||
expect('test:example.com'.isValidMatrixId, false);
|
||||
expect('@testexample.com'.isValidMatrixId, false);
|
||||
expect('@:example.com'.isValidMatrixId, false);
|
||||
expect('@:example.com'.isValidMatrixId, true);
|
||||
expect('@test:'.isValidMatrixId, false);
|
||||
expect(mxId.sigil, '@');
|
||||
expect('#test:example.com'.sigil, '#');
|
||||
|
@ -42,6 +43,8 @@ void main() {
|
|||
expect(mxId.domain, 'example.com');
|
||||
expect(mxId.equals('@Test:example.com'), true);
|
||||
expect(mxId.equals('@test:example.org'), false);
|
||||
expect('@user:domain:8448'.localpart, 'user');
|
||||
expect('@user:domain:8448'.domain, 'domain:8448');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -33,13 +33,13 @@ void main() {
|
|||
expect(content.isScheme('mxc'), true);
|
||||
|
||||
expect(content.getDownloadLink(client),
|
||||
'${client.api.homeserver.toString()}/_matrix/media/r0/download/exampleserver.abc/abcdefghijklmn');
|
||||
'${client.homeserver.toString()}/_matrix/media/r0/download/exampleserver.abc/abcdefghijklmn');
|
||||
expect(content.getThumbnail(client, width: 50, height: 50),
|
||||
'${client.api.homeserver.toString()}/_matrix/media/r0/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=crop');
|
||||
'${client.homeserver.toString()}/_matrix/media/r0/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=crop');
|
||||
expect(
|
||||
content.getThumbnail(client,
|
||||
width: 50, height: 50, method: ThumbnailMethod.scale),
|
||||
'${client.api.homeserver.toString()}/_matrix/media/r0/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=scale');
|
||||
'${client.homeserver.toString()}/_matrix/media/r0/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=scale');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -27,7 +27,9 @@ import 'package:famedlysdk/src/database/database.dart'
|
|||
import 'package:test/test.dart';
|
||||
|
||||
import 'fake_client.dart';
|
||||
import 'fake_matrix_api.dart';
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
void main() {
|
||||
|
@ -181,10 +183,6 @@ void main() {
|
|||
await room.sendReadReceipt('§1234:fakeServer.notExisting');
|
||||
});
|
||||
|
||||
test('enableEncryption', () async {
|
||||
await room.enableEncryption();
|
||||
});
|
||||
|
||||
test('requestParticipants', () async {
|
||||
final participants = await room.requestParticipants();
|
||||
expect(participants.length, 1);
|
||||
|
@ -349,9 +347,106 @@ void main() {
|
|||
});
|
||||
|
||||
test('sendEvent', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
final dynamic resp =
|
||||
await room.sendTextEvent('Hello world', 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, {
|
||||
'body': 'Hello world',
|
||||
'msgtype': 'm.text',
|
||||
});
|
||||
});
|
||||
|
||||
test('send edit', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
final dynamic resp = await room.sendTextEvent('Hello world',
|
||||
txid: 'testtxid', editEventId: '\$otherEvent');
|
||||
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, {
|
||||
'body': '* Hello world',
|
||||
'msgtype': 'm.text',
|
||||
'm.new_content': {
|
||||
'body': 'Hello world',
|
||||
'msgtype': 'm.text',
|
||||
},
|
||||
'm.relates_to': {
|
||||
'event_id': '\$otherEvent',
|
||||
'rel_type': 'm.replace',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('send reply', () async {
|
||||
var event = Event.fromJson({
|
||||
'event_id': '\$replyEvent',
|
||||
'content': {
|
||||
'body': 'Blah',
|
||||
'msgtype': 'm.text',
|
||||
},
|
||||
'type': 'm.room.message',
|
||||
'sender': '@alice:example.org',
|
||||
}, room);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
final dynamic resp = await room.sendTextEvent('Hello world',
|
||||
txid: 'testtxid', inReplyTo: event);
|
||||
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, {
|
||||
'body': '> <@alice:example.org> Blah\n\nHello world',
|
||||
'msgtype': 'm.text',
|
||||
'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',
|
||||
'm.relates_to': {
|
||||
'm.in_reply_to': {
|
||||
'event_id': '\$replyEvent',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('send reaction', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
final dynamic resp =
|
||||
await room.sendReaction('\$otherEvent', '🦊', txid: 'testtxid');
|
||||
expect(resp.startsWith('\$event'), true);
|
||||
final entry = FakeMatrixApi.calledEndpoints.entries
|
||||
.firstWhere((p) => p.key.contains('/send/m.reaction/'));
|
||||
final content = json.decode(entry.value.first);
|
||||
expect(content, {
|
||||
'm.relates_to': {
|
||||
'event_id': '\$otherEvent',
|
||||
'rel_type': 'm.annotation',
|
||||
'key': '🦊',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('send location', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
|
||||
final body = 'Middle of the ocean';
|
||||
final geoUri = 'geo:0.0,0.0';
|
||||
final dynamic resp =
|
||||
await room.sendLocation(body, geoUri, 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, {
|
||||
'msgtype': 'm.location',
|
||||
'body': body,
|
||||
'geo_uri': geoUri,
|
||||
});
|
||||
});
|
||||
|
||||
// Not working because there is no real file to test it...
|
||||
|
@ -375,6 +470,17 @@ void main() {
|
|||
expect(room.pushRuleState, PushRuleState.dont_notify);
|
||||
});
|
||||
|
||||
test('Test call methods', () async {
|
||||
await room.inviteToCall('1234', 1234, 'sdp', txid: '1234');
|
||||
await room.answerCall('1234', 'sdp', txid: '1234');
|
||||
await room.hangupCall('1234', txid: '1234');
|
||||
await room.sendCallCandidates('1234', [], txid: '1234');
|
||||
});
|
||||
|
||||
test('enableEncryption', () async {
|
||||
await room.enableEncryption();
|
||||
});
|
||||
|
||||
test('Enable encryption', () async {
|
||||
room.setState(
|
||||
Event(
|
||||
|
@ -402,13 +508,6 @@ void main() {
|
|||
await room.setPushRuleState(PushRuleState.notify);
|
||||
});
|
||||
|
||||
test('Test call methods', () async {
|
||||
await room.inviteToCall('1234', 1234, 'sdp', txid: '1234');
|
||||
await room.answerCall('1234', 'sdp', txid: '1234');
|
||||
await room.hangupCall('1234', txid: '1234');
|
||||
await room.sendCallCandidates('1234', [], txid: '1234');
|
||||
});
|
||||
|
||||
test('Test tag methods', () async {
|
||||
await room.addTag(TagType.Favourite, order: 0.1);
|
||||
await room.removeTag(TagType.Favourite);
|
||||
|
|
166
test/sync_filter_test.dart
Normal file
166
test/sync_filter_test.dart
Normal file
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* Ansible inventory script used at Famedly GmbH for managing many hosts
|
||||
* Copyright (C) 2020 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 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
const UPDATES = {
|
||||
'empty': {
|
||||
'next_batch': 'blah',
|
||||
'account_data': {
|
||||
'events': [],
|
||||
},
|
||||
'presences': {
|
||||
'events': [],
|
||||
},
|
||||
'rooms': {
|
||||
'join': {},
|
||||
'leave': {},
|
||||
'invite': {},
|
||||
},
|
||||
'to_device': {
|
||||
'events': [],
|
||||
},
|
||||
},
|
||||
'presence': {
|
||||
'next_batch': 'blah',
|
||||
'presence': {
|
||||
'events': [
|
||||
{
|
||||
'content': {
|
||||
'avatar_url': 'mxc://localhost:wefuiwegh8742w',
|
||||
'last_active_ago': 2478593,
|
||||
'presence': 'online',
|
||||
'currently_active': false,
|
||||
'status_msg': 'Making cupcakes'
|
||||
},
|
||||
'type': 'm.presence',
|
||||
'sender': '@example:localhost',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'account_data': {
|
||||
'next_batch': 'blah',
|
||||
'account_data': {
|
||||
'events': [
|
||||
{
|
||||
'type': 'blah',
|
||||
'content': {
|
||||
'beep': 'boop',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'invite': {
|
||||
'next_batch': 'blah',
|
||||
'rooms': {
|
||||
'invite': {
|
||||
'!room': {
|
||||
'invite_state': {
|
||||
'events': [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'leave': {
|
||||
'next_batch': 'blah',
|
||||
'rooms': {
|
||||
'leave': {
|
||||
'!room': <String, dynamic>{},
|
||||
},
|
||||
},
|
||||
},
|
||||
'join': {
|
||||
'next_batch': 'blah',
|
||||
'rooms': {
|
||||
'join': {
|
||||
'!room': {
|
||||
'timeline': {
|
||||
'events': [],
|
||||
},
|
||||
'state': {
|
||||
'events': [],
|
||||
},
|
||||
'account_data': {
|
||||
'events': [],
|
||||
},
|
||||
'ephemeral': {
|
||||
'events': [],
|
||||
},
|
||||
'unread_notifications': <String, dynamic>{},
|
||||
'summary': <String, dynamic>{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'to_device': {
|
||||
'next_batch': 'blah',
|
||||
'to_device': {
|
||||
'events': [
|
||||
{
|
||||
'type': 'beep',
|
||||
'content': {
|
||||
'blah': 'blubb',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
void testUpdates(bool Function(SyncUpdate s) test, Map<String, bool> expected) {
|
||||
for (final update in UPDATES.entries) {
|
||||
var sync = SyncUpdate.fromJson(update.value);
|
||||
expect(test(sync), expected[update.key]);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('Sync Filters', () {
|
||||
test('room update', () {
|
||||
var testFn = (SyncUpdate s) => s.hasRoomUpdate;
|
||||
final expected = {
|
||||
'empty': false,
|
||||
'presence': false,
|
||||
'account_data': true,
|
||||
'invite': true,
|
||||
'leave': true,
|
||||
'join': true,
|
||||
'to_device': true,
|
||||
};
|
||||
testUpdates(testFn, expected);
|
||||
});
|
||||
|
||||
test('presence update', () {
|
||||
var testFn = (SyncUpdate s) => s.hasPresenceUpdate;
|
||||
final expected = {
|
||||
'empty': false,
|
||||
'presence': true,
|
||||
'account_data': false,
|
||||
'invite': false,
|
||||
'leave': false,
|
||||
'join': false,
|
||||
'to_device': false,
|
||||
};
|
||||
testUpdates(testFn, expected);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -33,7 +33,8 @@ void main() {
|
|||
var updateCount = 0;
|
||||
var insertList = <int>[];
|
||||
|
||||
var client = Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
||||
var client = Client('testclient',
|
||||
httpClient: FakeMatrixApi(), sendMessageTimeoutSeconds: 5);
|
||||
|
||||
var room = Room(
|
||||
id: roomID, client: client, prev_batch: '1234', roomAccountData: {});
|
||||
|
@ -186,8 +187,12 @@ void main() {
|
|||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
|
||||
expect(updateCount, 7);
|
||||
await room.sendTextEvent('test', txid: 'errortxid');
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
|
||||
expect(updateCount, 9);
|
||||
await room.sendTextEvent('test', txid: 'errortxid2');
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
await room.sendTextEvent('test', txid: 'errortxid3');
|
||||
|
@ -214,29 +219,47 @@ void main() {
|
|||
});
|
||||
|
||||
test('Resend message', () async {
|
||||
await timeline.events[0].sendAgain(txid: '1234');
|
||||
timeline.events.clear();
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: roomID,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||
'sender': '@alice:example.com',
|
||||
'status': -1,
|
||||
'event_id': 'new-test-event',
|
||||
'origin_server_ts': testTimeStamp,
|
||||
'unsigned': {'transaction_id': 'newresend'},
|
||||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(timeline.events[0].status, -1);
|
||||
await timeline.events[0].sendAgain();
|
||||
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
|
||||
expect(updateCount, 17);
|
||||
|
||||
expect(insertList, [0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
expect(timeline.events.length, 6);
|
||||
expect(timeline.events.length, 1);
|
||||
expect(timeline.events[0].status, 1);
|
||||
});
|
||||
|
||||
test('Request history', () async {
|
||||
timeline.events.clear();
|
||||
await room.requestHistory();
|
||||
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
|
||||
expect(updateCount, 20);
|
||||
expect(timeline.events.length, 9);
|
||||
expect(timeline.events[6].eventId, '3143273582443PhrSn:example.org');
|
||||
expect(timeline.events[7].eventId, '2143273582443PhrSn:example.org');
|
||||
expect(timeline.events[8].eventId, '1143273582443PhrSn:example.org');
|
||||
expect(timeline.events.length, 3);
|
||||
expect(timeline.events[0].eventId, '3143273582443PhrSn:example.org');
|
||||
expect(timeline.events[1].eventId, '2143273582443PhrSn:example.org');
|
||||
expect(timeline.events[2].eventId, '1143273582443PhrSn:example.org');
|
||||
expect(room.prev_batch, 't47409-4357353_219380_26003_2265');
|
||||
await timeline.events[8].redact(reason: 'test', txid: '1234');
|
||||
await timeline.events[2].redact(reason: 'test', txid: '1234');
|
||||
});
|
||||
|
||||
test('Clear cache on limited timeline', () async {
|
||||
|
@ -251,5 +274,293 @@ void main() {
|
|||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(timeline.events.isEmpty, true);
|
||||
});
|
||||
|
||||
test('sort errors on top', () async {
|
||||
timeline.events.clear();
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: roomID,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||
'sender': '@alice:example.com',
|
||||
'status': -1,
|
||||
'event_id': 'abc',
|
||||
'origin_server_ts': testTimeStamp
|
||||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: roomID,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||
'sender': '@alice:example.com',
|
||||
'status': 2,
|
||||
'event_id': 'def',
|
||||
'origin_server_ts': testTimeStamp + 5
|
||||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(timeline.events[0].status, -1);
|
||||
expect(timeline.events[1].status, 2);
|
||||
});
|
||||
|
||||
test('sending event to failed update', () async {
|
||||
timeline.events.clear();
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: roomID,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||
'sender': '@alice:example.com',
|
||||
'status': 0,
|
||||
'event_id': 'will-fail',
|
||||
'origin_server_ts': testTimeStamp
|
||||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(timeline.events[0].status, 0);
|
||||
expect(timeline.events.length, 1);
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: roomID,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||
'sender': '@alice:example.com',
|
||||
'status': -1,
|
||||
'event_id': 'will-fail',
|
||||
'origin_server_ts': testTimeStamp
|
||||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(timeline.events[0].status, -1);
|
||||
expect(timeline.events.length, 1);
|
||||
});
|
||||
test('sending an event and the http request finishes first, 0 -> 1 -> 2',
|
||||
() async {
|
||||
timeline.events.clear();
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: roomID,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||
'sender': '@alice:example.com',
|
||||
'status': 0,
|
||||
'event_id': 'transaction',
|
||||
'origin_server_ts': testTimeStamp
|
||||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(timeline.events[0].status, 0);
|
||||
expect(timeline.events.length, 1);
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: roomID,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||
'sender': '@alice:example.com',
|
||||
'status': 1,
|
||||
'event_id': '\$event',
|
||||
'origin_server_ts': testTimeStamp,
|
||||
'unsigned': {'transaction_id': 'transaction'}
|
||||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(timeline.events[0].status, 1);
|
||||
expect(timeline.events.length, 1);
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: roomID,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||
'sender': '@alice:example.com',
|
||||
'status': 2,
|
||||
'event_id': '\$event',
|
||||
'origin_server_ts': testTimeStamp,
|
||||
'unsigned': {'transaction_id': 'transaction'}
|
||||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(timeline.events[0].status, 2);
|
||||
expect(timeline.events.length, 1);
|
||||
});
|
||||
test('sending an event where the sync reply arrives first, 0 -> 2 -> 1',
|
||||
() async {
|
||||
timeline.events.clear();
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: roomID,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||
'sender': '@alice:example.com',
|
||||
'event_id': 'transaction',
|
||||
'origin_server_ts': testTimeStamp,
|
||||
'unsigned': {
|
||||
MessageSendingStatusKey: 0,
|
||||
'transaction_id': 'transaction',
|
||||
},
|
||||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(timeline.events[0].status, 0);
|
||||
expect(timeline.events.length, 1);
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: roomID,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||
'sender': '@alice:example.com',
|
||||
'event_id': '\$event',
|
||||
'origin_server_ts': testTimeStamp,
|
||||
'unsigned': {
|
||||
'transaction_id': 'transaction',
|
||||
MessageSendingStatusKey: 2,
|
||||
},
|
||||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(timeline.events[0].status, 2);
|
||||
expect(timeline.events.length, 1);
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: roomID,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||
'sender': '@alice:example.com',
|
||||
'event_id': '\$event',
|
||||
'origin_server_ts': testTimeStamp,
|
||||
'unsigned': {
|
||||
'transaction_id': 'transaction',
|
||||
MessageSendingStatusKey: 1,
|
||||
},
|
||||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(timeline.events[0].status, 2);
|
||||
expect(timeline.events.length, 1);
|
||||
});
|
||||
test('sending an event 0 -> -1 -> 2', () async {
|
||||
timeline.events.clear();
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: roomID,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||
'sender': '@alice:example.com',
|
||||
'status': 0,
|
||||
'event_id': 'transaction',
|
||||
'origin_server_ts': testTimeStamp
|
||||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(timeline.events[0].status, 0);
|
||||
expect(timeline.events.length, 1);
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: roomID,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||
'sender': '@alice:example.com',
|
||||
'status': -1,
|
||||
'origin_server_ts': testTimeStamp,
|
||||
'unsigned': {'transaction_id': 'transaction'},
|
||||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(timeline.events[0].status, -1);
|
||||
expect(timeline.events.length, 1);
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: roomID,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||
'sender': '@alice:example.com',
|
||||
'status': 2,
|
||||
'event_id': '\$event',
|
||||
'origin_server_ts': testTimeStamp,
|
||||
'unsigned': {'transaction_id': 'transaction'},
|
||||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(timeline.events[0].status, 2);
|
||||
expect(timeline.events.length, 1);
|
||||
});
|
||||
test('sending an event 0 -> 2 -> -1', () async {
|
||||
timeline.events.clear();
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: roomID,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||
'sender': '@alice:example.com',
|
||||
'status': 0,
|
||||
'event_id': 'transaction',
|
||||
'origin_server_ts': testTimeStamp
|
||||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(timeline.events[0].status, 0);
|
||||
expect(timeline.events.length, 1);
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: roomID,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||
'sender': '@alice:example.com',
|
||||
'status': 2,
|
||||
'event_id': '\$event',
|
||||
'origin_server_ts': testTimeStamp,
|
||||
'unsigned': {'transaction_id': 'transaction'},
|
||||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(timeline.events[0].status, 2);
|
||||
expect(timeline.events.length, 1);
|
||||
client.onEvent.add(EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: roomID,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||
'sender': '@alice:example.com',
|
||||
'status': -1,
|
||||
'origin_server_ts': testTimeStamp,
|
||||
'unsigned': {'transaction_id': 'transaction'},
|
||||
},
|
||||
sortOrder: room.newSortOrder));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(timeline.events[0].status, 2);
|
||||
expect(timeline.events.length, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import 'fake_matrix_api.dart';
|
|||
void main() {
|
||||
/// All Tests related to the Event
|
||||
group('User', () {
|
||||
var client = Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
||||
var client = Client('testclient', httpClient: FakeMatrixApi());
|
||||
final user1 = User(
|
||||
'@alice:example.com',
|
||||
membership: 'join',
|
||||
|
@ -102,7 +102,7 @@ void main() {
|
|||
});
|
||||
test('startDirectChat', () async {
|
||||
await client.checkServer('https://fakeserver.notexisting');
|
||||
await client.login('test', '1234');
|
||||
await client.login(user: 'test', password: '1234');
|
||||
await user1.startDirectChat();
|
||||
});
|
||||
test('getPresence', () async {
|
||||
|
@ -132,6 +132,8 @@ void main() {
|
|||
await client.checkServer('https://fakeserver.notexisting');
|
||||
expect(user1.canChangePowerLevel, false);
|
||||
});
|
||||
client.dispose();
|
||||
test('dispose client', () async {
|
||||
await client.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
2
test_driver.sh
Normal file
2
test_driver.sh
Normal file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh -e
|
||||
pub run test_driver/famedlysdk_test.dart -p vm
|
|
@ -1,14 +1,11 @@
|
|||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/matrix_api.dart';
|
||||
import 'package:famedlysdk/src/utils/logs.dart';
|
||||
import '../test/fake_database.dart';
|
||||
import 'test_config.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
void main() => test();
|
||||
|
||||
const String homeserver = 'https://matrix.test.famedly.de';
|
||||
const String testUserA = '@tick:test.famedly.de';
|
||||
const String testPasswordA = 'test';
|
||||
const String testUserB = '@trick:test.famedly.de';
|
||||
const String testPasswordB = 'test';
|
||||
const String testMessage = 'Hello world';
|
||||
const String testMessage2 = 'Hello moon';
|
||||
const String testMessage3 = 'Hello sun';
|
||||
|
@ -17,186 +14,198 @@ const String testMessage5 = 'Hello earth';
|
|||
const String testMessage6 = 'Hello mars';
|
||||
|
||||
void test() async {
|
||||
print('++++ Login $testUserA ++++');
|
||||
var testClientA = Client('TestClientA', debug: false);
|
||||
testClientA.database = getDatabase();
|
||||
await testClientA.checkServer(homeserver);
|
||||
await testClientA.login(testUserA, testPasswordA);
|
||||
assert(testClientA.encryptionEnabled);
|
||||
Client testClientA, testClientB;
|
||||
|
||||
print('++++ Login $testUserB ++++');
|
||||
var testClientB = Client('TestClientB', debug: false);
|
||||
testClientB.database = getDatabase();
|
||||
await testClientB.checkServer(homeserver);
|
||||
await testClientB.login(testUserB, testPasswordA);
|
||||
assert(testClientB.encryptionEnabled);
|
||||
try {
|
||||
await olm.init();
|
||||
olm.Account();
|
||||
Logs.success('[LibOlm] Enabled');
|
||||
|
||||
print('++++ ($testUserA) Leave all rooms ++++');
|
||||
while (testClientA.rooms.isNotEmpty) {
|
||||
var room = testClientA.rooms.first;
|
||||
if (room.canonicalAlias?.isNotEmpty ?? false) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
await room.leave();
|
||||
await room.forget();
|
||||
} catch (_) {}
|
||||
}
|
||||
Logs.success('++++ Login Alice at ++++');
|
||||
testClientA = Client('TestClientA');
|
||||
testClientA.database = getDatabase();
|
||||
await testClientA.checkServer(TestUser.homeserver);
|
||||
await testClientA.login(
|
||||
user: TestUser.username, password: TestUser.password);
|
||||
assert(testClientA.encryptionEnabled);
|
||||
|
||||
print('++++ ($testUserB) Leave all rooms ++++');
|
||||
for (var i = 0; i < 3; i++) {
|
||||
if (testClientB.rooms.isNotEmpty) {
|
||||
var room = testClientB.rooms.first;
|
||||
Logs.success('++++ Login Bob ++++');
|
||||
testClientB = Client('TestClientB');
|
||||
testClientB.database = getDatabase();
|
||||
await testClientB.checkServer(TestUser.homeserver);
|
||||
await testClientB.login(
|
||||
user: TestUser.username2, password: TestUser.password);
|
||||
assert(testClientB.encryptionEnabled);
|
||||
|
||||
Logs.success('++++ (Alice) Leave all rooms ++++');
|
||||
while (testClientA.rooms.isNotEmpty) {
|
||||
var room = testClientA.rooms.first;
|
||||
if (room.canonicalAlias?.isNotEmpty ?? false) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
await room.leave();
|
||||
await room.forget();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
print('++++ Check if own olm device is verified by default ++++');
|
||||
assert(testClientA.userDeviceKeys.containsKey(testUserA));
|
||||
assert(testClientA.userDeviceKeys[testUserA].deviceKeys
|
||||
.containsKey(testClientA.deviceID));
|
||||
assert(testClientA
|
||||
.userDeviceKeys[testUserA].deviceKeys[testClientA.deviceID].verified);
|
||||
assert(!testClientA
|
||||
.userDeviceKeys[testUserA].deviceKeys[testClientA.deviceID].blocked);
|
||||
assert(testClientB.userDeviceKeys.containsKey(testUserB));
|
||||
assert(testClientB.userDeviceKeys[testUserB].deviceKeys
|
||||
.containsKey(testClientB.deviceID));
|
||||
assert(testClientB
|
||||
.userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID].verified);
|
||||
assert(!testClientB
|
||||
.userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID].blocked);
|
||||
Logs.success('++++ (Bob) Leave all rooms ++++');
|
||||
for (var i = 0; i < 3; i++) {
|
||||
if (testClientB.rooms.isNotEmpty) {
|
||||
var room = testClientB.rooms.first;
|
||||
try {
|
||||
await room.leave();
|
||||
await room.forget();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
print('++++ ($testUserA) Create room and invite $testUserB ++++');
|
||||
await testClientA.api.createRoom(invite: [testUserB]);
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
var room = testClientA.rooms.first;
|
||||
assert(room != null);
|
||||
final roomId = room.id;
|
||||
Logs.success('++++ Check if own olm device is verified by default ++++');
|
||||
assert(testClientA.userDeviceKeys.containsKey(TestUser.username));
|
||||
assert(testClientA.userDeviceKeys[TestUser.username].deviceKeys
|
||||
.containsKey(testClientA.deviceID));
|
||||
assert(testClientA.userDeviceKeys[TestUser.username]
|
||||
.deviceKeys[testClientA.deviceID].verified);
|
||||
assert(!testClientA.userDeviceKeys[TestUser.username]
|
||||
.deviceKeys[testClientA.deviceID].blocked);
|
||||
assert(testClientB.userDeviceKeys.containsKey(TestUser.username2));
|
||||
assert(testClientB.userDeviceKeys[TestUser.username2].deviceKeys
|
||||
.containsKey(testClientB.deviceID));
|
||||
assert(testClientB.userDeviceKeys[TestUser.username2]
|
||||
.deviceKeys[testClientB.deviceID].verified);
|
||||
assert(!testClientB.userDeviceKeys[TestUser.username2]
|
||||
.deviceKeys[testClientB.deviceID].blocked);
|
||||
|
||||
print('++++ ($testUserB) Join room ++++');
|
||||
var inviteRoom = testClientB.getRoomById(roomId);
|
||||
await inviteRoom.join();
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
assert(inviteRoom.membership == Membership.join);
|
||||
Logs.success('++++ (Alice) Create room and invite Bob ++++');
|
||||
await testClientA.createRoom(invite: [TestUser.username2]);
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
var room = testClientA.rooms.first;
|
||||
assert(room != null);
|
||||
final roomId = room.id;
|
||||
|
||||
print('++++ ($testUserA) Enable encryption ++++');
|
||||
assert(room.encrypted == false);
|
||||
await room.enableEncryption();
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(room.encrypted == true);
|
||||
assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) ==
|
||||
null);
|
||||
Logs.success('++++ (Bob) Join room ++++');
|
||||
var inviteRoom = testClientB.getRoomById(roomId);
|
||||
await inviteRoom.join();
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
assert(inviteRoom.membership == Membership.join);
|
||||
|
||||
print('++++ ($testUserA) Check known olm devices ++++');
|
||||
assert(testClientA.userDeviceKeys.containsKey(testUserB));
|
||||
assert(testClientA.userDeviceKeys[testUserB].deviceKeys
|
||||
.containsKey(testClientB.deviceID));
|
||||
assert(!testClientA
|
||||
.userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID].verified);
|
||||
assert(!testClientA
|
||||
.userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID].blocked);
|
||||
assert(testClientB.userDeviceKeys.containsKey(testUserA));
|
||||
assert(testClientB.userDeviceKeys[testUserA].deviceKeys
|
||||
.containsKey(testClientA.deviceID));
|
||||
assert(!testClientB
|
||||
.userDeviceKeys[testUserA].deviceKeys[testClientA.deviceID].verified);
|
||||
assert(!testClientB
|
||||
.userDeviceKeys[testUserA].deviceKeys[testClientA.deviceID].blocked);
|
||||
await testClientA.userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID]
|
||||
.setVerified(true);
|
||||
Logs.success('++++ (Alice) Enable encryption ++++');
|
||||
assert(room.encrypted == false);
|
||||
await room.enableEncryption();
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(room.encrypted == true);
|
||||
assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) ==
|
||||
null);
|
||||
|
||||
print('++++ Check if own olm device is verified by default ++++');
|
||||
assert(testClientA.userDeviceKeys.containsKey(testUserA));
|
||||
assert(testClientA.userDeviceKeys[testUserA].deviceKeys
|
||||
.containsKey(testClientA.deviceID));
|
||||
assert(testClientA
|
||||
.userDeviceKeys[testUserA].deviceKeys[testClientA.deviceID].verified);
|
||||
assert(testClientB.userDeviceKeys.containsKey(testUserB));
|
||||
assert(testClientB.userDeviceKeys[testUserB].deviceKeys
|
||||
.containsKey(testClientB.deviceID));
|
||||
assert(testClientB
|
||||
.userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID].verified);
|
||||
Logs.success('++++ (Alice) Check known olm devices ++++');
|
||||
assert(testClientA.userDeviceKeys.containsKey(TestUser.username2));
|
||||
assert(testClientA.userDeviceKeys[TestUser.username2].deviceKeys
|
||||
.containsKey(testClientB.deviceID));
|
||||
assert(!testClientA.userDeviceKeys[TestUser.username2]
|
||||
.deviceKeys[testClientB.deviceID].verified);
|
||||
assert(!testClientA.userDeviceKeys[TestUser.username2]
|
||||
.deviceKeys[testClientB.deviceID].blocked);
|
||||
assert(testClientB.userDeviceKeys.containsKey(TestUser.username));
|
||||
assert(testClientB.userDeviceKeys[TestUser.username].deviceKeys
|
||||
.containsKey(testClientA.deviceID));
|
||||
assert(!testClientB.userDeviceKeys[TestUser.username]
|
||||
.deviceKeys[testClientA.deviceID].verified);
|
||||
assert(!testClientB.userDeviceKeys[TestUser.username]
|
||||
.deviceKeys[testClientA.deviceID].blocked);
|
||||
await testClientA
|
||||
.userDeviceKeys[TestUser.username2].deviceKeys[testClientB.deviceID]
|
||||
.setVerified(true);
|
||||
|
||||
print("++++ ($testUserA) Send encrypted message: '$testMessage' ++++");
|
||||
await room.sendTextEvent(testMessage);
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) !=
|
||||
null);
|
||||
var currentSessionIdA = room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id();
|
||||
assert(room.client.encryption.keyManager
|
||||
Logs.success('++++ Check if own olm device is verified by default ++++');
|
||||
assert(testClientA.userDeviceKeys.containsKey(TestUser.username));
|
||||
assert(testClientA.userDeviceKeys[TestUser.username].deviceKeys
|
||||
.containsKey(testClientA.deviceID));
|
||||
assert(testClientA.userDeviceKeys[TestUser.username]
|
||||
.deviceKeys[testClientA.deviceID].verified);
|
||||
assert(testClientB.userDeviceKeys.containsKey(TestUser.username2));
|
||||
assert(testClientB.userDeviceKeys[TestUser.username2].deviceKeys
|
||||
.containsKey(testClientB.deviceID));
|
||||
assert(testClientB.userDeviceKeys[TestUser.username2]
|
||||
.deviceKeys[testClientB.deviceID].verified);
|
||||
|
||||
Logs.success("++++ (Alice) Send encrypted message: '$testMessage' ++++");
|
||||
await room.sendTextEvent(testMessage);
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) !=
|
||||
null);
|
||||
var currentSessionIdA = room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id();
|
||||
/*assert(room.client.encryption.keyManager
|
||||
.getInboundGroupSession(room.id, currentSessionIdA, '') !=
|
||||
null);
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
||||
1);
|
||||
assert(testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey]
|
||||
.first.sessionId ==
|
||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||
.first.sessionId);
|
||||
assert(inviteRoom.client.encryption.keyManager
|
||||
null);*/
|
||||
assert(testClientA.encryption.olmManager
|
||||
.olmSessions[testClientB.identityKey].length ==
|
||||
1);
|
||||
assert(testClientB.encryption.olmManager
|
||||
.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA.encryption.olmManager
|
||||
.olmSessions[testClientB.identityKey].first.sessionId ==
|
||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||
.first.sessionId);
|
||||
/*assert(inviteRoom.client.encryption.keyManager
|
||||
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
|
||||
null);
|
||||
assert(room.lastMessage == testMessage);
|
||||
assert(inviteRoom.lastMessage == testMessage);
|
||||
print(
|
||||
"++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
|
||||
null);*/
|
||||
assert(room.lastMessage == testMessage);
|
||||
assert(inviteRoom.lastMessage == testMessage);
|
||||
Logs.success(
|
||||
"++++ (Bob) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
|
||||
|
||||
print("++++ ($testUserA) Send again encrypted message: '$testMessage2' ++++");
|
||||
await room.sendTextEvent(testMessage2);
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
||||
1);
|
||||
assert(testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey]
|
||||
.first.sessionId ==
|
||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||
.first.sessionId);
|
||||
Logs.success(
|
||||
"++++ (Alice) Send again encrypted message: '$testMessage2' ++++");
|
||||
await room.sendTextEvent(testMessage2);
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(testClientA.encryption.olmManager
|
||||
.olmSessions[testClientB.identityKey].length ==
|
||||
1);
|
||||
assert(testClientB.encryption.olmManager
|
||||
.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA.encryption.olmManager
|
||||
.olmSessions[testClientB.identityKey].first.sessionId ==
|
||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||
.first.sessionId);
|
||||
|
||||
assert(room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id() ==
|
||||
currentSessionIdA);
|
||||
assert(room.client.encryption.keyManager
|
||||
assert(room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id() ==
|
||||
currentSessionIdA);
|
||||
/*assert(room.client.encryption.keyManager
|
||||
.getInboundGroupSession(room.id, currentSessionIdA, '') !=
|
||||
null);
|
||||
assert(room.lastMessage == testMessage2);
|
||||
assert(inviteRoom.lastMessage == testMessage2);
|
||||
print(
|
||||
"++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
|
||||
null);*/
|
||||
assert(room.lastMessage == testMessage2);
|
||||
assert(inviteRoom.lastMessage == testMessage2);
|
||||
Logs.success(
|
||||
"++++ (Bob) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
|
||||
|
||||
print("++++ ($testUserB) Send again encrypted message: '$testMessage3' ++++");
|
||||
await inviteRoom.sendTextEvent(testMessage3);
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
||||
1);
|
||||
assert(testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id() ==
|
||||
currentSessionIdA);
|
||||
var inviteRoomOutboundGroupSession = inviteRoom.client.encryption.keyManager
|
||||
.getOutboundGroupSession(inviteRoom.id);
|
||||
Logs.success(
|
||||
"++++ (Bob) Send again encrypted message: '$testMessage3' ++++");
|
||||
await inviteRoom.sendTextEvent(testMessage3);
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(testClientA.encryption.olmManager
|
||||
.olmSessions[testClientB.identityKey].length ==
|
||||
1);
|
||||
assert(testClientB.encryption.olmManager
|
||||
.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id() ==
|
||||
currentSessionIdA);
|
||||
var inviteRoomOutboundGroupSession = inviteRoom.client.encryption.keyManager
|
||||
.getOutboundGroupSession(inviteRoom.id);
|
||||
|
||||
assert(inviteRoomOutboundGroupSession != null);
|
||||
assert(inviteRoom.client.encryption.keyManager.getInboundGroupSession(
|
||||
assert(inviteRoomOutboundGroupSession != null);
|
||||
/*assert(inviteRoom.client.encryption.keyManager.getInboundGroupSession(
|
||||
inviteRoom.id,
|
||||
inviteRoomOutboundGroupSession.outboundGroupSession.session_id(),
|
||||
'') !=
|
||||
|
@ -205,146 +214,113 @@ void test() async {
|
|||
room.id,
|
||||
inviteRoomOutboundGroupSession.outboundGroupSession.session_id(),
|
||||
'') !=
|
||||
null);
|
||||
assert(inviteRoom.lastMessage == testMessage3);
|
||||
assert(room.lastMessage == testMessage3);
|
||||
print(
|
||||
"++++ ($testUserA) Received decrypted message: '${room.lastMessage}' ++++");
|
||||
null);*/
|
||||
assert(inviteRoom.lastMessage == testMessage3);
|
||||
assert(room.lastMessage == testMessage3);
|
||||
Logs.success(
|
||||
"++++ (Alice) Received decrypted message: '${room.lastMessage}' ++++");
|
||||
|
||||
print('++++ Login $testUserB in another client ++++');
|
||||
var testClientC =
|
||||
Client('TestClientC', debug: false, database: getDatabase());
|
||||
await testClientC.checkServer(homeserver);
|
||||
await testClientC.login(testUserB, testPasswordA);
|
||||
await Future.delayed(Duration(seconds: 3));
|
||||
Logs.success('++++ Login Bob in another client ++++');
|
||||
var testClientC = Client('TestClientC', database: getDatabase());
|
||||
await testClientC.checkServer(TestUser.homeserver);
|
||||
await testClientC.login(
|
||||
user: TestUser.username2, password: TestUser.password);
|
||||
await Future.delayed(Duration(seconds: 3));
|
||||
|
||||
print("++++ ($testUserA) Send again encrypted message: '$testMessage4' ++++");
|
||||
await room.sendTextEvent(testMessage4);
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
||||
1);
|
||||
assert(testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey]
|
||||
.first.sessionId ==
|
||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||
.first.sessionId);
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientC.identityKey].length ==
|
||||
1);
|
||||
assert(testClientC
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA.encryption.olmManager.olmSessions[testClientC.identityKey]
|
||||
.first.sessionId ==
|
||||
testClientC.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||
.first.sessionId);
|
||||
assert(room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id() !=
|
||||
currentSessionIdA);
|
||||
currentSessionIdA = room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id();
|
||||
assert(inviteRoom.client.encryption.keyManager
|
||||
Logs.success(
|
||||
"++++ (Alice) Send again encrypted message: '$testMessage4' ++++");
|
||||
await room.sendTextEvent(testMessage4);
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(testClientA.encryption.olmManager
|
||||
.olmSessions[testClientB.identityKey].length ==
|
||||
1);
|
||||
assert(testClientB.encryption.olmManager
|
||||
.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA.encryption.olmManager
|
||||
.olmSessions[testClientB.identityKey].first.sessionId ==
|
||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||
.first.sessionId);
|
||||
assert(testClientA.encryption.olmManager
|
||||
.olmSessions[testClientC.identityKey].length ==
|
||||
1);
|
||||
assert(testClientC.encryption.olmManager
|
||||
.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA.encryption.olmManager
|
||||
.olmSessions[testClientC.identityKey].first.sessionId ==
|
||||
testClientC.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||
.first.sessionId);
|
||||
assert(room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id() !=
|
||||
currentSessionIdA);
|
||||
currentSessionIdA = room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id();
|
||||
/*assert(inviteRoom.client.encryption.keyManager
|
||||
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
|
||||
null);
|
||||
assert(room.lastMessage == testMessage4);
|
||||
assert(inviteRoom.lastMessage == testMessage4);
|
||||
print(
|
||||
"++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
|
||||
null);*/
|
||||
assert(room.lastMessage == testMessage4);
|
||||
assert(inviteRoom.lastMessage == testMessage4);
|
||||
Logs.success(
|
||||
"++++ (Bob) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
|
||||
|
||||
print('++++ Logout $testUserB another client ++++');
|
||||
await testClientC.dispose();
|
||||
await testClientC.logout();
|
||||
testClientC = null;
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
Logs.success('++++ Logout Bob another client ++++');
|
||||
await testClientC.dispose();
|
||||
await testClientC.logout();
|
||||
testClientC = null;
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
|
||||
print("++++ ($testUserA) Send again encrypted message: '$testMessage6' ++++");
|
||||
await room.sendTextEvent(testMessage6);
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(testClientA
|
||||
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
||||
1);
|
||||
assert(testClientB
|
||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey]
|
||||
.first.sessionId ==
|
||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||
.first.sessionId);
|
||||
assert(room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id() !=
|
||||
currentSessionIdA);
|
||||
currentSessionIdA = room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id();
|
||||
assert(inviteRoom.client.encryption.keyManager
|
||||
Logs.success(
|
||||
"++++ (Alice) Send again encrypted message: '$testMessage6' ++++");
|
||||
await room.sendTextEvent(testMessage6);
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(testClientA.encryption.olmManager
|
||||
.olmSessions[testClientB.identityKey].length ==
|
||||
1);
|
||||
assert(testClientB.encryption.olmManager
|
||||
.olmSessions[testClientA.identityKey].length ==
|
||||
1);
|
||||
assert(testClientA.encryption.olmManager
|
||||
.olmSessions[testClientB.identityKey].first.sessionId ==
|
||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||
.first.sessionId);
|
||||
assert(room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id() !=
|
||||
currentSessionIdA);
|
||||
currentSessionIdA = room.client.encryption.keyManager
|
||||
.getOutboundGroupSession(room.id)
|
||||
.outboundGroupSession
|
||||
.session_id();
|
||||
/*assert(inviteRoom.client.encryption.keyManager
|
||||
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
|
||||
null);
|
||||
assert(room.lastMessage == testMessage6);
|
||||
assert(inviteRoom.lastMessage == testMessage6);
|
||||
print(
|
||||
"++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
|
||||
null);*/
|
||||
assert(room.lastMessage == testMessage6);
|
||||
assert(inviteRoom.lastMessage == testMessage6);
|
||||
Logs.success(
|
||||
"++++ (Bob) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
|
||||
|
||||
/* print('++++ ($testUserA) Restore user ++++');
|
||||
await testClientA.dispose();
|
||||
testClientA = null;
|
||||
testClientA = Client(
|
||||
'TestClientA',
|
||||
debug: false,
|
||||
database: getDatabase(),
|
||||
);
|
||||
testClientA.connect();
|
||||
await Future.delayed(Duration(seconds: 3));
|
||||
var restoredRoom = testClientA.rooms.first;
|
||||
assert(room != null);
|
||||
assert(restoredRoom.id == room.id);
|
||||
assert(restoredRoom.outboundGroupSession.session_id() ==
|
||||
room.outboundGroupSession.session_id());
|
||||
assert(restoredRoom.inboundGroupSessions.length == 4);
|
||||
assert(restoredRoom.inboundGroupSessions.length ==
|
||||
room.inboundGroupSessions.length);
|
||||
for (var i = 0; i < restoredRoom.inboundGroupSessions.length; i++) {
|
||||
assert(restoredRoom.inboundGroupSessions.keys.toList()[i] ==
|
||||
room.inboundGroupSessions.keys.toList()[i]);
|
||||
await room.leave();
|
||||
await room.forget();
|
||||
await inviteRoom.leave();
|
||||
await inviteRoom.forget();
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
} catch (e, s) {
|
||||
Logs.error('Test failed: ${e.toString()}', s);
|
||||
rethrow;
|
||||
} finally {
|
||||
Logs.success('++++ Logout Alice and Bob ++++');
|
||||
if (testClientA?.isLogged() ?? false) await testClientA.logoutAll();
|
||||
if (testClientA?.isLogged() ?? false) await testClientB.logoutAll();
|
||||
await testClientA?.dispose();
|
||||
await testClientB?.dispose();
|
||||
testClientA = null;
|
||||
testClientB = null;
|
||||
}
|
||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].length == 1);
|
||||
assert(testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].length == 1);
|
||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].first.session_id() ==
|
||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].first.session_id());
|
||||
|
||||
print("++++ ($testUserA) Send again encrypted message: '$testMessage5' ++++");
|
||||
await restoredRoom.sendTextEvent(testMessage5);
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].length == 1);
|
||||
assert(testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].length == 1);
|
||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].first.session_id() ==
|
||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].first.session_id());
|
||||
assert(restoredRoom.lastMessage == testMessage5);
|
||||
assert(inviteRoom.lastMessage == testMessage5);
|
||||
assert(testClientB.getRoomById(roomId).lastMessage == testMessage5);
|
||||
print(
|
||||
"++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++");*/
|
||||
|
||||
print('++++ Logout $testUserA and $testUserB ++++');
|
||||
await room.leave();
|
||||
await room.forget();
|
||||
await inviteRoom.leave();
|
||||
await inviteRoom.forget();
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
await testClientA.dispose();
|
||||
await testClientB.dispose();
|
||||
await testClientA.api.logoutAll();
|
||||
await testClientB.api.logoutAll();
|
||||
testClientA = null;
|
||||
testClientB = null;
|
||||
return;
|
||||
}
|
||||
|
|
6
test_driver/test_config.dart
Normal file
6
test_driver/test_config.dart
Normal file
|
@ -0,0 +1,6 @@
|
|||
class TestUser {
|
||||
static const String homeserver = 'https://enter-your-server.here';
|
||||
static const String username = 'alice';
|
||||
static const String username2 = 'bob';
|
||||
static const String password = '1234';
|
||||
}
|
Loading…
Reference in a new issue