Merge branch 'main' of https://gitlab.com/famedly/famedlysdk into yiffed
This commit is contained in:
commit
327334bd58
56 changed files with 2151 additions and 657 deletions
.gitlab-ci.ymlanalysis_options.yamlpubspec.lockpubspec.yaml
example
lib
encryption
cross_signing.dartencryption.dartkey_manager.dartkey_verification_manager.dartolm_manager.dartssss.dart
famedlysdk.dartutils
matrix_api
src
test
client_test.dartdevice_keys_list_test.dart
encryption
cross_signing_test.dartencrypt_decrypt_room_message_test.dartencrypt_decrypt_to_device_test.dartkey_manager_test.dartkey_request_test.dartkey_verification_test.dartolm_manager_test.dartonline_key_backup_test.dartssss_test.dart
event_test.dartfake_client.dartfake_matrix_api.dartmarkdown_test.dartmatrix_api_test.dartmatrix_database_test.dartmxc_uri_extension_test.dartroom_test.dartsync_filter_test.darttimeline_test.dartuser_test.darttest_driver
|
@ -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
|
|
@ -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
264
example/main.dart
Normal file
|
@ -0,0 +1,264 @@
|
|||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
runApp(FamedlySdkExampleApp());
|
||||
}
|
||||
|
||||
class FamedlySdkExampleApp extends StatelessWidget {
|
||||
static Client client = Client('Famedly SDK Example Client', debug: true);
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Famedly SDK Example App',
|
||||
home: LoginView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LoginView extends StatefulWidget {
|
||||
@override
|
||||
_LoginViewState createState() => _LoginViewState();
|
||||
}
|
||||
|
||||
class _LoginViewState extends State<LoginView> {
|
||||
final TextEditingController _homeserverController = TextEditingController();
|
||||
final TextEditingController _usernameController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
String _error;
|
||||
|
||||
void _loginAction() async {
|
||||
setState(() => _isLoading = true);
|
||||
setState(() => _error = null);
|
||||
try {
|
||||
if (await FamedlySdkExampleApp.client
|
||||
.checkServer(_homeserverController.text) ==
|
||||
false) {
|
||||
throw (Exception('Server not supported'));
|
||||
}
|
||||
if (await FamedlySdkExampleApp.client.login(
|
||||
_usernameController.text,
|
||||
_passwordController.text,
|
||||
) ==
|
||||
false) {
|
||||
throw (Exception('Username or password incorrect'));
|
||||
}
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (_) => ChatListView()),
|
||||
(route) => false,
|
||||
);
|
||||
} catch (e) {
|
||||
setState(() => _error = e.toString());
|
||||
}
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Login')),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
TextField(
|
||||
controller: _homeserverController,
|
||||
readOnly: _isLoading,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Homeserver',
|
||||
hintText: 'https://matrix.org',
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _usernameController,
|
||||
readOnly: _isLoading,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Username',
|
||||
hintText: '@username:domain',
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
readOnly: _isLoading,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
hintText: '****',
|
||||
errorText: _error,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
RaisedButton(
|
||||
child: _isLoading ? LinearProgressIndicator() : Text('Login'),
|
||||
onPressed: _isLoading ? null : _loginAction,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatListView extends StatefulWidget {
|
||||
@override
|
||||
_ChatListViewState createState() => _ChatListViewState();
|
||||
}
|
||||
|
||||
class _ChatListViewState extends State<ChatListView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Chats'),
|
||||
),
|
||||
body: StreamBuilder(
|
||||
stream: FamedlySdkExampleApp.client.onSync.stream,
|
||||
builder: (c, s) => ListView.builder(
|
||||
itemCount: FamedlySdkExampleApp.client.rooms.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
final room = FamedlySdkExampleApp.client.rooms[i];
|
||||
return ListTile(
|
||||
title: Text(room.displayname + ' (${room.notificationCount})'),
|
||||
subtitle: Text(room.lastMessage, maxLines: 1),
|
||||
leading: CircleAvatar(
|
||||
backgroundImage: NetworkImage(room.avatar.getThumbnail(
|
||||
FamedlySdkExampleApp.client,
|
||||
width: 64,
|
||||
height: 64,
|
||||
)),
|
||||
),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ChatView(room: room),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatView extends StatefulWidget {
|
||||
final Room room;
|
||||
|
||||
const ChatView({Key key, @required this.room}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ChatViewState createState() => _ChatViewState();
|
||||
}
|
||||
|
||||
class _ChatViewState extends State<ChatView> {
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
|
||||
void _sendAction() {
|
||||
print('Send Text');
|
||||
widget.room.sendTextEvent(_controller.text);
|
||||
_controller.clear();
|
||||
}
|
||||
|
||||
Timeline timeline;
|
||||
|
||||
Future<bool> getTimeline() async {
|
||||
timeline ??=
|
||||
await widget.room.getTimeline(onUpdate: () => setState(() => null));
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
timeline?.cancelSubscriptions();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: StreamBuilder<Object>(
|
||||
stream: widget.room.onUpdate.stream,
|
||||
builder: (context, snapshot) {
|
||||
return Text(widget.room.displayname);
|
||||
}),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FutureBuilder(
|
||||
future: getTimeline(),
|
||||
builder: (context, snapshot) => !snapshot.hasData
|
||||
? Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: ListView.builder(
|
||||
reverse: true,
|
||||
itemCount: timeline.events.length,
|
||||
itemBuilder: (BuildContext context, int i) => Opacity(
|
||||
opacity: timeline.events[i].status != 2 ? 0.5 : 1,
|
||||
child: ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
timeline.events[i].sender.calcDisplayname(),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
timeline.events[i].originServerTs
|
||||
.toIso8601String(),
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Text(timeline.events[i].body),
|
||||
leading: CircleAvatar(
|
||||
child: timeline.events[i].sender?.avatarUrl == null
|
||||
? Icon(Icons.person)
|
||||
: null,
|
||||
backgroundImage:
|
||||
timeline.events[i].sender?.avatarUrl != null
|
||||
? NetworkImage(
|
||||
timeline.events[i].sender?.avatarUrl
|
||||
?.getThumbnail(
|
||||
FamedlySdkExampleApp.client,
|
||||
width: 64,
|
||||
height: 64,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 60,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
labelText: 'Send a message ...',
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.send),
|
||||
onPressed: _sendAction,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -167,7 +167,7 @@ class CrossSigning {
|
|||
}
|
||||
if (signedKeys.isNotEmpty) {
|
||||
// post our new keys!
|
||||
await client.api.uploadKeySignatures(signedKeys);
|
||||
await client.uploadKeySignatures(signedKeys);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -67,6 +67,10 @@ class KeyVerificationManager {
|
|||
if (_requests.containsKey(transactionId)) {
|
||||
await _requests[transactionId].handlePayload(event.type, event.content);
|
||||
} else {
|
||||
if (!['m.key.verification.request', 'm.key.verification.start']
|
||||
.contains(event.type)) {
|
||||
return; // we can only start on these
|
||||
}
|
||||
final newKeyRequest =
|
||||
KeyVerification(encryption: encryption, userId: event.sender);
|
||||
await newKeyRequest.handlePayload(event.type, event.content);
|
||||
|
@ -111,6 +115,10 @@ class KeyVerificationManager {
|
|||
_requests.remove(transactionId);
|
||||
}
|
||||
} else if (event['sender'] != client.userID) {
|
||||
if (!['m.key.verification.request', 'm.key.verification.start']
|
||||
.contains(type)) {
|
||||
return; // we can only start on these
|
||||
}
|
||||
final room = client.getRoomById(update.roomID) ??
|
||||
Room(id: update.roomID, client: client);
|
||||
final newKeyRequest = KeyVerification(
|
||||
|
|
|
@ -18,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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -25,7 +25,6 @@ abstract class MessageTypes {
|
|||
static const String Audio = 'm.audio';
|
||||
static const String File = 'm.file';
|
||||
static const String Location = 'm.location';
|
||||
static const String Reply = 'm.relates_to';
|
||||
static const String Sticker = 'm.sticker';
|
||||
static const String BadEncrypted = 'm.bad.encrypted';
|
||||
static const String None = 'm.none';
|
||||
|
|
|
@ -22,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;
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
30
lib/src/utils/logs.dart
Normal 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()}' : ''),
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
44
lib/src/utils/sync_update_extension.dart
Normal file
44
lib/src/utils/sync_update_extension.dart
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import '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;
|
||||
}
|
|
@ -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'
|
||||
: '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/src/utils/logs.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
|
@ -74,9 +75,9 @@ void main() {
|
|||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
Logs.error('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
Logs.success('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/src/utils/logs.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
|
@ -33,9 +34,9 @@ void main() {
|
|||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
Logs.success('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/src/utils/logs.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
|
@ -30,9 +31,9 @@ void main() {
|
|||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
Logs.success('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/src/utils/logs.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
|
@ -35,15 +36,14 @@ void main() {
|
|||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
Logs.success('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
Client client;
|
||||
var otherClient =
|
||||
Client('othertestclient', debug: true, httpClient: FakeMatrixApi());
|
||||
var otherClient = Client('othertestclient', httpClient: FakeMatrixApi());
|
||||
DeviceKeys device;
|
||||
Map<String, dynamic> payload;
|
||||
|
||||
|
@ -54,7 +54,7 @@ void main() {
|
|||
otherClient.connect(
|
||||
newToken: 'abc',
|
||||
newUserID: '@othertest:fakeServer.notExisting',
|
||||
newHomeserver: otherClient.api.homeserver,
|
||||
newHomeserver: otherClient.homeserver,
|
||||
newDeviceName: 'Text Matrix Client',
|
||||
newDeviceID: 'FOXDEVICE',
|
||||
newOlmAccount: otherPickledOlmAccount,
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:famedlysdk/src/utils/logs.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
|
@ -30,9 +31,9 @@ void main() {
|
|||
olm.Account();
|
||||
} catch (_) {
|
||||
olmEnabled = false;
|
||||
print('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||
}
|
||||
print('[LibOlm] Enabled: $olmEnabled');
|
||||
Logs.success('[LibOlm] Enabled: $olmEnabled');
|
||||
|
||||
if (!olmEnabled) return;
|
||||
|
||||
|
|
|
@ -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')),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -29,21 +29,15 @@ const pickledOlmAccount =
|
|||
'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw';
|
||||
|
||||
Future<Client> getClient() async {
|
||||
final client = Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
||||
final client = Client('testclient', httpClient: FakeMatrixApi());
|
||||
client.database = getDatabase();
|
||||
await client.checkServer('https://fakeServer.notExisting');
|
||||
final resp = await client.api.login(
|
||||
type: 'm.login.password',
|
||||
user: 'test',
|
||||
password: '1234',
|
||||
initialDeviceDisplayName: 'Fluffy Matrix Client',
|
||||
);
|
||||
client.connect(
|
||||
newToken: resp.accessToken,
|
||||
newUserID: resp.userId,
|
||||
newHomeserver: client.api.homeserver,
|
||||
newToken: 'abcd',
|
||||
newUserID: '@test:fakeServer.notExisting',
|
||||
newHomeserver: client.homeserver,
|
||||
newDeviceName: 'Text Matrix Client',
|
||||
newDeviceID: resp.deviceId,
|
||||
newDeviceID: 'GHTYAJCE',
|
||||
newOlmAccount: pickledOlmAccount,
|
||||
);
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
|
|
|
@ -80,8 +80,12 @@ class FakeMatrixApi extends MockClient {
|
|||
res = {'displayname': ''};
|
||||
} else if (method == 'PUT' &&
|
||||
action.contains(
|
||||
'/client/r0/rooms/%211234%3AfakeServer.notExisting/send/')) {
|
||||
'/client/r0/rooms/!1234%3AfakeServer.notExisting/send/')) {
|
||||
res = {'event_id': '\$event${FakeMatrixApi.eventCounter++}'};
|
||||
} else if (action.contains('/client/r0/sync')) {
|
||||
res = {
|
||||
'next_batch': DateTime.now().millisecondsSinceEpoch.toString
|
||||
};
|
||||
} else {
|
||||
res = {
|
||||
'errcode': 'M_UNRECOGNIZED',
|
||||
|
@ -748,7 +752,7 @@ class FakeMatrixApi extends MockClient {
|
|||
'app_url': 'https://custom.app.example.org'
|
||||
}
|
||||
},
|
||||
'/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags':
|
||||
'/client/r0/user/%40alice%3Aexample.com/rooms/!localpart%3Aexample.com/tags':
|
||||
(var req) => {
|
||||
'tags': {
|
||||
'm.favourite': {'order': 0.1},
|
||||
|
@ -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':
|
||||
|
|
|
@ -54,11 +54,11 @@ void main() {
|
|||
});
|
||||
test('emotes', () {
|
||||
expect(markdown(':fox:', emotePacks),
|
||||
'<img src="mxc://roomfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
|
||||
'<img data-mx-emote="" src="mxc://roomfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
|
||||
expect(markdown(':user~fox:', emotePacks),
|
||||
'<img src="mxc://userfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
|
||||
'<img data-mx-emote="" src="mxc://userfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
|
||||
expect(markdown(':raccoon:', emotePacks),
|
||||
'<img src="mxc://raccoon" alt=":raccoon:" title=":raccoon:" height="32" vertical-align="middle" />');
|
||||
'<img data-mx-emote="" src="mxc://raccoon" alt=":raccoon:" title=":raccoon:" height="32" vertical-align="middle" />');
|
||||
expect(markdown(':invalid:', emotePacks), ':invalid:');
|
||||
expect(markdown(':room~invalid:', emotePacks), ':room~invalid:');
|
||||
});
|
||||
|
|
|
@ -33,7 +33,6 @@ void main() {
|
|||
group('Matrix API', () {
|
||||
final matrixApi = MatrixApi(
|
||||
httpClient: FakeMatrixApi(),
|
||||
debug: true,
|
||||
);
|
||||
test('MatrixException test', () async {
|
||||
final exception = MatrixException.fromJson({
|
||||
|
@ -1377,7 +1376,7 @@ void main() {
|
|||
'@alice:example.com', '!localpart:example.com');
|
||||
expect(
|
||||
FakeMatrixApi.api['GET'][
|
||||
'/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags']({}),
|
||||
'/client/r0/user/%40alice%3Aexample.com/rooms/!localpart%3Aexample.com/tags']({}),
|
||||
{'tags': response.map((k, v) => MapEntry(k, v.toJson()))},
|
||||
);
|
||||
|
||||
|
|
184
test/matrix_database_test.dart
Normal file
184
test/matrix_database_test.dart
Normal file
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'fake_database.dart';
|
||||
|
||||
void main() {
|
||||
group('Databse', () {
|
||||
final database = getDatabase();
|
||||
var clientId = -1;
|
||||
var room = Room(id: '!room:blubb');
|
||||
test('setupDatabase', () async {
|
||||
clientId = await database.insertClient(
|
||||
'testclient',
|
||||
'https://example.org',
|
||||
'blubb',
|
||||
'@test:example.org',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
});
|
||||
test('storeEventUpdate', () async {
|
||||
// store a simple update
|
||||
var update = EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: room.id,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'origin_server_ts': 100,
|
||||
'content': {'blah': 'blubb'},
|
||||
'event_id': '\$event-1',
|
||||
'sender': '@blah:blubb',
|
||||
},
|
||||
sortOrder: 0.0,
|
||||
);
|
||||
await database.storeEventUpdate(clientId, update);
|
||||
var event = await database.getEventById(clientId, '\$event-1', room);
|
||||
expect(event.eventId, '\$event-1');
|
||||
|
||||
// insert a transaction id
|
||||
update = EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: room.id,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'origin_server_ts': 100,
|
||||
'content': {'blah': 'blubb'},
|
||||
'event_id': 'transaction-1',
|
||||
'sender': '@blah:blubb',
|
||||
'status': 0,
|
||||
},
|
||||
sortOrder: 0.0,
|
||||
);
|
||||
await database.storeEventUpdate(clientId, update);
|
||||
event = await database.getEventById(clientId, 'transaction-1', room);
|
||||
expect(event.eventId, 'transaction-1');
|
||||
update = EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: room.id,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'origin_server_ts': 100,
|
||||
'content': {'blah': 'blubb'},
|
||||
'event_id': '\$event-2',
|
||||
'sender': '@blah:blubb',
|
||||
'unsigned': {
|
||||
'transaction_id': 'transaction-1',
|
||||
},
|
||||
'status': 1,
|
||||
},
|
||||
sortOrder: 0.0,
|
||||
);
|
||||
await database.storeEventUpdate(clientId, update);
|
||||
event = await database.getEventById(clientId, 'transaction-1', room);
|
||||
expect(event, null);
|
||||
event = await database.getEventById(clientId, '\$event-2', room);
|
||||
|
||||
// insert a transaction id if the event id for it already exists
|
||||
update = EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: room.id,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'origin_server_ts': 100,
|
||||
'content': {'blah': 'blubb'},
|
||||
'event_id': '\$event-3',
|
||||
'sender': '@blah:blubb',
|
||||
'status': 0,
|
||||
},
|
||||
sortOrder: 0.0,
|
||||
);
|
||||
await database.storeEventUpdate(clientId, update);
|
||||
event = await database.getEventById(clientId, '\$event-3', room);
|
||||
expect(event.eventId, '\$event-3');
|
||||
update = EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: room.id,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'origin_server_ts': 100,
|
||||
'content': {'blah': 'blubb'},
|
||||
'event_id': '\$event-3',
|
||||
'sender': '@blah:blubb',
|
||||
'status': 1,
|
||||
'unsigned': {
|
||||
'transaction_id': 'transaction-2',
|
||||
},
|
||||
},
|
||||
sortOrder: 0.0,
|
||||
);
|
||||
await database.storeEventUpdate(clientId, update);
|
||||
event = await database.getEventById(clientId, '\$event-3', room);
|
||||
expect(event.eventId, '\$event-3');
|
||||
expect(event.status, 1);
|
||||
event = await database.getEventById(clientId, 'transaction-2', room);
|
||||
expect(event, null);
|
||||
|
||||
// insert transaction id and not update status
|
||||
update = EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: room.id,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'origin_server_ts': 100,
|
||||
'content': {'blah': 'blubb'},
|
||||
'event_id': '\$event-4',
|
||||
'sender': '@blah:blubb',
|
||||
'status': 2,
|
||||
},
|
||||
sortOrder: 0.0,
|
||||
);
|
||||
await database.storeEventUpdate(clientId, update);
|
||||
event = await database.getEventById(clientId, '\$event-4', room);
|
||||
expect(event.eventId, '\$event-4');
|
||||
update = EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: room.id,
|
||||
eventType: 'm.room.message',
|
||||
content: {
|
||||
'type': 'm.room.message',
|
||||
'origin_server_ts': 100,
|
||||
'content': {'blah': 'blubb'},
|
||||
'event_id': '\$event-4',
|
||||
'sender': '@blah:blubb',
|
||||
'status': 1,
|
||||
'unsigned': {
|
||||
'transaction_id': 'transaction-3',
|
||||
},
|
||||
},
|
||||
sortOrder: 0.0,
|
||||
);
|
||||
await database.storeEventUpdate(clientId, update);
|
||||
event = await database.getEventById(clientId, '\$event-4', room);
|
||||
expect(event.eventId, '\$event-4');
|
||||
expect(event.status, 2);
|
||||
event = await database.getEventById(clientId, 'transaction-3', room);
|
||||
expect(event, null);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -33,13 +33,13 @@ void main() {
|
|||
expect(content.isScheme('mxc'), true);
|
||||
|
||||
expect(content.getDownloadLink(client),
|
||||
'${client.api.homeserver.toString()}/_matrix/media/r0/download/exampleserver.abc/abcdefghijklmn');
|
||||
'${client.homeserver.toString()}/_matrix/media/r0/download/exampleserver.abc/abcdefghijklmn');
|
||||
expect(content.getThumbnail(client, width: 50, height: 50),
|
||||
'${client.api.homeserver.toString()}/_matrix/media/r0/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=crop');
|
||||
'${client.homeserver.toString()}/_matrix/media/r0/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=crop');
|
||||
expect(
|
||||
content.getThumbnail(client,
|
||||
width: 50, height: 50, method: ThumbnailMethod.scale),
|
||||
'${client.api.homeserver.toString()}/_matrix/media/r0/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=scale');
|
||||
'${client.homeserver.toString()}/_matrix/media/r0/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=scale');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -27,7 +27,9 @@ import 'package:famedlysdk/src/database/database.dart'
|
|||
import 'package:test/test.dart';
|
||||
|
||||
import 'fake_client.dart';
|
||||
import 'fake_matrix_api.dart';
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
void main() {
|
||||
|
@ -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
166
test/sync_filter_test.dart
Normal file
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* Ansible inventory script used at Famedly GmbH for managing many hosts
|
||||
* Copyright (C) 2020 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
const UPDATES = {
|
||||
'empty': {
|
||||
'next_batch': 'blah',
|
||||
'account_data': {
|
||||
'events': [],
|
||||
},
|
||||
'presences': {
|
||||
'events': [],
|
||||
},
|
||||
'rooms': {
|
||||
'join': {},
|
||||
'leave': {},
|
||||
'invite': {},
|
||||
},
|
||||
'to_device': {
|
||||
'events': [],
|
||||
},
|
||||
},
|
||||
'presence': {
|
||||
'next_batch': 'blah',
|
||||
'presence': {
|
||||
'events': [
|
||||
{
|
||||
'content': {
|
||||
'avatar_url': 'mxc://localhost:wefuiwegh8742w',
|
||||
'last_active_ago': 2478593,
|
||||
'presence': 'online',
|
||||
'currently_active': false,
|
||||
'status_msg': 'Making cupcakes'
|
||||
},
|
||||
'type': 'm.presence',
|
||||
'sender': '@example:localhost',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'account_data': {
|
||||
'next_batch': 'blah',
|
||||
'account_data': {
|
||||
'events': [
|
||||
{
|
||||
'type': 'blah',
|
||||
'content': {
|
||||
'beep': 'boop',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'invite': {
|
||||
'next_batch': 'blah',
|
||||
'rooms': {
|
||||
'invite': {
|
||||
'!room': {
|
||||
'invite_state': {
|
||||
'events': [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'leave': {
|
||||
'next_batch': 'blah',
|
||||
'rooms': {
|
||||
'leave': {
|
||||
'!room': <String, dynamic>{},
|
||||
},
|
||||
},
|
||||
},
|
||||
'join': {
|
||||
'next_batch': 'blah',
|
||||
'rooms': {
|
||||
'join': {
|
||||
'!room': {
|
||||
'timeline': {
|
||||
'events': [],
|
||||
},
|
||||
'state': {
|
||||
'events': [],
|
||||
},
|
||||
'account_data': {
|
||||
'events': [],
|
||||
},
|
||||
'ephemeral': {
|
||||
'events': [],
|
||||
},
|
||||
'unread_notifications': <String, dynamic>{},
|
||||
'summary': <String, dynamic>{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'to_device': {
|
||||
'next_batch': 'blah',
|
||||
'to_device': {
|
||||
'events': [
|
||||
{
|
||||
'type': 'beep',
|
||||
'content': {
|
||||
'blah': 'blubb',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
void testUpdates(bool Function(SyncUpdate s) test, Map<String, bool> expected) {
|
||||
for (final update in UPDATES.entries) {
|
||||
var sync = SyncUpdate.fromJson(update.value);
|
||||
expect(test(sync), expected[update.key]);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('Sync Filters', () {
|
||||
test('room update', () {
|
||||
var testFn = (SyncUpdate s) => s.hasRoomUpdate;
|
||||
final expected = {
|
||||
'empty': false,
|
||||
'presence': false,
|
||||
'account_data': true,
|
||||
'invite': true,
|
||||
'leave': true,
|
||||
'join': true,
|
||||
'to_device': true,
|
||||
};
|
||||
testUpdates(testFn, expected);
|
||||
});
|
||||
|
||||
test('presence update', () {
|
||||
var testFn = (SyncUpdate s) => s.hasPresenceUpdate;
|
||||
final expected = {
|
||||
'empty': false,
|
||||
'presence': true,
|
||||
'account_data': false,
|
||||
'invite': false,
|
||||
'leave': false,
|
||||
'join': false,
|
||||
'to_device': false,
|
||||
};
|
||||
testUpdates(testFn, expected);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -33,7 +33,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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue