Merge branch 'main' of https://gitlab.com/famedly/famedlysdk into yiffed

This commit is contained in:
Inex Code 2020-08-12 15:31:47 +00:00
commit 327334bd58
56 changed files with 2151 additions and 657 deletions

View File

@ -68,7 +68,7 @@ build-api-doc:
paths:
- doc/api/
only:
- master
- main
build-doc:
tags:
@ -83,7 +83,7 @@ build-doc:
paths:
- doc-public
only:
- master
- main
pages:
tags:
@ -101,4 +101,4 @@ pages:
paths:
- public
only:
- master
- main

View File

@ -3,9 +3,11 @@ 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
- lib/src/utils/logs.dart

264
example/main.dart Normal file
View 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,
)
],
),
),
],
),
);
}
}

View File

@ -167,7 +167,7 @@ class CrossSigning {
}
if (signedKeys.isNotEmpty) {
// post our new keys!
await client.api.uploadKeySignatures(signedKeys);
await client.uploadKeySignatures(signedKeys);
}
}

View File

@ -72,12 +72,16 @@ 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(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
@ -263,6 +267,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,

View File

@ -18,6 +18,7 @@
import 'dart:convert';
import 'package:famedlysdk/src/utils/logs.dart';
import 'package:pedantic/pedantic.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/matrix_api.dart';
@ -43,7 +44,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 client.getRoomKeysBackup();
if (info.algorithm != RoomKeysAlgorithmType.v1Curve25519AesSha2) {
return false;
}
@ -84,10 +85,11 @@ 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(
@ -123,6 +125,8 @@ class KeyManager {
json.encode(content),
json.encode({}),
);
// Note to self: When adding key-backup that needs to be unawaited(), else
// we might accidentally end up with http requests inside of the sync loop
// TODO: somehow try to decrypt last message again
final room = client.getRoomById(roomId);
if (room != null) {
@ -261,10 +265,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 +288,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;
}
@ -334,7 +339,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 client.getRoomKeysBackup();
String backupPubKey;
try {
backupPubKey = decryption.init_with_private_key(privateKey);
@ -363,8 +368,9 @@ 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;
@ -381,9 +387,9 @@ class KeyManager {
}
Future<void> loadSingleKey(String roomId, String sessionId) async {
final info = await client.api.getRoomKeysBackup();
final info = await client.getRoomKeysBackup();
final ret =
await client.api.getRoomKeysSingleKey(roomId, sessionId, info.version);
await client.getRoomKeysSingleKey(roomId, sessionId, info.version);
final keys = RoomKeys.fromJson({
'rooms': {
roomId: {
@ -406,28 +412,31 @@ class KeyManager {
try {
await loadSingleKey(room.id, sessionId);
} catch (err, stacktrace) {
print('[KeyManager] Failed to access online key backup: ' +
err.toString());
print(stacktrace);
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();
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 +449,14 @@ 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);
}
}
/// Handle an incoming to_device event that is related to key sharing
@ -453,27 +467,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 +495,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 +506,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 +515,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
}
@ -553,15 +568,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;
@ -660,7 +684,7 @@ class RoomKeyRequest extends ToDeviceEvent {
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,

View File

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

View File

@ -18,6 +18,7 @@
import 'dart:convert';
import 'package:famedlysdk/src/utils/logs.dart';
import 'package:pedantic/pedantic.dart';
import 'package:canonical_json/canonical_json.dart';
import 'package:famedlysdk/famedlysdk.dart';
@ -119,9 +120,9 @@ 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();
}
@ -182,7 +183,7 @@ class OlmManager {
signJson(keysContent['device_keys'] as Map<String, dynamic>);
}
final response = await client.api.uploadDeviceKeys(
final response = await client.uploadDeviceKeys(
deviceKeys: uploadDeviceKeys
? MatrixDeviceKeys.fromJson(keysContent['device_keys'])
: null,
@ -231,7 +232,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 +335,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 +383,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;
@ -408,10 +409,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 +486,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;
}
}

View File

@ -22,6 +22,7 @@ import 'dart:convert';
import 'package:encrypt/encrypt.dart';
import 'package:crypto/crypto.dart';
import 'package:base58check/base58.dart';
import 'package:famedlysdk/src/utils/logs.dart';
import 'package:password_hash/password_hash.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/matrix_api.dart';
@ -221,7 +222,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
@ -253,14 +254,14 @@ class SSSS {
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}...');
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 +271,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,
@ -281,32 +282,33 @@ class SSSS {
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 +317,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 +332,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) {

View File

@ -19,6 +19,7 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:canonical_json/canonical_json.dart';
import 'package:famedlysdk/src/utils/logs.dart';
import 'package:pedantic/pedantic.dart';
import 'package:olm/olm.dart' as olm;
import 'package:famedlysdk/famedlysdk.dart';
@ -150,7 +151,7 @@ class KeyVerification {
}
void dispose() {
print('[Key Verification] disposing object...');
Logs.info('[Key Verification] disposing object...');
method?.dispose();
}
@ -202,7 +203,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 +217,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 +285,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 +299,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 +313,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 +524,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 +552,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 +569,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 +697,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');
}

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import 'package:famedlysdk/src/utils/logs.dart';
import 'package:olm/olm.dart' as olm;
import '../../src/database/database.dart' show DbOlmSessions;
@ -46,8 +47,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();
}
}

View File

@ -18,6 +18,7 @@
import 'dart:convert';
import 'package:famedlysdk/src/utils/logs.dart';
import 'package:olm/olm.dart' as olm;
import '../../src/database/database.dart' show DbOutboundGroupSession;
@ -44,10 +45,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);
}
}

View File

@ -18,6 +18,7 @@
import 'dart:convert';
import 'package:famedlysdk/src/utils/logs.dart';
import 'package:olm/olm.dart' as olm;
import 'package:famedlysdk/famedlysdk.dart';
@ -48,9 +49,11 @@ class SessionKey {
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);
}
}

View File

@ -28,6 +28,7 @@ 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/sync_update_extension.dart';
export 'package:famedlysdk/src/utils/to_device_event.dart';
export 'package:famedlysdk/src/client.dart';
export 'package:famedlysdk/src/event.dart';

View File

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

View File

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

View File

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

View File

@ -22,9 +22,9 @@ 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/logs.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;
@ -44,7 +44,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 +52,8 @@ class Client {
bool enableE2eeRecovery;
MatrixApi api;
@deprecated
MatrixApi get api => this;
Encryption encryption;
@ -81,14 +82,16 @@ 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}) {
Client(
this.clientName, {
this.database,
this.enableE2eeRecovery = false,
this.verificationMethods,
http.Client httpClient,
this.importantStateEvents,
this.pinUnreadRooms = false,
@deprecated bool debug,
}) {
verificationMethods ??= <KeyVerificationMethod>{};
importantStateEvents ??= <String>{};
importantStateEvents.addAll([
@ -100,17 +103,9 @@ class Client {
EventTypes.RoomCanonicalAlias,
EventTypes.RoomTombstone,
]);
api = MatrixApi(debug: debug, httpClient: httpClient);
onLoginStateChanged.stream.listen((loginState) {
if (debug) {
print('[LoginState]: ${loginState.toString()}');
}
});
this.httpClient = httpClient;
}
/// Whether debug prints should be displayed.
final bool debug;
/// The required name for this client.
final String clientName;
@ -130,7 +125,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 +148,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 +160,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 +240,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 +264,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 +272,7 @@ class Client {
return true;
} catch (_) {
api.homeserver = null;
homeserver = null;
rethrow;
}
}
@ -288,16 +280,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 +308,64 @@ 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();
@ -427,19 +416,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 +455,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;
}
@ -556,10 +541,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 +562,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 +591,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 +600,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 +616,15 @@ class Client {
return;
}
encryption = Encryption(
debug: debug, client: this, enableE2eeRecovery: enableE2eeRecovery);
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 +635,8 @@ class Client {
} else {
_id = await database.insertClient(
clientName,
api.homeserver.toString(),
api.accessToken,
homeserver.toString(),
accessToken,
_userID,
_deviceID,
_deviceName,
@ -671,6 +652,9 @@ class Client {
}
onLoginStateChanged.add(LoginState.logged);
Logs.success(
'Successfully connected as ${userID.localpart} with ${homeserver.toString()}',
);
return _sync();
}
@ -683,8 +667,8 @@ 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;
@ -697,13 +681,11 @@ class Client {
Future<void> _sync() async {
if (isLogged() == false || _disposed) return;
try {
_syncRequest = api
.sync(
_syncRequest = sync(
filter: syncFilters,
since: prevBatch,
timeout: prevBatch != null ? 30000 : null,
)
.catchError((e) {
).catchError((e) {
_lastSyncError = e;
return null;
});
@ -741,8 +723,7 @@ class Client {
if (isLogged() == false || _disposed) {
return;
}
print('Error during processing events: ' + e.toString());
print(s);
Logs.error('Error during processing events: ' + e.toString(), s);
onSyncError.add(SyncError(
exception: e is Exception ? e : Exception(e), stackTrace: s));
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
@ -821,10 +802,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),
@ -1160,11 +1141,16 @@ class Client {
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);
}
}
}
@ -1196,8 +1182,7 @@ class Client {
if (outdatedLists.isNotEmpty) {
// Request the missing device key lists from the server.
final response =
await api.requestDeviceKeys(outdatedLists, timeout: 10000);
final response = await requestDeviceKeys(outdatedLists, timeout: 10000);
for (final rawDeviceKeyListEntry in response.deviceKeys.entries) {
final userId = rawDeviceKeyListEntry.key;
@ -1332,27 +1317,49 @@ class Client {
}
}
}
await database?.transaction(() async {
for (final f in dbActions) {
await f();
}
});
} 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 +1370,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 +1401,7 @@ class Client {
}
Future<void> setMuteAllPushNotifications(bool muted) async {
await api.enablePushRule(
await enablePushRule(
'global',
PushRuleKind.override,
'.m.rule.master',
@ -1427,6 +1411,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 +1422,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;

View File

@ -1,3 +1,4 @@
import 'package:famedlysdk/src/utils/logs.dart';
import 'package:moor/moor.dart';
import 'dart:convert';
@ -6,6 +7,7 @@ import 'package:famedlysdk/matrix_api.dart' as api;
import 'package:olm/olm.dart' as olm;
import '../../matrix_api.dart';
import '../room.dart';
part 'database.g.dart';
@ -65,7 +67,7 @@ class Database extends _$Database {
if (executor.dialect == SqlDialect.sqlite) {
final ret = await customSelect('PRAGMA journal_mode=WAL').get();
if (ret.isNotEmpty) {
print('[Moor] Switched database to mode ' +
Logs.info('[Moor] Switched database to mode ' +
ret.first.data['journal_mode'].toString());
}
}
@ -112,8 +114,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;
@ -357,13 +360,40 @@ 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) &&
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);
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 {
await updateEventStatus(status, eventContent['event_id'], clientId,
eventContent['unsigned']['transaction_id'], chatId);
} 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
}
}
} else {
DbEvent oldEvent;
if (type == 'history') {

View File

@ -6033,6 +6033,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'),

View File

@ -208,6 +208,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;

View File

@ -20,6 +20,7 @@ import 'dart:convert';
import 'dart:typed_data';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/encryption.dart';
import 'package:famedlysdk/src/utils/logs.dart';
import 'package:famedlysdk/src/utils/receipt.dart';
import 'package:http/http.dart' as http;
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
@ -28,6 +29,12 @@ import './room.dart';
import 'utils/matrix_localizations.dart';
import './database/database.dart' show DbRoomState, DbEvent;
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 {
User get sender => room.getUserByMXIDSync(senderId ?? '@unknown');
@ -90,12 +97,18 @@ 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['prev_content'] is Map)
? unsigned['prev_content']
: null;
} catch (e, s) {
Logs.error('Event constructor crashed: ${e.toString()}', s);
}
this.stateKey = stateKey;
this.originServerTs = originServerTs;
}
@ -140,7 +153,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,10 +227,7 @@ 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
: content['msgtype'] ?? MessageTypes.Text;
String get messageType => content['msgtype'] ?? MessageTypes.Text;
void setRedactionEvent(Event redactedBecause) {
unsigned = {
@ -312,12 +324,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 +340,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
@ -480,9 +483,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 +519,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) {
@ -631,7 +630,6 @@ class Event extends MatrixEvent {
case MessageTypes.Text:
case MessageTypes.Notice:
case MessageTypes.None:
case MessageTypes.Reply:
localizedBody = body;
break;
}
@ -660,9 +658,85 @@ 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;
}
}

View File

@ -23,6 +23,7 @@ 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/logs.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';
@ -39,6 +40,8 @@ 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) &&
@ -369,21 +374,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 +396,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 +423,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},
@ -500,6 +505,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 +524,20 @@ 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 a [file] to this room after uploading it. Returns the mxc uri of
@ -529,6 +548,7 @@ class Room {
MatrixFile file, {
String txid,
Event inReplyTo,
String editEventId,
bool waitUntilSent = false,
MatrixImageFile thumbnail,
}) async {
@ -545,13 +565,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 +625,7 @@ class Room {
content,
txid: txid,
inReplyTo: inReplyTo,
editEventId: editEventId,
);
if (waitUntilSent) {
await sendResponse;
@ -615,7 +636,7 @@ class Room {
/// 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 {
{String type, String txid, Event inReplyTo, String editEventId}) async {
type = type ?? EventTypes.Message;
final sendType =
(encrypted && client.encryptionEnabled) ? EventTypes.Encrypted : type;
@ -645,28 +666,38 @@ class Room {
},
};
}
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 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();
});
final syncUpdate = SyncUpdate()
..rooms = (RoomsUpdate()
..join = (<String, JoinedRoomUpdate>{}..[id] = (JoinedRoomUpdate()
..timeline = (TimelineUpdate()
..events = [
MatrixEvent()
..content = content
..type = type
..eventId = messageID
..senderId = client.userID
..originServerTs = DateTime.now()
..unsigned = {
MessageSendingStatusKey: 0,
'transaction_id': messageID,
},
]))));
await client.handleSync(syncUpdate);
// Send the text and on success, store and display a *sent* event.
try {
@ -674,29 +705,23 @@ class Room {
? await client.encryption
.encryptGroupMessagePayload(id, content, type: type)
: content;
final res = await client.api.sendMessage(
final res = await client.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);
});
syncUpdate.rooms.join.values.first.timeline.events.first
.unsigned[MessageSendingStatusKey] = 1;
syncUpdate.rooms.join.values.first.timeline.events.first.eventId = res;
await client.handleSync(syncUpdate);
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);
});
} catch (e, s) {
Logs.warning(
'[Client] Problem while sending message: ' + e.toString(), s);
syncUpdate.rooms.join.values.first.timeline.events.first
.unsigned[MessageSendingStatusKey] = -1;
await client.handleSync(syncUpdate);
}
return null;
}
@ -706,7 +731,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 +757,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 +787,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 +795,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 +853,7 @@ class Room {
directChats[userID] = [id];
}
await client.api.setAccountData(
await client.setAccountData(
client.userID,
'm.direct',
directChats,
@ -846,7 +871,7 @@ class Room {
return;
} // Nothing to do here
await client.api.setRoomAccountData(
await client.setRoomAccountData(
client.userID,
id,
'm.direct',
@ -859,7 +884,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,
@ -992,7 +1017,7 @@ class Room {
}
}
if (participantListComplete) return getParticipants();
final matrixEvents = await client.api.requestMembers(id);
final matrixEvents = await client.requestMembers(id);
final users =
matrixEvents.map((e) => Event.fromMatrixEvent(e, this).asUser).toList();
for (final user in users) {
@ -1055,7 +1080,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,7 +1093,7 @@ 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,
@ -1110,7 +1135,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 +1169,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 +1267,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 +1294,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 +1322,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 +1335,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,7 +1349,7 @@ class Room {
{String type = 'offer', int version = 0, String txid}) async {
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
return await client.api.sendMessage(
return await client.sendMessage(
id,
EventTypes.CallInvite,
txid,
@ -1362,7 +1387,7 @@ class Room {
String txid,
}) async {
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
return await client.api.sendMessage(
return await client.sendMessage(
id,
EventTypes.CallCandidates,
txid,
@ -1382,7 +1407,7 @@ 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(
return await client.sendMessage(
id,
EventTypes.CallAnswer,
txid,
@ -1400,7 +1425,7 @@ class Room {
Future<String> hangupCall(String callId,
{int version = 0, String txid}) async {
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
return await client.api.sendMessage(
return await client.sendMessage(
id,
EventTypes.CallHangup,
txid,
@ -1436,7 +1461,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 +1486,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 +1512,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 +1539,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,
{

View File

@ -19,6 +19,7 @@
import 'dart:async';
import 'package:famedlysdk/matrix_api.dart';
import 'package:famedlysdk/src/utils/logs.dart';
import 'event.dart';
import 'room.dart';
@ -35,6 +36,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 +70,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 +89,17 @@ 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);
}
}
/// Don't forget to call this before you dismiss this object!
@ -122,33 +137,97 @@ 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'] ?? 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,41 +235,36 @@ class Timeline {
: null);
if (i < events.length) {
// we want to preserve the old sort order
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()}');
}
} catch (e, s) {
Logs.warning('Handle event update failed: ${e.toString()}', s);
}
}

View File

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

View File

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

30
lib/src/utils/logs.dart Normal file
View File

@ -0,0 +1,30 @@
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] ';
static void info(dynamic info) => print(
_prefixText + _infoPen(info.toString()),
);
static void success(dynamic obj, [dynamic stackTrace]) => print(
_prefixText + _successPen(obj.toString()),
);
static void warning(dynamic warning, [dynamic stackTrace]) => print(
_prefixText +
_warningPen(warning.toString()) +
(stackTrace != null ? '\n${stackTrace.toString()}' : ''),
);
static void error(dynamic obj, [dynamic stackTrace]) => print(
_prefixText +
_errorPen(obj.toString()) +
(stackTrace != null ? '\n${stackTrace.toString()}' : ''),
);
}

View File

@ -65,6 +65,7 @@ class EmoteSyntax extends InlineSyntax {
return true;
}
final element = Element.empty('img');
element.attributes['data-mx-emote'] = '';
element.attributes['src'] = htmlEscape.convert(mxc);
element.attributes['alt'] = htmlEscape.convert(emote);
element.attributes['title'] = htmlEscape.convert(emote);

View File

@ -1,6 +1,7 @@
/// Workaround until [File] in dart:io and dart:html is unified
import 'dart:typed_data';
import 'package:famedlysdk/matrix_api/model/message_types.dart';
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
import 'package:mime/mime.dart';
@ -22,7 +23,18 @@ class MatrixFile {
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,

View 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 'package:famedlysdk/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;
}

View File

@ -22,8 +22,8 @@ import 'dart:core';
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 +36,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'
: '';
}
}

View File

@ -22,6 +22,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.2"
ansicolor:
dependency: "direct main"
description:
name: ansicolor
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
args:
dependency: transitive
description:

View File

@ -21,6 +21,7 @@ dependencies:
password_hash: ^2.0.0
olm: ^1.2.1
matrix_file_e2ee: ^1.0.4
ansicolor: ^1.0.2
dev_dependencies:
test: ^1.0.0

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,9 +46,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;
@ -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')),

View File

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

View File

@ -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;
@ -32,9 +33,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;

View File

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

View File

@ -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;
@ -37,9 +38,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 +90,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',

View File

@ -50,7 +50,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 +67,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);
@ -160,7 +160,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 +211,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 +235,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 +244,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 +260,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 +307,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 +822,143 @@ 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');
});
});
}

View File

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

View File

@ -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},
@ -1982,21 +1986,21 @@ class FakeMatrixApi extends MockClient {
(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.room.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.room.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.room.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.room.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 +2010,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 +2046,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 +2095,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':

View File

@ -54,11 +54,11 @@ void main() {
});
test('emotes', () {
expect(markdown(':fox:', emotePacks),
'<img src="mxc:&#47;&#47;roomfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
'<img data-mx-emote="" src="mxc:&#47;&#47;roomfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
expect(markdown(':user~fox:', emotePacks),
'<img src="mxc:&#47;&#47;userfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
'<img data-mx-emote="" src="mxc:&#47;&#47;userfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
expect(markdown(':raccoon:', emotePacks),
'<img src="mxc:&#47;&#47;raccoon" alt=":raccoon:" title=":raccoon:" height="32" vertical-align="middle" />');
'<img data-mx-emote="" src="mxc:&#47;&#47;raccoon" alt=":raccoon:" title=":raccoon:" height="32" vertical-align="middle" />');
expect(markdown(':invalid:', emotePacks), ':invalid:');
expect(markdown(':room~invalid:', emotePacks), ':room~invalid:');
});

View File

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

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

View File

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

View File

@ -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() {
@ -349,9 +351,87 @@ 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': '🦊',
},
});
});
// Not working because there is no real file to test it...

166
test/sync_filter_test.dart Normal file
View 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);
});
});
}

View File

@ -33,7 +33,7 @@ void main() {
var updateCount = 0;
var insertList = <int>[];
var client = Client('testclient', debug: true, httpClient: FakeMatrixApi());
var client = Client('testclient', httpClient: FakeMatrixApi());
var room = Room(
id: roomID, client: client, prev_batch: '1234', roomAccountData: {});
@ -186,8 +186,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,14 +218,29 @@ void main() {
});
test('Resend message', () async {
await timeline.events[0].sendAgain(txid: '1234');
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));
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, 7);
expect(timeline.events[0].status, 1);
});
@ -231,12 +250,12 @@ void main() {
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, 10);
expect(timeline.events[7].eventId, '3143273582443PhrSn:example.org');
expect(timeline.events[8].eventId, '2143273582443PhrSn:example.org');
expect(timeline.events[9].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[9].redact(reason: 'test', txid: '1234');
});
test('Clear cache on limited timeline', () async {
@ -251,5 +270,253 @@ void main() {
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events.isEmpty, true);
});
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',
'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,
'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 -> -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);
});
});
}

View File

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

View File

@ -1,5 +1,6 @@
import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/matrix_api.dart';
import 'package:famedlysdk/src/utils/logs.dart';
import '../test/fake_database.dart';
void main() => test();
@ -17,21 +18,21 @@ const String testMessage5 = 'Hello earth';
const String testMessage6 = 'Hello mars';
void test() async {
print('++++ Login $testUserA ++++');
var testClientA = Client('TestClientA', debug: false);
Logs.success('++++ Login $testUserA ++++');
var testClientA = Client('TestClientA');
testClientA.database = getDatabase();
await testClientA.checkServer(homeserver);
await testClientA.login(testUserA, testPasswordA);
await testClientA.login(user: testUserA, password: testPasswordA);
assert(testClientA.encryptionEnabled);
print('++++ Login $testUserB ++++');
var testClientB = Client('TestClientB', debug: false);
Logs.success('++++ Login $testUserB ++++');
var testClientB = Client('TestClientB');
testClientB.database = getDatabase();
await testClientB.checkServer(homeserver);
await testClientB.login(testUserB, testPasswordA);
await testClientB.login(user: testUserB, password: testPasswordA);
assert(testClientB.encryptionEnabled);
print('++++ ($testUserA) Leave all rooms ++++');
Logs.success('++++ ($testUserA) Leave all rooms ++++');
while (testClientA.rooms.isNotEmpty) {
var room = testClientA.rooms.first;
if (room.canonicalAlias?.isNotEmpty ?? false) {
@ -43,7 +44,7 @@ void test() async {
} catch (_) {}
}
print('++++ ($testUserB) Leave all rooms ++++');
Logs.success('++++ ($testUserB) Leave all rooms ++++');
for (var i = 0; i < 3; i++) {
if (testClientB.rooms.isNotEmpty) {
var room = testClientB.rooms.first;
@ -54,7 +55,7 @@ void test() async {
}
}
print('++++ Check if own olm device is verified by default ++++');
Logs.success('++++ Check if own olm device is verified by default ++++');
assert(testClientA.userDeviceKeys.containsKey(testUserA));
assert(testClientA.userDeviceKeys[testUserA].deviceKeys
.containsKey(testClientA.deviceID));
@ -70,20 +71,20 @@ void test() async {
assert(!testClientB
.userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID].blocked);
print('++++ ($testUserA) Create room and invite $testUserB ++++');
await testClientA.api.createRoom(invite: [testUserB]);
Logs.success('++++ ($testUserA) Create room and invite $testUserB ++++');
await testClientA.createRoom(invite: [testUserB]);
await Future.delayed(Duration(seconds: 1));
var room = testClientA.rooms.first;
assert(room != null);
final roomId = room.id;
print('++++ ($testUserB) Join room ++++');
Logs.success('++++ ($testUserB) Join room ++++');
var inviteRoom = testClientB.getRoomById(roomId);
await inviteRoom.join();
await Future.delayed(Duration(seconds: 1));
assert(inviteRoom.membership == Membership.join);
print('++++ ($testUserA) Enable encryption ++++');
Logs.success('++++ ($testUserA) Enable encryption ++++');
assert(room.encrypted == false);
await room.enableEncryption();
await Future.delayed(Duration(seconds: 5));
@ -91,7 +92,7 @@ void test() async {
assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) ==
null);
print('++++ ($testUserA) Check known olm devices ++++');
Logs.success('++++ ($testUserA) Check known olm devices ++++');
assert(testClientA.userDeviceKeys.containsKey(testUserB));
assert(testClientA.userDeviceKeys[testUserB].deviceKeys
.containsKey(testClientB.deviceID));
@ -109,7 +110,7 @@ void test() async {
await testClientA.userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID]
.setVerified(true);
print('++++ Check if own olm device is verified by default ++++');
Logs.success('++++ Check if own olm device is verified by default ++++');
assert(testClientA.userDeviceKeys.containsKey(testUserA));
assert(testClientA.userDeviceKeys[testUserA].deviceKeys
.containsKey(testClientA.deviceID));
@ -121,7 +122,7 @@ void test() async {
assert(testClientB
.userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID].verified);
print("++++ ($testUserA) Send encrypted message: '$testMessage' ++++");
Logs.success("++++ ($testUserA) Send encrypted message: '$testMessage' ++++");
await room.sendTextEvent(testMessage);
await Future.delayed(Duration(seconds: 5));
assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) !=
@ -148,10 +149,11 @@ void test() async {
null);
assert(room.lastMessage == testMessage);
assert(inviteRoom.lastMessage == testMessage);
print(
Logs.success(
"++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
print("++++ ($testUserA) Send again encrypted message: '$testMessage2' ++++");
Logs.success(
"++++ ($testUserA) Send again encrypted message: '$testMessage2' ++++");
await room.sendTextEvent(testMessage2);
await Future.delayed(Duration(seconds: 5));
assert(testClientA
@ -175,10 +177,11 @@ void test() async {
null);
assert(room.lastMessage == testMessage2);
assert(inviteRoom.lastMessage == testMessage2);
print(
Logs.success(
"++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
print("++++ ($testUserB) Send again encrypted message: '$testMessage3' ++++");
Logs.success(
"++++ ($testUserB) Send again encrypted message: '$testMessage3' ++++");
await inviteRoom.sendTextEvent(testMessage3);
await Future.delayed(Duration(seconds: 5));
assert(testClientA
@ -208,17 +211,17 @@ void test() async {
null);
assert(inviteRoom.lastMessage == testMessage3);
assert(room.lastMessage == testMessage3);
print(
Logs.success(
"++++ ($testUserA) Received decrypted message: '${room.lastMessage}' ++++");
print('++++ Login $testUserB in another client ++++');
var testClientC =
Client('TestClientC', debug: false, database: getDatabase());
Logs.success('++++ Login $testUserB in another client ++++');
var testClientC = Client('TestClientC', database: getDatabase());
await testClientC.checkServer(homeserver);
await testClientC.login(testUserB, testPasswordA);
await testClientC.login(user: testUserB, password: testPasswordA);
await Future.delayed(Duration(seconds: 3));
print("++++ ($testUserA) Send again encrypted message: '$testMessage4' ++++");
Logs.success(
"++++ ($testUserA) Send again encrypted message: '$testMessage4' ++++");
await room.sendTextEvent(testMessage4);
await Future.delayed(Duration(seconds: 5));
assert(testClientA
@ -255,16 +258,17 @@ void test() async {
null);
assert(room.lastMessage == testMessage4);
assert(inviteRoom.lastMessage == testMessage4);
print(
Logs.success(
"++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
print('++++ Logout $testUserB another client ++++');
Logs.success('++++ Logout $testUserB another client ++++');
await testClientC.dispose();
await testClientC.logout();
testClientC = null;
await Future.delayed(Duration(seconds: 5));
print("++++ ($testUserA) Send again encrypted message: '$testMessage6' ++++");
Logs.success(
"++++ ($testUserA) Send again encrypted message: '$testMessage6' ++++");
await room.sendTextEvent(testMessage6);
await Future.delayed(Duration(seconds: 5));
assert(testClientA
@ -291,10 +295,10 @@ void test() async {
null);
assert(room.lastMessage == testMessage6);
assert(inviteRoom.lastMessage == testMessage6);
print(
Logs.success(
"++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
/* print('++++ ($testUserA) Restore user ++++');
/* Logs.success('++++ ($testUserA) Restore user ++++');
await testClientA.dispose();
testClientA = null;
testClientA = Client(
@ -321,7 +325,7 @@ void test() async {
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' ++++");
Logs.success("++++ ($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);
@ -331,10 +335,10 @@ void test() async {
assert(restoredRoom.lastMessage == testMessage5);
assert(inviteRoom.lastMessage == testMessage5);
assert(testClientB.getRoomById(roomId).lastMessage == testMessage5);
print(
Logs.success(
"++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++");*/
print('++++ Logout $testUserA and $testUserB ++++');
Logs.success('++++ Logout $testUserA and $testUserB ++++');
await room.leave();
await room.forget();
await inviteRoom.leave();
@ -342,8 +346,8 @@ void test() async {
await Future.delayed(Duration(seconds: 1));
await testClientA.dispose();
await testClientB.dispose();
await testClientA.api.logoutAll();
await testClientB.api.logoutAll();
await testClientA.logoutAll();
await testClientB.logoutAll();
testClientA = null;
testClientB = null;
return;