Merge commit '84cc925b08e97098d00c54fff9c1244f91055de3' into yiffed
This commit is contained in:
commit
daccf50590
|
@ -31,6 +31,7 @@ coverage_badge.svg
|
||||||
.pub-cache/
|
.pub-cache/
|
||||||
.pub/
|
.pub/
|
||||||
build/
|
build/
|
||||||
|
pubspec.lock
|
||||||
|
|
||||||
# Android related
|
# Android related
|
||||||
**/android/**/gradle-wrapper.jar
|
**/android/**/gradle-wrapper.jar
|
||||||
|
|
|
@ -46,6 +46,30 @@ coverage_without_olm:
|
||||||
- chmod +x ./test.sh
|
- chmod +x ./test.sh
|
||||||
- pub get
|
- pub get
|
||||||
- pub run test
|
- pub run test
|
||||||
|
|
||||||
|
e2ee_test:
|
||||||
|
tags:
|
||||||
|
- linux
|
||||||
|
stage: coverage
|
||||||
|
image: debian:testing
|
||||||
|
dependencies: []
|
||||||
|
script:
|
||||||
|
- apt update
|
||||||
|
- apt install -y curl gnupg2 git
|
||||||
|
- curl https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
|
||||||
|
- curl https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list > /etc/apt/sources.list.d/dart_stable.list
|
||||||
|
- apt update
|
||||||
|
- apt install -y dart chromium lcov libolm3 sqlite3 libsqlite3-dev
|
||||||
|
- ln -s /usr/lib/dart/bin/pub /usr/bin/
|
||||||
|
- useradd -m test
|
||||||
|
- chown -R 'test:' '.'
|
||||||
|
- chmod +x ./prepare.sh
|
||||||
|
- chmod +x ./test_driver.sh
|
||||||
|
- printf "abstract class TestUser {\n static const String homeserver = '$TEST_HOMESERVER';\n static const String username = '$TEST_USER1';\n static const String username2 = '$TEST_USER2';\n static const String password = '$TEST_USER_PASSWORD';\n}" > ./test_driver/test_config.dart
|
||||||
|
- su -c ./prepare.sh test
|
||||||
|
- su -c ./test_driver.sh test
|
||||||
|
timeout: 16m
|
||||||
|
resource_group: e2ee_test
|
||||||
|
|
||||||
code_analyze:
|
code_analyze:
|
||||||
tags:
|
tags:
|
||||||
|
@ -57,7 +81,7 @@ code_analyze:
|
||||||
- flutter format lib/ test/ test_driver/ --set-exit-if-changed
|
- flutter format lib/ test/ test_driver/ --set-exit-if-changed
|
||||||
- flutter analyze
|
- flutter analyze
|
||||||
|
|
||||||
build-api-doc:
|
build_api_doc:
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- docker
|
||||||
stage: builddocs
|
stage: builddocs
|
||||||
|
@ -68,9 +92,9 @@ build-api-doc:
|
||||||
paths:
|
paths:
|
||||||
- doc/api/
|
- doc/api/
|
||||||
only:
|
only:
|
||||||
- master
|
- main
|
||||||
|
|
||||||
build-doc:
|
build_doc:
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- docker
|
||||||
stage: builddocs
|
stage: builddocs
|
||||||
|
@ -83,7 +107,7 @@ build-doc:
|
||||||
paths:
|
paths:
|
||||||
- doc-public
|
- doc-public
|
||||||
only:
|
only:
|
||||||
- master
|
- main
|
||||||
|
|
||||||
pages:
|
pages:
|
||||||
tags:
|
tags:
|
||||||
|
@ -95,10 +119,10 @@ pages:
|
||||||
- mv doc-public ./home/doc
|
- mv doc-public ./home/doc
|
||||||
- mv home public
|
- mv home public
|
||||||
dependencies:
|
dependencies:
|
||||||
- build-api-doc
|
- build_api_doc
|
||||||
- build-doc
|
- build_doc
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- public
|
- public
|
||||||
only:
|
only:
|
||||||
- master
|
- main
|
|
@ -3,9 +3,10 @@ include: package:pedantic/analysis_options.yaml
|
||||||
linter:
|
linter:
|
||||||
rules:
|
rules:
|
||||||
- camel_case_types
|
- camel_case_types
|
||||||
|
- avoid_print
|
||||||
|
|
||||||
analyzer:
|
analyzer:
|
||||||
errors:
|
errors:
|
||||||
todo: ignore
|
todo: ignore
|
||||||
# exclude:
|
exclude:
|
||||||
# - path/to/excluded/files/**
|
- example/main.dart
|
|
@ -0,0 +1,264 @@
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(FamedlySdkExampleApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class FamedlySdkExampleApp extends StatelessWidget {
|
||||||
|
static Client client = Client('Famedly SDK Example Client', debug: true);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
title: 'Famedly SDK Example App',
|
||||||
|
home: LoginView(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoginView extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_LoginViewState createState() => _LoginViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginViewState extends State<LoginView> {
|
||||||
|
final TextEditingController _homeserverController = TextEditingController();
|
||||||
|
final TextEditingController _usernameController = TextEditingController();
|
||||||
|
final TextEditingController _passwordController = TextEditingController();
|
||||||
|
bool _isLoading = false;
|
||||||
|
String _error;
|
||||||
|
|
||||||
|
void _loginAction() async {
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
setState(() => _error = null);
|
||||||
|
try {
|
||||||
|
if (await FamedlySdkExampleApp.client
|
||||||
|
.checkServer(_homeserverController.text) ==
|
||||||
|
false) {
|
||||||
|
throw (Exception('Server not supported'));
|
||||||
|
}
|
||||||
|
if (await FamedlySdkExampleApp.client.login(
|
||||||
|
_usernameController.text,
|
||||||
|
_passwordController.text,
|
||||||
|
) ==
|
||||||
|
false) {
|
||||||
|
throw (Exception('Username or password incorrect'));
|
||||||
|
}
|
||||||
|
Navigator.of(context).pushAndRemoveUntil(
|
||||||
|
MaterialPageRoute(builder: (_) => ChatListView()),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _error = e.toString());
|
||||||
|
}
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text('Login')),
|
||||||
|
body: ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _homeserverController,
|
||||||
|
readOnly: _isLoading,
|
||||||
|
autocorrect: false,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Homeserver',
|
||||||
|
hintText: 'https://matrix.org',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
TextField(
|
||||||
|
controller: _usernameController,
|
||||||
|
readOnly: _isLoading,
|
||||||
|
autocorrect: false,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Username',
|
||||||
|
hintText: '@username:domain',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
TextField(
|
||||||
|
controller: _passwordController,
|
||||||
|
obscureText: true,
|
||||||
|
readOnly: _isLoading,
|
||||||
|
autocorrect: false,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Password',
|
||||||
|
hintText: '****',
|
||||||
|
errorText: _error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
RaisedButton(
|
||||||
|
child: _isLoading ? LinearProgressIndicator() : Text('Login'),
|
||||||
|
onPressed: _isLoading ? null : _loginAction,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatListView extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_ChatListViewState createState() => _ChatListViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatListViewState extends State<ChatListView> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Chats'),
|
||||||
|
),
|
||||||
|
body: StreamBuilder(
|
||||||
|
stream: FamedlySdkExampleApp.client.onSync.stream,
|
||||||
|
builder: (c, s) => ListView.builder(
|
||||||
|
itemCount: FamedlySdkExampleApp.client.rooms.length,
|
||||||
|
itemBuilder: (BuildContext context, int i) {
|
||||||
|
final room = FamedlySdkExampleApp.client.rooms[i];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(room.displayname + ' (${room.notificationCount})'),
|
||||||
|
subtitle: Text(room.lastMessage, maxLines: 1),
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundImage: NetworkImage(room.avatar.getThumbnail(
|
||||||
|
FamedlySdkExampleApp.client,
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
onTap: () => Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => ChatView(room: room),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatView extends StatefulWidget {
|
||||||
|
final Room room;
|
||||||
|
|
||||||
|
const ChatView({Key key, @required this.room}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ChatViewState createState() => _ChatViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatViewState extends State<ChatView> {
|
||||||
|
final TextEditingController _controller = TextEditingController();
|
||||||
|
|
||||||
|
void _sendAction() {
|
||||||
|
print('Send Text');
|
||||||
|
widget.room.sendTextEvent(_controller.text);
|
||||||
|
_controller.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Timeline timeline;
|
||||||
|
|
||||||
|
Future<bool> getTimeline() async {
|
||||||
|
timeline ??=
|
||||||
|
await widget.room.getTimeline(onUpdate: () => setState(() => null));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
timeline?.cancelSubscriptions();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: StreamBuilder<Object>(
|
||||||
|
stream: widget.room.onUpdate.stream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
return Text(widget.room.displayname);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: FutureBuilder(
|
||||||
|
future: getTimeline(),
|
||||||
|
builder: (context, snapshot) => !snapshot.hasData
|
||||||
|
? Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
)
|
||||||
|
: ListView.builder(
|
||||||
|
reverse: true,
|
||||||
|
itemCount: timeline.events.length,
|
||||||
|
itemBuilder: (BuildContext context, int i) => Opacity(
|
||||||
|
opacity: timeline.events[i].status != 2 ? 0.5 : 1,
|
||||||
|
child: ListTile(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
timeline.events[i].sender.calcDisplayname(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
timeline.events[i].originServerTs
|
||||||
|
.toIso8601String(),
|
||||||
|
style: TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Text(timeline.events[i].body),
|
||||||
|
leading: CircleAvatar(
|
||||||
|
child: timeline.events[i].sender?.avatarUrl == null
|
||||||
|
? Icon(Icons.person)
|
||||||
|
: null,
|
||||||
|
backgroundImage:
|
||||||
|
timeline.events[i].sender?.avatarUrl != null
|
||||||
|
? NetworkImage(
|
||||||
|
timeline.events[i].sender?.avatarUrl
|
||||||
|
?.getThumbnail(
|
||||||
|
FamedlySdkExampleApp.client,
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 60,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
labelText: 'Send a message ...',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.send),
|
||||||
|
onPressed: _sendAction,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
library encryption;
|
library encryption;
|
||||||
|
|
||||||
export './encryption/encryption.dart';
|
export 'encryption/encryption.dart';
|
||||||
export './encryption/key_manager.dart';
|
export 'encryption/key_manager.dart';
|
||||||
export './encryption/ssss.dart';
|
export 'encryption/ssss.dart';
|
||||||
export './encryption/utils/key_verification.dart';
|
export 'encryption/utils/key_verification.dart';
|
||||||
|
|
|
@ -16,12 +16,12 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'dart:typed_data';
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
|
||||||
|
|
||||||
|
import '../famedlysdk.dart';
|
||||||
import 'encryption.dart';
|
import 'encryption.dart';
|
||||||
|
|
||||||
const SELF_SIGNING_KEY = 'm.cross_signing.self_signing';
|
const SELF_SIGNING_KEY = 'm.cross_signing.self_signing';
|
||||||
|
@ -167,7 +167,7 @@ class CrossSigning {
|
||||||
}
|
}
|
||||||
if (signedKeys.isNotEmpty) {
|
if (signedKeys.isNotEmpty) {
|
||||||
// post our new keys!
|
// post our new keys!
|
||||||
await client.api.uploadKeySignatures(signedKeys);
|
await client.uploadKeySignatures(signedKeys);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,14 +17,18 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
|
||||||
import 'package:pedantic/pedantic.dart';
|
import 'package:pedantic/pedantic.dart';
|
||||||
import 'key_manager.dart';
|
|
||||||
import 'olm_manager.dart';
|
import '../famedlysdk.dart';
|
||||||
import 'key_verification_manager.dart';
|
import '../matrix_api.dart';
|
||||||
|
import '../src/utils/run_in_root.dart';
|
||||||
|
import '../src/utils/logs.dart';
|
||||||
import 'cross_signing.dart';
|
import 'cross_signing.dart';
|
||||||
|
import 'key_manager.dart';
|
||||||
|
import 'key_verification_manager.dart';
|
||||||
|
import 'olm_manager.dart';
|
||||||
import 'ssss.dart';
|
import 'ssss.dart';
|
||||||
|
|
||||||
class Encryption {
|
class Encryption {
|
||||||
|
@ -61,10 +65,12 @@ class Encryption {
|
||||||
|
|
||||||
Future<void> init(String olmAccount) async {
|
Future<void> init(String olmAccount) async {
|
||||||
await olmManager.init(olmAccount);
|
await olmManager.init(olmAccount);
|
||||||
|
_backgroundTasksRunning = true;
|
||||||
|
_backgroundTasks(); // start the background tasks
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleDeviceOneTimeKeysCount(Map<String, int> countJson) {
|
void handleDeviceOneTimeKeysCount(Map<String, int> countJson) {
|
||||||
olmManager.handleDeviceOneTimeKeysCount(countJson);
|
runInRoot(() => olmManager.handleDeviceOneTimeKeysCount(countJson));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onSync() {
|
void onSync() {
|
||||||
|
@ -72,20 +78,29 @@ class Encryption {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
|
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
|
||||||
if (['m.room_key', 'm.room_key_request', 'm.forwarded_room_key']
|
if (event.type == 'm.room_key') {
|
||||||
.contains(event.type)) {
|
// a new room key. We need to handle this asap, before other
|
||||||
// a new room key or thelike. We need to handle this asap, before other
|
|
||||||
// events in /sync are handled
|
// events in /sync are handled
|
||||||
await keyManager.handleToDeviceEvent(event);
|
await keyManager.handleToDeviceEvent(event);
|
||||||
}
|
}
|
||||||
|
if (['m.room_key_request', 'm.forwarded_room_key'].contains(event.type)) {
|
||||||
|
// "just" room key request things. We don't need these asap, so we handle
|
||||||
|
// them in the background
|
||||||
|
unawaited(runInRoot(() => keyManager.handleToDeviceEvent(event)));
|
||||||
|
}
|
||||||
if (event.type.startsWith('m.key.verification.')) {
|
if (event.type.startsWith('m.key.verification.')) {
|
||||||
// some key verification event. No need to handle it now, we can easily
|
// some key verification event. No need to handle it now, we can easily
|
||||||
// do this in the background
|
// do this in the background
|
||||||
unawaited(keyVerificationManager.handleToDeviceEvent(event));
|
unawaited(
|
||||||
|
runInRoot(() => keyVerificationManager.handleToDeviceEvent(event)));
|
||||||
}
|
}
|
||||||
if (event.type.startsWith('m.secret.')) {
|
if (event.type.startsWith('m.secret.')) {
|
||||||
// some ssss thing. We can do this in the background
|
// some ssss thing. We can do this in the background
|
||||||
unawaited(ssss.handleToDeviceEvent(event));
|
unawaited(runInRoot(() => ssss.handleToDeviceEvent(event)));
|
||||||
|
}
|
||||||
|
if (event.sender == client.userID) {
|
||||||
|
// maybe we need to re-try SSSS secrets
|
||||||
|
unawaited(runInRoot(() => ssss.periodicallyRequestMissingCache()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +114,13 @@ class Encryption {
|
||||||
update.content['content']['msgtype']
|
update.content['content']['msgtype']
|
||||||
.startsWith('m.key.verification.'))) {
|
.startsWith('m.key.verification.'))) {
|
||||||
// "just" key verification, no need to do this in sync
|
// "just" key verification, no need to do this in sync
|
||||||
unawaited(keyVerificationManager.handleEventUpdate(update));
|
unawaited(
|
||||||
|
runInRoot(() => keyVerificationManager.handleEventUpdate(update)));
|
||||||
|
}
|
||||||
|
if (update.content['sender'] == client.userID &&
|
||||||
|
!update.content['unsigned'].containsKey('transaction_id')) {
|
||||||
|
// maybe we need to re-try SSSS secrets
|
||||||
|
unawaited(runInRoot(() => ssss.periodicallyRequestMissingCache()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,17 +150,28 @@ class Encryption {
|
||||||
final decryptResult = inboundGroupSession.inboundGroupSession
|
final decryptResult = inboundGroupSession.inboundGroupSession
|
||||||
.decrypt(event.content['ciphertext']);
|
.decrypt(event.content['ciphertext']);
|
||||||
canRequestSession = false;
|
canRequestSession = false;
|
||||||
final messageIndexKey = event.eventId +
|
// we can't have the key be an int, else json-serializing will fail, thus we need it to be a string
|
||||||
|
final messageIndexKey = 'key-' + decryptResult.message_index.toString();
|
||||||
|
final messageIndexValue = event.eventId +
|
||||||
|
'|' +
|
||||||
event.originServerTs.millisecondsSinceEpoch.toString();
|
event.originServerTs.millisecondsSinceEpoch.toString();
|
||||||
var haveIndex = inboundGroupSession.indexes.containsKey(messageIndexKey);
|
var haveIndex = inboundGroupSession.indexes.containsKey(messageIndexKey);
|
||||||
if (haveIndex &&
|
if (haveIndex &&
|
||||||
inboundGroupSession.indexes[messageIndexKey] !=
|
inboundGroupSession.indexes[messageIndexKey] != messageIndexValue) {
|
||||||
decryptResult.message_index) {
|
|
||||||
// TODO: maybe clear outbound session, if it is ours
|
// TODO: maybe clear outbound session, if it is ours
|
||||||
|
// TODO: Make it so that we can't re-request the session keys, this is just for debugging
|
||||||
|
Logs.error('[Decrypt] Could not decrypt due to a corrupted session.');
|
||||||
|
Logs.error('[Decrypt] Want session: $roomId $sessionId $senderKey');
|
||||||
|
Logs.error(
|
||||||
|
'[Decrypt] Have sessoin: ${inboundGroupSession.roomId} ${inboundGroupSession.sessionId} ${inboundGroupSession.senderKey}');
|
||||||
|
Logs.error(
|
||||||
|
'[Decrypt] Want indexes: $messageIndexKey $messageIndexValue');
|
||||||
|
Logs.error(
|
||||||
|
'[Decrypt] Have indexes: $messageIndexKey ${inboundGroupSession.indexes[messageIndexKey]}');
|
||||||
|
canRequestSession = true;
|
||||||
throw (DecryptError.CHANNEL_CORRUPTED);
|
throw (DecryptError.CHANNEL_CORRUPTED);
|
||||||
}
|
}
|
||||||
inboundGroupSession.indexes[messageIndexKey] =
|
inboundGroupSession.indexes[messageIndexKey] = messageIndexValue;
|
||||||
decryptResult.message_index;
|
|
||||||
if (!haveIndex) {
|
if (!haveIndex) {
|
||||||
// now we persist the udpated indexes into the database.
|
// now we persist the udpated indexes into the database.
|
||||||
// the entry should always exist. In the case it doesn't, the following
|
// the entry should always exist. In the case it doesn't, the following
|
||||||
|
@ -263,6 +295,9 @@ class Encryption {
|
||||||
if (sess == null) {
|
if (sess == null) {
|
||||||
throw ('Unable to create new outbound group session');
|
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 Map<String, dynamic> mRelatesTo = payload.remove('m.relates_to');
|
||||||
final payloadContent = {
|
final payloadContent = {
|
||||||
'content': payload,
|
'content': payload,
|
||||||
|
@ -296,10 +331,41 @@ class Encryption {
|
||||||
return await olmManager.encryptToDeviceMessage(deviceKeys, type, payload);
|
return await olmManager.encryptToDeviceMessage(deviceKeys, type, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> autovalidateMasterOwnKey() async {
|
||||||
|
// check if we can set our own master key as verified, if it isn't yet
|
||||||
|
if (client.database != null &&
|
||||||
|
client.userDeviceKeys.containsKey(client.userID)) {
|
||||||
|
final masterKey = client.userDeviceKeys[client.userID].masterKey;
|
||||||
|
if (masterKey != null &&
|
||||||
|
!masterKey.directVerified &&
|
||||||
|
masterKey
|
||||||
|
.hasValidSignatureChain(onlyValidateUserIds: {client.userID})) {
|
||||||
|
await masterKey.setVerified(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this method is responsible for all background tasks, such as uploading online key backups
|
||||||
|
bool _backgroundTasksRunning = true;
|
||||||
|
void _backgroundTasks() {
|
||||||
|
if (!_backgroundTasksRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
keyManager.backgroundTasks();
|
||||||
|
|
||||||
|
autovalidateMasterOwnKey();
|
||||||
|
|
||||||
|
if (_backgroundTasksRunning) {
|
||||||
|
Timer(Duration(seconds: 10), _backgroundTasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
keyManager.dispose();
|
keyManager.dispose();
|
||||||
olmManager.dispose();
|
olmManager.dispose();
|
||||||
keyVerificationManager.dispose();
|
keyVerificationManager.dispose();
|
||||||
|
_backgroundTasksRunning = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,14 +18,18 @@
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:pedantic/pedantic.dart';
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
import 'package:pedantic/pedantic.dart';
|
||||||
|
|
||||||
import './encryption.dart';
|
import './encryption.dart';
|
||||||
import './utils/session_key.dart';
|
|
||||||
import './utils/outbound_group_session.dart';
|
import './utils/outbound_group_session.dart';
|
||||||
|
import './utils/session_key.dart';
|
||||||
|
import '../famedlysdk.dart';
|
||||||
|
import '../matrix_api.dart';
|
||||||
|
import '../src/database/database.dart';
|
||||||
|
import '../src/utils/logs.dart';
|
||||||
|
import '../src/utils/run_in_background.dart';
|
||||||
|
import '../src/utils/run_in_root.dart';
|
||||||
|
|
||||||
const MEGOLM_KEY = 'm.megolm_backup.v1';
|
const MEGOLM_KEY = 'm.megolm_backup.v1';
|
||||||
|
|
||||||
|
@ -43,7 +47,7 @@ class KeyManager {
|
||||||
encryption.ssss.setValidator(MEGOLM_KEY, (String secret) async {
|
encryption.ssss.setValidator(MEGOLM_KEY, (String secret) async {
|
||||||
final keyObj = olm.PkDecryption();
|
final keyObj = olm.PkDecryption();
|
||||||
try {
|
try {
|
||||||
final info = await client.api.getRoomKeysBackup();
|
final info = await getRoomKeysBackupInfo(false);
|
||||||
if (info.algorithm != RoomKeysAlgorithmType.v1Curve25519AesSha2) {
|
if (info.algorithm != RoomKeysAlgorithmType.v1Curve25519AesSha2) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -70,7 +74,16 @@ class KeyManager {
|
||||||
|
|
||||||
void setInboundGroupSession(String roomId, String sessionId, String senderKey,
|
void setInboundGroupSession(String roomId, String sessionId, String senderKey,
|
||||||
Map<String, dynamic> content,
|
Map<String, dynamic> content,
|
||||||
{bool forwarded = false}) {
|
{bool forwarded = false,
|
||||||
|
Map<String, String> senderClaimedKeys,
|
||||||
|
bool uploaded = false}) {
|
||||||
|
senderClaimedKeys ??= <String, String>{};
|
||||||
|
if (!senderClaimedKeys.containsKey('ed25519')) {
|
||||||
|
final device = client.getUserDeviceKeysByCurve25519Key(senderKey);
|
||||||
|
if (device != null) {
|
||||||
|
senderClaimedKeys['ed25519'] = device.ed25519Key;
|
||||||
|
}
|
||||||
|
}
|
||||||
final oldSession =
|
final oldSession =
|
||||||
getInboundGroupSession(roomId, sessionId, senderKey, otherRooms: false);
|
getInboundGroupSession(roomId, sessionId, senderKey, otherRooms: false);
|
||||||
if (content['algorithm'] != 'm.megolm.v1.aes-sha2') {
|
if (content['algorithm'] != 'm.megolm.v1.aes-sha2') {
|
||||||
|
@ -84,17 +97,22 @@ class KeyManager {
|
||||||
} else {
|
} else {
|
||||||
inboundGroupSession.create(content['session_key']);
|
inboundGroupSession.create(content['session_key']);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
inboundGroupSession.free();
|
inboundGroupSession.free();
|
||||||
print(
|
Logs.error(
|
||||||
'[LibOlm] Could not create new InboundGroupSession: ' + e.toString());
|
'[LibOlm] Could not create new InboundGroupSession: ' + e.toString(),
|
||||||
|
s);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final newSession = SessionKey(
|
final newSession = SessionKey(
|
||||||
content: content,
|
content: content,
|
||||||
inboundGroupSession: inboundGroupSession,
|
inboundGroupSession: inboundGroupSession,
|
||||||
indexes: {},
|
indexes: {},
|
||||||
|
roomId: roomId,
|
||||||
|
sessionId: sessionId,
|
||||||
key: client.userID,
|
key: client.userID,
|
||||||
|
senderKey: senderKey,
|
||||||
|
senderClaimedKeys: senderClaimedKeys,
|
||||||
);
|
);
|
||||||
final oldFirstIndex =
|
final oldFirstIndex =
|
||||||
oldSession?.inboundGroupSession?.first_known_index() ?? 0;
|
oldSession?.inboundGroupSession?.first_known_index() ?? 0;
|
||||||
|
@ -115,14 +133,23 @@ class KeyManager {
|
||||||
_inboundGroupSessions[roomId] = <String, SessionKey>{};
|
_inboundGroupSessions[roomId] = <String, SessionKey>{};
|
||||||
}
|
}
|
||||||
_inboundGroupSessions[roomId][sessionId] = newSession;
|
_inboundGroupSessions[roomId][sessionId] = newSession;
|
||||||
client.database?.storeInboundGroupSession(
|
client.database
|
||||||
|
?.storeInboundGroupSession(
|
||||||
client.id,
|
client.id,
|
||||||
roomId,
|
roomId,
|
||||||
sessionId,
|
sessionId,
|
||||||
inboundGroupSession.pickle(client.userID),
|
inboundGroupSession.pickle(client.userID),
|
||||||
json.encode(content),
|
json.encode(content),
|
||||||
json.encode({}),
|
json.encode({}),
|
||||||
);
|
senderKey,
|
||||||
|
json.encode(senderClaimedKeys),
|
||||||
|
)
|
||||||
|
?.then((_) {
|
||||||
|
if (uploaded) {
|
||||||
|
client.database
|
||||||
|
.markInboundGroupSessionAsUploaded(client.id, roomId, sessionId);
|
||||||
|
}
|
||||||
|
});
|
||||||
// TODO: somehow try to decrypt last message again
|
// TODO: somehow try to decrypt last message again
|
||||||
final room = client.getRoomById(roomId);
|
final room = client.getRoomById(roomId);
|
||||||
if (room != null) {
|
if (room != null) {
|
||||||
|
@ -135,7 +162,11 @@ class KeyManager {
|
||||||
{bool otherRooms = true}) {
|
{bool otherRooms = true}) {
|
||||||
if (_inboundGroupSessions.containsKey(roomId) &&
|
if (_inboundGroupSessions.containsKey(roomId) &&
|
||||||
_inboundGroupSessions[roomId].containsKey(sessionId)) {
|
_inboundGroupSessions[roomId].containsKey(sessionId)) {
|
||||||
return _inboundGroupSessions[roomId][sessionId];
|
final sess = _inboundGroupSessions[roomId][sessionId];
|
||||||
|
if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return sess;
|
||||||
}
|
}
|
||||||
if (!otherRooms) {
|
if (!otherRooms) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -143,7 +174,11 @@ class KeyManager {
|
||||||
// search if this session id is *somehow* found in another room
|
// search if this session id is *somehow* found in another room
|
||||||
for (final val in _inboundGroupSessions.values) {
|
for (final val in _inboundGroupSessions.values) {
|
||||||
if (val.containsKey(sessionId)) {
|
if (val.containsKey(sessionId)) {
|
||||||
return val[sessionId];
|
final sess = val[sessionId];
|
||||||
|
if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return sess;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -157,7 +192,11 @@ class KeyManager {
|
||||||
}
|
}
|
||||||
if (_inboundGroupSessions.containsKey(roomId) &&
|
if (_inboundGroupSessions.containsKey(roomId) &&
|
||||||
_inboundGroupSessions[roomId].containsKey(sessionId)) {
|
_inboundGroupSessions[roomId].containsKey(sessionId)) {
|
||||||
return _inboundGroupSessions[roomId][sessionId]; // nothing to do
|
final sess = _inboundGroupSessions[roomId][sessionId];
|
||||||
|
if (sess.senderKey != senderKey && sess.senderKey.isNotEmpty) {
|
||||||
|
return null; // sender keys do not match....better not do anything
|
||||||
|
}
|
||||||
|
return sess; // nothing to do
|
||||||
}
|
}
|
||||||
final session = await client.database
|
final session = await client.database
|
||||||
?.getDbInboundGroupSession(client.id, roomId, sessionId);
|
?.getDbInboundGroupSession(client.id, roomId, sessionId);
|
||||||
|
@ -166,10 +205,12 @@ class KeyManager {
|
||||||
final requestIdent = '$roomId|$sessionId|$senderKey';
|
final requestIdent = '$roomId|$sessionId|$senderKey';
|
||||||
if (client.enableE2eeRecovery &&
|
if (client.enableE2eeRecovery &&
|
||||||
room != null &&
|
room != null &&
|
||||||
!_requestedSessionIds.contains(requestIdent)) {
|
!_requestedSessionIds.contains(requestIdent) &&
|
||||||
|
!client.isUnknownSession) {
|
||||||
// do e2ee recovery
|
// do e2ee recovery
|
||||||
_requestedSessionIds.add(requestIdent);
|
_requestedSessionIds.add(requestIdent);
|
||||||
unawaited(request(room, sessionId, senderKey));
|
unawaited(runInRoot(() =>
|
||||||
|
request(room, sessionId, senderKey, askOnlyOwnDevices: true)));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -177,7 +218,8 @@ class KeyManager {
|
||||||
_inboundGroupSessions[roomId] = <String, SessionKey>{};
|
_inboundGroupSessions[roomId] = <String, SessionKey>{};
|
||||||
}
|
}
|
||||||
final sess = SessionKey.fromDb(session, client.userID);
|
final sess = SessionKey.fromDb(session, client.userID);
|
||||||
if (!sess.isValid) {
|
if (!sess.isValid ||
|
||||||
|
(sess.senderKey.isNotEmpty && sess.senderKey != senderKey)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
_inboundGroupSessions[roomId][sessionId] = sess;
|
_inboundGroupSessions[roomId][sessionId] = sess;
|
||||||
|
@ -261,10 +303,11 @@ class KeyManager {
|
||||||
final outboundGroupSession = olm.OutboundGroupSession();
|
final outboundGroupSession = olm.OutboundGroupSession();
|
||||||
try {
|
try {
|
||||||
outboundGroupSession.create();
|
outboundGroupSession.create();
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
outboundGroupSession.free();
|
outboundGroupSession.free();
|
||||||
print('[LibOlm] Unable to create new outboundGroupSession: ' +
|
Logs.error(
|
||||||
e.toString());
|
'[LibOlm] Unable to create new outboundGroupSession: ' + e.toString(),
|
||||||
|
s);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final rawSession = <String, dynamic>{
|
final rawSession = <String, dynamic>{
|
||||||
|
@ -283,14 +326,14 @@ class KeyManager {
|
||||||
key: client.userID,
|
key: client.userID,
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await client.sendToDevice(deviceKeys, 'm.room_key', rawSession);
|
await client.sendToDeviceEncrypted(deviceKeys, 'm.room_key', rawSession);
|
||||||
await storeOutboundGroupSession(roomId, sess);
|
await storeOutboundGroupSession(roomId, sess);
|
||||||
_outboundGroupSessions[roomId] = sess;
|
_outboundGroupSessions[roomId] = sess;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
print(
|
Logs.error(
|
||||||
'[LibOlm] Unable to send the session key to the participating devices: ' +
|
'[LibOlm] Unable to send the session key to the participating devices: ' +
|
||||||
e.toString());
|
e.toString(),
|
||||||
print(s);
|
s);
|
||||||
sess.dispose();
|
sess.dispose();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -327,6 +370,23 @@ class KeyManager {
|
||||||
return (await encryption.ssss.getCached(MEGOLM_KEY)) != null;
|
return (await encryption.ssss.getCached(MEGOLM_KEY)) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RoomKeysVersionResponse _roomKeysVersionCache;
|
||||||
|
DateTime _roomKeysVersionCacheDate;
|
||||||
|
Future<RoomKeysVersionResponse> getRoomKeysBackupInfo(
|
||||||
|
[bool useCache = true]) async {
|
||||||
|
if (_roomKeysVersionCache != null &&
|
||||||
|
_roomKeysVersionCacheDate != null &&
|
||||||
|
useCache &&
|
||||||
|
DateTime.now()
|
||||||
|
.subtract(Duration(minutes: 5))
|
||||||
|
.isBefore(_roomKeysVersionCacheDate)) {
|
||||||
|
return _roomKeysVersionCache;
|
||||||
|
}
|
||||||
|
_roomKeysVersionCache = await client.getRoomKeysBackup();
|
||||||
|
_roomKeysVersionCacheDate = DateTime.now();
|
||||||
|
return _roomKeysVersionCache;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> loadFromResponse(RoomKeys keys) async {
|
Future<void> loadFromResponse(RoomKeys keys) async {
|
||||||
if (!(await isCached())) {
|
if (!(await isCached())) {
|
||||||
return;
|
return;
|
||||||
|
@ -334,7 +394,7 @@ class KeyManager {
|
||||||
final privateKey =
|
final privateKey =
|
||||||
base64.decode(await encryption.ssss.getCached(MEGOLM_KEY));
|
base64.decode(await encryption.ssss.getCached(MEGOLM_KEY));
|
||||||
final decryption = olm.PkDecryption();
|
final decryption = olm.PkDecryption();
|
||||||
final info = await client.api.getRoomKeysBackup();
|
final info = await getRoomKeysBackupInfo();
|
||||||
String backupPubKey;
|
String backupPubKey;
|
||||||
try {
|
try {
|
||||||
backupPubKey = decryption.init_with_private_key(privateKey);
|
backupPubKey = decryption.init_with_private_key(privateKey);
|
||||||
|
@ -363,15 +423,20 @@ class KeyManager {
|
||||||
try {
|
try {
|
||||||
decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'],
|
decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'],
|
||||||
sessionData['mac'], sessionData['ciphertext']));
|
sessionData['mac'], sessionData['ciphertext']));
|
||||||
} catch (err) {
|
} catch (e, s) {
|
||||||
print('[LibOlm] Error decrypting room key: ' + err.toString());
|
Logs.error(
|
||||||
|
'[LibOlm] Error decrypting room key: ' + e.toString(), s);
|
||||||
}
|
}
|
||||||
if (decrypted != null) {
|
if (decrypted != null) {
|
||||||
decrypted['session_id'] = sessionId;
|
decrypted['session_id'] = sessionId;
|
||||||
decrypted['room_id'] = roomId;
|
decrypted['room_id'] = roomId;
|
||||||
setInboundGroupSession(
|
setInboundGroupSession(
|
||||||
roomId, sessionId, decrypted['sender_key'], decrypted,
|
roomId, sessionId, decrypted['sender_key'], decrypted,
|
||||||
forwarded: true);
|
forwarded: true,
|
||||||
|
senderClaimedKeys: decrypted['sender_claimed_keys'] != null
|
||||||
|
? Map<String, String>.from(decrypted['sender_claimed_keys'])
|
||||||
|
: null,
|
||||||
|
uploaded: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -381,9 +446,9 @@ class KeyManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadSingleKey(String roomId, String sessionId) async {
|
Future<void> loadSingleKey(String roomId, String sessionId) async {
|
||||||
final info = await client.api.getRoomKeysBackup();
|
final info = await getRoomKeysBackupInfo();
|
||||||
final ret =
|
final ret =
|
||||||
await client.api.getRoomKeysSingleKey(roomId, sessionId, info.version);
|
await client.getRoomKeysSingleKey(roomId, sessionId, info.version);
|
||||||
final keys = RoomKeys.fromJson({
|
final keys = RoomKeys.fromJson({
|
||||||
'rooms': {
|
'rooms': {
|
||||||
roomId: {
|
roomId: {
|
||||||
|
@ -397,37 +462,53 @@ class KeyManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request a certain key from another device
|
/// Request a certain key from another device
|
||||||
Future<void> request(Room room, String sessionId, String senderKey,
|
Future<void> request(
|
||||||
{bool tryOnlineBackup = true}) async {
|
Room room,
|
||||||
if (tryOnlineBackup) {
|
String sessionId,
|
||||||
|
String senderKey, {
|
||||||
|
bool tryOnlineBackup = true,
|
||||||
|
bool askOnlyOwnDevices = false,
|
||||||
|
}) async {
|
||||||
|
if (tryOnlineBackup && await isCached()) {
|
||||||
// let's first check our online key backup store thingy...
|
// let's first check our online key backup store thingy...
|
||||||
var hadPreviously =
|
var hadPreviously =
|
||||||
getInboundGroupSession(room.id, sessionId, senderKey) != null;
|
getInboundGroupSession(room.id, sessionId, senderKey) != null;
|
||||||
try {
|
try {
|
||||||
await loadSingleKey(room.id, sessionId);
|
await loadSingleKey(room.id, sessionId);
|
||||||
} catch (err, stacktrace) {
|
} catch (err, stacktrace) {
|
||||||
print('[KeyManager] Failed to access online key backup: ' +
|
if (err is MatrixException && err.errcode == 'M_NOT_FOUND') {
|
||||||
err.toString());
|
Logs.info(
|
||||||
print(stacktrace);
|
'[KeyManager] Key not in online key backup, requesting it from other devices...');
|
||||||
|
} else {
|
||||||
|
Logs.error(
|
||||||
|
'[KeyManager] Failed to access online key backup: ' +
|
||||||
|
err.toString(),
|
||||||
|
stacktrace);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!hadPreviously &&
|
if (!hadPreviously &&
|
||||||
getInboundGroupSession(room.id, sessionId, senderKey) != null) {
|
getInboundGroupSession(room.id, sessionId, senderKey) != null) {
|
||||||
return; // we managed to load the session from online backup, no need to care about it now
|
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
|
try {
|
||||||
// devices themself to know where to send the cancel to after receiving a reply
|
// while we just send the to-device event to '*', we still need to save the
|
||||||
final devices = await room.getUserDeviceKeys();
|
// devices themself to know where to send the cancel to after receiving a reply
|
||||||
final requestId = client.generateUniqueTransactionId();
|
final devices = await room.getUserDeviceKeys();
|
||||||
final request = KeyManagerKeyShareRequest(
|
if (askOnlyOwnDevices) {
|
||||||
requestId: requestId,
|
devices.removeWhere((d) => d.userId != client.userID);
|
||||||
devices: devices,
|
}
|
||||||
room: room,
|
final requestId = client.generateUniqueTransactionId();
|
||||||
sessionId: sessionId,
|
final request = KeyManagerKeyShareRequest(
|
||||||
senderKey: senderKey,
|
requestId: requestId,
|
||||||
);
|
devices: devices,
|
||||||
await client.sendToDevice(
|
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',
|
'm.room_key_request',
|
||||||
{
|
{
|
||||||
'action': 'request',
|
'action': 'request',
|
||||||
|
@ -440,9 +521,87 @@ class KeyManager {
|
||||||
'request_id': requestId,
|
'request_id': requestId,
|
||||||
'requesting_device_id': client.deviceID,
|
'requesting_device_id': client.deviceID,
|
||||||
},
|
},
|
||||||
encrypted: false,
|
);
|
||||||
toUsers: await room.requestParticipants());
|
outgoingShareRequests[request.requestId] = request;
|
||||||
outgoingShareRequests[request.requestId] = request;
|
} catch (e, s) {
|
||||||
|
Logs.error(
|
||||||
|
'[Key Manager] Sending key verification request failed: ' +
|
||||||
|
e.toString(),
|
||||||
|
s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isUploadingKeys = false;
|
||||||
|
Future<void> backgroundTasks() async {
|
||||||
|
if (_isUploadingKeys || client.database == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_isUploadingKeys = true;
|
||||||
|
try {
|
||||||
|
if (!(await isCached())) {
|
||||||
|
return; // we can't backup anyways
|
||||||
|
}
|
||||||
|
final dbSessions =
|
||||||
|
await client.database.getInboundGroupSessionsToUpload().get();
|
||||||
|
if (dbSessions.isEmpty) {
|
||||||
|
return; // nothing to do
|
||||||
|
}
|
||||||
|
final privateKey =
|
||||||
|
base64.decode(await encryption.ssss.getCached(MEGOLM_KEY));
|
||||||
|
// decryption is needed to calculate the public key and thus see if the claimed information is in fact valid
|
||||||
|
final decryption = olm.PkDecryption();
|
||||||
|
final info = await getRoomKeysBackupInfo(false);
|
||||||
|
String backupPubKey;
|
||||||
|
try {
|
||||||
|
backupPubKey = decryption.init_with_private_key(privateKey);
|
||||||
|
|
||||||
|
if (backupPubKey == null ||
|
||||||
|
info.algorithm != RoomKeysAlgorithmType.v1Curve25519AesSha2 ||
|
||||||
|
info.authData['public_key'] != backupPubKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final args = _GenerateUploadKeysArgs(
|
||||||
|
pubkey: backupPubKey,
|
||||||
|
dbSessions: <_DbInboundGroupSessionBundle>[],
|
||||||
|
userId: client.userID,
|
||||||
|
);
|
||||||
|
// we need to calculate verified beforehand, as else we pass a closure to an isolate
|
||||||
|
// with 500 keys they do, however, noticably block the UI, which is why we give brief async suspentions in here
|
||||||
|
// so that the event loop can progress
|
||||||
|
var i = 0;
|
||||||
|
for (final dbSession in dbSessions) {
|
||||||
|
final device =
|
||||||
|
client.getUserDeviceKeysByCurve25519Key(dbSession.senderKey);
|
||||||
|
args.dbSessions.add(_DbInboundGroupSessionBundle(
|
||||||
|
dbSession: dbSession,
|
||||||
|
verified: device?.verified ?? false,
|
||||||
|
));
|
||||||
|
i++;
|
||||||
|
if (i > 10) {
|
||||||
|
await Future.delayed(Duration(milliseconds: 1));
|
||||||
|
i = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final roomKeys =
|
||||||
|
await runInBackground<RoomKeys, _GenerateUploadKeysArgs>(
|
||||||
|
_generateUploadKeys, args);
|
||||||
|
Logs.info('[Key Manager] Uploading ${dbSessions.length} room keys...');
|
||||||
|
// upload the payload...
|
||||||
|
await client.storeRoomKeys(info.version, roomKeys);
|
||||||
|
// and now finally mark all the keys as uploaded
|
||||||
|
// no need to optimze this, as we only run it so seldomly and almost never with many keys at once
|
||||||
|
for (final dbSession in dbSessions) {
|
||||||
|
await client.database.markInboundGroupSessionAsUploaded(
|
||||||
|
client.id, dbSession.roomId, dbSession.sessionId);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
decryption.free();
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs.error('[Key Manager] Error uploading room keys: ' + e.toString(), s);
|
||||||
|
} finally {
|
||||||
|
_isUploadingKeys = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle an incoming to_device event that is related to key sharing
|
/// Handle an incoming to_device event that is related to key sharing
|
||||||
|
@ -453,27 +612,27 @@ class KeyManager {
|
||||||
}
|
}
|
||||||
if (event.content['action'] == 'request') {
|
if (event.content['action'] == 'request') {
|
||||||
// we are *receiving* a request
|
// we are *receiving* a request
|
||||||
print('[KeyManager] Received key sharing request...');
|
Logs.info('[KeyManager] Received key sharing request...');
|
||||||
if (!event.content.containsKey('body')) {
|
if (!event.content.containsKey('body')) {
|
||||||
print('[KeyManager] No body, doing nothing');
|
Logs.info('[KeyManager] No body, doing nothing');
|
||||||
return; // no body
|
return; // no body
|
||||||
}
|
}
|
||||||
if (!client.userDeviceKeys.containsKey(event.sender) ||
|
if (!client.userDeviceKeys.containsKey(event.sender) ||
|
||||||
!client.userDeviceKeys[event.sender].deviceKeys
|
!client.userDeviceKeys[event.sender].deviceKeys
|
||||||
.containsKey(event.content['requesting_device_id'])) {
|
.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
|
return; // device not found
|
||||||
}
|
}
|
||||||
final device = client.userDeviceKeys[event.sender]
|
final device = client.userDeviceKeys[event.sender]
|
||||||
.deviceKeys[event.content['requesting_device_id']];
|
.deviceKeys[event.content['requesting_device_id']];
|
||||||
if (device.userId == client.userID &&
|
if (device.userId == client.userID &&
|
||||||
device.deviceId == client.deviceID) {
|
device.deviceId == client.deviceID) {
|
||||||
print('[KeyManager] Request is by ourself, ignoring');
|
Logs.info('[KeyManager] Request is by ourself, ignoring');
|
||||||
return; // ignore requests by ourself
|
return; // ignore requests by ourself
|
||||||
}
|
}
|
||||||
final room = client.getRoomById(event.content['body']['room_id']);
|
final room = client.getRoomById(event.content['body']['room_id']);
|
||||||
if (room == null) {
|
if (room == null) {
|
||||||
print('[KeyManager] Unknown room, ignoring');
|
Logs.info('[KeyManager] Unknown room, ignoring');
|
||||||
return; // unknown room
|
return; // unknown room
|
||||||
}
|
}
|
||||||
final sessionId = event.content['body']['session_id'];
|
final sessionId = event.content['body']['session_id'];
|
||||||
|
@ -481,7 +640,7 @@ class KeyManager {
|
||||||
// okay, let's see if we have this session at all
|
// okay, let's see if we have this session at all
|
||||||
if ((await loadInboundGroupSession(room.id, sessionId, senderKey)) ==
|
if ((await loadInboundGroupSession(room.id, sessionId, senderKey)) ==
|
||||||
null) {
|
null) {
|
||||||
print('[KeyManager] Unknown session, ignoring');
|
Logs.info('[KeyManager] Unknown session, ignoring');
|
||||||
return; // we don't have this session anyways
|
return; // we don't have this session anyways
|
||||||
}
|
}
|
||||||
final request = KeyManagerKeyShareRequest(
|
final request = KeyManagerKeyShareRequest(
|
||||||
|
@ -492,7 +651,7 @@ class KeyManager {
|
||||||
senderKey: senderKey,
|
senderKey: senderKey,
|
||||||
);
|
);
|
||||||
if (incomingShareRequests.containsKey(request.requestId)) {
|
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
|
return; // we don't want to process one and the same request multiple times
|
||||||
}
|
}
|
||||||
incomingShareRequests[request.requestId] = request;
|
incomingShareRequests[request.requestId] = request;
|
||||||
|
@ -501,11 +660,12 @@ class KeyManager {
|
||||||
if (device.userId == client.userID &&
|
if (device.userId == client.userID &&
|
||||||
device.verified &&
|
device.verified &&
|
||||||
!device.blocked) {
|
!device.blocked) {
|
||||||
print('[KeyManager] All checks out, forwarding key...');
|
Logs.info('[KeyManager] All checks out, forwarding key...');
|
||||||
// alright, we can forward the key
|
// alright, we can forward the key
|
||||||
await roomKeyRequest.forwardKey();
|
await roomKeyRequest.forwardKey();
|
||||||
} else {
|
} else {
|
||||||
print('[KeyManager] Asking client, if the key should be forwarded');
|
Logs.info(
|
||||||
|
'[KeyManager] Asking client, if the key should be forwarded');
|
||||||
client.onRoomKeyRequest
|
client.onRoomKeyRequest
|
||||||
.add(roomKeyRequest); // let the client handle this
|
.add(roomKeyRequest); // let the client handle this
|
||||||
}
|
}
|
||||||
|
@ -541,11 +701,20 @@ class KeyManager {
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
return; // someone we didn't send our request to replied....better ignore this
|
return; // someone we didn't send our request to replied....better ignore this
|
||||||
}
|
}
|
||||||
|
// we add the sender key to the forwarded key chain
|
||||||
|
if (!(event.content['forwarding_curve25519_key_chain'] is List)) {
|
||||||
|
event.content['forwarding_curve25519_key_chain'] = <String>[];
|
||||||
|
}
|
||||||
|
event.content['forwarding_curve25519_key_chain']
|
||||||
|
.add(event.encryptedContent['sender_key']);
|
||||||
// TODO: verify that the keys work to decrypt a message
|
// TODO: verify that the keys work to decrypt a message
|
||||||
// alright, all checks out, let's go ahead and store this session
|
// alright, all checks out, let's go ahead and store this session
|
||||||
setInboundGroupSession(
|
setInboundGroupSession(
|
||||||
request.room.id, request.sessionId, request.senderKey, event.content,
|
request.room.id, request.sessionId, request.senderKey, event.content,
|
||||||
forwarded: true);
|
forwarded: true,
|
||||||
|
senderClaimedKeys: {
|
||||||
|
'ed25519': event.content['sender_claimed_ed25519_key'],
|
||||||
|
});
|
||||||
request.devices.removeWhere(
|
request.devices.removeWhere(
|
||||||
(k) => k.userId == device.userId && k.deviceId == device.deviceId);
|
(k) => k.userId == device.userId && k.deviceId == device.deviceId);
|
||||||
outgoingShareRequests.remove(request.requestId);
|
outgoingShareRequests.remove(request.requestId);
|
||||||
|
@ -553,15 +722,24 @@ class KeyManager {
|
||||||
if (request.devices.isEmpty) {
|
if (request.devices.isEmpty) {
|
||||||
return; // no need to send any cancellation
|
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(
|
await client.sendToDevice(
|
||||||
request.devices,
|
'm.room_key_request',
|
||||||
'm.room_key_request',
|
client.generateUniqueTransactionId(),
|
||||||
{
|
data,
|
||||||
'action': 'request_cancellation',
|
);
|
||||||
'request_id': request.requestId,
|
|
||||||
'requesting_device_id': client.deviceID,
|
|
||||||
},
|
|
||||||
encrypted: false);
|
|
||||||
} else if (event.type == 'm.room_key') {
|
} else if (event.type == 'm.room_key') {
|
||||||
if (event.encryptedContent == null) {
|
if (event.encryptedContent == null) {
|
||||||
return; // the event wasn't encrypted, this is a security risk;
|
return; // the event wasn't encrypted, this is a security risk;
|
||||||
|
@ -635,32 +813,23 @@ class RoomKeyRequest extends ToDeviceEvent {
|
||||||
var room = this.room;
|
var room = this.room;
|
||||||
final session = await keyManager.loadInboundGroupSession(
|
final session = await keyManager.loadInboundGroupSession(
|
||||||
room.id, request.sessionId, request.senderKey);
|
room.id, request.sessionId, request.senderKey);
|
||||||
var forwardedKeys = <dynamic>[keyManager.encryption.identityKey];
|
|
||||||
for (final key in session.forwardingCurve25519KeyChain) {
|
|
||||||
forwardedKeys.add(key);
|
|
||||||
}
|
|
||||||
var message = session.content;
|
var message = session.content;
|
||||||
message['forwarding_curve25519_key_chain'] = forwardedKeys;
|
message['forwarding_curve25519_key_chain'] =
|
||||||
|
List<String>.from(session.forwardingCurve25519KeyChain);
|
||||||
|
|
||||||
message['sender_key'] = request.senderKey;
|
message['sender_key'] =
|
||||||
|
(session.senderKey != null && session.senderKey.isNotEmpty)
|
||||||
|
? session.senderKey
|
||||||
|
: request.senderKey;
|
||||||
message['sender_claimed_ed25519_key'] =
|
message['sender_claimed_ed25519_key'] =
|
||||||
forwardedKeys.isEmpty ? keyManager.encryption.fingerprintKey : null;
|
session.senderClaimedKeys['ed25519'] ??
|
||||||
if (message['sender_claimed_ed25519_key'] == null) {
|
(session.forwardingCurve25519KeyChain.isEmpty
|
||||||
for (final value in keyManager.client.userDeviceKeys.values) {
|
? keyManager.encryption.fingerprintKey
|
||||||
for (final key in value.deviceKeys.values) {
|
: null);
|
||||||
if (key.curve25519Key == forwardedKeys.first) {
|
|
||||||
message['sender_claimed_ed25519_key'] = key.ed25519Key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (message['sender_claimed_ed25519_key'] != null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
message['session_key'] = session.inboundGroupSession
|
message['session_key'] = session.inboundGroupSession
|
||||||
.export_session(session.inboundGroupSession.first_known_index());
|
.export_session(session.inboundGroupSession.first_known_index());
|
||||||
// send the actual reply of the key back to the requester
|
// send the actual reply of the key back to the requester
|
||||||
await keyManager.client.sendToDevice(
|
await keyManager.client.sendToDeviceEncrypted(
|
||||||
[requestingDevice],
|
[requestingDevice],
|
||||||
'm.forwarded_room_key',
|
'm.forwarded_room_key',
|
||||||
message,
|
message,
|
||||||
|
@ -668,3 +837,67 @@ class RoomKeyRequest extends ToDeviceEvent {
|
||||||
keyManager.incomingShareRequests.remove(request.requestId);
|
keyManager.incomingShareRequests.remove(request.requestId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RoomKeys _generateUploadKeys(_GenerateUploadKeysArgs args) {
|
||||||
|
final enc = olm.PkEncryption();
|
||||||
|
try {
|
||||||
|
enc.set_recipient_key(args.pubkey);
|
||||||
|
// first we generate the payload to upload all the session keys in this chunk
|
||||||
|
final roomKeys = RoomKeys();
|
||||||
|
for (final dbSession in args.dbSessions) {
|
||||||
|
final sess = SessionKey.fromDb(dbSession.dbSession, args.userId);
|
||||||
|
if (!sess.isValid) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// create the room if it doesn't exist
|
||||||
|
if (!roomKeys.rooms.containsKey(sess.roomId)) {
|
||||||
|
roomKeys.rooms[sess.roomId] = RoomKeysRoom();
|
||||||
|
}
|
||||||
|
// generate the encrypted content
|
||||||
|
final payload = <String, dynamic>{
|
||||||
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||||
|
'forwarding_curve25519_key_chain': sess.forwardingCurve25519KeyChain,
|
||||||
|
'sender_key': sess.senderKey,
|
||||||
|
'sender_clencaimed_keys': sess.senderClaimedKeys,
|
||||||
|
'session_key': sess.inboundGroupSession
|
||||||
|
.export_session(sess.inboundGroupSession.first_known_index()),
|
||||||
|
};
|
||||||
|
// encrypt the content
|
||||||
|
final encrypted = enc.encrypt(json.encode(payload));
|
||||||
|
// fetch the device, if available...
|
||||||
|
//final device = args.client.getUserDeviceKeysByCurve25519Key(sess.senderKey);
|
||||||
|
// aaaand finally add the session key to our payload
|
||||||
|
roomKeys.rooms[sess.roomId].sessions[sess.sessionId] = RoomKeysSingleKey(
|
||||||
|
firstMessageIndex: sess.inboundGroupSession.first_known_index(),
|
||||||
|
forwardedCount: sess.forwardingCurve25519KeyChain.length,
|
||||||
|
isVerified: dbSession.verified, //device?.verified ?? false,
|
||||||
|
sessionData: {
|
||||||
|
'ephemeral': encrypted.ephemeral,
|
||||||
|
'ciphertext': encrypted.ciphertext,
|
||||||
|
'mac': encrypted.mac,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return roomKeys;
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs.error('[Key Manager] Error generating payload ' + e.toString(), s);
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
enc.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DbInboundGroupSessionBundle {
|
||||||
|
_DbInboundGroupSessionBundle({this.dbSession, this.verified});
|
||||||
|
|
||||||
|
DbInboundGroupSession dbSession;
|
||||||
|
bool verified;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GenerateUploadKeysArgs {
|
||||||
|
_GenerateUploadKeysArgs({this.pubkey, this.dbSessions, this.userId});
|
||||||
|
|
||||||
|
String pubkey;
|
||||||
|
List<_DbInboundGroupSessionBundle> dbSessions;
|
||||||
|
String userId;
|
||||||
|
}
|
||||||
|
|
|
@ -16,9 +16,9 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import '../famedlysdk.dart';
|
||||||
import './encryption.dart';
|
import 'encryption.dart';
|
||||||
import './utils/key_verification.dart';
|
import 'utils/key_verification.dart';
|
||||||
|
|
||||||
class KeyVerificationManager {
|
class KeyVerificationManager {
|
||||||
final Encryption encryption;
|
final Encryption encryption;
|
||||||
|
@ -67,6 +67,10 @@ class KeyVerificationManager {
|
||||||
if (_requests.containsKey(transactionId)) {
|
if (_requests.containsKey(transactionId)) {
|
||||||
await _requests[transactionId].handlePayload(event.type, event.content);
|
await _requests[transactionId].handlePayload(event.type, event.content);
|
||||||
} else {
|
} else {
|
||||||
|
if (!['m.key.verification.request', 'm.key.verification.start']
|
||||||
|
.contains(event.type)) {
|
||||||
|
return; // we can only start on these
|
||||||
|
}
|
||||||
final newKeyRequest =
|
final newKeyRequest =
|
||||||
KeyVerification(encryption: encryption, userId: event.sender);
|
KeyVerification(encryption: encryption, userId: event.sender);
|
||||||
await newKeyRequest.handlePayload(event.type, event.content);
|
await newKeyRequest.handlePayload(event.type, event.content);
|
||||||
|
@ -111,6 +115,10 @@ class KeyVerificationManager {
|
||||||
_requests.remove(transactionId);
|
_requests.remove(transactionId);
|
||||||
}
|
}
|
||||||
} else if (event['sender'] != client.userID) {
|
} 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) ??
|
final room = client.getRoomById(update.roomID) ??
|
||||||
Room(id: update.roomID, client: client);
|
Room(id: update.roomID, client: client);
|
||||||
final newKeyRequest = KeyVerification(
|
final newKeyRequest = KeyVerification(
|
||||||
|
|
|
@ -18,13 +18,16 @@
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:pedantic/pedantic.dart';
|
|
||||||
import 'package:canonical_json/canonical_json.dart';
|
import 'package:canonical_json/canonical_json.dart';
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
import 'package:famedlysdk/matrix_api.dart';
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
import './encryption.dart';
|
import 'package:pedantic/pedantic.dart';
|
||||||
import './utils/olm_session.dart';
|
|
||||||
|
import '../encryption/utils/json_signature_check_extension.dart';
|
||||||
|
import '../src/utils/logs.dart';
|
||||||
|
import 'encryption.dart';
|
||||||
|
import 'utils/olm_session.dart';
|
||||||
|
|
||||||
class OlmManager {
|
class OlmManager {
|
||||||
final Encryption encryption;
|
final Encryption encryption;
|
||||||
|
@ -73,7 +76,8 @@ class OlmManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a signature to this json from this olm account.
|
/// Adds a signature to this json from this olm account and returns the signed
|
||||||
|
/// json.
|
||||||
Map<String, dynamic> signJson(Map<String, dynamic> payload) {
|
Map<String, dynamic> signJson(Map<String, dynamic> payload) {
|
||||||
if (!enabled) throw ('Encryption is disabled');
|
if (!enabled) throw ('Encryption is disabled');
|
||||||
final Map<String, dynamic> unsigned = payload['unsigned'];
|
final Map<String, dynamic> unsigned = payload['unsigned'];
|
||||||
|
@ -103,6 +107,7 @@ class OlmManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks the signature of a signed json object.
|
/// Checks the signature of a signed json object.
|
||||||
|
@deprecated
|
||||||
bool checkJsonSignature(String key, Map<String, dynamic> signedJson,
|
bool checkJsonSignature(String key, Map<String, dynamic> signedJson,
|
||||||
String userId, String deviceId) {
|
String userId, String deviceId) {
|
||||||
if (!enabled) throw ('Encryption is disabled');
|
if (!enabled) throw ('Encryption is disabled');
|
||||||
|
@ -119,15 +124,17 @@ class OlmManager {
|
||||||
try {
|
try {
|
||||||
olmutil.ed25519_verify(key, message, signature);
|
olmutil.ed25519_verify(key, message, signature);
|
||||||
isValid = true;
|
isValid = true;
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
isValid = false;
|
isValid = false;
|
||||||
print('[LibOlm] Signature check failed: ' + e.toString());
|
Logs.error('[LibOlm] Signature check failed: ' + e.toString(), s);
|
||||||
} finally {
|
} finally {
|
||||||
olmutil.free();
|
olmutil.free();
|
||||||
}
|
}
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _uploadKeysLock = false;
|
||||||
|
|
||||||
/// Generates new one time keys, signs everything and upload it to the server.
|
/// Generates new one time keys, signs everything and upload it to the server.
|
||||||
Future<bool> uploadKeys(
|
Future<bool> uploadKeys(
|
||||||
{bool uploadDeviceKeys = false, int oldKeyCount = 0}) async {
|
{bool uploadDeviceKeys = false, int oldKeyCount = 0}) async {
|
||||||
|
@ -135,62 +142,71 @@ class OlmManager {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate one-time keys
|
if (_uploadKeysLock) {
|
||||||
// we generate 2/3rds of max, so that other keys people may still have can
|
return false;
|
||||||
// still be used
|
|
||||||
final oneTimeKeysCount =
|
|
||||||
(_olmAccount.max_number_of_one_time_keys() * 2 / 3).floor() -
|
|
||||||
oldKeyCount;
|
|
||||||
_olmAccount.generate_one_time_keys(oneTimeKeysCount);
|
|
||||||
final Map<String, dynamic> oneTimeKeys =
|
|
||||||
json.decode(_olmAccount.one_time_keys());
|
|
||||||
|
|
||||||
// now sign all the one-time keys
|
|
||||||
final signedOneTimeKeys = <String, dynamic>{};
|
|
||||||
for (final entry in oneTimeKeys['curve25519'].entries) {
|
|
||||||
final key = entry.key;
|
|
||||||
final value = entry.value;
|
|
||||||
signedOneTimeKeys['signed_curve25519:$key'] = <String, dynamic>{};
|
|
||||||
signedOneTimeKeys['signed_curve25519:$key'] = signJson({
|
|
||||||
'key': value,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
_uploadKeysLock = true;
|
||||||
|
|
||||||
// and now generate the payload to upload
|
try {
|
||||||
final keysContent = <String, dynamic>{
|
// generate one-time keys
|
||||||
if (uploadDeviceKeys)
|
// we generate 2/3rds of max, so that other keys people may still have can
|
||||||
'device_keys': {
|
// still be used
|
||||||
'user_id': client.userID,
|
final oneTimeKeysCount =
|
||||||
'device_id': client.deviceID,
|
(_olmAccount.max_number_of_one_time_keys() * 2 / 3).floor() -
|
||||||
'algorithms': [
|
oldKeyCount;
|
||||||
'm.olm.v1.curve25519-aes-sha2',
|
_olmAccount.generate_one_time_keys(oneTimeKeysCount);
|
||||||
'm.megolm.v1.aes-sha2'
|
final Map<String, dynamic> oneTimeKeys =
|
||||||
],
|
json.decode(_olmAccount.one_time_keys());
|
||||||
'keys': <String, dynamic>{},
|
|
||||||
},
|
// now sign all the one-time keys
|
||||||
};
|
final signedOneTimeKeys = <String, dynamic>{};
|
||||||
if (uploadDeviceKeys) {
|
for (final entry in oneTimeKeys['curve25519'].entries) {
|
||||||
final Map<String, dynamic> keys =
|
final key = entry.key;
|
||||||
json.decode(_olmAccount.identity_keys());
|
|
||||||
for (final entry in keys.entries) {
|
|
||||||
final algorithm = entry.key;
|
|
||||||
final value = entry.value;
|
final value = entry.value;
|
||||||
keysContent['device_keys']['keys']['$algorithm:${client.deviceID}'] =
|
signedOneTimeKeys['signed_curve25519:$key'] = <String, dynamic>{};
|
||||||
value;
|
signedOneTimeKeys['signed_curve25519:$key'] = signJson({
|
||||||
|
'key': value,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
keysContent['device_keys'] =
|
|
||||||
signJson(keysContent['device_keys'] as Map<String, dynamic>);
|
|
||||||
}
|
|
||||||
|
|
||||||
final response = await client.api.uploadDeviceKeys(
|
// and now generate the payload to upload
|
||||||
deviceKeys: uploadDeviceKeys
|
final keysContent = <String, dynamic>{
|
||||||
? MatrixDeviceKeys.fromJson(keysContent['device_keys'])
|
if (uploadDeviceKeys)
|
||||||
: null,
|
'device_keys': {
|
||||||
oneTimeKeys: signedOneTimeKeys,
|
'user_id': client.userID,
|
||||||
);
|
'device_id': client.deviceID,
|
||||||
_olmAccount.mark_keys_as_published();
|
'algorithms': [
|
||||||
await client.database?.updateClientKeys(pickledOlmAccount, client.id);
|
'm.olm.v1.curve25519-aes-sha2',
|
||||||
return response['signed_curve25519'] == oneTimeKeysCount;
|
'm.megolm.v1.aes-sha2'
|
||||||
|
],
|
||||||
|
'keys': <String, dynamic>{},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (uploadDeviceKeys) {
|
||||||
|
final Map<String, dynamic> keys =
|
||||||
|
json.decode(_olmAccount.identity_keys());
|
||||||
|
for (final entry in keys.entries) {
|
||||||
|
final algorithm = entry.key;
|
||||||
|
final value = entry.value;
|
||||||
|
keysContent['device_keys']['keys']['$algorithm:${client.deviceID}'] =
|
||||||
|
value;
|
||||||
|
}
|
||||||
|
keysContent['device_keys'] =
|
||||||
|
signJson(keysContent['device_keys'] as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await client.uploadDeviceKeys(
|
||||||
|
deviceKeys: uploadDeviceKeys
|
||||||
|
? MatrixDeviceKeys.fromJson(keysContent['device_keys'])
|
||||||
|
: null,
|
||||||
|
oneTimeKeys: signedOneTimeKeys,
|
||||||
|
);
|
||||||
|
_olmAccount.mark_keys_as_published();
|
||||||
|
await client.database?.updateClientKeys(pickledOlmAccount, client.id);
|
||||||
|
return response['signed_curve25519'] == oneTimeKeysCount;
|
||||||
|
} finally {
|
||||||
|
_uploadKeysLock = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleDeviceOneTimeKeysCount(Map<String, int> countJson) {
|
void handleDeviceOneTimeKeysCount(Map<String, int> countJson) {
|
||||||
|
@ -231,7 +247,7 @@ class OlmManager {
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
if (event.content['algorithm'] != 'm.olm.v1.curve25519-aes-sha2') {
|
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)) {
|
if (!event.content['ciphertext'].containsKey(identityKey)) {
|
||||||
throw ("The message isn't sent for this device");
|
throw ("The message isn't sent for this device");
|
||||||
|
@ -334,7 +350,7 @@ class OlmManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await startOutgoingOlmSessions([device]);
|
await startOutgoingOlmSessions([device]);
|
||||||
await client.sendToDevice([device], 'm.dummy', {});
|
await client.sendToDeviceEncrypted([device], 'm.dummy', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ToDeviceEvent> decryptToDeviceEvent(ToDeviceEvent event) async {
|
Future<ToDeviceEvent> decryptToDeviceEvent(ToDeviceEvent event) async {
|
||||||
|
@ -382,7 +398,7 @@ class OlmManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
final response =
|
final response =
|
||||||
await client.api.requestOneTimeKeys(requestingKeysFrom, timeout: 10000);
|
await client.requestOneTimeKeys(requestingKeysFrom, timeout: 10000);
|
||||||
|
|
||||||
for (var userKeysEntry in response.oneTimeKeys.entries) {
|
for (var userKeysEntry in response.oneTimeKeys.entries) {
|
||||||
final userId = userKeysEntry.key;
|
final userId = userKeysEntry.key;
|
||||||
|
@ -393,8 +409,7 @@ class OlmManager {
|
||||||
final identityKey =
|
final identityKey =
|
||||||
client.userDeviceKeys[userId].deviceKeys[deviceId].curve25519Key;
|
client.userDeviceKeys[userId].deviceKeys[deviceId].curve25519Key;
|
||||||
for (Map<String, dynamic> deviceKey in deviceKeysEntry.value.values) {
|
for (Map<String, dynamic> deviceKey in deviceKeysEntry.value.values) {
|
||||||
if (!checkJsonSignature(
|
if (!deviceKey.checkJsonSignature(fingerprintKey, userId, deviceId)) {
|
||||||
fingerprintKey, deviceKey, userId, deviceId)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var session = olm.Session();
|
var session = olm.Session();
|
||||||
|
@ -408,10 +423,12 @@ class OlmManager {
|
||||||
lastReceived:
|
lastReceived:
|
||||||
DateTime.now(), // we want to use a newly created session
|
DateTime.now(), // we want to use a newly created session
|
||||||
));
|
));
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
session.free();
|
session.free();
|
||||||
print('[LibOlm] Could not create new outbound olm session: ' +
|
Logs.error(
|
||||||
e.toString());
|
'[LibOlm] Could not create new outbound olm session: ' +
|
||||||
|
e.toString(),
|
||||||
|
s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -483,8 +500,9 @@ class OlmManager {
|
||||||
try {
|
try {
|
||||||
data[device.userId][device.deviceId] =
|
data[device.userId][device.deviceId] =
|
||||||
await encryptToDeviceMessagePayload(device, type, payload);
|
await encryptToDeviceMessagePayload(device, type, payload);
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
print('[LibOlm] Error encrypting to-device event: ' + e.toString());
|
Logs.error(
|
||||||
|
'[LibOlm] Error encrypting to-device event: ' + e.toString(), s);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,16 +16,19 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'dart:typed_data';
|
import 'dart:core';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:encrypt/encrypt.dart';
|
|
||||||
import 'package:crypto/crypto.dart';
|
|
||||||
import 'package:base58check/base58.dart';
|
import 'package:base58check/base58.dart';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:encrypt/encrypt.dart';
|
||||||
import 'package:password_hash/password_hash.dart';
|
import 'package:password_hash/password_hash.dart';
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
|
||||||
|
|
||||||
|
import '../famedlysdk.dart';
|
||||||
|
import '../matrix_api.dart';
|
||||||
|
import '../src/database/database.dart';
|
||||||
|
import '../src/utils/logs.dart';
|
||||||
import 'encryption.dart';
|
import 'encryption.dart';
|
||||||
|
|
||||||
const CACHE_TYPES = <String>[
|
const CACHE_TYPES = <String>[
|
||||||
|
@ -46,8 +49,15 @@ class SSSS {
|
||||||
Client get client => encryption.client;
|
Client get client => encryption.client;
|
||||||
final pendingShareRequests = <String, _ShareRequest>{};
|
final pendingShareRequests = <String, _ShareRequest>{};
|
||||||
final _validators = <String, Future<bool> Function(String)>{};
|
final _validators = <String, Future<bool> Function(String)>{};
|
||||||
|
final Map<String, DbSSSSCache> _cache = <String, DbSSSSCache>{};
|
||||||
SSSS(this.encryption);
|
SSSS(this.encryption);
|
||||||
|
|
||||||
|
// for testing
|
||||||
|
Future<void> clearCache() async {
|
||||||
|
await client.database?.clearSSSSCache(client.id);
|
||||||
|
_cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
static _DerivedKeys deriveKeys(Uint8List key, String name) {
|
static _DerivedKeys deriveKeys(Uint8List key, String name) {
|
||||||
final zerosalt = Uint8List(8);
|
final zerosalt = Uint8List(8);
|
||||||
final prk = Hmac(sha256, zerosalt).convert(key);
|
final prk = Hmac(sha256, zerosalt).convert(key);
|
||||||
|
@ -132,7 +142,7 @@ class SSSS {
|
||||||
}
|
}
|
||||||
final generator = PBKDF2(hashAlgorithm: sha512);
|
final generator = PBKDF2(hashAlgorithm: sha512);
|
||||||
return Uint8List.fromList(generator.generateKey(passphrase, info.salt,
|
return Uint8List.fromList(generator.generateKey(passphrase, info.salt,
|
||||||
info.iterations, info.bits != null ? info.bits / 8 : 32));
|
info.iterations, info.bits != null ? (info.bits / 8).ceil() : 32));
|
||||||
}
|
}
|
||||||
|
|
||||||
void setValidator(String type, Future<bool> Function(String) validator) {
|
void setValidator(String type, Future<bool> Function(String) validator) {
|
||||||
|
@ -171,16 +181,22 @@ class SSSS {
|
||||||
if (client.database == null) {
|
if (client.database == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
// check if it is still valid
|
||||||
|
final keys = keyIdsFromType(type);
|
||||||
|
final isValid = (dbEntry) =>
|
||||||
|
keys.contains(dbEntry.keyId) &&
|
||||||
|
client.accountData[type].content['encrypted'][dbEntry.keyId]
|
||||||
|
['ciphertext'] ==
|
||||||
|
dbEntry.ciphertext;
|
||||||
|
if (_cache.containsKey(type) && isValid(_cache[type])) {
|
||||||
|
return _cache[type].content;
|
||||||
|
}
|
||||||
final ret = await client.database.getSSSSCache(client.id, type);
|
final ret = await client.database.getSSSSCache(client.id, type);
|
||||||
if (ret == null) {
|
if (ret == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// check if it is still valid
|
if (isValid(ret)) {
|
||||||
final keys = keyIdsFromType(type);
|
_cache[type] = ret;
|
||||||
if (keys.contains(ret.keyId) &&
|
|
||||||
client.accountData[type].content['encrypted'][ret.keyId]
|
|
||||||
['ciphertext'] ==
|
|
||||||
ret.ciphertext) {
|
|
||||||
return ret.content;
|
return ret.content;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -221,7 +237,7 @@ class SSSS {
|
||||||
'mac': encrypted.mac,
|
'mac': encrypted.mac,
|
||||||
};
|
};
|
||||||
// store the thing in your account data
|
// 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) {
|
if (CACHE_TYPES.contains(type) && client.database != null) {
|
||||||
// cache the thing
|
// cache the thing
|
||||||
await client.database
|
await client.database
|
||||||
|
@ -242,25 +258,34 @@ class SSSS {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> maybeRequestAll(List<DeviceKeys> devices) async {
|
Future<void> maybeRequestAll([List<DeviceKeys> devices]) async {
|
||||||
for (final type in CACHE_TYPES) {
|
for (final type in CACHE_TYPES) {
|
||||||
final secret = await getCached(type);
|
if (keyIdsFromType(type) != null) {
|
||||||
if (secret == null) {
|
final secret = await getCached(type);
|
||||||
await request(type, devices);
|
if (secret == null) {
|
||||||
|
await request(type, devices);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> request(String type, List<DeviceKeys> devices) async {
|
Future<void> request(String type, [List<DeviceKeys> devices]) async {
|
||||||
// only send to own, verified devices
|
// only send to own, verified devices
|
||||||
print('[SSSS] Requesting type ${type}...');
|
Logs.info('[SSSS] Requesting type ${type}...');
|
||||||
|
if (devices == null || devices.isEmpty) {
|
||||||
|
if (!client.userDeviceKeys.containsKey(client.userID)) {
|
||||||
|
Logs.warning('[SSSS] User does not have any devices');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
devices = client.userDeviceKeys[client.userID].deviceKeys.values.toList();
|
||||||
|
}
|
||||||
devices.removeWhere((DeviceKeys d) =>
|
devices.removeWhere((DeviceKeys d) =>
|
||||||
d.userId != client.userID ||
|
d.userId != client.userID ||
|
||||||
!d.verified ||
|
!d.verified ||
|
||||||
d.blocked ||
|
d.blocked ||
|
||||||
d.deviceId == client.deviceID);
|
d.deviceId == client.deviceID);
|
||||||
if (devices.isEmpty) {
|
if (devices.isEmpty) {
|
||||||
print('[SSSS] Warn: No devices');
|
Logs.warning('[SSSS] No devices');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final requestId = client.generateUniqueTransactionId();
|
final requestId = client.generateUniqueTransactionId();
|
||||||
|
@ -270,7 +295,7 @@ class SSSS {
|
||||||
devices: devices,
|
devices: devices,
|
||||||
);
|
);
|
||||||
pendingShareRequests[requestId] = request;
|
pendingShareRequests[requestId] = request;
|
||||||
await client.sendToDevice(devices, 'm.secret.request', {
|
await client.sendToDeviceEncrypted(devices, 'm.secret.request', {
|
||||||
'action': 'request',
|
'action': 'request',
|
||||||
'requesting_device_id': client.deviceID,
|
'requesting_device_id': client.deviceID,
|
||||||
'request_id': requestId,
|
'request_id': requestId,
|
||||||
|
@ -278,35 +303,57 @@ class SSSS {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DateTime _lastCacheRequest;
|
||||||
|
bool _isPeriodicallyRequestingMissingCache = false;
|
||||||
|
Future<void> periodicallyRequestMissingCache() async {
|
||||||
|
if (_isPeriodicallyRequestingMissingCache ||
|
||||||
|
(_lastCacheRequest != null &&
|
||||||
|
DateTime.now()
|
||||||
|
.subtract(Duration(minutes: 15))
|
||||||
|
.isBefore(_lastCacheRequest)) ||
|
||||||
|
client.isUnknownSession) {
|
||||||
|
// we are already requesting right now or we attempted to within the last 15 min
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_lastCacheRequest = DateTime.now();
|
||||||
|
_isPeriodicallyRequestingMissingCache = true;
|
||||||
|
try {
|
||||||
|
await maybeRequestAll();
|
||||||
|
} finally {
|
||||||
|
_isPeriodicallyRequestingMissingCache = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
|
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
|
||||||
if (event.type == 'm.secret.request') {
|
if (event.type == 'm.secret.request') {
|
||||||
// got a request to share a secret
|
// got a request to share a secret
|
||||||
print('[SSSS] Received sharing request...');
|
Logs.info('[SSSS] Received sharing request...');
|
||||||
if (event.sender != client.userID ||
|
if (event.sender != client.userID ||
|
||||||
!client.userDeviceKeys.containsKey(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
|
return; // we aren't asking for it ourselves, so ignore
|
||||||
}
|
}
|
||||||
if (event.content['action'] != 'request') {
|
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
|
return; // not actually requesting, so ignore
|
||||||
}
|
}
|
||||||
final device = client.userDeviceKeys[client.userID]
|
final device = client.userDeviceKeys[client.userID]
|
||||||
.deviceKeys[event.content['requesting_device_id']];
|
.deviceKeys[event.content['requesting_device_id']];
|
||||||
if (device == null || !device.verified || device.blocked) {
|
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
|
return; // nope....unknown or untrusted device
|
||||||
}
|
}
|
||||||
// alright, all seems fine...let's check if we actually have the secret they are asking for
|
// alright, all seems fine...let's check if we actually have the secret they are asking for
|
||||||
final type = event.content['name'];
|
final type = event.content['name'];
|
||||||
final secret = await getCached(type);
|
final secret = await getCached(type);
|
||||||
if (secret == null) {
|
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
|
return; // seems like we don't have this, either
|
||||||
}
|
}
|
||||||
// okay, all checks out...time to share this secret!
|
// okay, all checks out...time to share this secret!
|
||||||
print('[SSSS] Replying with secret for ${type}');
|
Logs.info('[SSSS] Replying with secret for ${type}');
|
||||||
await client.sendToDevice(
|
await client.sendToDeviceEncrypted(
|
||||||
[device],
|
[device],
|
||||||
'm.secret.send',
|
'm.secret.send',
|
||||||
{
|
{
|
||||||
|
@ -315,11 +362,11 @@ class SSSS {
|
||||||
});
|
});
|
||||||
} else if (event.type == 'm.secret.send') {
|
} else if (event.type == 'm.secret.send') {
|
||||||
// receiving a secret we asked for
|
// receiving a secret we asked for
|
||||||
print('[SSSS] Received shared secret...');
|
Logs.info('[SSSS] Received shared secret...');
|
||||||
if (event.sender != client.userID ||
|
if (event.sender != client.userID ||
|
||||||
!pendingShareRequests.containsKey(event.content['request_id']) ||
|
!pendingShareRequests.containsKey(event.content['request_id']) ||
|
||||||
event.encryptedContent == null) {
|
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
|
return; // we have no idea what we just received
|
||||||
}
|
}
|
||||||
final request = pendingShareRequests[event.content['request_id']];
|
final request = pendingShareRequests[event.content['request_id']];
|
||||||
|
@ -330,26 +377,26 @@ class SSSS {
|
||||||
d.curve25519Key == event.encryptedContent['sender_key'],
|
d.curve25519Key == event.encryptedContent['sender_key'],
|
||||||
orElse: () => null);
|
orElse: () => null);
|
||||||
if (device == 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
|
return; // someone replied whom we didn't send the share request to
|
||||||
}
|
}
|
||||||
final secret = event.content['secret'];
|
final secret = event.content['secret'];
|
||||||
if (!(event.content['secret'] is String)) {
|
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?
|
return; // the secret wasn't a string....wut?
|
||||||
}
|
}
|
||||||
// let's validate if the secret is, well, valid
|
// let's validate if the secret is, well, valid
|
||||||
if (_validators.containsKey(request.type) &&
|
if (_validators.containsKey(request.type) &&
|
||||||
!(await _validators[request.type](secret))) {
|
!(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
|
return; // didn't pass the validator
|
||||||
}
|
}
|
||||||
pendingShareRequests.remove(request.requestId);
|
pendingShareRequests.remove(request.requestId);
|
||||||
if (request.start.add(Duration(minutes: 15)).isBefore(DateTime.now())) {
|
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
|
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) {
|
if (client.database != null) {
|
||||||
final keyId = keyIdFromType(request.type);
|
final keyId = keyIdFromType(request.type);
|
||||||
if (keyId != null) {
|
if (keyId != null) {
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import 'package:canonical_json/canonical_json.dart';
|
||||||
|
import 'package:famedlysdk/src/utils/logs.dart';
|
||||||
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
|
extension JsonSignatureCheckExtension on Map<String, dynamic> {
|
||||||
|
/// Checks the signature of a signed json object.
|
||||||
|
bool checkJsonSignature(String key, String userId, String deviceId) {
|
||||||
|
final Map<String, dynamic> signatures = this['signatures'];
|
||||||
|
if (signatures == null || !signatures.containsKey(userId)) return false;
|
||||||
|
remove('unsigned');
|
||||||
|
remove('signatures');
|
||||||
|
if (!signatures[userId].containsKey('ed25519:$deviceId')) return false;
|
||||||
|
final String signature = signatures[userId]['ed25519:$deviceId'];
|
||||||
|
final canonical = canonicalJson.encode(this);
|
||||||
|
final message = String.fromCharCodes(canonical);
|
||||||
|
var isValid = false;
|
||||||
|
final olmutil = olm.Utility();
|
||||||
|
try {
|
||||||
|
olmutil.ed25519_verify(key, message, signature);
|
||||||
|
isValid = true;
|
||||||
|
} catch (e, s) {
|
||||||
|
isValid = false;
|
||||||
|
Logs.error('[LibOlm] Signature check failed: ' + e.toString(), s);
|
||||||
|
} finally {
|
||||||
|
olmutil.free();
|
||||||
|
}
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,12 +18,14 @@
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:canonical_json/canonical_json.dart';
|
|
||||||
import 'package:pedantic/pedantic.dart';
|
|
||||||
import 'package:olm/olm.dart' as olm;
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
|
||||||
|
|
||||||
|
import 'package:canonical_json/canonical_json.dart';
|
||||||
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
import 'package:pedantic/pedantic.dart';
|
||||||
|
|
||||||
|
import '../../famedlysdk.dart';
|
||||||
|
import '../../matrix_api.dart';
|
||||||
|
import '../../src/utils/logs.dart';
|
||||||
import '../encryption.dart';
|
import '../encryption.dart';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -150,7 +152,7 @@ class KeyVerification {
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
print('[Key Verification] disposing object...');
|
Logs.info('[Key Verification] disposing object...');
|
||||||
method?.dispose();
|
method?.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,7 +204,8 @@ class KeyVerification {
|
||||||
await Future.delayed(Duration(milliseconds: 50));
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
}
|
}
|
||||||
_handlePayloadLock = true;
|
_handlePayloadLock = true;
|
||||||
print('[Key Verification] Received type ${type}: ' + payload.toString());
|
Logs.info(
|
||||||
|
'[Key Verification] Received type ${type}: ' + payload.toString());
|
||||||
try {
|
try {
|
||||||
var thisLastStep = lastStep;
|
var thisLastStep = lastStep;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
@ -215,7 +218,10 @@ class KeyVerification {
|
||||||
DateTime.fromMillisecondsSinceEpoch(payload['timestamp']);
|
DateTime.fromMillisecondsSinceEpoch(payload['timestamp']);
|
||||||
if (now.subtract(Duration(minutes: 10)).isAfter(verifyTime) ||
|
if (now.subtract(Duration(minutes: 10)).isAfter(verifyTime) ||
|
||||||
now.add(Duration(minutes: 5)).isBefore(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;
|
return;
|
||||||
}
|
}
|
||||||
// verify it has a method we can use
|
// verify it has a method we can use
|
||||||
|
@ -280,6 +286,13 @@ class KeyVerification {
|
||||||
}
|
}
|
||||||
method = _makeVerificationMethod(payload['method'], this);
|
method = _makeVerificationMethod(payload['method'], this);
|
||||||
if (lastStep == null) {
|
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)) {
|
if (!method.validateStart(payload)) {
|
||||||
await cancel('m.unknown_method');
|
await cancel('m.unknown_method');
|
||||||
return;
|
return;
|
||||||
|
@ -287,7 +300,7 @@ class KeyVerification {
|
||||||
startPaylaod = payload;
|
startPaylaod = payload;
|
||||||
setState(KeyVerificationState.askAccept);
|
setState(KeyVerificationState.askAccept);
|
||||||
} else {
|
} else {
|
||||||
print('handling start in method.....');
|
Logs.info('handling start in method.....');
|
||||||
await method.handlePayload(type, payload);
|
await method.handlePayload(type, payload);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -301,18 +314,20 @@ class KeyVerification {
|
||||||
setState(KeyVerificationState.error);
|
setState(KeyVerificationState.error);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
await method.handlePayload(type, payload);
|
if (method != null) {
|
||||||
|
await method.handlePayload(type, payload);
|
||||||
|
} else {
|
||||||
|
await cancel('m.invalid_message');
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (lastStep == thisLastStep) {
|
if (lastStep == thisLastStep) {
|
||||||
lastStep = type;
|
lastStep = type;
|
||||||
}
|
}
|
||||||
} catch (err, stacktrace) {
|
} catch (err, stacktrace) {
|
||||||
print('[Key Verification] An error occured: ' + err.toString());
|
Logs.error(
|
||||||
print(stacktrace);
|
'[Key Verification] An error occured: ' + err.toString(), stacktrace);
|
||||||
if (deviceId != null) {
|
await cancel('m.invalid_message');
|
||||||
await cancel('m.invalid_message');
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
_handlePayloadLock = false;
|
_handlePayloadLock = false;
|
||||||
}
|
}
|
||||||
|
@ -510,11 +525,13 @@ class KeyVerification {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cancel([String code = 'm.unknown']) async {
|
Future<void> cancel([String code = 'm.unknown', bool quiet = false]) async {
|
||||||
await send('m.key.verification.cancel', {
|
if (!quiet && (deviceId != null || room != null)) {
|
||||||
'reason': code,
|
await send('m.key.verification.cancel', {
|
||||||
'code': code,
|
'reason': code,
|
||||||
});
|
'code': code,
|
||||||
|
});
|
||||||
|
}
|
||||||
canceled = true;
|
canceled = true;
|
||||||
canceledCode = code;
|
canceledCode = code;
|
||||||
setState(KeyVerificationState.error);
|
setState(KeyVerificationState.error);
|
||||||
|
@ -536,9 +553,10 @@ class KeyVerification {
|
||||||
|
|
||||||
Future<void> send(String type, Map<String, dynamic> payload) async {
|
Future<void> send(String type, Map<String, dynamic> payload) async {
|
||||||
makePayload(payload);
|
makePayload(payload);
|
||||||
print('[Key Verification] Sending type ${type}: ' + payload.toString());
|
Logs.info('[Key Verification] Sending type ${type}: ' + payload.toString());
|
||||||
if (room != null) {
|
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)) {
|
if (['m.key.verification.request'].contains(type)) {
|
||||||
payload['msgtype'] = type;
|
payload['msgtype'] = type;
|
||||||
payload['to'] = userId;
|
payload['to'] = userId;
|
||||||
|
@ -552,8 +570,9 @@ class KeyVerification {
|
||||||
encryption.keyVerificationManager.addRequest(this);
|
encryption.keyVerificationManager.addRequest(this);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
print('[Key Verification] Sending to ${userId} device ${deviceId}');
|
Logs.info(
|
||||||
await client.sendToDevice(
|
'[Key Verification] Sending to ${userId} device ${deviceId}...');
|
||||||
|
await client.sendToDeviceEncrypted(
|
||||||
[client.userDeviceKeys[userId].deviceKeys[deviceId]], type, payload);
|
[client.userDeviceKeys[userId].deviceKeys[deviceId]], type, payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -679,8 +698,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (err, stacktrace) {
|
} catch (err, stacktrace) {
|
||||||
print('[Key Verification SAS] An error occured: ' + err.toString());
|
Logs.error('[Key Verification SAS] An error occured: ' + err.toString(),
|
||||||
print(stacktrace);
|
stacktrace);
|
||||||
if (request.deviceId != null) {
|
if (request.deviceId != null) {
|
||||||
await request.cancel('m.invalid_message');
|
await request.cancel('m.invalid_message');
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
import '../../src/database/database.dart' show DbOlmSessions;
|
import '../../src/database/database.dart' show DbOlmSessions;
|
||||||
|
import '../../src/utils/logs.dart';
|
||||||
|
|
||||||
class OlmSession {
|
class OlmSession {
|
||||||
String identityKey;
|
String identityKey;
|
||||||
|
@ -46,8 +48,8 @@ class OlmSession {
|
||||||
lastReceived =
|
lastReceived =
|
||||||
dbEntry.lastReceived ?? DateTime.fromMillisecondsSinceEpoch(0);
|
dbEntry.lastReceived ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||||
assert(sessionId == session.session_id());
|
assert(sessionId == session.session_id());
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
print('[LibOlm] Could not unpickle olm session: ' + e.toString());
|
Logs.error('[LibOlm] Could not unpickle olm session: ' + e.toString(), s);
|
||||||
dispose();
|
dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,9 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
import '../../src/database/database.dart' show DbOutboundGroupSession;
|
import '../../src/database/database.dart' show DbOutboundGroupSession;
|
||||||
|
import '../../src/utils/logs.dart';
|
||||||
|
|
||||||
class OutboundGroupSession {
|
class OutboundGroupSession {
|
||||||
List<String> devices;
|
List<String> devices;
|
||||||
|
@ -44,10 +46,11 @@ class OutboundGroupSession {
|
||||||
devices = List<String>.from(json.decode(dbEntry.deviceIds));
|
devices = List<String>.from(json.decode(dbEntry.deviceIds));
|
||||||
creationTime = dbEntry.creationTime;
|
creationTime = dbEntry.creationTime;
|
||||||
sentMessages = dbEntry.sentMessages;
|
sentMessages = dbEntry.sentMessages;
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
dispose();
|
dispose();
|
||||||
print(
|
Logs.error(
|
||||||
'[LibOlm] Unable to unpickle outboundGroupSession: ' + e.toString());
|
'[LibOlm] Unable to unpickle outboundGroupSession: ' + e.toString(),
|
||||||
|
s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,41 +19,87 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
|
||||||
|
|
||||||
|
import '../../famedlysdk.dart';
|
||||||
import '../../src/database/database.dart' show DbInboundGroupSession;
|
import '../../src/database/database.dart' show DbInboundGroupSession;
|
||||||
|
import '../../src/utils/logs.dart';
|
||||||
|
|
||||||
class SessionKey {
|
class SessionKey {
|
||||||
Map<String, dynamic> content;
|
Map<String, dynamic> content;
|
||||||
Map<String, int> indexes;
|
Map<String, String> indexes;
|
||||||
olm.InboundGroupSession inboundGroupSession;
|
olm.InboundGroupSession inboundGroupSession;
|
||||||
final String key;
|
final String key;
|
||||||
List<dynamic> get forwardingCurve25519KeyChain =>
|
List<String> get forwardingCurve25519KeyChain =>
|
||||||
content['forwarding_curve25519_key_chain'] ?? [];
|
(content['forwarding_curve25519_key_chain'] != null
|
||||||
String get senderClaimedEd25519Key =>
|
? List<String>.from(content['forwarding_curve25519_key_chain'])
|
||||||
content['sender_claimed_ed25519_key'] ?? '';
|
: null) ??
|
||||||
String get senderKey => content['sender_key'] ?? '';
|
<String>[];
|
||||||
|
Map<String, String> senderClaimedKeys;
|
||||||
|
String senderKey;
|
||||||
bool get isValid => inboundGroupSession != null;
|
bool get isValid => inboundGroupSession != null;
|
||||||
|
String roomId;
|
||||||
|
String sessionId;
|
||||||
|
|
||||||
SessionKey({this.content, this.inboundGroupSession, this.key, this.indexes});
|
SessionKey(
|
||||||
|
{this.content,
|
||||||
|
this.inboundGroupSession,
|
||||||
|
this.key,
|
||||||
|
this.indexes,
|
||||||
|
this.roomId,
|
||||||
|
this.sessionId,
|
||||||
|
String senderKey,
|
||||||
|
Map<String, String> senderClaimedKeys}) {
|
||||||
|
_setSenderKey(senderKey);
|
||||||
|
_setSenderClaimedKeys(senderClaimedKeys);
|
||||||
|
}
|
||||||
|
|
||||||
SessionKey.fromDb(DbInboundGroupSession dbEntry, String key) : key = key {
|
SessionKey.fromDb(DbInboundGroupSession dbEntry, String key) : key = key {
|
||||||
final parsedContent = Event.getMapFromPayload(dbEntry.content);
|
final parsedContent = Event.getMapFromPayload(dbEntry.content);
|
||||||
final parsedIndexes = Event.getMapFromPayload(dbEntry.indexes);
|
final parsedIndexes = Event.getMapFromPayload(dbEntry.indexes);
|
||||||
|
final parsedSenderClaimedKeys =
|
||||||
|
Event.getMapFromPayload(dbEntry.senderClaimedKeys);
|
||||||
content =
|
content =
|
||||||
parsedContent != null ? Map<String, dynamic>.from(parsedContent) : null;
|
parsedContent != null ? Map<String, dynamic>.from(parsedContent) : null;
|
||||||
indexes = parsedIndexes != null
|
// we need to try...catch as the map used to be <String, int> and that will throw an error.
|
||||||
? Map<String, int>.from(parsedIndexes)
|
try {
|
||||||
: <String, int>{};
|
indexes = parsedIndexes != null
|
||||||
|
? Map<String, String>.from(parsedIndexes)
|
||||||
|
: <String, String>{};
|
||||||
|
} catch (e) {
|
||||||
|
indexes = <String, String>{};
|
||||||
|
}
|
||||||
|
roomId = dbEntry.roomId;
|
||||||
|
sessionId = dbEntry.sessionId;
|
||||||
|
_setSenderKey(dbEntry.senderKey);
|
||||||
|
_setSenderClaimedKeys(Map<String, String>.from(parsedSenderClaimedKeys));
|
||||||
|
|
||||||
inboundGroupSession = olm.InboundGroupSession();
|
inboundGroupSession = olm.InboundGroupSession();
|
||||||
try {
|
try {
|
||||||
inboundGroupSession.unpickle(key, dbEntry.pickle);
|
inboundGroupSession.unpickle(key, dbEntry.pickle);
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
dispose();
|
dispose();
|
||||||
print('[LibOlm] Unable to unpickle inboundGroupSession: ' + e.toString());
|
Logs.error(
|
||||||
|
'[LibOlm] Unable to unpickle inboundGroupSession: ' + e.toString(),
|
||||||
|
s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setSenderKey(String key) {
|
||||||
|
senderKey = key ?? content['sender_key'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setSenderClaimedKeys(Map<String, String> keys) {
|
||||||
|
senderClaimedKeys = (keys != null && keys.isNotEmpty)
|
||||||
|
? keys
|
||||||
|
: (content['sender_claimed_keys'] is Map
|
||||||
|
? Map<String, String>.from(content['sender_claimed_keys'])
|
||||||
|
: (content['sender_claimed_ed25519_key'] is String
|
||||||
|
? <String, String>{
|
||||||
|
'ed25519': content['sender_claimed_ed25519_key']
|
||||||
|
}
|
||||||
|
: <String, String>{}));
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final data = <String, dynamic>{};
|
final data = <String, dynamic>{};
|
||||||
if (content != null) {
|
if (content != null) {
|
||||||
|
|
|
@ -19,19 +19,20 @@
|
||||||
library famedlysdk;
|
library famedlysdk;
|
||||||
|
|
||||||
export 'matrix_api.dart';
|
export 'matrix_api.dart';
|
||||||
export 'package:famedlysdk/src/utils/room_update.dart';
|
export 'src/utils/room_update.dart';
|
||||||
export 'package:famedlysdk/src/utils/event_update.dart';
|
export 'src/utils/event_update.dart';
|
||||||
export 'package:famedlysdk/src/utils/device_keys_list.dart';
|
export 'src/utils/device_keys_list.dart';
|
||||||
export 'package:famedlysdk/src/utils/matrix_file.dart';
|
export 'src/utils/matrix_file.dart';
|
||||||
export 'package:famedlysdk/src/utils/matrix_id_string_extension.dart';
|
export 'src/utils/matrix_id_string_extension.dart';
|
||||||
export 'package:famedlysdk/src/utils/uri_extension.dart';
|
export 'src/utils/uri_extension.dart';
|
||||||
export 'package:famedlysdk/src/utils/matrix_localizations.dart';
|
export 'src/utils/matrix_localizations.dart';
|
||||||
export 'package:famedlysdk/src/utils/receipt.dart';
|
export 'src/utils/receipt.dart';
|
||||||
export 'package:famedlysdk/src/utils/states_map.dart';
|
export 'src/utils/states_map.dart';
|
||||||
export 'package:famedlysdk/src/utils/to_device_event.dart';
|
export 'src/utils/sync_update_extension.dart';
|
||||||
export 'package:famedlysdk/src/client.dart';
|
export 'src/utils/to_device_event.dart';
|
||||||
export 'package:famedlysdk/src/event.dart';
|
export 'src/client.dart';
|
||||||
export 'package:famedlysdk/src/room.dart';
|
export 'src/event.dart';
|
||||||
export 'package:famedlysdk/src/timeline.dart';
|
export 'src/room.dart';
|
||||||
export 'package:famedlysdk/src/user.dart';
|
export 'src/timeline.dart';
|
||||||
export 'package:famedlysdk/src/database/database.dart' show Database;
|
export 'src/user.dart';
|
||||||
|
export 'src/database/database.dart' show Database;
|
||||||
|
|
|
@ -18,49 +18,49 @@
|
||||||
|
|
||||||
library matrix_api;
|
library matrix_api;
|
||||||
|
|
||||||
export 'package:famedlysdk/matrix_api/matrix_api.dart';
|
export 'matrix_api/matrix_api.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/basic_event_with_sender.dart';
|
export 'matrix_api/model/basic_event.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/basic_event.dart';
|
export 'matrix_api/model/basic_event_with_sender.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/device.dart';
|
export 'matrix_api/model/basic_room_event.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/basic_room_event.dart';
|
export 'matrix_api/model/device.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/event_context.dart';
|
export 'matrix_api/model/event_context.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/matrix_event.dart';
|
export 'matrix_api/model/event_types.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/event_types.dart';
|
export 'matrix_api/model/events_sync_update.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/events_sync_update.dart';
|
export 'matrix_api/model/filter.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/filter.dart';
|
export 'matrix_api/model/keys_query_response.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/keys_query_response.dart';
|
export 'matrix_api/model/login_response.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/login_response.dart';
|
export 'matrix_api/model/login_types.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/login_types.dart';
|
export 'matrix_api/model/matrix_event.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/matrix_exception.dart';
|
export 'matrix_api/model/matrix_exception.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/matrix_keys.dart';
|
export 'matrix_api/model/matrix_keys.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/message_types.dart';
|
export 'matrix_api/model/message_types.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/presence_content.dart';
|
export 'matrix_api/model/notifications_query_response.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/notifications_query_response.dart';
|
export 'matrix_api/model/one_time_keys_claim_response.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/one_time_keys_claim_response.dart';
|
export 'matrix_api/model/open_graph_data.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/open_graph_data.dart';
|
export 'matrix_api/model/open_id_credentials.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/open_id_credentials.dart';
|
export 'matrix_api/model/presence.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/presence.dart';
|
export 'matrix_api/model/presence_content.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/profile.dart';
|
export 'matrix_api/model/profile.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/public_rooms_response.dart';
|
export 'matrix_api/model/public_rooms_response.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/push_rule_set.dart';
|
export 'matrix_api/model/push_rule_set.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/pusher.dart';
|
export 'matrix_api/model/pusher.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/request_token_response.dart';
|
export 'matrix_api/model/request_token_response.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/room_alias_informations.dart';
|
export 'matrix_api/model/room_alias_informations.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/room_keys_info.dart';
|
export 'matrix_api/model/room_keys_info.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/room_keys_keys.dart';
|
export 'matrix_api/model/room_keys_keys.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/room_summary.dart';
|
export 'matrix_api/model/room_summary.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/server_capabilities.dart';
|
export 'matrix_api/model/server_capabilities.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/stripped_state_event.dart';
|
export 'matrix_api/model/stripped_state_event.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/supported_protocol.dart';
|
export 'matrix_api/model/supported_protocol.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/supported_versions.dart';
|
export 'matrix_api/model/supported_versions.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/sync_update.dart';
|
export 'matrix_api/model/sync_update.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/tag.dart';
|
export 'matrix_api/model/tag.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/third_party_identifier.dart';
|
export 'matrix_api/model/third_party_identifier.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/third_party_location.dart';
|
export 'matrix_api/model/third_party_location.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/third_party_user.dart';
|
export 'matrix_api/model/third_party_user.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/timeline_history_response.dart';
|
export 'matrix_api/model/timeline_history_response.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/turn_server_credentials.dart';
|
export 'matrix_api/model/turn_server_credentials.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/upload_key_signatures_response.dart';
|
export 'matrix_api/model/upload_key_signatures_response.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/user_search_result.dart';
|
export 'matrix_api/model/user_search_result.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/well_known_informations.dart';
|
export 'matrix_api/model/well_known_informations.dart';
|
||||||
export 'package:famedlysdk/matrix_api/model/who_is_info.dart';
|
export 'matrix_api/model/who_is_info.dart';
|
||||||
|
|
|
@ -19,19 +19,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:famedlysdk/matrix_api/model/filter.dart';
|
|
||||||
import 'package:famedlysdk/matrix_api/model/keys_query_response.dart';
|
|
||||||
import 'package:famedlysdk/matrix_api/model/login_types.dart';
|
|
||||||
import 'package:famedlysdk/matrix_api/model/notifications_query_response.dart';
|
|
||||||
import 'package:famedlysdk/matrix_api/model/open_graph_data.dart';
|
|
||||||
import 'package:famedlysdk/matrix_api/model/profile.dart';
|
|
||||||
import 'package:famedlysdk/matrix_api/model/request_token_response.dart';
|
|
||||||
import 'package:famedlysdk/matrix_api/model/server_capabilities.dart';
|
|
||||||
import 'package:famedlysdk/matrix_api/model/supported_versions.dart';
|
|
||||||
import 'package:famedlysdk/matrix_api/model/sync_update.dart';
|
|
||||||
import 'package:famedlysdk/matrix_api/model/third_party_location.dart';
|
|
||||||
import 'package:famedlysdk/matrix_api/model/timeline_history_response.dart';
|
|
||||||
import 'package:famedlysdk/matrix_api/model/user_search_result.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
import 'package:moor/moor.dart';
|
import 'package:moor/moor.dart';
|
||||||
|
@ -39,25 +26,38 @@ import 'package:moor/moor.dart';
|
||||||
import 'model/device.dart';
|
import 'model/device.dart';
|
||||||
import 'model/event_context.dart';
|
import 'model/event_context.dart';
|
||||||
import 'model/events_sync_update.dart';
|
import 'model/events_sync_update.dart';
|
||||||
|
import 'model/filter.dart';
|
||||||
|
import 'model/keys_query_response.dart';
|
||||||
import 'model/login_response.dart';
|
import 'model/login_response.dart';
|
||||||
|
import 'model/login_types.dart';
|
||||||
import 'model/matrix_event.dart';
|
import 'model/matrix_event.dart';
|
||||||
import 'model/matrix_exception.dart';
|
import 'model/matrix_exception.dart';
|
||||||
import 'model/matrix_keys.dart';
|
import 'model/matrix_keys.dart';
|
||||||
|
import 'model/notifications_query_response.dart';
|
||||||
import 'model/one_time_keys_claim_response.dart';
|
import 'model/one_time_keys_claim_response.dart';
|
||||||
|
import 'model/open_graph_data.dart';
|
||||||
import 'model/open_id_credentials.dart';
|
import 'model/open_id_credentials.dart';
|
||||||
import 'model/presence_content.dart';
|
import 'model/presence_content.dart';
|
||||||
|
import 'model/profile.dart';
|
||||||
import 'model/public_rooms_response.dart';
|
import 'model/public_rooms_response.dart';
|
||||||
import 'model/push_rule_set.dart';
|
import 'model/push_rule_set.dart';
|
||||||
import 'model/pusher.dart';
|
import 'model/pusher.dart';
|
||||||
|
import 'model/request_token_response.dart';
|
||||||
import 'model/room_alias_informations.dart';
|
import 'model/room_alias_informations.dart';
|
||||||
import 'model/room_keys_info.dart';
|
import 'model/room_keys_info.dart';
|
||||||
import 'model/room_keys_keys.dart';
|
import 'model/room_keys_keys.dart';
|
||||||
|
import 'model/server_capabilities.dart';
|
||||||
import 'model/supported_protocol.dart';
|
import 'model/supported_protocol.dart';
|
||||||
|
import 'model/supported_versions.dart';
|
||||||
|
import 'model/sync_update.dart';
|
||||||
import 'model/tag.dart';
|
import 'model/tag.dart';
|
||||||
import 'model/third_party_identifier.dart';
|
import 'model/third_party_identifier.dart';
|
||||||
|
import 'model/third_party_location.dart';
|
||||||
import 'model/third_party_user.dart';
|
import 'model/third_party_user.dart';
|
||||||
|
import 'model/timeline_history_response.dart';
|
||||||
import 'model/turn_server_credentials.dart';
|
import 'model/turn_server_credentials.dart';
|
||||||
import 'model/upload_key_signatures_response.dart';
|
import 'model/upload_key_signatures_response.dart';
|
||||||
|
import 'model/user_search_result.dart';
|
||||||
import 'model/well_known_informations.dart';
|
import 'model/well_known_informations.dart';
|
||||||
import 'model/who_is_info.dart';
|
import 'model/who_is_info.dart';
|
||||||
|
|
||||||
|
@ -88,9 +88,6 @@ class MatrixApi {
|
||||||
/// timeout which is usually 30 seconds.
|
/// timeout which is usually 30 seconds.
|
||||||
int syncTimeoutSec;
|
int syncTimeoutSec;
|
||||||
|
|
||||||
/// Whether debug prints should be displayed.
|
|
||||||
final bool debug;
|
|
||||||
|
|
||||||
http.Client httpClient = http.Client();
|
http.Client httpClient = http.Client();
|
||||||
|
|
||||||
bool get _testMode =>
|
bool get _testMode =>
|
||||||
|
@ -101,7 +98,6 @@ class MatrixApi {
|
||||||
MatrixApi({
|
MatrixApi({
|
||||||
this.homeserver,
|
this.homeserver,
|
||||||
this.accessToken,
|
this.accessToken,
|
||||||
this.debug = false,
|
|
||||||
http.Client httpClient,
|
http.Client httpClient,
|
||||||
this.syncTimeoutSec = 30,
|
this.syncTimeoutSec = 30,
|
||||||
}) {
|
}) {
|
||||||
|
@ -161,11 +157,6 @@ class MatrixApi {
|
||||||
headers['Authorization'] = 'Bearer ${accessToken}';
|
headers['Authorization'] = 'Bearer ${accessToken}';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
print(
|
|
||||||
'[REQUEST ${describeEnum(type)}] $action, Data: ${jsonEncode(data)}');
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Response resp;
|
http.Response resp;
|
||||||
var jsonResp = <String, dynamic>{};
|
var jsonResp = <String, dynamic>{};
|
||||||
try {
|
try {
|
||||||
|
@ -212,8 +203,6 @@ class MatrixApi {
|
||||||
|
|
||||||
throw exception;
|
throw exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debug) print('[RESPONSE] ${jsonResp.toString()}');
|
|
||||||
_timeoutFactor = 1;
|
_timeoutFactor = 1;
|
||||||
} on TimeoutException catch (_) {
|
} on TimeoutException catch (_) {
|
||||||
_timeoutFactor *= 2;
|
_timeoutFactor *= 2;
|
||||||
|
@ -787,7 +776,7 @@ class MatrixApi {
|
||||||
String stateKey = '',
|
String stateKey = '',
|
||||||
]) async {
|
]) async {
|
||||||
final response = await request(RequestType.PUT,
|
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);
|
data: content);
|
||||||
return response['event_id'];
|
return response['event_id'];
|
||||||
}
|
}
|
||||||
|
@ -803,7 +792,7 @@ class MatrixApi {
|
||||||
Map<String, dynamic> content,
|
Map<String, dynamic> content,
|
||||||
) async {
|
) async {
|
||||||
final response = await request(RequestType.PUT,
|
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);
|
data: content);
|
||||||
return response['event_id'];
|
return response['event_id'];
|
||||||
}
|
}
|
||||||
|
@ -818,7 +807,7 @@ class MatrixApi {
|
||||||
String reason,
|
String reason,
|
||||||
}) async {
|
}) async {
|
||||||
final response = await request(RequestType.PUT,
|
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: {
|
data: {
|
||||||
if (reason != null) 'reason': reason,
|
if (reason != null) 'reason': reason,
|
||||||
});
|
});
|
||||||
|
@ -1300,7 +1289,6 @@ class MatrixApi {
|
||||||
streamedRequest.contentLength = await file.length;
|
streamedRequest.contentLength = await file.length;
|
||||||
streamedRequest.sink.add(file);
|
streamedRequest.sink.add(file);
|
||||||
streamedRequest.sink.close();
|
streamedRequest.sink.close();
|
||||||
if (debug) print('[UPLOADING] $fileName');
|
|
||||||
var streamedResponse = _testMode ? null : await streamedRequest.send();
|
var streamedResponse = _testMode ? null : await streamedRequest.send();
|
||||||
Map<String, dynamic> jsonResponse = json.decode(
|
Map<String, dynamic> jsonResponse = json.decode(
|
||||||
String.fromCharCodes(_testMode
|
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.
|
/// 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
|
/// https://matrix.org/docs/spec/client_server/r0.6.1#put-matrix-client-r0-sendtodevice-eventtype-txnid
|
||||||
Future<void> sendToDevice(String eventType, String txnId,
|
Future<void> sendToDevice(
|
||||||
Map<String, Map<String, Map<String, dynamic>>> messages) async {
|
String eventType,
|
||||||
|
String txnId,
|
||||||
|
Map<String, Map<String, Map<String, dynamic>>> messages,
|
||||||
|
) async {
|
||||||
await request(
|
await request(
|
||||||
RequestType.PUT,
|
RequestType.PUT,
|
||||||
'/client/r0/sendToDevice/${Uri.encodeComponent(eventType)}/${Uri.encodeComponent(txnId)}',
|
'/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 {
|
Future<Map<String, Tag>> requestRoomTags(String userId, String roomId) async {
|
||||||
final response = await request(
|
final response = await request(
|
||||||
RequestType.GET,
|
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(
|
return (response['tags'] as Map).map(
|
||||||
(k, v) => MapEntry(k, Tag.fromJson(v)),
|
(k, v) => MapEntry(k, Tag.fromJson(v)),
|
||||||
|
@ -1750,7 +1741,7 @@ class MatrixApi {
|
||||||
double order,
|
double order,
|
||||||
}) async {
|
}) async {
|
||||||
await request(RequestType.PUT,
|
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: {
|
data: {
|
||||||
if (order != null) 'order': order,
|
if (order != null) 'order': order,
|
||||||
});
|
});
|
||||||
|
@ -1762,7 +1753,7 @@ class MatrixApi {
|
||||||
Future<void> removeRoomTag(String userId, String roomId, String tag) async {
|
Future<void> removeRoomTag(String userId, String roomId, String tag) async {
|
||||||
await request(
|
await request(
|
||||||
RequestType.DELETE,
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1777,7 +1768,7 @@ class MatrixApi {
|
||||||
) async {
|
) async {
|
||||||
await request(
|
await request(
|
||||||
RequestType.PUT,
|
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,
|
data: content,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
@ -1791,7 +1782,7 @@ class MatrixApi {
|
||||||
) async {
|
) async {
|
||||||
return await request(
|
return await request(
|
||||||
RequestType.GET,
|
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 {
|
) async {
|
||||||
await request(
|
await request(
|
||||||
RequestType.PUT,
|
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,
|
data: content,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
@ -1821,7 +1812,7 @@ class MatrixApi {
|
||||||
) async {
|
) async {
|
||||||
return await request(
|
return await request(
|
||||||
RequestType.GET,
|
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 {
|
Future<WhoIsInfo> requestWhoIsInfo(String userId) async {
|
||||||
final response = await request(
|
final response = await request(
|
||||||
RequestType.GET,
|
RequestType.GET,
|
||||||
'/client/r0/admin/whois/${Uri.encodeQueryComponent(userId)}',
|
'/client/r0/admin/whois/${Uri.encodeComponent(userId)}',
|
||||||
);
|
);
|
||||||
return WhoIsInfo.fromJson(response);
|
return WhoIsInfo.fromJson(response);
|
||||||
}
|
}
|
||||||
|
@ -1845,7 +1836,7 @@ class MatrixApi {
|
||||||
String filter,
|
String filter,
|
||||||
}) async {
|
}) async {
|
||||||
final response = await request(RequestType.GET,
|
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: {
|
query: {
|
||||||
if (filter != null) 'filter': filter,
|
if (filter != null) 'filter': filter,
|
||||||
if (limit != null) 'limit': limit.toString(),
|
if (limit != null) 'limit': limit.toString(),
|
||||||
|
@ -1862,7 +1853,7 @@ class MatrixApi {
|
||||||
int score,
|
int score,
|
||||||
) async {
|
) async {
|
||||||
await request(RequestType.POST,
|
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: {
|
data: {
|
||||||
'reason': reason,
|
'reason': reason,
|
||||||
'score': score,
|
'score': score,
|
||||||
|
@ -2071,7 +2062,7 @@ class MatrixApi {
|
||||||
return RoomKeysRoom.fromJson(ret);
|
return RoomKeysRoom.fromJson(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes room ekys for a room
|
/// Deletes room keys for a room
|
||||||
/// https://matrix.org/docs/spec/client_server/unstable#delete-matrix-client-r0-room-keys-keys-roomid
|
/// https://matrix.org/docs/spec/client_server/unstable#delete-matrix-client-r0-room-keys-keys-roomid
|
||||||
Future<RoomKeysUpdateResponse> deleteRoomKeysRoom(
|
Future<RoomKeysUpdateResponse> deleteRoomKeysRoom(
|
||||||
String roomId, String version) async {
|
String roomId, String version) async {
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:famedlysdk/matrix_api/model/basic_event.dart';
|
import 'basic_event.dart';
|
||||||
|
|
||||||
class BasicRoomEvent extends BasicEvent {
|
class BasicRoomEvent extends BasicEvent {
|
||||||
String roomId;
|
String roomId;
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
abstract class EventTypes {
|
abstract class EventTypes {
|
||||||
static const String Message = 'm.room.message';
|
static const String Message = 'm.room.message';
|
||||||
static const String Sticker = 'm.sticker';
|
static const String Sticker = 'm.sticker';
|
||||||
|
static const String Reaction = 'm.reaction';
|
||||||
static const String Redaction = 'm.room.redaction';
|
static const String Redaction = 'm.room.redaction';
|
||||||
static const String RoomAliases = 'm.room.aliases';
|
static const String RoomAliases = 'm.room.aliases';
|
||||||
static const String RoomCanonicalAlias = 'm.room.canonical_alias';
|
static const String RoomCanonicalAlias = 'm.room.canonical_alias';
|
||||||
|
@ -35,9 +36,9 @@ abstract class EventTypes {
|
||||||
static const String HistoryVisibility = 'm.room.history_visibility';
|
static const String HistoryVisibility = 'm.room.history_visibility';
|
||||||
static const String Encryption = 'm.room.encryption';
|
static const String Encryption = 'm.room.encryption';
|
||||||
static const String Encrypted = 'm.room.encrypted';
|
static const String Encrypted = 'm.room.encrypted';
|
||||||
static const String CallInvite = 'm.room.call.invite';
|
static const String CallInvite = 'm.call.invite';
|
||||||
static const String CallAnswer = 'm.room.call.answer';
|
static const String CallAnswer = 'm.call.answer';
|
||||||
static const String CallCandidates = 'm.room.call.candidates';
|
static const String CallCandidates = 'm.call.candidates';
|
||||||
static const String CallHangup = 'm.room.call.hangup';
|
static const String CallHangup = 'm.call.hangup';
|
||||||
static const String Unknown = 'm.unknown';
|
static const String Unknown = 'm.unknown';
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,9 @@ class KeysQueryResponse {
|
||||||
Map<String, MatrixCrossSigningKey> userSigningKeys;
|
Map<String, MatrixCrossSigningKey> userSigningKeys;
|
||||||
|
|
||||||
KeysQueryResponse.fromJson(Map<String, dynamic> json) {
|
KeysQueryResponse.fromJson(Map<String, dynamic> json) {
|
||||||
failures = Map<String, dynamic>.from(json['failures']);
|
failures = json['failures'] != null
|
||||||
|
? Map<String, dynamic>.from(json['failures'])
|
||||||
|
: null;
|
||||||
deviceKeys = json['device_keys'] != null
|
deviceKeys = json['device_keys'] != null
|
||||||
? (json['device_keys'] as Map).map(
|
? (json['device_keys'] as Map).map(
|
||||||
(k, v) => MapEntry(
|
(k, v) => MapEntry(
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:famedlysdk/matrix_api/model/stripped_state_event.dart';
|
import 'stripped_state_event.dart';
|
||||||
|
|
||||||
class MatrixEvent extends StrippedStateEvent {
|
class MatrixEvent extends StrippedStateEvent {
|
||||||
String eventId;
|
String eventId;
|
||||||
|
|
|
@ -25,7 +25,6 @@ abstract class MessageTypes {
|
||||||
static const String Audio = 'm.audio';
|
static const String Audio = 'm.audio';
|
||||||
static const String File = 'm.file';
|
static const String File = 'm.file';
|
||||||
static const String Location = 'm.location';
|
static const String Location = 'm.location';
|
||||||
static const String Reply = 'm.relates_to';
|
|
||||||
static const String Sticker = 'm.sticker';
|
static const String Sticker = 'm.sticker';
|
||||||
static const String BadEncrypted = 'm.bad.encrypted';
|
static const String BadEncrypted = 'm.bad.encrypted';
|
||||||
static const String None = 'm.none';
|
static const String None = 'm.none';
|
||||||
|
|
|
@ -22,6 +22,12 @@ class RoomKeysSingleKey {
|
||||||
bool isVerified;
|
bool isVerified;
|
||||||
Map<String, dynamic> sessionData;
|
Map<String, dynamic> sessionData;
|
||||||
|
|
||||||
|
RoomKeysSingleKey(
|
||||||
|
{this.firstMessageIndex,
|
||||||
|
this.forwardedCount,
|
||||||
|
this.isVerified,
|
||||||
|
this.sessionData});
|
||||||
|
|
||||||
RoomKeysSingleKey.fromJson(Map<String, dynamic> json) {
|
RoomKeysSingleKey.fromJson(Map<String, dynamic> json) {
|
||||||
firstMessageIndex = json['first_message_index'];
|
firstMessageIndex = json['first_message_index'];
|
||||||
forwardedCount = json['forwarded_count'];
|
forwardedCount = json['forwarded_count'];
|
||||||
|
@ -42,6 +48,10 @@ class RoomKeysSingleKey {
|
||||||
class RoomKeysRoom {
|
class RoomKeysRoom {
|
||||||
Map<String, RoomKeysSingleKey> sessions;
|
Map<String, RoomKeysSingleKey> sessions;
|
||||||
|
|
||||||
|
RoomKeysRoom({this.sessions}) {
|
||||||
|
sessions ??= <String, RoomKeysSingleKey>{};
|
||||||
|
}
|
||||||
|
|
||||||
RoomKeysRoom.fromJson(Map<String, dynamic> json) {
|
RoomKeysRoom.fromJson(Map<String, dynamic> json) {
|
||||||
sessions = (json['sessions'] as Map)
|
sessions = (json['sessions'] as Map)
|
||||||
.map((k, v) => MapEntry(k, RoomKeysSingleKey.fromJson(v)));
|
.map((k, v) => MapEntry(k, RoomKeysSingleKey.fromJson(v)));
|
||||||
|
@ -57,6 +67,10 @@ class RoomKeysRoom {
|
||||||
class RoomKeys {
|
class RoomKeys {
|
||||||
Map<String, RoomKeysRoom> rooms;
|
Map<String, RoomKeysRoom> rooms;
|
||||||
|
|
||||||
|
RoomKeys({this.rooms}) {
|
||||||
|
rooms ??= <String, RoomKeysRoom>{};
|
||||||
|
}
|
||||||
|
|
||||||
RoomKeys.fromJson(Map<String, dynamic> json) {
|
RoomKeys.fromJson(Map<String, dynamic> json) {
|
||||||
rooms = (json['rooms'] as Map)
|
rooms = (json['rooms'] as Map)
|
||||||
.map((k, v) => MapEntry(k, RoomKeysRoom.fromJson(v)));
|
.map((k, v) => MapEntry(k, RoomKeysRoom.fromJson(v)));
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:famedlysdk/matrix_api/model/basic_event_with_sender.dart';
|
import 'basic_event_with_sender.dart';
|
||||||
|
|
||||||
class StrippedStateEvent extends BasicEventWithSender {
|
class StrippedStateEvent extends BasicEventWithSender {
|
||||||
String stateKey;
|
String stateKey;
|
||||||
|
|
|
@ -315,8 +315,8 @@ class DeviceListsUpdate {
|
||||||
List<String> changed;
|
List<String> changed;
|
||||||
List<String> left;
|
List<String> left;
|
||||||
DeviceListsUpdate.fromJson(Map<String, dynamic> json) {
|
DeviceListsUpdate.fromJson(Map<String, dynamic> json) {
|
||||||
changed = List<String>.from(json['changed']);
|
changed = List<String>.from(json['changed'] ?? []);
|
||||||
left = List<String>.from(json['left']);
|
left = List<String>.from(json['left'] ?? []);
|
||||||
}
|
}
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final data = <String, dynamic>{};
|
final data = <String, dynamic>{};
|
||||||
|
|
|
@ -20,7 +20,7 @@ class TurnServerCredentials {
|
||||||
String username;
|
String username;
|
||||||
String password;
|
String password;
|
||||||
List<String> uris;
|
List<String> uris;
|
||||||
int ttl;
|
num ttl;
|
||||||
|
|
||||||
TurnServerCredentials.fromJson(Map<String, dynamic> json) {
|
TurnServerCredentials.fromJson(Map<String, dynamic> json) {
|
||||||
username = json['username'];
|
username = json['username'];
|
||||||
|
|
|
@ -20,22 +20,20 @@ import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:core';
|
import 'dart:core';
|
||||||
|
|
||||||
import 'package:famedlysdk/encryption.dart';
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
|
||||||
import 'package:famedlysdk/src/room.dart';
|
|
||||||
import 'package:famedlysdk/src/utils/device_keys_list.dart';
|
|
||||||
import 'package:famedlysdk/src/utils/matrix_file.dart';
|
|
||||||
import 'package:famedlysdk/src/utils/to_device_event.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:pedantic/pedantic.dart';
|
|
||||||
|
|
||||||
|
import '../encryption.dart';
|
||||||
|
import '../famedlysdk.dart';
|
||||||
import 'database/database.dart' show Database;
|
import 'database/database.dart' show Database;
|
||||||
import 'event.dart';
|
import 'event.dart';
|
||||||
import 'room.dart';
|
import 'room.dart';
|
||||||
import 'user.dart';
|
import 'user.dart';
|
||||||
|
import 'utils/device_keys_list.dart';
|
||||||
import 'utils/event_update.dart';
|
import 'utils/event_update.dart';
|
||||||
|
import 'utils/logs.dart';
|
||||||
|
import 'utils/matrix_file.dart';
|
||||||
import 'utils/room_update.dart';
|
import 'utils/room_update.dart';
|
||||||
|
import 'utils/to_device_event.dart';
|
||||||
|
|
||||||
typedef RoomSorter = int Function(Room a, Room b);
|
typedef RoomSorter = int Function(Room a, Room b);
|
||||||
|
|
||||||
|
@ -44,7 +42,7 @@ enum LoginState { logged, loggedOut }
|
||||||
/// Represents a Matrix client to communicate with a
|
/// Represents a Matrix client to communicate with a
|
||||||
/// [Matrix](https://matrix.org) homeserver and is the entry point for this
|
/// [Matrix](https://matrix.org) homeserver and is the entry point for this
|
||||||
/// SDK.
|
/// SDK.
|
||||||
class Client {
|
class Client extends MatrixApi {
|
||||||
int _id;
|
int _id;
|
||||||
int get id => _id;
|
int get id => _id;
|
||||||
|
|
||||||
|
@ -52,7 +50,8 @@ class Client {
|
||||||
|
|
||||||
bool enableE2eeRecovery;
|
bool enableE2eeRecovery;
|
||||||
|
|
||||||
MatrixApi api;
|
@deprecated
|
||||||
|
MatrixApi get api => this;
|
||||||
|
|
||||||
Encryption encryption;
|
Encryption encryption;
|
||||||
|
|
||||||
|
@ -60,15 +59,18 @@ class Client {
|
||||||
|
|
||||||
Set<String> importantStateEvents;
|
Set<String> importantStateEvents;
|
||||||
|
|
||||||
|
Set<String> roomPreviewLastEvents;
|
||||||
|
|
||||||
|
int sendMessageTimeoutSeconds;
|
||||||
|
|
||||||
/// Create a client
|
/// Create a client
|
||||||
/// clientName = unique identifier of this client
|
/// [clientName] = unique identifier of this client
|
||||||
/// debug: Print debug output?
|
/// [database]: The database instance to use
|
||||||
/// database: The database instance to use
|
/// [enableE2eeRecovery]: Enable additional logic to try to recover from bad e2ee sessions
|
||||||
/// enableE2eeRecovery: Enable additional logic to try to recover from bad e2ee sessions
|
/// [verificationMethods]: A set of all the verification methods this client can handle. Includes:
|
||||||
/// verificationMethods: A set of all the verification methods this client can handle. Includes:
|
|
||||||
/// KeyVerificationMethod.numbers: Compare numbers. Most basic, should be supported
|
/// KeyVerificationMethod.numbers: Compare numbers. Most basic, should be supported
|
||||||
/// KeyVerificationMethod.emoji: Compare emojis
|
/// KeyVerificationMethod.emoji: Compare emojis
|
||||||
/// importantStateEvents: A set of all the important state events to load when the client connects.
|
/// [importantStateEvents]: A set of all the important state events to load when the client connects.
|
||||||
/// To speed up performance only a set of state events is loaded on startup, those that are
|
/// To speed up performance only a set of state events is loaded on startup, those that are
|
||||||
/// needed to display a room list. All the remaining state events are automatically post-loaded
|
/// needed to display a room list. All the remaining state events are automatically post-loaded
|
||||||
/// when opening the timeline of a room or manually by calling `room.postLoad()`.
|
/// when opening the timeline of a room or manually by calling `room.postLoad()`.
|
||||||
|
@ -81,16 +83,22 @@ class Client {
|
||||||
/// - m.room.canonical_alias
|
/// - m.room.canonical_alias
|
||||||
/// - m.room.tombstone
|
/// - m.room.tombstone
|
||||||
/// - *some* m.room.member events, where needed
|
/// - *some* m.room.member events, where needed
|
||||||
Client(this.clientName,
|
/// [roomPreviewLastEvents]: The event types that should be used to calculate the last event
|
||||||
{this.debug = false,
|
/// in a room for the room list.
|
||||||
this.database,
|
Client(
|
||||||
this.enableE2eeRecovery = false,
|
this.clientName, {
|
||||||
this.verificationMethods,
|
this.database,
|
||||||
http.Client httpClient,
|
this.enableE2eeRecovery = false,
|
||||||
this.importantStateEvents,
|
this.verificationMethods,
|
||||||
this.pinUnreadRooms = false}) {
|
http.Client httpClient,
|
||||||
|
this.importantStateEvents,
|
||||||
|
this.roomPreviewLastEvents,
|
||||||
|
this.pinUnreadRooms = false,
|
||||||
|
this.sendMessageTimeoutSeconds = 60,
|
||||||
|
@deprecated bool debug,
|
||||||
|
}) {
|
||||||
verificationMethods ??= <KeyVerificationMethod>{};
|
verificationMethods ??= <KeyVerificationMethod>{};
|
||||||
importantStateEvents ??= <String>{};
|
importantStateEvents ??= {};
|
||||||
importantStateEvents.addAll([
|
importantStateEvents.addAll([
|
||||||
EventTypes.RoomName,
|
EventTypes.RoomName,
|
||||||
EventTypes.RoomAvatar,
|
EventTypes.RoomAvatar,
|
||||||
|
@ -100,17 +108,15 @@ class Client {
|
||||||
EventTypes.RoomCanonicalAlias,
|
EventTypes.RoomCanonicalAlias,
|
||||||
EventTypes.RoomTombstone,
|
EventTypes.RoomTombstone,
|
||||||
]);
|
]);
|
||||||
api = MatrixApi(debug: debug, httpClient: httpClient);
|
roomPreviewLastEvents ??= {};
|
||||||
onLoginStateChanged.stream.listen((loginState) {
|
roomPreviewLastEvents.addAll([
|
||||||
if (debug) {
|
EventTypes.Message,
|
||||||
print('[LoginState]: ${loginState.toString()}');
|
EventTypes.Encrypted,
|
||||||
}
|
EventTypes.Sticker,
|
||||||
});
|
]);
|
||||||
|
this.httpClient = httpClient ?? http.Client();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether debug prints should be displayed.
|
|
||||||
final bool debug;
|
|
||||||
|
|
||||||
/// The required name for this client.
|
/// The required name for this client.
|
||||||
final String clientName;
|
final String clientName;
|
||||||
|
|
||||||
|
@ -130,7 +136,7 @@ class Client {
|
||||||
String _deviceName;
|
String _deviceName;
|
||||||
|
|
||||||
/// Returns the current login state.
|
/// 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.
|
/// A list of all rooms the user is participating or invited.
|
||||||
List<Room> get rooms => _rooms;
|
List<Room> get rooms => _rooms;
|
||||||
|
@ -153,7 +159,7 @@ class Client {
|
||||||
|
|
||||||
/// Warning! This endpoint is for testing only!
|
/// Warning! This endpoint is for testing only!
|
||||||
set rooms(List<Room> newList) {
|
set rooms(List<Room> newList) {
|
||||||
print('Warning! This endpoint is for testing only!');
|
Logs.warning('Warning! This endpoint is for testing only!');
|
||||||
_rooms = newList;
|
_rooms = newList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,21 +171,6 @@ class Client {
|
||||||
|
|
||||||
int _transactionCounter = 0;
|
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() {
|
String generateUniqueTransactionId() {
|
||||||
_transactionCounter++;
|
_transactionCounter++;
|
||||||
return '${clientName}-${_transactionCounter}-${DateTime.now().millisecondsSinceEpoch}';
|
return '${clientName}-${_transactionCounter}-${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
@ -260,8 +251,20 @@ class Client {
|
||||||
/// Throws FormatException, TimeoutException and MatrixException on error.
|
/// Throws FormatException, TimeoutException and MatrixException on error.
|
||||||
Future<bool> checkServer(dynamic serverUrl) async {
|
Future<bool> checkServer(dynamic serverUrl) async {
|
||||||
try {
|
try {
|
||||||
api.homeserver = (serverUrl is Uri) ? serverUrl : Uri.parse(serverUrl);
|
if (serverUrl is Uri) {
|
||||||
final versions = await api.requestSupportedVersions();
|
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++) {
|
for (var i = 0; i < versions.versions.length; i++) {
|
||||||
if (versions.versions[i] == 'r0.5.0' ||
|
if (versions.versions[i] == 'r0.5.0' ||
|
||||||
|
@ -272,7 +275,7 @@ class Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final loginTypes = await api.requestLoginTypes();
|
final loginTypes = await requestLoginTypes();
|
||||||
if (loginTypes.flows.indexWhere((f) => f.type == 'm.login.password') ==
|
if (loginTypes.flows.indexWhere((f) => f.type == 'm.login.password') ==
|
||||||
-1) {
|
-1) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -280,7 +283,7 @@ class Client {
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
api.homeserver = null;
|
homeserver = null;
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -288,16 +291,17 @@ class Client {
|
||||||
/// Checks to see if a username is available, and valid, for the server.
|
/// 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.
|
/// Returns the fully-qualified Matrix user ID (MXID) that has been registered.
|
||||||
/// You have to call [checkServer] first to set a homeserver.
|
/// You have to call [checkServer] first to set a homeserver.
|
||||||
Future<void> register({
|
@override
|
||||||
String kind,
|
Future<LoginResponse> register({
|
||||||
String username,
|
String username,
|
||||||
String password,
|
String password,
|
||||||
Map<String, dynamic> auth,
|
|
||||||
String deviceId,
|
String deviceId,
|
||||||
String initialDeviceDisplayName,
|
String initialDeviceDisplayName,
|
||||||
bool inhibitLogin,
|
bool inhibitLogin,
|
||||||
|
Map<String, dynamic> auth,
|
||||||
|
String kind,
|
||||||
}) async {
|
}) async {
|
||||||
final response = await api.register(
|
final response = await super.register(
|
||||||
username: username,
|
username: username,
|
||||||
password: password,
|
password: password,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
|
@ -315,68 +319,78 @@ class Client {
|
||||||
await connect(
|
await connect(
|
||||||
newToken: response.accessToken,
|
newToken: response.accessToken,
|
||||||
newUserID: response.userId,
|
newUserID: response.userId,
|
||||||
newHomeserver: api.homeserver,
|
newHomeserver: homeserver,
|
||||||
newDeviceName: initialDeviceDisplayName ?? '',
|
newDeviceName: initialDeviceDisplayName ?? '',
|
||||||
newDeviceID: response.deviceId);
|
newDeviceID: response.deviceId);
|
||||||
return;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the login and allows the client to call all APIs which require
|
/// Handles the login and allows the client to call all APIs which require
|
||||||
/// authentication. Returns false if the login was not successful. Throws
|
/// authentication. Returns false if the login was not successful. Throws
|
||||||
/// MatrixException if login was not successful.
|
/// MatrixException if login was not successful.
|
||||||
/// You have to call [checkServer] first to set a homeserver.
|
/// You have to call [checkServer] first to set a homeserver.
|
||||||
Future<bool> login(
|
@override
|
||||||
String username,
|
Future<LoginResponse> login({
|
||||||
String password, {
|
String type = 'm.login.password',
|
||||||
String initialDeviceDisplayName,
|
String userIdentifierType = 'm.id.user',
|
||||||
|
String user,
|
||||||
|
String medium,
|
||||||
|
String address,
|
||||||
|
String password,
|
||||||
|
String token,
|
||||||
String deviceId,
|
String deviceId,
|
||||||
|
String initialDeviceDisplayName,
|
||||||
}) async {
|
}) async {
|
||||||
var data = <String, dynamic>{
|
final loginResp = await super.login(
|
||||||
'type': 'm.login.password',
|
type: type,
|
||||||
'user': username,
|
userIdentifierType: userIdentifierType,
|
||||||
'identifier': {
|
user: user,
|
||||||
'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,
|
|
||||||
password: password,
|
password: password,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
initialDeviceDisplayName: initialDeviceDisplayName,
|
initialDeviceDisplayName: initialDeviceDisplayName,
|
||||||
|
medium: medium,
|
||||||
|
address: address,
|
||||||
|
token: token,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Connect if there is an access token in the response.
|
// Connect if there is an access token in the response.
|
||||||
if (loginResp.accessToken == null ||
|
if (loginResp.accessToken == null ||
|
||||||
loginResp.deviceId == null ||
|
loginResp.deviceId == null ||
|
||||||
loginResp.userId == 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(
|
await connect(
|
||||||
newToken: loginResp.accessToken,
|
newToken: loginResp.accessToken,
|
||||||
newUserID: loginResp.userId,
|
newUserID: loginResp.userId,
|
||||||
newHomeserver: api.homeserver,
|
newHomeserver: homeserver,
|
||||||
newDeviceName: initialDeviceDisplayName ?? '',
|
newDeviceName: initialDeviceDisplayName ?? '',
|
||||||
newDeviceID: loginResp.deviceId,
|
newDeviceID: loginResp.deviceId,
|
||||||
);
|
);
|
||||||
return true;
|
return loginResp;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a logout command to the homeserver and clears all local data,
|
/// Sends a logout command to the homeserver and clears all local data,
|
||||||
/// including all persistent data from the store.
|
/// including all persistent data from the store.
|
||||||
|
@override
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
try {
|
try {
|
||||||
await api.logout();
|
await super.logout();
|
||||||
} catch (exception) {
|
} catch (e, s) {
|
||||||
print(exception);
|
Logs.error(e, s);
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
await clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends a logout command to the homeserver and clears all local data,
|
||||||
|
/// including all persistent data from the store.
|
||||||
|
@override
|
||||||
|
Future<void> logoutAll() async {
|
||||||
|
try {
|
||||||
|
await super.logoutAll();
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs.error(e, s);
|
||||||
rethrow;
|
rethrow;
|
||||||
} finally {
|
} finally {
|
||||||
await clear();
|
await clear();
|
||||||
|
@ -427,19 +441,19 @@ class Client {
|
||||||
if (cache && _profileCache.containsKey(userId)) {
|
if (cache && _profileCache.containsKey(userId)) {
|
||||||
return _profileCache[userId];
|
return _profileCache[userId];
|
||||||
}
|
}
|
||||||
final profile = await api.requestProfile(userId);
|
final profile = await requestProfile(userId);
|
||||||
_profileCache[userId] = profile;
|
_profileCache[userId] = profile;
|
||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Room>> get archive async {
|
Future<List<Room>> get archive async {
|
||||||
var archiveList = <Room>[];
|
var archiveList = <Room>[];
|
||||||
final sync = await api.sync(
|
final syncResp = await sync(
|
||||||
filter: '{"room":{"include_leave":true,"timeline":{"limit":10}}}',
|
filter: '{"room":{"include_leave":true,"timeline":{"limit":10}}}',
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
);
|
);
|
||||||
if (sync.rooms.leave is Map<String, dynamic>) {
|
if (syncResp.rooms.leave is Map<String, dynamic>) {
|
||||||
for (var entry in sync.rooms.leave.entries) {
|
for (var entry in syncResp.rooms.leave.entries) {
|
||||||
final id = entry.key;
|
final id = entry.key;
|
||||||
final room = entry.value;
|
final room = entry.value;
|
||||||
var leftRoom = Room(
|
var leftRoom = Room(
|
||||||
|
@ -466,14 +480,10 @@ class Client {
|
||||||
return archiveList;
|
return archiveList;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Changes the user's displayname.
|
|
||||||
Future<void> setDisplayname(String displayname) =>
|
|
||||||
api.setDisplayname(userID, displayname);
|
|
||||||
|
|
||||||
/// Uploads a new user avatar for this user.
|
/// Uploads a new user avatar for this user.
|
||||||
Future<void> setAvatar(MatrixFile file) async {
|
Future<void> setAvatar(MatrixFile file) async {
|
||||||
final uploadResp = await api.upload(file.bytes, file.name);
|
final uploadResp = await upload(file.bytes, file.name);
|
||||||
await api.setAvatarUrl(userID, Uri.parse(uploadResp));
|
await setAvatarUrl(userID, Uri.parse(uploadResp));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -517,7 +527,7 @@ class Client {
|
||||||
StreamController.broadcast();
|
StreamController.broadcast();
|
||||||
|
|
||||||
/// Synchronization erros are coming here.
|
/// Synchronization erros are coming here.
|
||||||
final StreamController<SyncError> onSyncError = StreamController.broadcast();
|
final StreamController<SdkError> onSyncError = StreamController.broadcast();
|
||||||
|
|
||||||
/// Synchronization erros are coming here.
|
/// Synchronization erros are coming here.
|
||||||
final StreamController<ToDeviceEventDecryptionError> onOlmError =
|
final StreamController<ToDeviceEventDecryptionError> onOlmError =
|
||||||
|
@ -556,10 +566,6 @@ class Client {
|
||||||
final StreamController<KeyVerification> onKeyVerificationRequest =
|
final StreamController<KeyVerification> onKeyVerificationRequest =
|
||||||
StreamController.broadcast();
|
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
|
/// How long should the app wait until it retrys the synchronisation after
|
||||||
/// an error?
|
/// an error?
|
||||||
int syncErrorTimeoutSec = 3;
|
int syncErrorTimeoutSec = 3;
|
||||||
|
@ -581,7 +587,7 @@ class Client {
|
||||||
/// "type": "m.login.password",
|
/// "type": "m.login.password",
|
||||||
/// "user": "test",
|
/// "user": "test",
|
||||||
/// "password": "1234",
|
/// "password": "1234",
|
||||||
/// "initial_device_display_name": "Fluffy Matrix Client"
|
/// "initial_device_display_name": "Matrix Client"
|
||||||
/// });
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
|
@ -610,8 +616,8 @@ class Client {
|
||||||
final account = await database.getClient(clientName);
|
final account = await database.getClient(clientName);
|
||||||
if (account != null) {
|
if (account != null) {
|
||||||
_id = account.clientId;
|
_id = account.clientId;
|
||||||
api.homeserver = Uri.parse(account.homeserverUrl);
|
homeserver = Uri.parse(account.homeserverUrl);
|
||||||
api.accessToken = account.token;
|
accessToken = account.token;
|
||||||
_userID = account.userId;
|
_userID = account.userId;
|
||||||
_deviceID = account.deviceId;
|
_deviceID = account.deviceId;
|
||||||
_deviceName = account.deviceName;
|
_deviceName = account.deviceName;
|
||||||
|
@ -619,15 +625,15 @@ class Client {
|
||||||
olmAccount = account.olmAccount;
|
olmAccount = account.olmAccount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
api.accessToken = newToken ?? api.accessToken;
|
accessToken = newToken ?? accessToken;
|
||||||
api.homeserver = newHomeserver ?? api.homeserver;
|
homeserver = newHomeserver ?? homeserver;
|
||||||
_userID = newUserID ?? _userID;
|
_userID = newUserID ?? _userID;
|
||||||
_deviceID = newDeviceID ?? _deviceID;
|
_deviceID = newDeviceID ?? _deviceID;
|
||||||
_deviceName = newDeviceName ?? _deviceName;
|
_deviceName = newDeviceName ?? _deviceName;
|
||||||
prevBatch = newPrevBatch ?? prevBatch;
|
prevBatch = newPrevBatch ?? prevBatch;
|
||||||
olmAccount = newOlmAccount ?? olmAccount;
|
olmAccount = newOlmAccount ?? olmAccount;
|
||||||
|
|
||||||
if (api.accessToken == null || api.homeserver == null || _userID == null) {
|
if (accessToken == null || homeserver == null || _userID == null) {
|
||||||
// we aren't logged in
|
// we aren't logged in
|
||||||
encryption?.dispose();
|
encryption?.dispose();
|
||||||
encryption = null;
|
encryption = null;
|
||||||
|
@ -635,15 +641,16 @@ class Client {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
encryption = Encryption(
|
encryption?.dispose();
|
||||||
debug: debug, client: this, enableE2eeRecovery: enableE2eeRecovery);
|
encryption =
|
||||||
|
Encryption(client: this, enableE2eeRecovery: enableE2eeRecovery);
|
||||||
await encryption.init(olmAccount);
|
await encryption.init(olmAccount);
|
||||||
|
|
||||||
if (database != null) {
|
if (database != null) {
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
await database.updateClient(
|
await database.updateClient(
|
||||||
api.homeserver.toString(),
|
homeserver.toString(),
|
||||||
api.accessToken,
|
accessToken,
|
||||||
_userID,
|
_userID,
|
||||||
_deviceID,
|
_deviceID,
|
||||||
_deviceName,
|
_deviceName,
|
||||||
|
@ -654,8 +661,8 @@ class Client {
|
||||||
} else {
|
} else {
|
||||||
_id = await database.insertClient(
|
_id = await database.insertClient(
|
||||||
clientName,
|
clientName,
|
||||||
api.homeserver.toString(),
|
homeserver.toString(),
|
||||||
api.accessToken,
|
accessToken,
|
||||||
_userID,
|
_userID,
|
||||||
_deviceID,
|
_deviceID,
|
||||||
_deviceName,
|
_deviceName,
|
||||||
|
@ -671,7 +678,11 @@ class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoginStateChanged.add(LoginState.logged);
|
onLoginStateChanged.add(LoginState.logged);
|
||||||
|
Logs.success(
|
||||||
|
'Successfully connected as ${userID.localpart} with ${homeserver.toString()}',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Always do a _sync after login, even if backgroundSync is set to off
|
||||||
return _sync();
|
return _sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -683,42 +694,64 @@ class Client {
|
||||||
/// Resets all settings and stops the synchronisation.
|
/// Resets all settings and stops the synchronisation.
|
||||||
void clear() {
|
void clear() {
|
||||||
database?.clear(id);
|
database?.clear(id);
|
||||||
_id = api.accessToken =
|
_id = accessToken =
|
||||||
api.homeserver = _userID = _deviceID = _deviceName = prevBatch = null;
|
homeserver = _userID = _deviceID = _deviceName = prevBatch = null;
|
||||||
_rooms = [];
|
_rooms = [];
|
||||||
encryption?.dispose();
|
encryption?.dispose();
|
||||||
encryption = null;
|
encryption = null;
|
||||||
onLoginStateChanged.add(LoginState.loggedOut);
|
onLoginStateChanged.add(LoginState.loggedOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SyncUpdate> _syncRequest;
|
bool _backgroundSync = true;
|
||||||
Exception _lastSyncError;
|
Future<void> _currentSync, _retryDelay = Future.value();
|
||||||
|
bool get syncPending => _currentSync != null;
|
||||||
|
|
||||||
Future<void> _sync() async {
|
/// Controls the background sync (automatically looping forever if turned on).
|
||||||
if (isLogged() == false || _disposed) return;
|
set backgroundSync(bool enabled) {
|
||||||
|
_backgroundSync = enabled;
|
||||||
|
if (_backgroundSync) {
|
||||||
|
_sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Immediately start a sync and wait for completion.
|
||||||
|
/// If there is an active sync already, wait for the active sync instead.
|
||||||
|
Future<void> oneShotSync() {
|
||||||
|
return _sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _sync() {
|
||||||
|
if (_currentSync == null) {
|
||||||
|
_currentSync = _innerSync();
|
||||||
|
_currentSync.whenComplete(() {
|
||||||
|
_currentSync = null;
|
||||||
|
if (_backgroundSync && isLogged() && !_disposed) {
|
||||||
|
_sync();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return _currentSync;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _innerSync() async {
|
||||||
|
await _retryDelay;
|
||||||
|
_retryDelay = Future.delayed(Duration(seconds: syncErrorTimeoutSec));
|
||||||
|
if (!isLogged() || _disposed) return null;
|
||||||
try {
|
try {
|
||||||
_syncRequest = api
|
final syncResp = await sync(
|
||||||
.sync(
|
|
||||||
filter: syncFilters,
|
filter: syncFilters,
|
||||||
since: prevBatch,
|
since: prevBatch,
|
||||||
timeout: prevBatch != null ? 30000 : null,
|
timeout: prevBatch != null ? 30000 : null,
|
||||||
)
|
);
|
||||||
.catchError((e) {
|
|
||||||
_lastSyncError = e;
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
final hash = _syncRequest.hashCode;
|
|
||||||
final syncResp = await _syncRequest;
|
|
||||||
if (syncResp == null) throw _lastSyncError;
|
|
||||||
if (hash != _syncRequest.hashCode) return;
|
|
||||||
if (database != null) {
|
if (database != null) {
|
||||||
await database.transaction(() async {
|
_currentTransaction = database.transaction(() async {
|
||||||
await handleSync(syncResp);
|
await handleSync(syncResp);
|
||||||
if (prevBatch != syncResp.nextBatch) {
|
if (prevBatch != syncResp.nextBatch) {
|
||||||
await database.storePrevBatch(syncResp.nextBatch, id);
|
await database.storePrevBatch(syncResp.nextBatch, id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
await _currentTransaction;
|
||||||
} else {
|
} else {
|
||||||
await handleSync(syncResp);
|
await handleSync(syncResp);
|
||||||
}
|
}
|
||||||
|
@ -733,19 +766,19 @@ class Client {
|
||||||
if (encryptionEnabled) {
|
if (encryptionEnabled) {
|
||||||
encryption.onSync();
|
encryption.onSync();
|
||||||
}
|
}
|
||||||
if (hash == _syncRequest.hashCode) unawaited(_sync());
|
_retryDelay = Future.value();
|
||||||
} on MatrixException catch (exception) {
|
} on MatrixException catch (e) {
|
||||||
onError.add(exception);
|
onError.add(e);
|
||||||
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
|
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
if (isLogged() == false || _disposed) {
|
if (!isLogged() || _disposed) return;
|
||||||
return;
|
Logs.error('Error during processing events: ' + e.toString(), s);
|
||||||
}
|
onSyncError.add(SdkError(
|
||||||
print('Error during processing events: ' + e.toString());
|
|
||||||
print(s);
|
|
||||||
onSyncError.add(SyncError(
|
|
||||||
exception: e is Exception ? e : Exception(e), stackTrace: s));
|
exception: e is Exception ? e : Exception(e), stackTrace: s));
|
||||||
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
|
if (e is MatrixException &&
|
||||||
|
e.errcode == MatrixError.M_UNKNOWN_TOKEN.toString().split('.').last) {
|
||||||
|
Logs.warning('The user has been logged out!');
|
||||||
|
clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -767,6 +800,7 @@ class Client {
|
||||||
await _handleRooms(sync.rooms.leave, Membership.leave,
|
await _handleRooms(sync.rooms.leave, Membership.leave,
|
||||||
sortAtTheEnd: sortAtTheEnd);
|
sortAtTheEnd: sortAtTheEnd);
|
||||||
}
|
}
|
||||||
|
_sortRooms();
|
||||||
}
|
}
|
||||||
if (sync.presence != null) {
|
if (sync.presence != null) {
|
||||||
for (final newPresence in sync.presence) {
|
for (final newPresence in sync.presence) {
|
||||||
|
@ -821,10 +855,10 @@ class Client {
|
||||||
try {
|
try {
|
||||||
toDeviceEvent = await encryption.decryptToDeviceEvent(toDeviceEvent);
|
toDeviceEvent = await encryption.decryptToDeviceEvent(toDeviceEvent);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
print(
|
Logs.error(
|
||||||
'[LibOlm] Could not decrypt to device event from ${toDeviceEvent.sender} with content: ${toDeviceEvent.content}');
|
'[LibOlm] Could not decrypt to device event from ${toDeviceEvent.sender} with content: ${toDeviceEvent.content}\n${e.toString()}',
|
||||||
print(e);
|
s);
|
||||||
print(s);
|
|
||||||
onOlmError.add(
|
onOlmError.add(
|
||||||
ToDeviceEventDecryptionError(
|
ToDeviceEventDecryptionError(
|
||||||
exception: e is Exception ? e : Exception(e),
|
exception: e is Exception ? e : Exception(e),
|
||||||
|
@ -1014,14 +1048,22 @@ class Client {
|
||||||
}
|
}
|
||||||
onEvent.add(update);
|
onEvent.add(update);
|
||||||
|
|
||||||
if (event['type'] == 'm.call.invite') {
|
final rawUnencryptedEvent = update.content;
|
||||||
onCallInvite.add(Event.fromJson(event, room, sortOrder));
|
|
||||||
} else if (event['type'] == 'm.call.hangup') {
|
if (prevBatch != null && type == 'timeline') {
|
||||||
onCallHangup.add(Event.fromJson(event, room, sortOrder));
|
if (rawUnencryptedEvent['type'] == EventTypes.CallInvite) {
|
||||||
} else if (event['type'] == 'm.call.answer') {
|
onCallInvite
|
||||||
onCallAnswer.add(Event.fromJson(event, room, sortOrder));
|
.add(Event.fromJson(rawUnencryptedEvent, room, sortOrder));
|
||||||
} else if (event['type'] == 'm.call.candidates') {
|
} else if (rawUnencryptedEvent['type'] == EventTypes.CallHangup) {
|
||||||
onCallCandidates.add(Event.fromJson(event, room, sortOrder));
|
onCallHangup
|
||||||
|
.add(Event.fromJson(rawUnencryptedEvent, room, sortOrder));
|
||||||
|
} else if (rawUnencryptedEvent['type'] == EventTypes.CallAnswer) {
|
||||||
|
onCallAnswer
|
||||||
|
.add(Event.fromJson(rawUnencryptedEvent, room, sortOrder));
|
||||||
|
} else if (rawUnencryptedEvent['type'] == EventTypes.CallCandidates) {
|
||||||
|
onCallCandidates
|
||||||
|
.add(Event.fromJson(rawUnencryptedEvent, room, sortOrder));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1084,50 +1126,53 @@ class Client {
|
||||||
}
|
}
|
||||||
if (rooms[j].onUpdate != null) rooms[j].onUpdate.add(rooms[j].id);
|
if (rooms[j].onUpdate != null) rooms[j].onUpdate.add(rooms[j].id);
|
||||||
}
|
}
|
||||||
_sortRooms();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateRoomsByEventUpdate(EventUpdate eventUpdate) {
|
void _updateRoomsByEventUpdate(EventUpdate eventUpdate) {
|
||||||
if (eventUpdate.type == 'history') return;
|
if (eventUpdate.type == 'history') return;
|
||||||
// Search the room in the rooms
|
|
||||||
num j = 0;
|
final room = getRoomById(eventUpdate.roomID);
|
||||||
for (j = 0; j < rooms.length; j++) {
|
if (room == null) return;
|
||||||
if (rooms[j].id == eventUpdate.roomID) break;
|
|
||||||
|
switch (eventUpdate.type) {
|
||||||
|
case 'timeline':
|
||||||
|
case 'state':
|
||||||
|
case 'invite_state':
|
||||||
|
var stateEvent =
|
||||||
|
Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder);
|
||||||
|
var prevState = room.getState(stateEvent.type, stateEvent.stateKey);
|
||||||
|
if (prevState != null && prevState.sortOrder > stateEvent.sortOrder) {
|
||||||
|
Logs.warning('''
|
||||||
|
A new ${eventUpdate.type} event of the type ${stateEvent.type} has arrived with a previews
|
||||||
|
sort order ${stateEvent.sortOrder} than the current ${stateEvent.type} event with a
|
||||||
|
sort order of ${prevState.sortOrder}. This should never happen...''');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (stateEvent.type == EventTypes.Redaction) {
|
||||||
|
final String redacts = eventUpdate.content['redacts'];
|
||||||
|
room.states.states.forEach(
|
||||||
|
(String key, Map<String, Event> states) => states.forEach(
|
||||||
|
(String key, Event state) {
|
||||||
|
if (state.eventId == redacts) {
|
||||||
|
state.setRedactionEvent(stateEvent);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
room.setState(stateEvent);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'account_data':
|
||||||
|
room.roomAccountData[eventUpdate.eventType] =
|
||||||
|
BasicRoomEvent.fromJson(eventUpdate.content);
|
||||||
|
break;
|
||||||
|
case 'ephemeral':
|
||||||
|
room.ephemerals[eventUpdate.eventType] =
|
||||||
|
BasicRoomEvent.fromJson(eventUpdate.content);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
final found = (j < rooms.length && rooms[j].id == eventUpdate.roomID);
|
room.onUpdate.add(room.id);
|
||||||
if (!found) return;
|
|
||||||
if (eventUpdate.type == 'timeline' ||
|
|
||||||
eventUpdate.type == 'state' ||
|
|
||||||
eventUpdate.type == 'invite_state') {
|
|
||||||
var stateEvent =
|
|
||||||
Event.fromJson(eventUpdate.content, rooms[j], eventUpdate.sortOrder);
|
|
||||||
if (stateEvent.type == EventTypes.Redaction) {
|
|
||||||
final String redacts = eventUpdate.content['redacts'];
|
|
||||||
rooms[j].states.states.forEach(
|
|
||||||
(String key, Map<String, Event> states) => states.forEach(
|
|
||||||
(String key, Event state) {
|
|
||||||
if (state.eventId == redacts) {
|
|
||||||
state.setRedactionEvent(stateEvent);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
var prevState = rooms[j].getState(stateEvent.type, stateEvent.stateKey);
|
|
||||||
if (prevState != null &&
|
|
||||||
prevState.originServerTs.millisecondsSinceEpoch >
|
|
||||||
stateEvent.originServerTs.millisecondsSinceEpoch) return;
|
|
||||||
rooms[j].setState(stateEvent);
|
|
||||||
}
|
|
||||||
} else if (eventUpdate.type == 'account_data') {
|
|
||||||
rooms[j].roomAccountData[eventUpdate.eventType] =
|
|
||||||
BasicRoomEvent.fromJson(eventUpdate.content);
|
|
||||||
} else if (eventUpdate.type == 'ephemeral') {
|
|
||||||
rooms[j].ephemerals[eventUpdate.eventType] =
|
|
||||||
BasicRoomEvent.fromJson(eventUpdate.content);
|
|
||||||
}
|
|
||||||
if (rooms[j].onUpdate != null) rooms[j].onUpdate.add(rooms[j].id);
|
|
||||||
if (['timeline', 'account_data'].contains(eventUpdate.type)) _sortRooms();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _sortLock = false;
|
bool _sortLock = false;
|
||||||
|
@ -1156,21 +1201,39 @@ class Client {
|
||||||
Map<String, DeviceKeysList> get userDeviceKeys => _userDeviceKeys;
|
Map<String, DeviceKeysList> get userDeviceKeys => _userDeviceKeys;
|
||||||
Map<String, DeviceKeysList> _userDeviceKeys = {};
|
Map<String, DeviceKeysList> _userDeviceKeys = {};
|
||||||
|
|
||||||
|
/// Gets user device keys by its curve25519 key. Returns null if it isn't found
|
||||||
|
DeviceKeys getUserDeviceKeysByCurve25519Key(String senderKey) {
|
||||||
|
for (final user in userDeviceKeys.values) {
|
||||||
|
final device = user.deviceKeys.values
|
||||||
|
.firstWhere((e) => e.curve25519Key == senderKey, orElse: () => null);
|
||||||
|
if (device != null) {
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Future<Set<String>> _getUserIdsInEncryptedRooms() async {
|
Future<Set<String>> _getUserIdsInEncryptedRooms() async {
|
||||||
var userIds = <String>{};
|
var userIds = <String>{};
|
||||||
for (var i = 0; i < rooms.length; i++) {
|
for (var i = 0; i < rooms.length; i++) {
|
||||||
if (rooms[i].encrypted) {
|
if (rooms[i].encrypted) {
|
||||||
var userList = await rooms[i].requestParticipants();
|
try {
|
||||||
for (var user in userList) {
|
var userList = await rooms[i].requestParticipants();
|
||||||
if ([Membership.join, Membership.invite].contains(user.membership)) {
|
for (var user in userList) {
|
||||||
userIds.add(user.id);
|
if ([Membership.join, Membership.invite]
|
||||||
|
.contains(user.membership)) {
|
||||||
|
userIds.add(user.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs.error('[E2EE] Failed to fetch participants: ' + e.toString(), s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return userIds;
|
return userIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Map<String, DateTime> _keyQueryFailures = {};
|
||||||
Future<void> _updateUserDeviceKeys() async {
|
Future<void> _updateUserDeviceKeys() async {
|
||||||
try {
|
try {
|
||||||
if (!isLogged()) return;
|
if (!isLogged()) return;
|
||||||
|
@ -1189,15 +1252,19 @@ class Client {
|
||||||
_userDeviceKeys[userId] = DeviceKeysList(userId, this);
|
_userDeviceKeys[userId] = DeviceKeysList(userId, this);
|
||||||
}
|
}
|
||||||
var deviceKeysList = userDeviceKeys[userId];
|
var deviceKeysList = userDeviceKeys[userId];
|
||||||
if (deviceKeysList.outdated) {
|
if (deviceKeysList.outdated &&
|
||||||
|
(!_keyQueryFailures.containsKey(userId.domain) ||
|
||||||
|
DateTime.now()
|
||||||
|
.subtract(Duration(minutes: 5))
|
||||||
|
.isAfter(_keyQueryFailures[userId.domain]))) {
|
||||||
outdatedLists[userId] = [];
|
outdatedLists[userId] = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outdatedLists.isNotEmpty) {
|
if (outdatedLists.isNotEmpty) {
|
||||||
// Request the missing device key lists from the server.
|
// Request the missing device key lists from the server.
|
||||||
final response =
|
if (!isLogged()) return;
|
||||||
await api.requestDeviceKeys(outdatedLists, timeout: 10000);
|
final response = await requestDeviceKeys(outdatedLists, timeout: 10000);
|
||||||
|
|
||||||
for (final rawDeviceKeyListEntry in response.deviceKeys.entries) {
|
for (final rawDeviceKeyListEntry in response.deviceKeys.entries) {
|
||||||
final userId = rawDeviceKeyListEntry.key;
|
final userId = rawDeviceKeyListEntry.key;
|
||||||
|
@ -1331,28 +1398,57 @@ class Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
await database?.transaction(() async {
|
// now process all the failures
|
||||||
for (final f in dbActions) {
|
if (response.failures != null) {
|
||||||
await f();
|
for (final failureDomain in response.failures.keys) {
|
||||||
|
_keyQueryFailures[failureDomain] = DateTime.now();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
} catch (e) {
|
|
||||||
print('[LibOlm] Unable to update user device keys: ' + e.toString());
|
if (dbActions.isNotEmpty) {
|
||||||
|
await database?.transaction(() async {
|
||||||
|
for (final f in dbActions) {
|
||||||
|
await f();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs.error(
|
||||||
|
'[LibOlm] Unable to update user device keys: ' + e.toString(), s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send an (unencrypted) to device [message] of a specific [eventType] to all
|
||||||
|
/// devices of a set of [users].
|
||||||
|
Future<void> sendToDevicesOfUserIds(
|
||||||
|
Set<String> users,
|
||||||
|
String eventType,
|
||||||
|
Map<String, dynamic> message, {
|
||||||
|
String messageId,
|
||||||
|
}) async {
|
||||||
|
// Send with send-to-device messaging
|
||||||
|
var data = <String, Map<String, Map<String, dynamic>>>{};
|
||||||
|
for (var user in users) {
|
||||||
|
data[user] = {};
|
||||||
|
data[user]['*'] = message;
|
||||||
|
}
|
||||||
|
await sendToDevice(
|
||||||
|
eventType, messageId ?? generateUniqueTransactionId(), data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/// Sends an encrypted [message] of this [type] to these [deviceKeys]. To send
|
/// 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].
|
/// the request to all devices of the current user, pass an empty list to [deviceKeys].
|
||||||
Future<void> sendToDevice(
|
Future<void> sendToDeviceEncrypted(
|
||||||
List<DeviceKeys> deviceKeys,
|
List<DeviceKeys> deviceKeys,
|
||||||
String type,
|
String eventType,
|
||||||
Map<String, dynamic> message, {
|
Map<String, dynamic> message, {
|
||||||
bool encrypted = true,
|
String messageId,
|
||||||
List<User> toUsers,
|
|
||||||
bool onlyVerified = false,
|
bool onlyVerified = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (encrypted && !encryptionEnabled) return;
|
if (!encryptionEnabled) return;
|
||||||
// Don't send this message to blocked devices, and if specified onlyVerified
|
// Don't send this message to blocked devices, and if specified onlyVerified
|
||||||
// then only send it to verified devices
|
// then only send it to verified devices
|
||||||
if (deviceKeys.isNotEmpty) {
|
if (deviceKeys.isNotEmpty) {
|
||||||
|
@ -1363,36 +1459,13 @@ class Client {
|
||||||
if (deviceKeys.isEmpty) return;
|
if (deviceKeys.isEmpty) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sendToDeviceMessage = message;
|
|
||||||
|
|
||||||
// Send with send-to-device messaging
|
// Send with send-to-device messaging
|
||||||
var data = <String, Map<String, Map<String, dynamic>>>{};
|
var data = <String, Map<String, Map<String, dynamic>>>{};
|
||||||
if (deviceKeys.isEmpty) {
|
data =
|
||||||
if (toUsers == null) {
|
await encryption.encryptToDeviceMessage(deviceKeys, eventType, message);
|
||||||
data[userID] = {};
|
eventType = EventTypes.Encrypted;
|
||||||
data[userID]['*'] = sendToDeviceMessage;
|
await sendToDevice(
|
||||||
} else {
|
eventType, messageId ?? generateUniqueTransactionId(), data);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether all push notifications are muted using the [.m.rule.master]
|
/// Whether all push notifications are muted using the [.m.rule.master]
|
||||||
|
@ -1417,7 +1490,7 @@ class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setMuteAllPushNotifications(bool muted) async {
|
Future<void> setMuteAllPushNotifications(bool muted) async {
|
||||||
await api.enablePushRule(
|
await enablePushRule(
|
||||||
'global',
|
'global',
|
||||||
PushRuleKind.override,
|
PushRuleKind.override,
|
||||||
'.m.rule.master',
|
'.m.rule.master',
|
||||||
|
@ -1427,6 +1500,7 @@ class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Changes the password. You should either set oldPasswort or another authentication flow.
|
/// Changes the password. You should either set oldPasswort or another authentication flow.
|
||||||
|
@override
|
||||||
Future<void> changePassword(String newPassword,
|
Future<void> changePassword(String newPassword,
|
||||||
{String oldPassword, Map<String, dynamic> auth}) async {
|
{String oldPassword, Map<String, dynamic> auth}) async {
|
||||||
try {
|
try {
|
||||||
|
@ -1437,7 +1511,7 @@ class Client {
|
||||||
'password': oldPassword,
|
'password': oldPassword,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
await api.changePassword(newPassword, auth: auth);
|
await super.changePassword(newPassword, auth: auth);
|
||||||
} on MatrixException catch (matrixException) {
|
} on MatrixException catch (matrixException) {
|
||||||
if (!matrixException.requireAdditionalAuthentication) {
|
if (!matrixException.requireAdditionalAuthentication) {
|
||||||
rethrow;
|
rethrow;
|
||||||
|
@ -1465,20 +1539,74 @@ class Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clear all local cached messages and perform a new clean sync.
|
||||||
|
Future<void> clearLocalCachedMessages() async {
|
||||||
|
prevBatch = null;
|
||||||
|
rooms.forEach((r) => r.prev_batch = null);
|
||||||
|
await database?.clearCache(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A list of mxids of users who are ignored.
|
||||||
|
List<String> get ignoredUsers => (accountData
|
||||||
|
.containsKey('m.ignored_user_list') &&
|
||||||
|
accountData['m.ignored_user_list'].content['ignored_users'] is Map)
|
||||||
|
? List<String>.from(
|
||||||
|
accountData['m.ignored_user_list'].content['ignored_users'].keys)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
/// Ignore another user. This will clear the local cached messages to
|
||||||
|
/// hide all previous messages from this user.
|
||||||
|
Future<void> ignoreUser(String userId) async {
|
||||||
|
if (!userId.isValidMatrixId) {
|
||||||
|
throw Exception('$userId is not a valid mxid!');
|
||||||
|
}
|
||||||
|
await setAccountData(userID, 'm.ignored_user_list', {
|
||||||
|
'ignored_users': Map.fromEntries(
|
||||||
|
(ignoredUsers..add(userId)).map((key) => MapEntry(key, {}))),
|
||||||
|
});
|
||||||
|
await clearLocalCachedMessages();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unignore a user. This will clear the local cached messages and request
|
||||||
|
/// them again from the server to avoid gaps in the timeline.
|
||||||
|
Future<void> unignoreUser(String userId) async {
|
||||||
|
if (!userId.isValidMatrixId) {
|
||||||
|
throw Exception('$userId is not a valid mxid!');
|
||||||
|
}
|
||||||
|
if (!ignoredUsers.contains(userId)) {
|
||||||
|
throw Exception('$userId is not in the ignore list!');
|
||||||
|
}
|
||||||
|
await setAccountData(userID, 'm.ignored_user_list', {
|
||||||
|
'ignored_users': Map.fromEntries(
|
||||||
|
(ignoredUsers..remove(userId)).map((key) => MapEntry(key, {}))),
|
||||||
|
});
|
||||||
|
await clearLocalCachedMessages();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
bool _disposed = false;
|
bool _disposed = false;
|
||||||
|
Future _currentTransaction = Future.sync(() => {});
|
||||||
|
|
||||||
/// Stops the synchronization and closes the database. After this
|
/// Stops the synchronization and closes the database. After this
|
||||||
/// you can safely make this Client instance null.
|
/// you can safely make this Client instance null.
|
||||||
Future<void> dispose({bool closeDatabase = false}) async {
|
Future<void> dispose({bool closeDatabase = false}) async {
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
try {
|
||||||
|
await _currentTransaction;
|
||||||
|
} catch (_) {
|
||||||
|
// No-OP
|
||||||
|
}
|
||||||
if (closeDatabase) await database?.close();
|
if (closeDatabase) await database?.close();
|
||||||
database = null;
|
database = null;
|
||||||
|
encryption?.dispose();
|
||||||
|
encryption = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SyncError {
|
class SdkError {
|
||||||
Exception exception;
|
Exception exception;
|
||||||
StackTrace stackTrace;
|
StackTrace stackTrace;
|
||||||
SyncError({this.exception, this.stackTrace});
|
SdkError({this.exception, this.stackTrace});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,51 @@
|
||||||
import 'package:moor/moor.dart';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart' as sdk;
|
import 'package:moor/moor.dart';
|
||||||
import 'package:famedlysdk/matrix_api.dart' as api;
|
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
|
import '../../famedlysdk.dart' as sdk;
|
||||||
|
import '../../matrix_api.dart' as api;
|
||||||
import '../../matrix_api.dart';
|
import '../../matrix_api.dart';
|
||||||
|
import '../client.dart';
|
||||||
|
import '../room.dart';
|
||||||
|
import '../utils/logs.dart';
|
||||||
|
|
||||||
part 'database.g.dart';
|
part 'database.g.dart';
|
||||||
|
|
||||||
|
extension MigratorExtension on Migrator {
|
||||||
|
Future<void> createIndexIfNotExists(Index index) async {
|
||||||
|
try {
|
||||||
|
await createIndex(index);
|
||||||
|
} catch (err) {
|
||||||
|
if (!err.toString().toLowerCase().contains('already exists')) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createTableIfNotExists(TableInfo<Table, DataClass> table) async {
|
||||||
|
try {
|
||||||
|
await createTable(table);
|
||||||
|
} catch (err) {
|
||||||
|
if (!err.toString().toLowerCase().contains('already exists')) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addColumnIfNotExists(
|
||||||
|
TableInfo<Table, DataClass> table, GeneratedColumn column) async {
|
||||||
|
try {
|
||||||
|
await addColumn(table, column);
|
||||||
|
} catch (err) {
|
||||||
|
if (!err.toString().toLowerCase().contains('duplicate column name')) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@UseMoor(
|
@UseMoor(
|
||||||
include: {'database.moor'},
|
include: {'database.moor'},
|
||||||
)
|
)
|
||||||
|
@ -18,56 +55,87 @@ class Database extends _$Database {
|
||||||
Database.connect(DatabaseConnection connection) : super.connect(connection);
|
Database.connect(DatabaseConnection connection) : super.connect(connection);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 5;
|
int get schemaVersion => 6;
|
||||||
|
|
||||||
int get maxFileSize => 1 * 1024 * 1024;
|
int get maxFileSize => 1 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// Update errors are coming here.
|
||||||
|
final StreamController<SdkError> onError = StreamController.broadcast();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
onCreate: (Migrator m) {
|
onCreate: (Migrator m) async {
|
||||||
return m.createAll();
|
try {
|
||||||
|
await m.createAll();
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs.error(e, s);
|
||||||
|
onError.add(SdkError(exception: e, stackTrace: s));
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onUpgrade: (Migrator m, int from, int to) async {
|
onUpgrade: (Migrator m, int from, int to) async {
|
||||||
// this appears to be only called once, so multiple consecutive upgrades have to be handled appropriately in here
|
try {
|
||||||
if (from == 1) {
|
// this appears to be only called once, so multiple consecutive upgrades have to be handled appropriately in here
|
||||||
await m.createIndex(userDeviceKeysIndex);
|
if (from == 1) {
|
||||||
await m.createIndex(userDeviceKeysKeyIndex);
|
await m.createIndexIfNotExists(userDeviceKeysIndex);
|
||||||
await m.createIndex(olmSessionsIndex);
|
await m.createIndexIfNotExists(userDeviceKeysKeyIndex);
|
||||||
await m.createIndex(outboundGroupSessionsIndex);
|
await m.createIndexIfNotExists(olmSessionsIndex);
|
||||||
await m.createIndex(inboundGroupSessionsIndex);
|
await m.createIndexIfNotExists(outboundGroupSessionsIndex);
|
||||||
await m.createIndex(roomsIndex);
|
await m.createIndexIfNotExists(inboundGroupSessionsIndex);
|
||||||
await m.createIndex(eventsIndex);
|
await m.createIndexIfNotExists(roomsIndex);
|
||||||
await m.createIndex(roomStatesIndex);
|
await m.createIndexIfNotExists(eventsIndex);
|
||||||
await m.createIndex(accountDataIndex);
|
await m.createIndexIfNotExists(roomStatesIndex);
|
||||||
await m.createIndex(roomAccountDataIndex);
|
await m.createIndexIfNotExists(accountDataIndex);
|
||||||
await m.createIndex(presencesIndex);
|
await m.createIndexIfNotExists(roomAccountDataIndex);
|
||||||
from++;
|
await m.createIndexIfNotExists(presencesIndex);
|
||||||
}
|
from++;
|
||||||
if (from == 2) {
|
}
|
||||||
await m.deleteTable('outbound_group_sessions');
|
if (from == 2) {
|
||||||
await m.createTable(outboundGroupSessions);
|
await m.deleteTable('outbound_group_sessions');
|
||||||
from++;
|
await m.createTable(outboundGroupSessions);
|
||||||
}
|
from++;
|
||||||
if (from == 3) {
|
}
|
||||||
await m.createTable(userCrossSigningKeys);
|
if (from == 3) {
|
||||||
await m.createTable(ssssCache);
|
await m.createTableIfNotExists(userCrossSigningKeys);
|
||||||
// mark all keys as outdated so that the cross signing keys will be fetched
|
await m.createTableIfNotExists(ssssCache);
|
||||||
await m.issueCustomQuery(
|
// mark all keys as outdated so that the cross signing keys will be fetched
|
||||||
'UPDATE user_device_keys SET outdated = true');
|
await customStatement(
|
||||||
from++;
|
'UPDATE user_device_keys SET outdated = true');
|
||||||
}
|
from++;
|
||||||
if (from == 4) {
|
}
|
||||||
await m.addColumn(olmSessions, olmSessions.lastReceived);
|
if (from == 4) {
|
||||||
from++;
|
await m.addColumnIfNotExists(
|
||||||
|
olmSessions, olmSessions.lastReceived);
|
||||||
|
from++;
|
||||||
|
}
|
||||||
|
if (from == 5) {
|
||||||
|
await m.addColumnIfNotExists(
|
||||||
|
inboundGroupSessions, inboundGroupSessions.uploaded);
|
||||||
|
await m.addColumnIfNotExists(
|
||||||
|
inboundGroupSessions, inboundGroupSessions.senderKey);
|
||||||
|
await m.addColumnIfNotExists(
|
||||||
|
inboundGroupSessions, inboundGroupSessions.senderClaimedKeys);
|
||||||
|
from++;
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs.error(e, s);
|
||||||
|
onError.add(SdkError(exception: e, stackTrace: s));
|
||||||
|
rethrow;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeOpen: (_) async {
|
beforeOpen: (_) async {
|
||||||
if (executor.dialect == SqlDialect.sqlite) {
|
try {
|
||||||
final ret = await customSelect('PRAGMA journal_mode=WAL').get();
|
if (executor.dialect == SqlDialect.sqlite) {
|
||||||
if (ret.isNotEmpty) {
|
final ret = await customSelect('PRAGMA journal_mode=WAL').get();
|
||||||
print('[Moor] Switched database to mode ' +
|
if (ret.isNotEmpty) {
|
||||||
ret.first.data['journal_mode'].toString());
|
Logs.info('[Moor] Switched database to mode ' +
|
||||||
|
ret.first.data['journal_mode'].toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs.error(e, s);
|
||||||
|
onError.add(SdkError(exception: e, stackTrace: s));
|
||||||
|
rethrow;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -75,6 +143,7 @@ class Database extends _$Database {
|
||||||
Future<DbClient> getClient(String name) async {
|
Future<DbClient> getClient(String name) async {
|
||||||
final res = await dbGetClient(name).get();
|
final res = await dbGetClient(name).get();
|
||||||
if (res.isEmpty) return null;
|
if (res.isEmpty) return null;
|
||||||
|
await markPendingEventsAsError(res.first.clientId);
|
||||||
return res.first;
|
return res.first;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,8 +181,9 @@ class Database extends _$Database {
|
||||||
var session = olm.Session();
|
var session = olm.Session();
|
||||||
session.unpickle(userId, row.pickle);
|
session.unpickle(userId, row.pickle);
|
||||||
res[row.identityKey].add(session);
|
res[row.identityKey].add(session);
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
print('[LibOlm] Could not unpickle olm session: ' + e.toString());
|
Logs.error(
|
||||||
|
'[LibOlm] Could not unpickle olm session: ' + e.toString(), s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
|
@ -329,7 +399,7 @@ class Database extends _$Database {
|
||||||
// Is the timeline limited? Then all previous messages should be
|
// Is the timeline limited? Then all previous messages should be
|
||||||
// removed from the database!
|
// removed from the database!
|
||||||
if (roomUpdate.limitedTimeline) {
|
if (roomUpdate.limitedTimeline) {
|
||||||
await removeRoomEvents(clientId, roomUpdate.id);
|
await removeSuccessfulRoomEvents(clientId, roomUpdate.id);
|
||||||
await updateRoomSortOrder(0.0, 0.0, clientId, roomUpdate.id);
|
await updateRoomSortOrder(0.0, 0.0, clientId, roomUpdate.id);
|
||||||
await setRoomPrevBatch(roomUpdate.prev_batch, clientId, roomUpdate.id);
|
await setRoomPrevBatch(roomUpdate.prev_batch, clientId, roomUpdate.id);
|
||||||
}
|
}
|
||||||
|
@ -357,14 +427,50 @@ class Database extends _$Database {
|
||||||
if (type == 'timeline' || type == 'history') {
|
if (type == 'timeline' || type == 'history') {
|
||||||
// calculate the status
|
// calculate the status
|
||||||
var status = 2;
|
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 (eventContent['status'] is num) status = eventContent['status'];
|
||||||
if ((status == 1 || status == -1) &&
|
var storeNewEvent = !((status == 1 || status == -1) &&
|
||||||
eventContent['unsigned'] is Map<String, dynamic> &&
|
eventContent['unsigned'] is Map<String, dynamic> &&
|
||||||
eventContent['unsigned']['transaction_id'] is String) {
|
eventContent['unsigned']['transaction_id'] is String);
|
||||||
// status changed and we have an old transaction id --> update event id and stuffs
|
if (!storeNewEvent) {
|
||||||
await updateEventStatus(status, eventContent['event_id'], clientId,
|
final allOldEvents =
|
||||||
eventContent['unsigned']['transaction_id'], chatId);
|
await getEvent(clientId, eventContent['event_id'], chatId).get();
|
||||||
} else {
|
if (allOldEvents.isNotEmpty) {
|
||||||
|
// we were likely unable to change transaction_id -> event_id.....because the event ID already exists!
|
||||||
|
// So, we try to fetch the old event
|
||||||
|
// the transaction id event will automatically be deleted further down
|
||||||
|
final oldEvent = allOldEvents.first;
|
||||||
|
// do we update the status? We should allow 0 -> -1 updates and status increases
|
||||||
|
if (status > oldEvent.status ||
|
||||||
|
(oldEvent.status == 0 && status == -1)) {
|
||||||
|
// update the status
|
||||||
|
await updateEventStatusOnly(
|
||||||
|
status, clientId, eventContent['event_id'], chatId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// status changed and we have an old transaction id --> update event id and stuffs
|
||||||
|
try {
|
||||||
|
final updated = await updateEventStatus(
|
||||||
|
status,
|
||||||
|
eventContent['event_id'],
|
||||||
|
clientId,
|
||||||
|
eventContent['unsigned']['transaction_id'],
|
||||||
|
chatId);
|
||||||
|
if (updated == 0) {
|
||||||
|
storeNewEvent = true;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// we could not update the transaction id to the event id....so it already exists
|
||||||
|
// as we just tried to fetch the event previously this is a race condition if the event comes down sync in the mean time
|
||||||
|
// that means that the status we already have in the database is likely more accurate
|
||||||
|
// than our status. So, we just ignore this error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (storeNewEvent) {
|
||||||
DbEvent oldEvent;
|
DbEvent oldEvent;
|
||||||
if (type == 'history') {
|
if (type == 'history') {
|
||||||
final allOldEvents =
|
final allOldEvents =
|
||||||
|
@ -394,6 +500,7 @@ class Database extends _$Database {
|
||||||
|
|
||||||
// is there a transaction id? Then delete the event with this id.
|
// is there a transaction id? Then delete the event with this id.
|
||||||
if (status != -1 &&
|
if (status != -1 &&
|
||||||
|
status != 0 &&
|
||||||
eventUpdate.content['unsigned'] is Map &&
|
eventUpdate.content['unsigned'] is Map &&
|
||||||
eventUpdate.content['unsigned']['transaction_id'] is String) {
|
eventUpdate.content['unsigned']['transaction_id'] is String) {
|
||||||
await removeEvent(clientId,
|
await removeEvent(clientId,
|
||||||
|
|
|
@ -2062,19 +2062,26 @@ class DbInboundGroupSession extends DataClass
|
||||||
final String pickle;
|
final String pickle;
|
||||||
final String content;
|
final String content;
|
||||||
final String indexes;
|
final String indexes;
|
||||||
|
final bool uploaded;
|
||||||
|
final String senderKey;
|
||||||
|
final String senderClaimedKeys;
|
||||||
DbInboundGroupSession(
|
DbInboundGroupSession(
|
||||||
{@required this.clientId,
|
{@required this.clientId,
|
||||||
@required this.roomId,
|
@required this.roomId,
|
||||||
@required this.sessionId,
|
@required this.sessionId,
|
||||||
@required this.pickle,
|
@required this.pickle,
|
||||||
this.content,
|
this.content,
|
||||||
this.indexes});
|
this.indexes,
|
||||||
|
this.uploaded,
|
||||||
|
this.senderKey,
|
||||||
|
this.senderClaimedKeys});
|
||||||
factory DbInboundGroupSession.fromData(
|
factory DbInboundGroupSession.fromData(
|
||||||
Map<String, dynamic> data, GeneratedDatabase db,
|
Map<String, dynamic> data, GeneratedDatabase db,
|
||||||
{String prefix}) {
|
{String prefix}) {
|
||||||
final effectivePrefix = prefix ?? '';
|
final effectivePrefix = prefix ?? '';
|
||||||
final intType = db.typeSystem.forDartType<int>();
|
final intType = db.typeSystem.forDartType<int>();
|
||||||
final stringType = db.typeSystem.forDartType<String>();
|
final stringType = db.typeSystem.forDartType<String>();
|
||||||
|
final boolType = db.typeSystem.forDartType<bool>();
|
||||||
return DbInboundGroupSession(
|
return DbInboundGroupSession(
|
||||||
clientId:
|
clientId:
|
||||||
intType.mapFromDatabaseResponse(data['${effectivePrefix}client_id']),
|
intType.mapFromDatabaseResponse(data['${effectivePrefix}client_id']),
|
||||||
|
@ -2088,6 +2095,12 @@ class DbInboundGroupSession extends DataClass
|
||||||
stringType.mapFromDatabaseResponse(data['${effectivePrefix}content']),
|
stringType.mapFromDatabaseResponse(data['${effectivePrefix}content']),
|
||||||
indexes:
|
indexes:
|
||||||
stringType.mapFromDatabaseResponse(data['${effectivePrefix}indexes']),
|
stringType.mapFromDatabaseResponse(data['${effectivePrefix}indexes']),
|
||||||
|
uploaded:
|
||||||
|
boolType.mapFromDatabaseResponse(data['${effectivePrefix}uploaded']),
|
||||||
|
senderKey: stringType
|
||||||
|
.mapFromDatabaseResponse(data['${effectivePrefix}sender_key']),
|
||||||
|
senderClaimedKeys: stringType.mapFromDatabaseResponse(
|
||||||
|
data['${effectivePrefix}sender_claimed_keys']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
|
@ -2111,6 +2124,15 @@ class DbInboundGroupSession extends DataClass
|
||||||
if (!nullToAbsent || indexes != null) {
|
if (!nullToAbsent || indexes != null) {
|
||||||
map['indexes'] = Variable<String>(indexes);
|
map['indexes'] = Variable<String>(indexes);
|
||||||
}
|
}
|
||||||
|
if (!nullToAbsent || uploaded != null) {
|
||||||
|
map['uploaded'] = Variable<bool>(uploaded);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || senderKey != null) {
|
||||||
|
map['sender_key'] = Variable<String>(senderKey);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || senderClaimedKeys != null) {
|
||||||
|
map['sender_claimed_keys'] = Variable<String>(senderClaimedKeys);
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2124,6 +2146,10 @@ class DbInboundGroupSession extends DataClass
|
||||||
pickle: serializer.fromJson<String>(json['pickle']),
|
pickle: serializer.fromJson<String>(json['pickle']),
|
||||||
content: serializer.fromJson<String>(json['content']),
|
content: serializer.fromJson<String>(json['content']),
|
||||||
indexes: serializer.fromJson<String>(json['indexes']),
|
indexes: serializer.fromJson<String>(json['indexes']),
|
||||||
|
uploaded: serializer.fromJson<bool>(json['uploaded']),
|
||||||
|
senderKey: serializer.fromJson<String>(json['sender_key']),
|
||||||
|
senderClaimedKeys:
|
||||||
|
serializer.fromJson<String>(json['sender_claimed_keys']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
|
@ -2136,6 +2162,9 @@ class DbInboundGroupSession extends DataClass
|
||||||
'pickle': serializer.toJson<String>(pickle),
|
'pickle': serializer.toJson<String>(pickle),
|
||||||
'content': serializer.toJson<String>(content),
|
'content': serializer.toJson<String>(content),
|
||||||
'indexes': serializer.toJson<String>(indexes),
|
'indexes': serializer.toJson<String>(indexes),
|
||||||
|
'uploaded': serializer.toJson<bool>(uploaded),
|
||||||
|
'sender_key': serializer.toJson<String>(senderKey),
|
||||||
|
'sender_claimed_keys': serializer.toJson<String>(senderClaimedKeys),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2145,7 +2174,10 @@ class DbInboundGroupSession extends DataClass
|
||||||
String sessionId,
|
String sessionId,
|
||||||
String pickle,
|
String pickle,
|
||||||
String content,
|
String content,
|
||||||
String indexes}) =>
|
String indexes,
|
||||||
|
bool uploaded,
|
||||||
|
String senderKey,
|
||||||
|
String senderClaimedKeys}) =>
|
||||||
DbInboundGroupSession(
|
DbInboundGroupSession(
|
||||||
clientId: clientId ?? this.clientId,
|
clientId: clientId ?? this.clientId,
|
||||||
roomId: roomId ?? this.roomId,
|
roomId: roomId ?? this.roomId,
|
||||||
|
@ -2153,6 +2185,9 @@ class DbInboundGroupSession extends DataClass
|
||||||
pickle: pickle ?? this.pickle,
|
pickle: pickle ?? this.pickle,
|
||||||
content: content ?? this.content,
|
content: content ?? this.content,
|
||||||
indexes: indexes ?? this.indexes,
|
indexes: indexes ?? this.indexes,
|
||||||
|
uploaded: uploaded ?? this.uploaded,
|
||||||
|
senderKey: senderKey ?? this.senderKey,
|
||||||
|
senderClaimedKeys: senderClaimedKeys ?? this.senderClaimedKeys,
|
||||||
);
|
);
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
@ -2162,7 +2197,10 @@ class DbInboundGroupSession extends DataClass
|
||||||
..write('sessionId: $sessionId, ')
|
..write('sessionId: $sessionId, ')
|
||||||
..write('pickle: $pickle, ')
|
..write('pickle: $pickle, ')
|
||||||
..write('content: $content, ')
|
..write('content: $content, ')
|
||||||
..write('indexes: $indexes')
|
..write('indexes: $indexes, ')
|
||||||
|
..write('uploaded: $uploaded, ')
|
||||||
|
..write('senderKey: $senderKey, ')
|
||||||
|
..write('senderClaimedKeys: $senderClaimedKeys')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
@ -2174,8 +2212,16 @@ class DbInboundGroupSession extends DataClass
|
||||||
roomId.hashCode,
|
roomId.hashCode,
|
||||||
$mrjc(
|
$mrjc(
|
||||||
sessionId.hashCode,
|
sessionId.hashCode,
|
||||||
$mrjc(pickle.hashCode,
|
$mrjc(
|
||||||
$mrjc(content.hashCode, indexes.hashCode))))));
|
pickle.hashCode,
|
||||||
|
$mrjc(
|
||||||
|
content.hashCode,
|
||||||
|
$mrjc(
|
||||||
|
indexes.hashCode,
|
||||||
|
$mrjc(
|
||||||
|
uploaded.hashCode,
|
||||||
|
$mrjc(senderKey.hashCode,
|
||||||
|
senderClaimedKeys.hashCode)))))))));
|
||||||
@override
|
@override
|
||||||
bool operator ==(dynamic other) =>
|
bool operator ==(dynamic other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
|
@ -2185,7 +2231,10 @@ class DbInboundGroupSession extends DataClass
|
||||||
other.sessionId == this.sessionId &&
|
other.sessionId == this.sessionId &&
|
||||||
other.pickle == this.pickle &&
|
other.pickle == this.pickle &&
|
||||||
other.content == this.content &&
|
other.content == this.content &&
|
||||||
other.indexes == this.indexes);
|
other.indexes == this.indexes &&
|
||||||
|
other.uploaded == this.uploaded &&
|
||||||
|
other.senderKey == this.senderKey &&
|
||||||
|
other.senderClaimedKeys == this.senderClaimedKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
class InboundGroupSessionsCompanion
|
class InboundGroupSessionsCompanion
|
||||||
|
@ -2196,6 +2245,9 @@ class InboundGroupSessionsCompanion
|
||||||
final Value<String> pickle;
|
final Value<String> pickle;
|
||||||
final Value<String> content;
|
final Value<String> content;
|
||||||
final Value<String> indexes;
|
final Value<String> indexes;
|
||||||
|
final Value<bool> uploaded;
|
||||||
|
final Value<String> senderKey;
|
||||||
|
final Value<String> senderClaimedKeys;
|
||||||
const InboundGroupSessionsCompanion({
|
const InboundGroupSessionsCompanion({
|
||||||
this.clientId = const Value.absent(),
|
this.clientId = const Value.absent(),
|
||||||
this.roomId = const Value.absent(),
|
this.roomId = const Value.absent(),
|
||||||
|
@ -2203,6 +2255,9 @@ class InboundGroupSessionsCompanion
|
||||||
this.pickle = const Value.absent(),
|
this.pickle = const Value.absent(),
|
||||||
this.content = const Value.absent(),
|
this.content = const Value.absent(),
|
||||||
this.indexes = const Value.absent(),
|
this.indexes = const Value.absent(),
|
||||||
|
this.uploaded = const Value.absent(),
|
||||||
|
this.senderKey = const Value.absent(),
|
||||||
|
this.senderClaimedKeys = const Value.absent(),
|
||||||
});
|
});
|
||||||
InboundGroupSessionsCompanion.insert({
|
InboundGroupSessionsCompanion.insert({
|
||||||
@required int clientId,
|
@required int clientId,
|
||||||
|
@ -2211,6 +2266,9 @@ class InboundGroupSessionsCompanion
|
||||||
@required String pickle,
|
@required String pickle,
|
||||||
this.content = const Value.absent(),
|
this.content = const Value.absent(),
|
||||||
this.indexes = const Value.absent(),
|
this.indexes = const Value.absent(),
|
||||||
|
this.uploaded = const Value.absent(),
|
||||||
|
this.senderKey = const Value.absent(),
|
||||||
|
this.senderClaimedKeys = const Value.absent(),
|
||||||
}) : clientId = Value(clientId),
|
}) : clientId = Value(clientId),
|
||||||
roomId = Value(roomId),
|
roomId = Value(roomId),
|
||||||
sessionId = Value(sessionId),
|
sessionId = Value(sessionId),
|
||||||
|
@ -2222,6 +2280,9 @@ class InboundGroupSessionsCompanion
|
||||||
Expression<String> pickle,
|
Expression<String> pickle,
|
||||||
Expression<String> content,
|
Expression<String> content,
|
||||||
Expression<String> indexes,
|
Expression<String> indexes,
|
||||||
|
Expression<bool> uploaded,
|
||||||
|
Expression<String> senderKey,
|
||||||
|
Expression<String> senderClaimedKeys,
|
||||||
}) {
|
}) {
|
||||||
return RawValuesInsertable({
|
return RawValuesInsertable({
|
||||||
if (clientId != null) 'client_id': clientId,
|
if (clientId != null) 'client_id': clientId,
|
||||||
|
@ -2230,6 +2291,9 @@ class InboundGroupSessionsCompanion
|
||||||
if (pickle != null) 'pickle': pickle,
|
if (pickle != null) 'pickle': pickle,
|
||||||
if (content != null) 'content': content,
|
if (content != null) 'content': content,
|
||||||
if (indexes != null) 'indexes': indexes,
|
if (indexes != null) 'indexes': indexes,
|
||||||
|
if (uploaded != null) 'uploaded': uploaded,
|
||||||
|
if (senderKey != null) 'sender_key': senderKey,
|
||||||
|
if (senderClaimedKeys != null) 'sender_claimed_keys': senderClaimedKeys,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2239,7 +2303,10 @@ class InboundGroupSessionsCompanion
|
||||||
Value<String> sessionId,
|
Value<String> sessionId,
|
||||||
Value<String> pickle,
|
Value<String> pickle,
|
||||||
Value<String> content,
|
Value<String> content,
|
||||||
Value<String> indexes}) {
|
Value<String> indexes,
|
||||||
|
Value<bool> uploaded,
|
||||||
|
Value<String> senderKey,
|
||||||
|
Value<String> senderClaimedKeys}) {
|
||||||
return InboundGroupSessionsCompanion(
|
return InboundGroupSessionsCompanion(
|
||||||
clientId: clientId ?? this.clientId,
|
clientId: clientId ?? this.clientId,
|
||||||
roomId: roomId ?? this.roomId,
|
roomId: roomId ?? this.roomId,
|
||||||
|
@ -2247,6 +2314,9 @@ class InboundGroupSessionsCompanion
|
||||||
pickle: pickle ?? this.pickle,
|
pickle: pickle ?? this.pickle,
|
||||||
content: content ?? this.content,
|
content: content ?? this.content,
|
||||||
indexes: indexes ?? this.indexes,
|
indexes: indexes ?? this.indexes,
|
||||||
|
uploaded: uploaded ?? this.uploaded,
|
||||||
|
senderKey: senderKey ?? this.senderKey,
|
||||||
|
senderClaimedKeys: senderClaimedKeys ?? this.senderClaimedKeys,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2271,6 +2341,15 @@ class InboundGroupSessionsCompanion
|
||||||
if (indexes.present) {
|
if (indexes.present) {
|
||||||
map['indexes'] = Variable<String>(indexes.value);
|
map['indexes'] = Variable<String>(indexes.value);
|
||||||
}
|
}
|
||||||
|
if (uploaded.present) {
|
||||||
|
map['uploaded'] = Variable<bool>(uploaded.value);
|
||||||
|
}
|
||||||
|
if (senderKey.present) {
|
||||||
|
map['sender_key'] = Variable<String>(senderKey.value);
|
||||||
|
}
|
||||||
|
if (senderClaimedKeys.present) {
|
||||||
|
map['sender_claimed_keys'] = Variable<String>(senderClaimedKeys.value);
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2328,9 +2407,45 @@ class InboundGroupSessions extends Table
|
||||||
$customConstraints: '');
|
$customConstraints: '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final VerificationMeta _uploadedMeta = const VerificationMeta('uploaded');
|
||||||
|
GeneratedBoolColumn _uploaded;
|
||||||
|
GeneratedBoolColumn get uploaded => _uploaded ??= _constructUploaded();
|
||||||
|
GeneratedBoolColumn _constructUploaded() {
|
||||||
|
return GeneratedBoolColumn('uploaded', $tableName, true,
|
||||||
|
$customConstraints: 'DEFAULT false',
|
||||||
|
defaultValue: const CustomExpression<bool>('false'));
|
||||||
|
}
|
||||||
|
|
||||||
|
final VerificationMeta _senderKeyMeta = const VerificationMeta('senderKey');
|
||||||
|
GeneratedTextColumn _senderKey;
|
||||||
|
GeneratedTextColumn get senderKey => _senderKey ??= _constructSenderKey();
|
||||||
|
GeneratedTextColumn _constructSenderKey() {
|
||||||
|
return GeneratedTextColumn('sender_key', $tableName, true,
|
||||||
|
$customConstraints: '');
|
||||||
|
}
|
||||||
|
|
||||||
|
final VerificationMeta _senderClaimedKeysMeta =
|
||||||
|
const VerificationMeta('senderClaimedKeys');
|
||||||
|
GeneratedTextColumn _senderClaimedKeys;
|
||||||
|
GeneratedTextColumn get senderClaimedKeys =>
|
||||||
|
_senderClaimedKeys ??= _constructSenderClaimedKeys();
|
||||||
|
GeneratedTextColumn _constructSenderClaimedKeys() {
|
||||||
|
return GeneratedTextColumn('sender_claimed_keys', $tableName, true,
|
||||||
|
$customConstraints: '');
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<GeneratedColumn> get $columns =>
|
List<GeneratedColumn> get $columns => [
|
||||||
[clientId, roomId, sessionId, pickle, content, indexes];
|
clientId,
|
||||||
|
roomId,
|
||||||
|
sessionId,
|
||||||
|
pickle,
|
||||||
|
content,
|
||||||
|
indexes,
|
||||||
|
uploaded,
|
||||||
|
senderKey,
|
||||||
|
senderClaimedKeys
|
||||||
|
];
|
||||||
@override
|
@override
|
||||||
InboundGroupSessions get asDslTable => this;
|
InboundGroupSessions get asDslTable => this;
|
||||||
@override
|
@override
|
||||||
|
@ -2375,6 +2490,20 @@ class InboundGroupSessions extends Table
|
||||||
context.handle(_indexesMeta,
|
context.handle(_indexesMeta,
|
||||||
indexes.isAcceptableOrUnknown(data['indexes'], _indexesMeta));
|
indexes.isAcceptableOrUnknown(data['indexes'], _indexesMeta));
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('uploaded')) {
|
||||||
|
context.handle(_uploadedMeta,
|
||||||
|
uploaded.isAcceptableOrUnknown(data['uploaded'], _uploadedMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('sender_key')) {
|
||||||
|
context.handle(_senderKeyMeta,
|
||||||
|
senderKey.isAcceptableOrUnknown(data['sender_key'], _senderKeyMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('sender_claimed_keys')) {
|
||||||
|
context.handle(
|
||||||
|
_senderClaimedKeysMeta,
|
||||||
|
senderClaimedKeys.isAcceptableOrUnknown(
|
||||||
|
data['sender_claimed_keys'], _senderClaimedKeysMeta));
|
||||||
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5669,6 +5798,9 @@ abstract class _$Database extends GeneratedDatabase {
|
||||||
pickle: row.readString('pickle'),
|
pickle: row.readString('pickle'),
|
||||||
content: row.readString('content'),
|
content: row.readString('content'),
|
||||||
indexes: row.readString('indexes'),
|
indexes: row.readString('indexes'),
|
||||||
|
uploaded: row.readBool('uploaded'),
|
||||||
|
senderKey: row.readString('sender_key'),
|
||||||
|
senderClaimedKeys: row.readString('sender_claimed_keys'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5701,17 +5833,26 @@ abstract class _$Database extends GeneratedDatabase {
|
||||||
readsFrom: {inboundGroupSessions}).map(_rowToDbInboundGroupSession);
|
readsFrom: {inboundGroupSessions}).map(_rowToDbInboundGroupSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> storeInboundGroupSession(int client_id, String room_id,
|
Future<int> storeInboundGroupSession(
|
||||||
String session_id, String pickle, String content, String indexes) {
|
int client_id,
|
||||||
|
String room_id,
|
||||||
|
String session_id,
|
||||||
|
String pickle,
|
||||||
|
String content,
|
||||||
|
String indexes,
|
||||||
|
String sender_key,
|
||||||
|
String sender_claimed_keys) {
|
||||||
return customInsert(
|
return customInsert(
|
||||||
'INSERT OR REPLACE INTO inbound_group_sessions (client_id, room_id, session_id, pickle, content, indexes) VALUES (:client_id, :room_id, :session_id, :pickle, :content, :indexes)',
|
'INSERT OR REPLACE INTO inbound_group_sessions (client_id, room_id, session_id, pickle, content, indexes, sender_key, sender_claimed_keys) VALUES (:client_id, :room_id, :session_id, :pickle, :content, :indexes, :sender_key, :sender_claimed_keys)',
|
||||||
variables: [
|
variables: [
|
||||||
Variable.withInt(client_id),
|
Variable.withInt(client_id),
|
||||||
Variable.withString(room_id),
|
Variable.withString(room_id),
|
||||||
Variable.withString(session_id),
|
Variable.withString(session_id),
|
||||||
Variable.withString(pickle),
|
Variable.withString(pickle),
|
||||||
Variable.withString(content),
|
Variable.withString(content),
|
||||||
Variable.withString(indexes)
|
Variable.withString(indexes),
|
||||||
|
Variable.withString(sender_key),
|
||||||
|
Variable.withString(sender_claimed_keys)
|
||||||
],
|
],
|
||||||
updates: {inboundGroupSessions},
|
updates: {inboundGroupSessions},
|
||||||
);
|
);
|
||||||
|
@ -5732,6 +5873,27 @@ abstract class _$Database extends GeneratedDatabase {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Selectable<DbInboundGroupSession> getInboundGroupSessionsToUpload() {
|
||||||
|
return customSelect(
|
||||||
|
'SELECT * FROM inbound_group_sessions WHERE uploaded = false LIMIT 500',
|
||||||
|
variables: [],
|
||||||
|
readsFrom: {inboundGroupSessions}).map(_rowToDbInboundGroupSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> markInboundGroupSessionAsUploaded(
|
||||||
|
int client_id, String room_id, String session_id) {
|
||||||
|
return customUpdate(
|
||||||
|
'UPDATE inbound_group_sessions SET uploaded = true WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id',
|
||||||
|
variables: [
|
||||||
|
Variable.withInt(client_id),
|
||||||
|
Variable.withString(room_id),
|
||||||
|
Variable.withString(session_id)
|
||||||
|
],
|
||||||
|
updates: {inboundGroupSessions},
|
||||||
|
updateKind: UpdateKind.update,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> storeUserDeviceKeysInfo(
|
Future<int> storeUserDeviceKeysInfo(
|
||||||
int client_id, String user_id, bool outdated) {
|
int client_id, String user_id, bool outdated) {
|
||||||
return customInsert(
|
return customInsert(
|
||||||
|
@ -6033,6 +6195,21 @@ abstract class _$Database extends GeneratedDatabase {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> updateEventStatusOnly(
|
||||||
|
int status, int client_id, String event_id, String room_id) {
|
||||||
|
return customUpdate(
|
||||||
|
'UPDATE events SET status = :status WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id',
|
||||||
|
variables: [
|
||||||
|
Variable.withInt(status),
|
||||||
|
Variable.withInt(client_id),
|
||||||
|
Variable.withString(event_id),
|
||||||
|
Variable.withString(room_id)
|
||||||
|
],
|
||||||
|
updates: {events},
|
||||||
|
updateKind: UpdateKind.update,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
DbRoomState _rowToDbRoomState(QueryRow row) {
|
DbRoomState _rowToDbRoomState(QueryRow row) {
|
||||||
return DbRoomState(
|
return DbRoomState(
|
||||||
clientId: row.readInt('client_id'),
|
clientId: row.readInt('client_id'),
|
||||||
|
@ -6302,9 +6479,9 @@ abstract class _$Database extends GeneratedDatabase {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> removeRoomEvents(int client_id, String room_id) {
|
Future<int> removeSuccessfulRoomEvents(int client_id, String room_id) {
|
||||||
return customUpdate(
|
return customUpdate(
|
||||||
'DELETE FROM events WHERE client_id = :client_id AND room_id = :room_id',
|
'DELETE FROM events WHERE client_id = :client_id AND room_id = :room_id AND status <> -1 AND status <> 0',
|
||||||
variables: [Variable.withInt(client_id), Variable.withString(room_id)],
|
variables: [Variable.withInt(client_id), Variable.withString(room_id)],
|
||||||
updates: {events},
|
updates: {events},
|
||||||
updateKind: UpdateKind.delete,
|
updateKind: UpdateKind.delete,
|
||||||
|
@ -6337,6 +6514,15 @@ abstract class _$Database extends GeneratedDatabase {
|
||||||
readsFrom: {files}).map(_rowToDbFile);
|
readsFrom: {files}).map(_rowToDbFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> markPendingEventsAsError(int client_id) {
|
||||||
|
return customUpdate(
|
||||||
|
'UPDATE events SET status = -1 WHERE client_id = :client_id AND status = 0',
|
||||||
|
variables: [Variable.withInt(client_id)],
|
||||||
|
updates: {events},
|
||||||
|
updateKind: UpdateKind.update,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Iterable<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>();
|
Iterable<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>();
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -71,6 +71,9 @@ CREATE TABLE inbound_group_sessions (
|
||||||
pickle TEXT NOT NULL,
|
pickle TEXT NOT NULL,
|
||||||
content TEXT,
|
content TEXT,
|
||||||
indexes TEXT,
|
indexes TEXT,
|
||||||
|
uploaded BOOLEAN DEFAULT false,
|
||||||
|
sender_key TEXT,
|
||||||
|
sender_claimed_keys TEXT,
|
||||||
UNIQUE(client_id, room_id, session_id)
|
UNIQUE(client_id, room_id, session_id)
|
||||||
) AS DbInboundGroupSession;
|
) AS DbInboundGroupSession;
|
||||||
CREATE INDEX inbound_group_sessions_index ON inbound_group_sessions(client_id);
|
CREATE INDEX inbound_group_sessions_index ON inbound_group_sessions(client_id);
|
||||||
|
@ -186,8 +189,10 @@ removeOutboundGroupSession: DELETE FROM outbound_group_sessions WHERE client_id
|
||||||
dbGetInboundGroupSessionKey: SELECT * FROM inbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id;
|
dbGetInboundGroupSessionKey: SELECT * FROM inbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id;
|
||||||
dbGetInboundGroupSessionKeys: SELECT * FROM inbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id;
|
dbGetInboundGroupSessionKeys: SELECT * FROM inbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id;
|
||||||
getAllInboundGroupSessions: SELECT * FROM inbound_group_sessions WHERE client_id = :client_id;
|
getAllInboundGroupSessions: SELECT * FROM inbound_group_sessions WHERE client_id = :client_id;
|
||||||
storeInboundGroupSession: INSERT OR REPLACE INTO inbound_group_sessions (client_id, room_id, session_id, pickle, content, indexes) VALUES (:client_id, :room_id, :session_id, :pickle, :content, :indexes);
|
storeInboundGroupSession: INSERT OR REPLACE INTO inbound_group_sessions (client_id, room_id, session_id, pickle, content, indexes, sender_key, sender_claimed_keys) VALUES (:client_id, :room_id, :session_id, :pickle, :content, :indexes, :sender_key, :sender_claimed_keys);
|
||||||
updateInboundGroupSessionIndexes: UPDATE inbound_group_sessions SET indexes = :indexes WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id;
|
updateInboundGroupSessionIndexes: UPDATE inbound_group_sessions SET indexes = :indexes WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id;
|
||||||
|
getInboundGroupSessionsToUpload: SELECT * FROM inbound_group_sessions WHERE uploaded = false LIMIT 500;
|
||||||
|
markInboundGroupSessionAsUploaded: UPDATE inbound_group_sessions SET uploaded = true WHERE client_id = :client_id AND room_id = :room_id AND session_id = :session_id;
|
||||||
storeUserDeviceKeysInfo: INSERT OR REPLACE INTO user_device_keys (client_id, user_id, outdated) VALUES (:client_id, :user_id, :outdated);
|
storeUserDeviceKeysInfo: INSERT OR REPLACE INTO user_device_keys (client_id, user_id, outdated) VALUES (:client_id, :user_id, :outdated);
|
||||||
setVerifiedUserDeviceKey: UPDATE user_device_keys_key SET verified = :verified WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;
|
setVerifiedUserDeviceKey: UPDATE user_device_keys_key SET verified = :verified WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;
|
||||||
setBlockedUserDeviceKey: UPDATE user_device_keys_key SET blocked = :blocked WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;
|
setBlockedUserDeviceKey: UPDATE user_device_keys_key SET blocked = :blocked WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;
|
||||||
|
@ -208,6 +213,7 @@ getAllAccountData: SELECT * FROM account_data WHERE client_id = :client_id;
|
||||||
storeAccountData: INSERT OR REPLACE INTO account_data (client_id, type, content) VALUES (:client_id, :type, :content);
|
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;
|
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;
|
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;
|
getImportantRoomStates: SELECT * FROM room_states WHERE client_id = :client_id AND type IN :events;
|
||||||
getAllRoomStates: SELECT * FROM room_states WHERE client_id = :client_id;
|
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;
|
getUnimportantRoomStatesForRoom: SELECT * FROM room_states WHERE client_id = :client_id AND room_id = :room_id AND type NOT IN :events;
|
||||||
|
@ -224,6 +230,7 @@ getRoom: SELECT * FROM rooms WHERE client_id = :client_id AND room_id = :room_id
|
||||||
getEvent: SELECT * FROM events WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id;
|
getEvent: SELECT * FROM events WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id;
|
||||||
removeEvent: DELETE FROM events WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id;
|
removeEvent: DELETE FROM events WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id;
|
||||||
removeRoom: DELETE FROM rooms WHERE client_id = :client_id AND room_id = :room_id;
|
removeRoom: DELETE FROM rooms WHERE client_id = :client_id AND room_id = :room_id;
|
||||||
removeRoomEvents: DELETE FROM events WHERE client_id = :client_id AND room_id = :room_id;
|
removeSuccessfulRoomEvents: DELETE FROM events WHERE client_id = :client_id AND room_id = :room_id AND status <> -1 AND status <> 0;
|
||||||
storeFile: INSERT OR REPLACE INTO files (mxc_uri, bytes, saved_at) VALUES (:mxc_uri, :bytes, :time);
|
storeFile: INSERT OR REPLACE INTO files (mxc_uri, bytes, saved_at) VALUES (:mxc_uri, :bytes, :time);
|
||||||
dbGetFile: SELECT * FROM files WHERE mxc_uri = :mxc_uri;
|
dbGetFile: SELECT * FROM files WHERE mxc_uri = :mxc_uri;
|
||||||
|
markPendingEventsAsError: UPDATE events SET status = -1 WHERE client_id = :client_id AND status = 0;
|
||||||
|
|
|
@ -18,15 +18,23 @@
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
|
||||||
import 'package:famedlysdk/encryption.dart';
|
|
||||||
import 'package:famedlysdk/src/utils/receipt.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
||||||
|
|
||||||
|
import '../encryption.dart';
|
||||||
|
import '../famedlysdk.dart';
|
||||||
import '../matrix_api.dart';
|
import '../matrix_api.dart';
|
||||||
import './room.dart';
|
import 'database/database.dart' show DbRoomState, DbEvent;
|
||||||
|
import 'room.dart';
|
||||||
import 'utils/matrix_localizations.dart';
|
import 'utils/matrix_localizations.dart';
|
||||||
import './database/database.dart' show DbRoomState, DbEvent;
|
import 'utils/receipt.dart';
|
||||||
|
|
||||||
|
abstract class RelationshipTypes {
|
||||||
|
static const String Reply = 'm.in_reply_to';
|
||||||
|
static const String Edit = 'm.replace';
|
||||||
|
static const String Reaction = 'm.annotation';
|
||||||
|
}
|
||||||
|
|
||||||
/// All data exchanged over Matrix is expressed as an "event". Typically each client action (e.g. sending a message) correlates with exactly one event.
|
/// 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 {
|
class Event extends MatrixEvent {
|
||||||
|
@ -90,12 +98,20 @@ class Event extends MatrixEvent {
|
||||||
this.senderId = senderId;
|
this.senderId = senderId;
|
||||||
this.unsigned = unsigned;
|
this.unsigned = unsigned;
|
||||||
// synapse unfortunatley isn't following the spec and tosses the prev_content
|
// synapse unfortunatley isn't following the spec and tosses the prev_content
|
||||||
// into the unsigned block
|
// into the unsigned block.
|
||||||
this.prevContent = prevContent != null && prevContent.isNotEmpty
|
// Currently we are facing a very strange bug in web which is impossible to debug.
|
||||||
? prevContent
|
// It may be because of this line so we put this in try-catch until we can fix it.
|
||||||
: (unsigned != null && unsigned['prev_content'] is Map
|
try {
|
||||||
? unsigned['prev_content']
|
this.prevContent = (prevContent != null && prevContent.isNotEmpty)
|
||||||
: null);
|
? prevContent
|
||||||
|
: (unsigned != null &&
|
||||||
|
unsigned.containsKey('prev_content') &&
|
||||||
|
unsigned['prev_content'] is Map)
|
||||||
|
? unsigned['prev_content']
|
||||||
|
: null;
|
||||||
|
} catch (_) {
|
||||||
|
// A strange bug in dart web makes this crash
|
||||||
|
}
|
||||||
this.stateKey = stateKey;
|
this.stateKey = stateKey;
|
||||||
this.originServerTs = originServerTs;
|
this.originServerTs = originServerTs;
|
||||||
}
|
}
|
||||||
|
@ -140,7 +156,9 @@ class Event extends MatrixEvent {
|
||||||
final unsigned = Event.getMapFromPayload(jsonPayload['unsigned']);
|
final unsigned = Event.getMapFromPayload(jsonPayload['unsigned']);
|
||||||
final prevContent = Event.getMapFromPayload(jsonPayload['prev_content']);
|
final prevContent = Event.getMapFromPayload(jsonPayload['prev_content']);
|
||||||
return Event(
|
return Event(
|
||||||
status: jsonPayload['status'] ?? defaultStatus,
|
status: jsonPayload['status'] ??
|
||||||
|
unsigned[MessageSendingStatusKey] ??
|
||||||
|
defaultStatus,
|
||||||
stateKey: jsonPayload['state_key'],
|
stateKey: jsonPayload['state_key'],
|
||||||
prevContent: prevContent,
|
prevContent: prevContent,
|
||||||
content: content,
|
content: content,
|
||||||
|
@ -212,9 +230,8 @@ class Event extends MatrixEvent {
|
||||||
unsigned: unsigned,
|
unsigned: unsigned,
|
||||||
room: room);
|
room: room);
|
||||||
|
|
||||||
String get messageType => (content['m.relates_to'] is Map &&
|
String get messageType => type == EventTypes.Sticker
|
||||||
content['m.relates_to']['m.in_reply_to'] != null)
|
? MessageTypes.Sticker
|
||||||
? MessageTypes.Reply
|
|
||||||
: content['msgtype'] ?? MessageTypes.Text;
|
: content['msgtype'] ?? MessageTypes.Text;
|
||||||
|
|
||||||
void setRedactionEvent(Event redactedBecause) {
|
void setRedactionEvent(Event redactedBecause) {
|
||||||
|
@ -312,12 +329,13 @@ class Event extends MatrixEvent {
|
||||||
/// Try to send this event again. Only works with events of status -1.
|
/// Try to send this event again. Only works with events of status -1.
|
||||||
Future<String> sendAgain({String txid}) async {
|
Future<String> sendAgain({String txid}) async {
|
||||||
if (status != -1) return null;
|
if (status != -1) return null;
|
||||||
await remove();
|
// we do not remove the event here. It will automatically be updated
|
||||||
final eventID = await room.sendEvent(
|
// in the `sendEvent` method to transition -1 -> 0 -> 1 -> 2
|
||||||
|
final newEventId = await room.sendEvent(
|
||||||
content,
|
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.
|
/// Whether the client is allowed to redact this event.
|
||||||
|
@ -327,20 +345,10 @@ class Event extends MatrixEvent {
|
||||||
Future<dynamic> redact({String reason, String txid}) =>
|
Future<dynamic> redact({String reason, String txid}) =>
|
||||||
room.redactEvent(eventId, reason: reason, txid: 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.
|
/// Searches for the reply event in the given timeline.
|
||||||
Future<Event> getReplyEvent(Timeline timeline) async {
|
Future<Event> getReplyEvent(Timeline timeline) async {
|
||||||
if (!isReply) return null;
|
if (relationshipType != RelationshipTypes.Reply) return null;
|
||||||
final String replyEventId =
|
return await timeline.getEventById(relationshipEventId);
|
||||||
content['m.relates_to']['m.in_reply_to']['event_id'];
|
|
||||||
return await timeline.getEventById(replyEventId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If this event is encrypted and the decryption was not successful because
|
/// If this event is encrypted and the decryption was not successful because
|
||||||
|
@ -367,7 +375,8 @@ class Event extends MatrixEvent {
|
||||||
/// contain an attachment, this throws an error. Set [getThumbnail] to
|
/// contain an attachment, this throws an error. Set [getThumbnail] to
|
||||||
/// true to download the thumbnail instead.
|
/// true to download the thumbnail instead.
|
||||||
Future<MatrixFile> downloadAndDecryptAttachment(
|
Future<MatrixFile> downloadAndDecryptAttachment(
|
||||||
{bool getThumbnail = false}) async {
|
{bool getThumbnail = false,
|
||||||
|
Future<Uint8List> Function(String) downloadCallback}) async {
|
||||||
if (![EventTypes.Message, EventTypes.Sticker].contains(type)) {
|
if (![EventTypes.Message, EventTypes.Sticker].contains(type)) {
|
||||||
throw ("This event has the type '$type' and so it can't contain an attachment.");
|
throw ("This event has the type '$type' and so it can't contain an attachment.");
|
||||||
}
|
}
|
||||||
|
@ -397,7 +406,7 @@ class Event extends MatrixEvent {
|
||||||
// Is this file storeable?
|
// Is this file storeable?
|
||||||
final infoMap =
|
final infoMap =
|
||||||
getThumbnail ? content['info']['thumbnail_info'] : content['info'];
|
getThumbnail ? content['info']['thumbnail_info'] : content['info'];
|
||||||
final storeable = room.client.database != null &&
|
var storeable = room.client.database != null &&
|
||||||
infoMap is Map<String, dynamic> &&
|
infoMap is Map<String, dynamic> &&
|
||||||
infoMap['size'] is int &&
|
infoMap['size'] is int &&
|
||||||
infoMap['size'] <= room.client.database.maxFileSize;
|
infoMap['size'] <= room.client.database.maxFileSize;
|
||||||
|
@ -408,8 +417,13 @@ class Event extends MatrixEvent {
|
||||||
|
|
||||||
// Download the file
|
// Download the file
|
||||||
if (uint8list == null) {
|
if (uint8list == null) {
|
||||||
|
downloadCallback ??= (String url) async {
|
||||||
|
return (await http.get(url)).bodyBytes;
|
||||||
|
};
|
||||||
uint8list =
|
uint8list =
|
||||||
(await http.get(mxContent.getDownloadLink(room.client))).bodyBytes;
|
await downloadCallback(mxContent.getDownloadLink(room.client));
|
||||||
|
storeable = storeable &&
|
||||||
|
uint8list.lengthInBytes < room.client.database.maxFileSize;
|
||||||
if (storeable) {
|
if (storeable) {
|
||||||
await room.client.database
|
await room.client.database
|
||||||
.storeFile(mxContent.toString(), uint8list, DateTime.now());
|
.storeFile(mxContent.toString(), uint8list, DateTime.now());
|
||||||
|
@ -480,9 +494,8 @@ class Event extends MatrixEvent {
|
||||||
final targetName = stateKeyUser.calcDisplayname();
|
final targetName = stateKeyUser.calcDisplayname();
|
||||||
// Has the membership changed?
|
// Has the membership changed?
|
||||||
final newMembership = content['membership'] ?? '';
|
final newMembership = content['membership'] ?? '';
|
||||||
final oldMembership = unsigned['prev_content'] is Map<String, dynamic>
|
final oldMembership =
|
||||||
? unsigned['prev_content']['membership'] ?? ''
|
prevContent != null ? prevContent['membership'] ?? '' : '';
|
||||||
: '';
|
|
||||||
if (newMembership != oldMembership) {
|
if (newMembership != oldMembership) {
|
||||||
if (oldMembership == 'invite' && newMembership == 'join') {
|
if (oldMembership == 'invite' && newMembership == 'join') {
|
||||||
text = i18n.acceptedTheInvitation(targetName);
|
text = i18n.acceptedTheInvitation(targetName);
|
||||||
|
@ -517,15 +530,12 @@ class Event extends MatrixEvent {
|
||||||
}
|
}
|
||||||
} else if (newMembership == 'join') {
|
} else if (newMembership == 'join') {
|
||||||
final newAvatar = content['avatar_url'] ?? '';
|
final newAvatar = content['avatar_url'] ?? '';
|
||||||
final oldAvatar = unsigned['prev_content'] is Map<String, dynamic>
|
final oldAvatar =
|
||||||
? unsigned['prev_content']['avatar_url'] ?? ''
|
prevContent != null ? prevContent['avatar_url'] ?? '' : '';
|
||||||
: '';
|
|
||||||
|
|
||||||
final newDisplayname = content['displayname'] ?? '';
|
final newDisplayname = content['displayname'] ?? '';
|
||||||
final oldDisplayname =
|
final oldDisplayname =
|
||||||
unsigned['prev_content'] is Map<String, dynamic>
|
prevContent != null ? prevContent['displayname'] ?? '' : '';
|
||||||
? unsigned['prev_content']['displayname'] ?? ''
|
|
||||||
: '';
|
|
||||||
|
|
||||||
// Has the user avatar changed?
|
// Has the user avatar changed?
|
||||||
if (newAvatar != oldAvatar) {
|
if (newAvatar != oldAvatar) {
|
||||||
|
@ -583,6 +593,18 @@ class Event extends MatrixEvent {
|
||||||
localizedBody += '. ' + i18n.needPantalaimonWarning;
|
localizedBody += '. ' + i18n.needPantalaimonWarning;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case EventTypes.CallAnswer:
|
||||||
|
localizedBody = i18n.answeredTheCall(senderName);
|
||||||
|
break;
|
||||||
|
case EventTypes.CallHangup:
|
||||||
|
localizedBody = i18n.endedTheCall(senderName);
|
||||||
|
break;
|
||||||
|
case EventTypes.CallInvite:
|
||||||
|
localizedBody = i18n.startedACall(senderName);
|
||||||
|
break;
|
||||||
|
case EventTypes.CallCandidates:
|
||||||
|
localizedBody = i18n.sentCallInformations(senderName);
|
||||||
|
break;
|
||||||
case EventTypes.Encrypted:
|
case EventTypes.Encrypted:
|
||||||
case EventTypes.Message:
|
case EventTypes.Message:
|
||||||
switch (messageType) {
|
switch (messageType) {
|
||||||
|
@ -631,7 +653,6 @@ class Event extends MatrixEvent {
|
||||||
case MessageTypes.Text:
|
case MessageTypes.Text:
|
||||||
case MessageTypes.Notice:
|
case MessageTypes.Notice:
|
||||||
case MessageTypes.None:
|
case MessageTypes.None:
|
||||||
case MessageTypes.Reply:
|
|
||||||
localizedBody = body;
|
localizedBody = body;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -660,9 +681,130 @@ class Event extends MatrixEvent {
|
||||||
|
|
||||||
static const Set<String> textOnlyMessageTypes = {
|
static const Set<String> textOnlyMessageTypes = {
|
||||||
MessageTypes.Text,
|
MessageTypes.Text,
|
||||||
MessageTypes.Reply,
|
|
||||||
MessageTypes.Notice,
|
MessageTypes.Notice,
|
||||||
MessageTypes.Emote,
|
MessageTypes.Emote,
|
||||||
MessageTypes.None,
|
MessageTypes.None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// returns if this event matches the passed event or transaction id
|
||||||
|
bool matchesEventOrTransactionId(String search) {
|
||||||
|
if (search == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (eventId == search) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return unsigned != null && unsigned['transaction_id'] == search;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the relationship type of an event. `null` if there is none
|
||||||
|
String get relationshipType {
|
||||||
|
if (content == null || !(content['m.relates_to'] is Map)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (content['m.relates_to'].containsKey('rel_type')) {
|
||||||
|
return content['m.relates_to']['rel_type'];
|
||||||
|
}
|
||||||
|
if (content['m.relates_to'].containsKey('m.in_reply_to')) {
|
||||||
|
return RelationshipTypes.Reply;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the event ID that this relationship will reference. `null` if there is none
|
||||||
|
String get relationshipEventId {
|
||||||
|
if (content == null || !(content['m.relates_to'] is Map)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (content['m.relates_to'].containsKey('event_id')) {
|
||||||
|
return content['m.relates_to']['event_id'];
|
||||||
|
}
|
||||||
|
if (content['m.relates_to']['m.in_reply_to'] is Map &&
|
||||||
|
content['m.relates_to']['m.in_reply_to'].containsKey('event_id')) {
|
||||||
|
return content['m.relates_to']['m.in_reply_to']['event_id'];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get wether this event has aggregated events from a certain [type]
|
||||||
|
/// To be able to do that you need to pass a [timeline]
|
||||||
|
bool hasAggregatedEvents(Timeline timeline, String type) =>
|
||||||
|
timeline.aggregatedEvents.containsKey(eventId) &&
|
||||||
|
timeline.aggregatedEvents[eventId].containsKey(type);
|
||||||
|
|
||||||
|
/// Get all the aggregated event objects for a given [type]. To be able to do this
|
||||||
|
/// you have to pass a [timeline]
|
||||||
|
Set<Event> aggregatedEvents(Timeline timeline, String type) =>
|
||||||
|
hasAggregatedEvents(timeline, type)
|
||||||
|
? timeline.aggregatedEvents[eventId][type]
|
||||||
|
: <Event>{};
|
||||||
|
|
||||||
|
/// Fetches the event to be rendered, taking into account all the edits and the like.
|
||||||
|
/// It needs a [timeline] for that.
|
||||||
|
Event getDisplayEvent(Timeline timeline) {
|
||||||
|
if (hasAggregatedEvents(timeline, RelationshipTypes.Edit)) {
|
||||||
|
// alright, we have an edit
|
||||||
|
final allEditEvents = aggregatedEvents(timeline, RelationshipTypes.Edit)
|
||||||
|
// we only allow edits made by the original author themself
|
||||||
|
.where((e) => e.senderId == senderId && e.type == EventTypes.Message)
|
||||||
|
.toList();
|
||||||
|
// we need to check again if it isn't empty, as we potentially removed all
|
||||||
|
// aggregated edits
|
||||||
|
if (allEditEvents.isNotEmpty) {
|
||||||
|
allEditEvents.sort((a, b) => a.sortOrder - b.sortOrder > 0 ? 1 : -1);
|
||||||
|
var rawEvent = allEditEvents.last.toJson();
|
||||||
|
// update the content of the new event to render
|
||||||
|
if (rawEvent['content']['m.new_content'] is Map) {
|
||||||
|
rawEvent['content'] = rawEvent['content']['m.new_content'];
|
||||||
|
}
|
||||||
|
return Event.fromJson(rawEvent, room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns if a message is a rich message
|
||||||
|
bool get isRichMessage =>
|
||||||
|
content['format'] == 'org.matrix.custom.html' &&
|
||||||
|
content['formatted_body'] is String;
|
||||||
|
|
||||||
|
// regexes to fetch the number of emotes, including emoji, and if the message consists of only those
|
||||||
|
// to match an emoji we can use the following regex:
|
||||||
|
// (?:\x{00a9}|\x{00ae}|[\x{2000}-\x{3300}]|\x{d83c}[\x{d000}-\x{dfff}]|\x{d83d}[\x{d000}-\x{dfff}]|\x{d83e}[\x{d000}-\x{dfff}])[\x{fe00}-\x{fe0f}]?
|
||||||
|
// we need to replace \x{0000} with \u0000, the comment is left in the other format to be able to paste into regex101.com
|
||||||
|
// to see if there is a custom emote, we use the following regex: <img[^>]+data-mx-(?:emote|emoticon)(?==|>|\s)[^>]*>
|
||||||
|
// now we combind the two to have four regexes:
|
||||||
|
// 1. are there only emoji, or whitespace
|
||||||
|
// 2. are there only emoji, emotes, or whitespace
|
||||||
|
// 3. count number of emoji
|
||||||
|
// 4- count number of emoji or emotes
|
||||||
|
static final RegExp _onlyEmojiRegex = RegExp(
|
||||||
|
r'^((?:\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])[\ufe00-\ufe0f]?|\s)*$',
|
||||||
|
caseSensitive: false,
|
||||||
|
multiLine: false);
|
||||||
|
static final RegExp _onlyEmojiEmoteRegex = RegExp(
|
||||||
|
r'^((?:\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])[\ufe00-\ufe0f]?|<img[^>]+data-mx-(?:emote|emoticon)(?==|>|\s)[^>]*>|\s)*$',
|
||||||
|
caseSensitive: false,
|
||||||
|
multiLine: false);
|
||||||
|
static final RegExp _countEmojiRegex = RegExp(
|
||||||
|
r'((?:\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])[\ufe00-\ufe0f]?)',
|
||||||
|
caseSensitive: false,
|
||||||
|
multiLine: false);
|
||||||
|
static final RegExp _countEmojiEmoteRegex = RegExp(
|
||||||
|
r'((?:\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])[\ufe00-\ufe0f]?|<img[^>]+data-mx-(?:emote|emoticon)(?==|>|\s)[^>]*>)',
|
||||||
|
caseSensitive: false,
|
||||||
|
multiLine: false);
|
||||||
|
|
||||||
|
/// Returns if a given event only has emotes, emojis or whitespace as content.
|
||||||
|
/// This is useful to determine if stand-alone emotes should be displayed bigger.
|
||||||
|
bool get onlyEmotes => isRichMessage
|
||||||
|
? _onlyEmojiEmoteRegex.hasMatch(content['formatted_body'])
|
||||||
|
: _onlyEmojiRegex.hasMatch(content['body'] ?? '');
|
||||||
|
|
||||||
|
/// Gets the number of emotes in a given message. This is useful to determine if
|
||||||
|
/// emotes should be displayed bigger. WARNING: This does **not** test if there are
|
||||||
|
/// only emotes. Use `event.onlyEmotes` for that!
|
||||||
|
int get numberEmotes => isRichMessage
|
||||||
|
? _countEmojiEmoteRegex.allMatches(content['formatted_body']).length
|
||||||
|
: _countEmojiRegex.allMatches(content['body'] ?? '').length;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,27 +18,30 @@
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
|
||||||
import 'package:famedlysdk/src/client.dart';
|
|
||||||
import 'package:famedlysdk/src/event.dart';
|
|
||||||
import 'package:famedlysdk/src/utils/event_update.dart';
|
|
||||||
import 'package:famedlysdk/src/utils/room_update.dart';
|
|
||||||
import 'package:famedlysdk/src/utils/matrix_file.dart';
|
|
||||||
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
|
||||||
import 'package:html_unescape/html_unescape.dart';
|
import 'package:html_unescape/html_unescape.dart';
|
||||||
|
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
||||||
|
|
||||||
import './user.dart';
|
import '../famedlysdk.dart';
|
||||||
|
import '../matrix_api.dart';
|
||||||
|
import 'client.dart';
|
||||||
|
import 'database/database.dart' show DbRoom;
|
||||||
|
import 'event.dart';
|
||||||
import 'timeline.dart';
|
import 'timeline.dart';
|
||||||
|
import 'user.dart';
|
||||||
|
import 'utils/event_update.dart';
|
||||||
|
import 'utils/logs.dart';
|
||||||
|
import 'utils/markdown.dart';
|
||||||
|
import 'utils/matrix_file.dart';
|
||||||
import 'utils/matrix_localizations.dart';
|
import 'utils/matrix_localizations.dart';
|
||||||
|
import 'utils/room_update.dart';
|
||||||
import 'utils/states_map.dart';
|
import 'utils/states_map.dart';
|
||||||
import './utils/markdown.dart';
|
|
||||||
import './database/database.dart' show DbRoom;
|
|
||||||
|
|
||||||
enum PushRuleState { notify, mentions_only, dont_notify }
|
enum PushRuleState { notify, mentions_only, dont_notify }
|
||||||
enum JoinRules { public, knock, invite, private }
|
enum JoinRules { public, knock, invite, private }
|
||||||
enum GuestAccess { can_join, forbidden }
|
enum GuestAccess { can_join, forbidden }
|
||||||
enum HistoryVisibility { invited, joined, shared, world_readable }
|
enum HistoryVisibility { invited, joined, shared, world_readable }
|
||||||
|
const String MessageSendingStatusKey =
|
||||||
|
'com.famedly.famedlysdk.message_sending_status';
|
||||||
|
|
||||||
/// Represents a Matrix room.
|
/// Represents a Matrix room.
|
||||||
class Room {
|
class Room {
|
||||||
|
@ -104,7 +107,9 @@ class Room {
|
||||||
/// Flag if the room is partial, meaning not all state events have been loaded yet
|
/// Flag if the room is partial, meaning not all state events have been loaded yet
|
||||||
bool partial = true;
|
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 {
|
Future<void> postLoad() async {
|
||||||
if (!partial || client.database == null) {
|
if (!partial || client.database == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -132,8 +137,8 @@ class Room {
|
||||||
if (state.type == EventTypes.Encrypted && client.encryptionEnabled) {
|
if (state.type == EventTypes.Encrypted && client.encryptionEnabled) {
|
||||||
try {
|
try {
|
||||||
state = client.encryption.decryptRoomEventSync(id, state);
|
state = client.encryption.decryptRoomEventSync(id, state);
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
print('[LibOlm] Could not decrypt room state: ' + e.toString());
|
Logs.error('[LibOlm] Could not decrypt room state: ' + e.toString(), s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!(state.stateKey is String) &&
|
if (!(state.stateKey is String) &&
|
||||||
|
@ -259,7 +264,13 @@ class Room {
|
||||||
// perfect, it is only used for the room preview in the room list and sorting
|
// perfect, it is only used for the room preview in the room list and sorting
|
||||||
// said room list, so it should be good enough.
|
// said room list, so it should be good enough.
|
||||||
var lastTime = DateTime.fromMillisecondsSinceEpoch(0);
|
var lastTime = DateTime.fromMillisecondsSinceEpoch(0);
|
||||||
var lastEvent = getState(EventTypes.Message);
|
final lastEvents = <Event>[
|
||||||
|
for (var type in client.roomPreviewLastEvents) getState(type)
|
||||||
|
]..removeWhere((e) => e == null);
|
||||||
|
|
||||||
|
var lastEvent = lastEvents.isEmpty
|
||||||
|
? null
|
||||||
|
: lastEvents.reduce((a, b) => a.sortOrder > b.sortOrder ? a : b);
|
||||||
if (lastEvent == null) {
|
if (lastEvent == null) {
|
||||||
states.forEach((final String key, final entry) {
|
states.forEach((final String key, final entry) {
|
||||||
if (!entry.containsKey('')) return;
|
if (!entry.containsKey('')) return;
|
||||||
|
@ -369,21 +380,21 @@ class Room {
|
||||||
|
|
||||||
/// Call the Matrix API to change the name of this room. Returns the event ID of the
|
/// Call the Matrix API to change the name of this room. Returns the event ID of the
|
||||||
/// new m.room.name event.
|
/// new m.room.name event.
|
||||||
Future<String> setName(String newName) => client.api.sendState(
|
Future<String> setName(String newName) => client.sendState(
|
||||||
id,
|
id,
|
||||||
EventTypes.RoomName,
|
EventTypes.RoomName,
|
||||||
{'name': newName},
|
{'name': newName},
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Call the Matrix API to change the topic of this room.
|
/// 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,
|
id,
|
||||||
EventTypes.RoomTopic,
|
EventTypes.RoomTopic,
|
||||||
{'topic': newName},
|
{'topic': newName},
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Add a tag to the room.
|
/// 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,
|
client.userID,
|
||||||
id,
|
id,
|
||||||
tag,
|
tag,
|
||||||
|
@ -391,7 +402,7 @@ class Room {
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Removes a tag from the room.
|
/// Removes a tag from the room.
|
||||||
Future<void> removeTag(String tag) => client.api.removeRoomTag(
|
Future<void> removeTag(String tag) => client.removeRoomTag(
|
||||||
client.userID,
|
client.userID,
|
||||||
id,
|
id,
|
||||||
tag,
|
tag,
|
||||||
|
@ -418,7 +429,7 @@ class Room {
|
||||||
|
|
||||||
/// Call the Matrix API to change the pinned events of this room.
|
/// Call the Matrix API to change the pinned events of this room.
|
||||||
Future<String> setPinnedEvents(List<String> pinnedEventIds) =>
|
Future<String> setPinnedEvents(List<String> pinnedEventIds) =>
|
||||||
client.api.sendState(
|
client.sendState(
|
||||||
id,
|
id,
|
||||||
EventTypes.RoomPinnedEvents,
|
EventTypes.RoomPinnedEvents,
|
||||||
{'pinned': pinnedEventIds},
|
{'pinned': pinnedEventIds},
|
||||||
|
@ -432,9 +443,10 @@ class Room {
|
||||||
name = name.replaceAll(RegExp(r'[^\w-]'), '');
|
name = name.replaceAll(RegExp(r'[^\w-]'), '');
|
||||||
return name.toLowerCase();
|
return name.toLowerCase();
|
||||||
};
|
};
|
||||||
|
final allMxcs = <String>{}; // for easy dedupint
|
||||||
final addEmotePack = (String packName, Map<String, dynamic> content,
|
final addEmotePack = (String packName, Map<String, dynamic> content,
|
||||||
[String packNameOverride]) {
|
[String packNameOverride]) {
|
||||||
if (!(content['short'] is Map)) {
|
if (!(content['emoticons'] is Map) && !(content['short'] is Map)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (content['pack'] is Map && content['pack']['name'] is String) {
|
if (content['pack'] is Map && content['pack']['name'] is String) {
|
||||||
|
@ -447,34 +459,37 @@ class Room {
|
||||||
if (!packs.containsKey(packName)) {
|
if (!packs.containsKey(packName)) {
|
||||||
packs[packName] = <String, String>{};
|
packs[packName] = <String, String>{};
|
||||||
}
|
}
|
||||||
content['short'].forEach((key, value) {
|
if (content['emoticons'] is Map) {
|
||||||
if (key is String && value is String && value.startsWith('mxc://')) {
|
content['emoticons'].forEach((key, value) {
|
||||||
packs[packName][key] = value;
|
if (key is String &&
|
||||||
}
|
value is Map &&
|
||||||
});
|
value['url'] is String &&
|
||||||
};
|
value['url'].startsWith('mxc://')) {
|
||||||
// first add all the room emotes
|
if (allMxcs.add(value['url'])) {
|
||||||
final allRoomEmotes = states.states['im.ponies.room_emotes'];
|
packs[packName][key] = value['url'];
|
||||||
if (allRoomEmotes != null) {
|
}
|
||||||
for (final entry in allRoomEmotes.entries) {
|
}
|
||||||
final stateKey = entry.key;
|
});
|
||||||
final event = entry.value;
|
} else {
|
||||||
addEmotePack(stateKey.isEmpty ? 'room' : stateKey, event.content);
|
content['short'].forEach((key, value) {
|
||||||
|
if (key is String && value is String && value.startsWith('mxc://')) {
|
||||||
|
if (allMxcs.add(value)) {
|
||||||
|
packs[packName][key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
// next add all the user emotes
|
// first add all the user emotes
|
||||||
final userEmotes = client.accountData['im.ponies.user_emotes'];
|
final userEmotes = client.accountData['im.ponies.user_emotes'];
|
||||||
if (userEmotes != null) {
|
if (userEmotes != null) {
|
||||||
addEmotePack('user', userEmotes.content);
|
addEmotePack('user', userEmotes.content);
|
||||||
}
|
}
|
||||||
// finally add all the external emote rooms
|
// next add all the external emote rooms
|
||||||
final emoteRooms = client.accountData['im.ponies.emote_rooms'];
|
final emoteRooms = client.accountData['im.ponies.emote_rooms'];
|
||||||
if (emoteRooms != null && emoteRooms.content['rooms'] is Map) {
|
if (emoteRooms != null && emoteRooms.content['rooms'] is Map) {
|
||||||
for (final roomEntry in emoteRooms.content['rooms'].entries) {
|
for (final roomEntry in emoteRooms.content['rooms'].entries) {
|
||||||
final roomId = roomEntry.key;
|
final roomId = roomEntry.key;
|
||||||
if (roomId == id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final room = client.getRoomById(roomId);
|
final room = client.getRoomById(roomId);
|
||||||
if (room != null && roomEntry.value is Map) {
|
if (room != null && roomEntry.value is Map) {
|
||||||
for (final stateKeyEntry in roomEntry.value.entries) {
|
for (final stateKeyEntry in roomEntry.value.entries) {
|
||||||
|
@ -492,6 +507,15 @@ class Room {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// finally add all the room emotes
|
||||||
|
final allRoomEmotes = states.states['im.ponies.room_emotes'];
|
||||||
|
if (allRoomEmotes != null) {
|
||||||
|
for (final entry in allRoomEmotes.entries) {
|
||||||
|
final stateKey = entry.key;
|
||||||
|
final event = entry.value;
|
||||||
|
addEmotePack(stateKey.isEmpty ? 'room' : stateKey, event.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
return packs;
|
return packs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -500,6 +524,7 @@ class Room {
|
||||||
Future<String> sendTextEvent(String message,
|
Future<String> sendTextEvent(String message,
|
||||||
{String txid,
|
{String txid,
|
||||||
Event inReplyTo,
|
Event inReplyTo,
|
||||||
|
String editEventId,
|
||||||
bool parseMarkdown = true,
|
bool parseMarkdown = true,
|
||||||
Map<String, Map<String, String>> emotePacks}) {
|
Map<String, Map<String, String>> emotePacks}) {
|
||||||
final event = <String, dynamic>{
|
final event = <String, dynamic>{
|
||||||
|
@ -518,7 +543,31 @@ class Room {
|
||||||
event['formatted_body'] = html;
|
event['formatted_body'] = html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sendEvent(event, txid: txid, inReplyTo: inReplyTo);
|
return sendEvent(event,
|
||||||
|
txid: txid, inReplyTo: inReplyTo, editEventId: editEventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends a reaction to an event with an [eventId] and the content [key] into a room.
|
||||||
|
/// Returns the event ID generated by the server for this reaction.
|
||||||
|
Future<String> sendReaction(String eventId, String key, {String txid}) {
|
||||||
|
return sendEvent({
|
||||||
|
'm.relates_to': {
|
||||||
|
'rel_type': RelationshipTypes.Reaction,
|
||||||
|
'event_id': eventId,
|
||||||
|
'key': key,
|
||||||
|
},
|
||||||
|
}, type: EventTypes.Reaction, txid: txid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends the location with description [body] and geo URI [geoUri] into a room.
|
||||||
|
/// Returns the event ID generated by the server for this message.
|
||||||
|
Future<String> sendLocation(String body, String geoUri, {String txid}) {
|
||||||
|
final event = <String, dynamic>{
|
||||||
|
'msgtype': 'm.location',
|
||||||
|
'body': body,
|
||||||
|
'geo_uri': geoUri,
|
||||||
|
};
|
||||||
|
return sendEvent(event, txid: txid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a [file] to this room after uploading it. Returns the mxc uri of
|
/// Sends a [file] to this room after uploading it. Returns the mxc uri of
|
||||||
|
@ -529,6 +578,7 @@ class Room {
|
||||||
MatrixFile file, {
|
MatrixFile file, {
|
||||||
String txid,
|
String txid,
|
||||||
Event inReplyTo,
|
Event inReplyTo,
|
||||||
|
String editEventId,
|
||||||
bool waitUntilSent = false,
|
bool waitUntilSent = false,
|
||||||
MatrixImageFile thumbnail,
|
MatrixImageFile thumbnail,
|
||||||
}) async {
|
}) async {
|
||||||
|
@ -545,13 +595,13 @@ class Room {
|
||||||
uploadThumbnail = encryptedThumbnail.toMatrixFile();
|
uploadThumbnail = encryptedThumbnail.toMatrixFile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final uploadResp = await client.api.upload(
|
final uploadResp = await client.upload(
|
||||||
uploadFile.bytes,
|
uploadFile.bytes,
|
||||||
uploadFile.name,
|
uploadFile.name,
|
||||||
contentType: uploadFile.mimeType,
|
contentType: uploadFile.mimeType,
|
||||||
);
|
);
|
||||||
final thumbnailUploadResp = uploadThumbnail != null
|
final thumbnailUploadResp = uploadThumbnail != null
|
||||||
? await client.api.upload(
|
? await client.upload(
|
||||||
uploadThumbnail.bytes,
|
uploadThumbnail.bytes,
|
||||||
uploadThumbnail.name,
|
uploadThumbnail.name,
|
||||||
contentType: uploadThumbnail.mimeType,
|
contentType: uploadThumbnail.mimeType,
|
||||||
|
@ -605,6 +655,7 @@ class Room {
|
||||||
content,
|
content,
|
||||||
txid: txid,
|
txid: txid,
|
||||||
inReplyTo: inReplyTo,
|
inReplyTo: inReplyTo,
|
||||||
|
editEventId: editEventId,
|
||||||
);
|
);
|
||||||
if (waitUntilSent) {
|
if (waitUntilSent) {
|
||||||
await sendResponse;
|
await sendResponse;
|
||||||
|
@ -612,13 +663,35 @@ class Room {
|
||||||
return uploadResp;
|
return uploadResp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> _sendContent(
|
||||||
|
String type,
|
||||||
|
Map<String, dynamic> content, {
|
||||||
|
String txid,
|
||||||
|
}) async {
|
||||||
|
txid ??= client.generateUniqueTransactionId();
|
||||||
|
final mustEncrypt = encrypted && client.encryptionEnabled;
|
||||||
|
final sendMessageContent = mustEncrypt
|
||||||
|
? await client.encryption
|
||||||
|
.encryptGroupMessagePayload(id, content, type: type)
|
||||||
|
: content;
|
||||||
|
return await client.sendMessage(
|
||||||
|
id,
|
||||||
|
mustEncrypt ? EventTypes.Encrypted : type,
|
||||||
|
txid,
|
||||||
|
sendMessageContent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Sends an event to this room with this json as a content. Returns the
|
/// Sends an event to this room with this json as a content. Returns the
|
||||||
/// event ID generated from the server.
|
/// event ID generated from the server.
|
||||||
Future<String> sendEvent(Map<String, dynamic> content,
|
Future<String> sendEvent(
|
||||||
{String type, String txid, Event inReplyTo}) async {
|
Map<String, dynamic> content, {
|
||||||
|
String type,
|
||||||
|
String txid,
|
||||||
|
Event inReplyTo,
|
||||||
|
String editEventId,
|
||||||
|
}) async {
|
||||||
type = type ?? EventTypes.Message;
|
type = type ?? EventTypes.Message;
|
||||||
final sendType =
|
|
||||||
(encrypted && client.encryptionEnabled) ? EventTypes.Encrypted : type;
|
|
||||||
|
|
||||||
// Create new transaction id
|
// Create new transaction id
|
||||||
String messageID;
|
String messageID;
|
||||||
|
@ -645,60 +718,72 @@ class Room {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (editEventId != null) {
|
||||||
final sortOrder = newSortOrder;
|
final newContent = Map<String, dynamic>.from(content);
|
||||||
// Display a *sending* event and store it.
|
content['m.new_content'] = newContent;
|
||||||
var eventUpdate = EventUpdate(
|
content['m.relates_to'] = {
|
||||||
type: 'timeline',
|
'event_id': editEventId,
|
||||||
roomID: id,
|
'rel_type': RelationshipTypes.Edit,
|
||||||
eventType: type,
|
};
|
||||||
sortOrder: sortOrder,
|
if (content['body'] is String) {
|
||||||
content: {
|
content['body'] = '* ' + content['body'];
|
||||||
'type': type,
|
}
|
||||||
'event_id': messageID,
|
if (content['formatted_body'] is String) {
|
||||||
'sender': client.userID,
|
content['formatted_body'] = '* ' + content['formatted_body'];
|
||||||
'status': 0,
|
}
|
||||||
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
|
}
|
||||||
'content': content
|
final sentDate = DateTime.now();
|
||||||
},
|
final syncUpdate = SyncUpdate()
|
||||||
);
|
..rooms = (RoomsUpdate()
|
||||||
client.onEvent.add(eventUpdate);
|
..join = (<String, JoinedRoomUpdate>{}..[id] = (JoinedRoomUpdate()
|
||||||
await client.database?.transaction(() async {
|
..timeline = (TimelineUpdate()
|
||||||
await client.database.storeEventUpdate(client.id, eventUpdate);
|
..events = [
|
||||||
await updateSortOrder();
|
MatrixEvent()
|
||||||
});
|
..content = content
|
||||||
|
..type = type
|
||||||
|
..eventId = messageID
|
||||||
|
..senderId = client.userID
|
||||||
|
..originServerTs = sentDate
|
||||||
|
..unsigned = {
|
||||||
|
MessageSendingStatusKey: 0,
|
||||||
|
'transaction_id': messageID,
|
||||||
|
},
|
||||||
|
]))));
|
||||||
|
await _handleFakeSync(syncUpdate);
|
||||||
|
|
||||||
// Send the text and on success, store and display a *sent* event.
|
// Send the text and on success, store and display a *sent* event.
|
||||||
try {
|
String res;
|
||||||
final sendMessageContent = encrypted && client.encryptionEnabled
|
while (res == null) {
|
||||||
? await client.encryption
|
try {
|
||||||
.encryptGroupMessagePayload(id, content, type: type)
|
res = await _sendContent(
|
||||||
: content;
|
type,
|
||||||
final res = await client.api.sendMessage(
|
content,
|
||||||
id,
|
txid: messageID,
|
||||||
sendType,
|
);
|
||||||
messageID,
|
} catch (e, s) {
|
||||||
sendMessageContent,
|
if ((DateTime.now().millisecondsSinceEpoch -
|
||||||
);
|
sentDate.millisecondsSinceEpoch) <
|
||||||
eventUpdate.content['status'] = 1;
|
(1000 * client.sendMessageTimeoutSeconds)) {
|
||||||
eventUpdate.content['unsigned'] = {'transaction_id': messageID};
|
Logs.warning('[Client] Problem while sending message because of "' +
|
||||||
eventUpdate.content['event_id'] = res;
|
e.toString() +
|
||||||
client.onEvent.add(eventUpdate);
|
'". Try again in 1 seconds...');
|
||||||
await client.database?.transaction(() async {
|
await Future.delayed(Duration(seconds: 1));
|
||||||
await client.database.storeEventUpdate(client.id, eventUpdate);
|
} else {
|
||||||
});
|
Logs.warning(
|
||||||
return res;
|
'[Client] Problem while sending message: ' + e.toString(), s);
|
||||||
} catch (exception) {
|
syncUpdate.rooms.join.values.first.timeline.events.first
|
||||||
print('[Client] Error while sending: ' + exception.toString());
|
.unsigned[MessageSendingStatusKey] = -1;
|
||||||
// On error, set status to -1
|
await _handleFakeSync(syncUpdate);
|
||||||
eventUpdate.content['status'] = -1;
|
return null;
|
||||||
eventUpdate.content['unsigned'] = {'transaction_id': messageID};
|
}
|
||||||
client.onEvent.add(eventUpdate);
|
}
|
||||||
await client.database?.transaction(() async {
|
|
||||||
await client.database.storeEventUpdate(client.id, eventUpdate);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return null;
|
syncUpdate.rooms.join.values.first.timeline.events.first
|
||||||
|
.unsigned[MessageSendingStatusKey] = 1;
|
||||||
|
syncUpdate.rooms.join.values.first.timeline.events.first.eventId = res;
|
||||||
|
await _handleFakeSync(syncUpdate);
|
||||||
|
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Call the Matrix API to join this room if the user is not already a member.
|
/// Call the Matrix API to join this room if the user is not already a member.
|
||||||
|
@ -706,7 +791,7 @@ class Room {
|
||||||
/// automatically be set.
|
/// automatically be set.
|
||||||
Future<void> join() async {
|
Future<void> join() async {
|
||||||
try {
|
try {
|
||||||
await client.api.joinRoom(id);
|
await client.joinRoom(id);
|
||||||
final invitation = getState(EventTypes.RoomMember, client.userID);
|
final invitation = getState(EventTypes.RoomMember, client.userID);
|
||||||
if (invitation != null &&
|
if (invitation != null &&
|
||||||
invitation.content['is_direct'] is bool &&
|
invitation.content['is_direct'] is bool &&
|
||||||
|
@ -732,25 +817,25 @@ class Room {
|
||||||
/// chat, this will be removed too.
|
/// chat, this will be removed too.
|
||||||
Future<void> leave() async {
|
Future<void> leave() async {
|
||||||
if (directChatMatrixID != '') await removeFromDirectChat();
|
if (directChatMatrixID != '') await removeFromDirectChat();
|
||||||
await client.api.leaveRoom(id);
|
await client.leaveRoom(id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Call the Matrix API to forget this room if you already left it.
|
/// Call the Matrix API to forget this room if you already left it.
|
||||||
Future<void> forget() async {
|
Future<void> forget() async {
|
||||||
await client.database?.forgetRoom(client.id, id);
|
await client.database?.forgetRoom(client.id, id);
|
||||||
await client.api.forgetRoom(id);
|
await client.forgetRoom(id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Call the Matrix API to kick a user from this room.
|
/// 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.
|
/// 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.
|
/// 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].
|
/// 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
|
/// Returns the event ID of the new state event. If there is no known
|
||||||
|
@ -762,7 +847,7 @@ class Room {
|
||||||
if (powerMap['users'] == null) powerMap['users'] = {};
|
if (powerMap['users'] == null) powerMap['users'] = {};
|
||||||
powerMap['users'][userID] = power;
|
powerMap['users'][userID] = power;
|
||||||
|
|
||||||
return await client.api.sendState(
|
return await client.sendState(
|
||||||
id,
|
id,
|
||||||
EventTypes.RoomPowerLevels,
|
EventTypes.RoomPowerLevels,
|
||||||
powerMap,
|
powerMap,
|
||||||
|
@ -770,14 +855,14 @@ class Room {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Call the Matrix API to invite a user to this 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
|
/// 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**
|
/// be received maximum. When the request is answered, [onHistoryReceived] will be triggered **before**
|
||||||
/// the historical events will be published in the onEvent stream.
|
/// the historical events will be published in the onEvent stream.
|
||||||
Future<void> requestHistory(
|
Future<void> requestHistory(
|
||||||
{int historyCount = DefaultHistoryCount, onHistoryReceived}) async {
|
{int historyCount = DefaultHistoryCount, onHistoryReceived}) async {
|
||||||
final resp = await client.api.requestMessages(
|
final resp = await client.requestMessages(
|
||||||
id,
|
id,
|
||||||
prev_batch,
|
prev_batch,
|
||||||
Direction.b,
|
Direction.b,
|
||||||
|
@ -828,7 +913,7 @@ class Room {
|
||||||
directChats[userID] = [id];
|
directChats[userID] = [id];
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.api.setAccountData(
|
await client.setAccountData(
|
||||||
client.userID,
|
client.userID,
|
||||||
'm.direct',
|
'm.direct',
|
||||||
directChats,
|
directChats,
|
||||||
|
@ -846,7 +931,7 @@ class Room {
|
||||||
return;
|
return;
|
||||||
} // Nothing to do here
|
} // Nothing to do here
|
||||||
|
|
||||||
await client.api.setRoomAccountData(
|
await client.setRoomAccountData(
|
||||||
client.userID,
|
client.userID,
|
||||||
id,
|
id,
|
||||||
'm.direct',
|
'm.direct',
|
||||||
|
@ -859,7 +944,7 @@ class Room {
|
||||||
Future<void> sendReadReceipt(String eventID) async {
|
Future<void> sendReadReceipt(String eventID) async {
|
||||||
notificationCount = 0;
|
notificationCount = 0;
|
||||||
await client.database?.resetNotificationCount(client.id, id);
|
await client.database?.resetNotificationCount(client.id, id);
|
||||||
await client.api.sendReadMarker(
|
await client.sendReadMarker(
|
||||||
id,
|
id,
|
||||||
eventID,
|
eventID,
|
||||||
readReceiptLocationEventId: eventID,
|
readReceiptLocationEventId: eventID,
|
||||||
|
@ -981,6 +1066,8 @@ class Room {
|
||||||
return userList;
|
return userList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _requestedParticipants = false;
|
||||||
|
|
||||||
/// Request the full list of participants from the server. The local list
|
/// Request the full list of participants from the server. The local list
|
||||||
/// from the store is not complete if the client uses lazy loading.
|
/// from the store is not complete if the client uses lazy loading.
|
||||||
Future<List<User>> requestParticipants() async {
|
Future<List<User>> requestParticipants() async {
|
||||||
|
@ -991,13 +1078,16 @@ class Room {
|
||||||
setState(user);
|
setState(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (participantListComplete) return getParticipants();
|
if (_requestedParticipants || participantListComplete) {
|
||||||
final matrixEvents = await client.api.requestMembers(id);
|
return getParticipants();
|
||||||
|
}
|
||||||
|
final matrixEvents = await client.requestMembers(id);
|
||||||
final users =
|
final users =
|
||||||
matrixEvents.map((e) => Event.fromMatrixEvent(e, this).asUser).toList();
|
matrixEvents.map((e) => Event.fromMatrixEvent(e, this).asUser).toList();
|
||||||
for (final user in users) {
|
for (final user in users) {
|
||||||
setState(user); // at *least* cache this in-memory
|
setState(user); // at *least* cache this in-memory
|
||||||
}
|
}
|
||||||
|
_requestedParticipants = true;
|
||||||
users.removeWhere(
|
users.removeWhere(
|
||||||
(u) => [Membership.leave, Membership.ban].contains(u.membership));
|
(u) => [Membership.leave, Membership.ban].contains(u.membership));
|
||||||
return users;
|
return users;
|
||||||
|
@ -1055,7 +1145,7 @@ class Room {
|
||||||
if (mxID == null || !_requestingMatrixIds.add(mxID)) return null;
|
if (mxID == null || !_requestingMatrixIds.add(mxID)) return null;
|
||||||
Map<String, dynamic> resp;
|
Map<String, dynamic> resp;
|
||||||
try {
|
try {
|
||||||
resp = await client.api.requestStateContent(
|
resp = await client.requestStateContent(
|
||||||
id,
|
id,
|
||||||
EventTypes.RoomMember,
|
EventTypes.RoomMember,
|
||||||
mxID,
|
mxID,
|
||||||
|
@ -1068,10 +1158,10 @@ class Room {
|
||||||
}
|
}
|
||||||
if (resp == null && requestProfile) {
|
if (resp == null && requestProfile) {
|
||||||
try {
|
try {
|
||||||
final profile = await client.api.requestProfile(mxID);
|
final profile = await client.requestProfile(mxID);
|
||||||
resp = {
|
resp = {
|
||||||
'displayname': profile.displayname,
|
'displayname': profile.displayname,
|
||||||
'avatar_url': profile.avatarUrl,
|
'avatar_url': profile.avatarUrl.toString(),
|
||||||
};
|
};
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
_requestingMatrixIds.remove(mxID);
|
_requestingMatrixIds.remove(mxID);
|
||||||
|
@ -1110,7 +1200,7 @@ class Room {
|
||||||
|
|
||||||
/// Searches for the event on the server. Returns null if not found.
|
/// Searches for the event on the server. Returns null if not found.
|
||||||
Future<Event> getEventById(String eventID) async {
|
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);
|
return Event.fromMatrixEvent(matrixEvent, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1144,8 +1234,8 @@ class Room {
|
||||||
/// Uploads a new user avatar for this room. Returns the event ID of the new
|
/// Uploads a new user avatar for this room. Returns the event ID of the new
|
||||||
/// m.room.avatar event.
|
/// m.room.avatar event.
|
||||||
Future<String> setAvatar(MatrixFile file) async {
|
Future<String> setAvatar(MatrixFile file) async {
|
||||||
final uploadResp = await client.api.upload(file.bytes, file.name);
|
final uploadResp = await client.upload(file.bytes, file.name);
|
||||||
return await client.api.sendState(
|
return await client.sendState(
|
||||||
id,
|
id,
|
||||||
EventTypes.RoomAvatar,
|
EventTypes.RoomAvatar,
|
||||||
{'url': uploadResp},
|
{'url': uploadResp},
|
||||||
|
@ -1242,23 +1332,23 @@ class Room {
|
||||||
// All push notifications should be sent to the user
|
// All push notifications should be sent to the user
|
||||||
case PushRuleState.notify:
|
case PushRuleState.notify:
|
||||||
if (pushRuleState == PushRuleState.dont_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) {
|
} else if (pushRuleState == PushRuleState.mentions_only) {
|
||||||
await client.api.deletePushRule('global', PushRuleKind.room, id);
|
await client.deletePushRule('global', PushRuleKind.room, id);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
// Only when someone mentions the user, a push notification should be sent
|
// Only when someone mentions the user, a push notification should be sent
|
||||||
case PushRuleState.mentions_only:
|
case PushRuleState.mentions_only:
|
||||||
if (pushRuleState == PushRuleState.dont_notify) {
|
if (pushRuleState == PushRuleState.dont_notify) {
|
||||||
await client.api.deletePushRule('global', PushRuleKind.override, id);
|
await client.deletePushRule('global', PushRuleKind.override, id);
|
||||||
await client.api.setPushRule(
|
await client.setPushRule(
|
||||||
'global',
|
'global',
|
||||||
PushRuleKind.room,
|
PushRuleKind.room,
|
||||||
id,
|
id,
|
||||||
[PushRuleAction.dont_notify],
|
[PushRuleAction.dont_notify],
|
||||||
);
|
);
|
||||||
} else if (pushRuleState == PushRuleState.notify) {
|
} else if (pushRuleState == PushRuleState.notify) {
|
||||||
await client.api.setPushRule(
|
await client.setPushRule(
|
||||||
'global',
|
'global',
|
||||||
PushRuleKind.room,
|
PushRuleKind.room,
|
||||||
id,
|
id,
|
||||||
|
@ -1269,9 +1359,9 @@ class Room {
|
||||||
// No push notification should be ever sent for this room.
|
// No push notification should be ever sent for this room.
|
||||||
case PushRuleState.dont_notify:
|
case PushRuleState.dont_notify:
|
||||||
if (pushRuleState == PushRuleState.mentions_only) {
|
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',
|
'global',
|
||||||
PushRuleKind.override,
|
PushRuleKind.override,
|
||||||
id,
|
id,
|
||||||
|
@ -1297,7 +1387,7 @@ class Room {
|
||||||
}
|
}
|
||||||
var data = <String, dynamic>{};
|
var data = <String, dynamic>{};
|
||||||
if (reason != null) data['reason'] = reason;
|
if (reason != null) data['reason'] = reason;
|
||||||
return await client.api.redact(
|
return await client.redact(
|
||||||
id,
|
id,
|
||||||
eventId,
|
eventId,
|
||||||
messageID,
|
messageID,
|
||||||
|
@ -1310,7 +1400,7 @@ class Room {
|
||||||
'typing': isTyping,
|
'typing': isTyping,
|
||||||
};
|
};
|
||||||
if (timeout != null) data['timeout'] = timeout;
|
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.
|
/// This is sent by the caller when they wish to establish a call.
|
||||||
|
@ -1324,16 +1414,16 @@ class Room {
|
||||||
{String type = 'offer', int version = 0, String txid}) async {
|
{String type = 'offer', int version = 0, String txid}) async {
|
||||||
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
|
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
|
||||||
return await client.api.sendMessage(
|
final content = {
|
||||||
id,
|
'call_id': callId,
|
||||||
|
'lifetime': lifetime,
|
||||||
|
'offer': {'sdp': sdp, 'type': type},
|
||||||
|
'version': version,
|
||||||
|
};
|
||||||
|
return await _sendContent(
|
||||||
EventTypes.CallInvite,
|
EventTypes.CallInvite,
|
||||||
txid,
|
content,
|
||||||
{
|
txid: txid,
|
||||||
'call_id': callId,
|
|
||||||
'lifetime': lifetime,
|
|
||||||
'offer': {'sdp': sdp, 'type': type},
|
|
||||||
'version': version,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1362,15 +1452,15 @@ class Room {
|
||||||
String txid,
|
String txid,
|
||||||
}) async {
|
}) async {
|
||||||
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
|
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
|
||||||
return await client.api.sendMessage(
|
final content = {
|
||||||
id,
|
'call_id': callId,
|
||||||
|
'candidates': candidates,
|
||||||
|
'version': version,
|
||||||
|
};
|
||||||
|
return await _sendContent(
|
||||||
EventTypes.CallCandidates,
|
EventTypes.CallCandidates,
|
||||||
txid,
|
content,
|
||||||
{
|
txid: txid,
|
||||||
'call_id': callId,
|
|
||||||
'candidates': candidates,
|
|
||||||
'version': version,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1382,15 +1472,15 @@ class Room {
|
||||||
Future<String> answerCall(String callId, String sdp,
|
Future<String> answerCall(String callId, String sdp,
|
||||||
{String type = 'answer', int version = 0, String txid}) async {
|
{String type = 'answer', int version = 0, String txid}) async {
|
||||||
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
|
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
|
||||||
return await client.api.sendMessage(
|
final content = {
|
||||||
id,
|
'call_id': callId,
|
||||||
|
'answer': {'sdp': sdp, 'type': type},
|
||||||
|
'version': version,
|
||||||
|
};
|
||||||
|
return await _sendContent(
|
||||||
EventTypes.CallAnswer,
|
EventTypes.CallAnswer,
|
||||||
txid,
|
content,
|
||||||
{
|
txid: txid,
|
||||||
'call_id': callId,
|
|
||||||
'answer': {'sdp': sdp, 'type': type},
|
|
||||||
'version': version,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1400,14 +1490,15 @@ class Room {
|
||||||
Future<String> hangupCall(String callId,
|
Future<String> hangupCall(String callId,
|
||||||
{int version = 0, String txid}) async {
|
{int version = 0, String txid}) async {
|
||||||
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
|
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
|
||||||
return await client.api.sendMessage(
|
|
||||||
id,
|
final content = {
|
||||||
|
'call_id': callId,
|
||||||
|
'version': version,
|
||||||
|
};
|
||||||
|
return await _sendContent(
|
||||||
EventTypes.CallHangup,
|
EventTypes.CallHangup,
|
||||||
txid,
|
content,
|
||||||
{
|
txid: txid,
|
||||||
'call_id': callId,
|
|
||||||
'version': version,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1436,7 +1527,7 @@ class Room {
|
||||||
|
|
||||||
/// Changes the join rules. You should check first if the user is able to change it.
|
/// Changes the join rules. You should check first if the user is able to change it.
|
||||||
Future<void> setJoinRules(JoinRules joinRules) async {
|
Future<void> setJoinRules(JoinRules joinRules) async {
|
||||||
await client.api.sendState(
|
await client.sendState(
|
||||||
id,
|
id,
|
||||||
EventTypes.RoomJoinRules,
|
EventTypes.RoomJoinRules,
|
||||||
{
|
{
|
||||||
|
@ -1461,7 +1552,7 @@ class Room {
|
||||||
|
|
||||||
/// Changes the guest access. You should check first if the user is able to change it.
|
/// Changes the guest access. You should check first if the user is able to change it.
|
||||||
Future<void> setGuestAccess(GuestAccess guestAccess) async {
|
Future<void> setGuestAccess(GuestAccess guestAccess) async {
|
||||||
await client.api.sendState(
|
await client.sendState(
|
||||||
id,
|
id,
|
||||||
EventTypes.GuestAccess,
|
EventTypes.GuestAccess,
|
||||||
{
|
{
|
||||||
|
@ -1487,7 +1578,7 @@ class Room {
|
||||||
|
|
||||||
/// Changes the history visibility. You should check first if the user is able to change it.
|
/// Changes the history visibility. You should check first if the user is able to change it.
|
||||||
Future<void> setHistoryVisibility(HistoryVisibility historyVisibility) async {
|
Future<void> setHistoryVisibility(HistoryVisibility historyVisibility) async {
|
||||||
await client.api.sendState(
|
await client.sendState(
|
||||||
id,
|
id,
|
||||||
EventTypes.HistoryVisibility,
|
EventTypes.HistoryVisibility,
|
||||||
{
|
{
|
||||||
|
@ -1514,7 +1605,7 @@ class Room {
|
||||||
Future<void> enableEncryption({int algorithmIndex = 0}) async {
|
Future<void> enableEncryption({int algorithmIndex = 0}) async {
|
||||||
if (encrypted) throw ('Encryption is already enabled!');
|
if (encrypted) throw ('Encryption is already enabled!');
|
||||||
final algorithm = Client.supportedGroupEncryptionAlgorithms[algorithmIndex];
|
final algorithm = Client.supportedGroupEncryptionAlgorithms[algorithmIndex];
|
||||||
await client.api.sendState(
|
await client.sendState(
|
||||||
id,
|
id,
|
||||||
EventTypes.Encryption,
|
EventTypes.Encryption,
|
||||||
{
|
{
|
||||||
|
@ -1545,4 +1636,15 @@ class Room {
|
||||||
}
|
}
|
||||||
await client.encryption.keyManager.request(this, sessionId, senderKey);
|
await client.encryption.keyManager.request(this, sessionId, senderKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handleFakeSync(SyncUpdate syncUpdate,
|
||||||
|
{bool sortAtTheEnd = false}) async {
|
||||||
|
if (client.database != null) {
|
||||||
|
await client.database.transaction(() async {
|
||||||
|
await client.handleSync(syncUpdate, sortAtTheEnd: sortAtTheEnd);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await client.handleSync(syncUpdate, sortAtTheEnd: sortAtTheEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,11 +18,11 @@
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
import '../matrix_api.dart';
|
||||||
|
|
||||||
import 'event.dart';
|
import 'event.dart';
|
||||||
import 'room.dart';
|
import 'room.dart';
|
||||||
import 'utils/event_update.dart';
|
import 'utils/event_update.dart';
|
||||||
|
import 'utils/logs.dart';
|
||||||
import 'utils/room_update.dart';
|
import 'utils/room_update.dart';
|
||||||
|
|
||||||
typedef onTimelineUpdateCallback = void Function();
|
typedef onTimelineUpdateCallback = void Function();
|
||||||
|
@ -35,6 +35,9 @@ class Timeline {
|
||||||
final Room room;
|
final Room room;
|
||||||
List<Event> events = [];
|
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 onTimelineUpdateCallback onUpdate;
|
||||||
final onTimelineInsertCallback onInsert;
|
final onTimelineInsertCallback onInsert;
|
||||||
|
|
||||||
|
@ -66,7 +69,10 @@ class Timeline {
|
||||||
await room.requestHistory(
|
await room.requestHistory(
|
||||||
historyCount: historyCount,
|
historyCount: historyCount,
|
||||||
onHistoryReceived: () {
|
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));
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
|
@ -82,9 +88,18 @@ class Timeline {
|
||||||
// to be received via the onEvent stream, it is unneeded to call sortAndUpdate
|
// to be received via the onEvent stream, it is unneeded to call sortAndUpdate
|
||||||
roomSub ??= room.client.onRoomUpdate.stream
|
roomSub ??= room.client.onRoomUpdate.stream
|
||||||
.where((r) => r.id == room.id && r.limitedTimeline == true)
|
.where((r) => r.id == room.id && r.limitedTimeline == true)
|
||||||
.listen((r) => events.clear());
|
.listen((r) {
|
||||||
|
events.clear();
|
||||||
|
aggregatedEvents.clear();
|
||||||
|
});
|
||||||
sessionIdReceivedSub ??=
|
sessionIdReceivedSub ??=
|
||||||
room.onSessionKeyReceived.stream.listen(_sessionKeyReceived);
|
room.onSessionKeyReceived.stream.listen(_sessionKeyReceived);
|
||||||
|
|
||||||
|
// we want to populate our aggregated events
|
||||||
|
for (final e in events) {
|
||||||
|
addAggregatedEvent(e);
|
||||||
|
}
|
||||||
|
_sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Don't forget to call this before you dismiss this object!
|
/// Don't forget to call this before you dismiss this object!
|
||||||
|
@ -122,33 +137,101 @@ class Timeline {
|
||||||
}
|
}
|
||||||
|
|
||||||
int _findEvent({String event_id, String unsigned_txid}) {
|
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;
|
int i;
|
||||||
for (i = 0; i < events.length; i++) {
|
for (i = 0; i < events.length; i++) {
|
||||||
if (events[i].eventId == event_id ||
|
final searchHaystack = <String>{};
|
||||||
(unsigned_txid != null && events[i].eventId == unsigned_txid)) break;
|
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;
|
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 {
|
void _handleEventUpdate(EventUpdate eventUpdate) async {
|
||||||
try {
|
try {
|
||||||
if (eventUpdate.roomID != room.id) return;
|
if (eventUpdate.roomID != room.id) return;
|
||||||
|
|
||||||
if (eventUpdate.type == 'timeline' || eventUpdate.type == 'history') {
|
if (eventUpdate.type == 'timeline' || eventUpdate.type == 'history') {
|
||||||
|
var status = eventUpdate.content['status'] ??
|
||||||
|
(eventUpdate.content['unsigned'] is Map<String, dynamic>
|
||||||
|
? eventUpdate.content['unsigned'][MessageSendingStatusKey]
|
||||||
|
: null) ??
|
||||||
|
2;
|
||||||
// Redaction events are handled as modification for existing events.
|
// Redaction events are handled as modification for existing events.
|
||||||
if (eventUpdate.eventType == EventTypes.Redaction) {
|
if (eventUpdate.eventType == EventTypes.Redaction) {
|
||||||
final eventId = _findEvent(event_id: eventUpdate.content['redacts']);
|
final eventId = _findEvent(event_id: eventUpdate.content['redacts']);
|
||||||
if (eventId != null) {
|
if (eventId < events.length) {
|
||||||
|
removeAggregatedEvent(events[eventId]);
|
||||||
events[eventId].setRedactionEvent(Event.fromJson(
|
events[eventId].setRedactionEvent(Event.fromJson(
|
||||||
eventUpdate.content, room, eventUpdate.sortOrder));
|
eventUpdate.content, room, eventUpdate.sortOrder));
|
||||||
}
|
}
|
||||||
} else if (eventUpdate.content['status'] == -2) {
|
} else if (status == -2) {
|
||||||
var i = _findEvent(event_id: eventUpdate.content['event_id']);
|
var i = _findEvent(event_id: eventUpdate.content['event_id']);
|
||||||
if (i < events.length) events.removeAt(i);
|
if (i < events.length) {
|
||||||
}
|
removeAggregatedEvent(events[i]);
|
||||||
// Is this event already in the timeline?
|
events.removeAt(i);
|
||||||
else if (eventUpdate.content['unsigned'] is Map &&
|
}
|
||||||
eventUpdate.content['unsigned']['transaction_id'] is String) {
|
} else {
|
||||||
var i = _findEvent(
|
var i = _findEvent(
|
||||||
event_id: eventUpdate.content['event_id'],
|
event_id: eventUpdate.content['event_id'],
|
||||||
unsigned_txid: eventUpdate.content['unsigned'] is Map
|
unsigned_txid: eventUpdate.content['unsigned'] is Map
|
||||||
|
@ -156,55 +239,51 @@ class Timeline {
|
||||||
: null);
|
: null);
|
||||||
|
|
||||||
if (i < events.length) {
|
if (i < events.length) {
|
||||||
final tempSortOrder = events[i].sortOrder;
|
// if the old status is larger than the new one, we also want to preserve the old status
|
||||||
|
final oldStatus = events[i].status;
|
||||||
events[i] = Event.fromJson(
|
events[i] = Event.fromJson(
|
||||||
eventUpdate.content, room, eventUpdate.sortOrder);
|
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();
|
_sort();
|
||||||
} catch (e) {
|
if (onUpdate != null) onUpdate();
|
||||||
if (room.client.debug) {
|
} catch (e, s) {
|
||||||
print('[WARNING] (_handleEventUpdate) ${e.toString()}');
|
Logs.warning('Handle event update failed: ${e.toString()}', s);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool sortLock = false;
|
bool _sortLock = false;
|
||||||
|
|
||||||
void sort() {
|
void _sort() {
|
||||||
if (sortLock || events.length < 2) return;
|
if (_sortLock || events.length < 2) return;
|
||||||
sortLock = true;
|
_sortLock = true;
|
||||||
events?.sort((a, b) => b.sortOrder - a.sortOrder > 0 ? 1 : -1);
|
events?.sort((a, b) {
|
||||||
sortLock = false;
|
if (b.status == -1 && a.status != -1) {
|
||||||
}
|
return 1;
|
||||||
|
}
|
||||||
void sortAndUpdate() async {
|
if (a.status == -1 && b.status != -1) {
|
||||||
sort();
|
return -1;
|
||||||
if (onUpdate != null) onUpdate();
|
}
|
||||||
|
return b.sortOrder - a.sortOrder > 0 ? 1 : -1;
|
||||||
|
});
|
||||||
|
_sortLock = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,10 +16,10 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import '../famedlysdk.dart';
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
import '../matrix_api.dart';
|
||||||
import 'package:famedlysdk/src/room.dart';
|
import 'event.dart';
|
||||||
import 'package:famedlysdk/src/event.dart';
|
import 'room.dart';
|
||||||
|
|
||||||
/// Represents a Matrix User which may be a participant in a Matrix Room.
|
/// Represents a Matrix User which may be a participant in a Matrix Room.
|
||||||
class User extends Event {
|
class User extends Event {
|
||||||
|
@ -146,7 +146,7 @@ class User extends Event {
|
||||||
if (roomID != null) return roomID;
|
if (roomID != null) return roomID;
|
||||||
|
|
||||||
// Start a new direct chat
|
// Start a new direct chat
|
||||||
final newRoomID = await room.client.api.createRoom(
|
final newRoomID = await room.client.createRoom(
|
||||||
invite: [id],
|
invite: [id],
|
||||||
isDirect: true,
|
isDirect: true,
|
||||||
preset: CreateRoomPreset.trusted_private_chat,
|
preset: CreateRoomPreset.trusted_private_chat,
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:canonical_json/canonical_json.dart';
|
import 'package:canonical_json/canonical_json.dart';
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
import '../../encryption.dart';
|
||||||
import 'package:famedlysdk/encryption.dart';
|
import '../../matrix_api.dart';
|
||||||
|
|
||||||
import '../client.dart';
|
import '../client.dart';
|
||||||
import '../user.dart';
|
|
||||||
import '../room.dart';
|
|
||||||
import '../database/database.dart'
|
import '../database/database.dart'
|
||||||
show DbUserDeviceKey, DbUserDeviceKeysKey, DbUserCrossSigningKey;
|
show DbUserDeviceKey, DbUserDeviceKeysKey, DbUserCrossSigningKey;
|
||||||
import '../event.dart';
|
import '../event.dart';
|
||||||
|
import '../room.dart';
|
||||||
|
import '../user.dart';
|
||||||
|
|
||||||
enum UserVerifiedStatus { verified, unknown, unknownDevice }
|
enum UserVerifiedStatus { verified, unknown, unknownDevice }
|
||||||
|
|
||||||
|
@ -157,14 +157,20 @@ abstract class SignableKey extends MatrixSignableKey {
|
||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasValidSignatureChain({bool verifiedOnly = true, Set<String> visited}) {
|
bool hasValidSignatureChain(
|
||||||
|
{bool verifiedOnly = true,
|
||||||
|
Set<String> visited,
|
||||||
|
Set<String> onlyValidateUserIds}) {
|
||||||
if (!client.encryptionEnabled) {
|
if (!client.encryptionEnabled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
visited ??= <String>{};
|
visited ??= <String>{};
|
||||||
|
onlyValidateUserIds ??= <String>{};
|
||||||
final setKey = '${userId};${identifier}';
|
final setKey = '${userId};${identifier}';
|
||||||
if (visited.contains(setKey)) {
|
if (visited.contains(setKey) ||
|
||||||
return false; // prevent recursion
|
(onlyValidateUserIds.isNotEmpty &&
|
||||||
|
!onlyValidateUserIds.contains(userId))) {
|
||||||
|
return false; // prevent recursion & validate hasValidSignatureChain
|
||||||
}
|
}
|
||||||
visited.add(setKey);
|
visited.add(setKey);
|
||||||
for (final signatureEntries in signatures.entries) {
|
for (final signatureEntries in signatures.entries) {
|
||||||
|
@ -189,6 +195,13 @@ abstract class SignableKey extends MatrixSignableKey {
|
||||||
} else {
|
} else {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (onlyValidateUserIds.isNotEmpty &&
|
||||||
|
!onlyValidateUserIds.contains(key.userId)) {
|
||||||
|
// we don't want to verify keys from this user
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (key.blocked) {
|
if (key.blocked) {
|
||||||
continue; // we can't be bothered about this keys signatures
|
continue; // we can't be bothered about this keys signatures
|
||||||
}
|
}
|
||||||
|
@ -228,7 +241,9 @@ abstract class SignableKey extends MatrixSignableKey {
|
||||||
}
|
}
|
||||||
// or else we just recurse into that key and chack if it works out
|
// or else we just recurse into that key and chack if it works out
|
||||||
final haveChain = key.hasValidSignatureChain(
|
final haveChain = key.hasValidSignatureChain(
|
||||||
verifiedOnly: verifiedOnly, visited: visited);
|
verifiedOnly: verifiedOnly,
|
||||||
|
visited: visited,
|
||||||
|
onlyValidateUserIds: onlyValidateUserIds);
|
||||||
if (haveChain) {
|
if (haveChain) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
import '../../famedlysdk.dart';
|
import '../../famedlysdk.dart';
|
||||||
import '../../matrix_api.dart';
|
import '../../matrix_api.dart';
|
||||||
|
import 'logs.dart';
|
||||||
|
|
||||||
/// Represents a new event (e.g. a message in a room) or an update for an
|
/// Represents a new event (e.g. a message in a room) or an update for an
|
||||||
/// already known event.
|
/// already known event.
|
||||||
|
@ -57,8 +58,8 @@ class EventUpdate {
|
||||||
content: decrpytedEvent.toJson(),
|
content: decrpytedEvent.toJson(),
|
||||||
sortOrder: sortOrder,
|
sortOrder: sortOrder,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
print('[LibOlm] Could not decrypt megolm event: ' + e.toString());
|
Logs.error('[LibOlm] Could not decrypt megolm event: ' + e.toString(), s);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Famedly Matrix SDK
|
||||||
|
* Copyright (C) 2020 Famedly GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:ansicolor/ansicolor.dart';
|
||||||
|
|
||||||
|
abstract class Logs {
|
||||||
|
static final AnsiPen _infoPen = AnsiPen()..blue();
|
||||||
|
static final AnsiPen _warningPen = AnsiPen()..yellow();
|
||||||
|
static final AnsiPen _successPen = AnsiPen()..green();
|
||||||
|
static final AnsiPen _errorPen = AnsiPen()..red();
|
||||||
|
|
||||||
|
static const String _prefixText = '[Famedly Matrix SDK] ';
|
||||||
|
|
||||||
|
// ignore: avoid_print
|
||||||
|
static void info(dynamic info) => print(
|
||||||
|
_prefixText + _infoPen(info.toString()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ignore: avoid_print
|
||||||
|
static void success(dynamic obj, [dynamic stackTrace]) => print(
|
||||||
|
_prefixText + _successPen(obj.toString()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ignore: avoid_print
|
||||||
|
static void warning(dynamic warning, [dynamic stackTrace]) => print(
|
||||||
|
_prefixText +
|
||||||
|
_warningPen(warning.toString()) +
|
||||||
|
(stackTrace != null ? '\n${stackTrace.toString()}' : ''),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ignore: avoid_print
|
||||||
|
static void error(dynamic obj, [dynamic stackTrace]) => print(
|
||||||
|
_prefixText +
|
||||||
|
_errorPen(obj.toString()) +
|
||||||
|
(stackTrace != null ? '\n${stackTrace.toString()}' : ''),
|
||||||
|
);
|
||||||
|
}
|
|
@ -65,6 +65,7 @@ class EmoteSyntax extends InlineSyntax {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
final element = Element.empty('img');
|
final element = Element.empty('img');
|
||||||
|
element.attributes['data-mx-emoticon'] = '';
|
||||||
element.attributes['src'] = htmlEscape.convert(mxc);
|
element.attributes['src'] = htmlEscape.convert(mxc);
|
||||||
element.attributes['alt'] = htmlEscape.convert(emote);
|
element.attributes['alt'] = htmlEscape.convert(emote);
|
||||||
element.attributes['title'] = htmlEscape.convert(emote);
|
element.attributes['title'] = htmlEscape.convert(emote);
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
/// Workaround until [File] in dart:io and dart:html is unified
|
/// Workaround until [File] in dart:io and dart:html is unified
|
||||||
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
|
|
||||||
|
import '../../matrix_api/model/message_types.dart';
|
||||||
|
|
||||||
class MatrixFile {
|
class MatrixFile {
|
||||||
Uint8List bytes;
|
Uint8List bytes;
|
||||||
String name;
|
String name;
|
||||||
|
@ -16,13 +19,25 @@ class MatrixFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
MatrixFile({this.bytes, this.name, this.mimeType}) {
|
MatrixFile({this.bytes, this.name, this.mimeType}) {
|
||||||
mimeType ??= lookupMimeType(name, headerBytes: bytes);
|
mimeType ??=
|
||||||
|
lookupMimeType(name, headerBytes: bytes) ?? 'application/octet-stream';
|
||||||
name = name.split('/').last.toLowerCase();
|
name = name.split('/').last.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
int get size => bytes.length;
|
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 => ({
|
Map<String, dynamic> get info => ({
|
||||||
'mimetype': mimeType,
|
'mimetype': mimeType,
|
||||||
|
|
|
@ -3,14 +3,29 @@ extension MatrixIdExtension on String {
|
||||||
|
|
||||||
static const int MAX_LENGTH = 255;
|
static const int MAX_LENGTH = 255;
|
||||||
|
|
||||||
|
List<String> _getParts() {
|
||||||
|
final s = substring(1);
|
||||||
|
final ix = s.indexOf(':');
|
||||||
|
if (ix == -1) {
|
||||||
|
return [substring(1)];
|
||||||
|
}
|
||||||
|
return [s.substring(0, ix), s.substring(ix + 1)];
|
||||||
|
}
|
||||||
|
|
||||||
bool get isValidMatrixId {
|
bool get isValidMatrixId {
|
||||||
if (isEmpty ?? true) return false;
|
if (isEmpty ?? true) return false;
|
||||||
if (length > MAX_LENGTH) return false;
|
if (length > MAX_LENGTH) return false;
|
||||||
if (!VALID_SIGILS.contains(substring(0, 1))) {
|
if (!VALID_SIGILS.contains(substring(0, 1))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final parts = substring(1).split(':');
|
// event IDs do not have to have a domain
|
||||||
if (parts.length != 2 || parts[0].isEmpty || parts[1].isEmpty) {
|
if (substring(0, 1) == '\$') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// all other matrix IDs have to have a domain
|
||||||
|
final parts = _getParts();
|
||||||
|
// the localpart can be an empty string, e.g. for aliases
|
||||||
|
if (parts.length != 2 || parts[1].isEmpty) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -18,10 +33,9 @@ extension MatrixIdExtension on String {
|
||||||
|
|
||||||
String get sigil => isValidMatrixId ? substring(0, 1) : null;
|
String get sigil => isValidMatrixId ? substring(0, 1) : null;
|
||||||
|
|
||||||
String get localpart =>
|
String get localpart => isValidMatrixId ? _getParts().first : null;
|
||||||
isValidMatrixId ? substring(1).split(':').first : null;
|
|
||||||
|
|
||||||
String get domain => isValidMatrixId ? substring(1).split(':')[1] : null;
|
String get domain => isValidMatrixId ? _getParts().last : null;
|
||||||
|
|
||||||
bool equals(String other) => toLowerCase() == other?.toLowerCase();
|
bool equals(String other) => toLowerCase() == other?.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,6 +109,14 @@ abstract class MatrixLocalizations {
|
||||||
String couldNotDecryptMessage(String errorText);
|
String couldNotDecryptMessage(String errorText);
|
||||||
|
|
||||||
String unknownEvent(String typeKey);
|
String unknownEvent(String typeKey);
|
||||||
|
|
||||||
|
String startedACall(String senderName);
|
||||||
|
|
||||||
|
String endedTheCall(String senderName);
|
||||||
|
|
||||||
|
String answeredTheCall(String senderName);
|
||||||
|
|
||||||
|
String sentCallInformations(String senderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
extension HistoryVisibilityDisplayString on HistoryVisibility {
|
extension HistoryVisibilityDisplayString on HistoryVisibility {
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
import '../../matrix_api.dart';
|
||||||
|
|
||||||
/// Represents a new room or an update for an
|
/// Represents a new room or an update for an
|
||||||
/// already known room.
|
/// already known room.
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Famedly Matrix SDK
|
||||||
|
* Copyright (C) 2020 Famedly GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:isolate/isolate.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
Future<T> runInBackground<T, U>(
|
||||||
|
FutureOr<T> Function(U arg) function, U arg) async {
|
||||||
|
final isolate = await IsolateRunner.spawn();
|
||||||
|
try {
|
||||||
|
return await isolate.run(function, arg);
|
||||||
|
} finally {
|
||||||
|
await isolate.close();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Famedly Matrix SDK
|
||||||
|
* Copyright (C) 2020 Famedly GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'logs.dart';
|
||||||
|
|
||||||
|
Future<T> runInRoot<T>(FutureOr<T> Function() fn) async {
|
||||||
|
return await Zone.root.run(() async {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs.error('Error thrown in root zone: ' + e.toString(), s);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import '../../famedlysdk.dart';
|
||||||
|
|
||||||
import '../../matrix_api.dart';
|
import '../../matrix_api.dart';
|
||||||
|
|
||||||
/// Matrix room states are addressed by a tuple of the [type] and an
|
/// Matrix room states are addressed by a tuple of the [type] and an
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Famedly Matrix SDK
|
||||||
|
* Copyright (C) 2020 Famedly GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import '../../matrix_api.dart';
|
||||||
|
|
||||||
|
/// This extension adds easy-to-use filters for the sync update, meant to be used on the `client.onSync` stream, e.g.
|
||||||
|
/// `client.onSync.stream.where((s) => s.hasRoomUpdate)`. Multiple filters can easily be
|
||||||
|
/// combind with boolean logic: `client.onSync.stream.where((s) => s.hasRoomUpdate || s.hasPresenceUpdate)`
|
||||||
|
extension SyncUpdateFilters on SyncUpdate {
|
||||||
|
/// Returns true if this sync updat has a room update
|
||||||
|
/// That means there is account data, if there is a room in one of the `join`, `leave` or `invite` blocks of the sync or if there is a to_device event.
|
||||||
|
bool get hasRoomUpdate {
|
||||||
|
// if we have an account data change we need to re-render, as `m.direct` might have changed
|
||||||
|
if (accountData?.isNotEmpty ?? false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// check for a to_device event
|
||||||
|
if (toDevice?.isNotEmpty ?? false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// return if there are rooms to update
|
||||||
|
return (rooms?.join?.isNotEmpty ?? false) ||
|
||||||
|
(rooms?.invite?.isNotEmpty ?? false) ||
|
||||||
|
(rooms?.leave?.isNotEmpty ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns if this sync update has presence updates
|
||||||
|
bool get hasPresenceUpdate => presence != null && presence.isNotEmpty;
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
import '../../matrix_api.dart';
|
||||||
|
|
||||||
class ToDeviceEvent extends BasicEventWithSender {
|
class ToDeviceEvent extends BasicEventWithSender {
|
||||||
Map<String, dynamic> encryptedContent;
|
Map<String, dynamic> encryptedContent;
|
||||||
|
|
|
@ -16,14 +16,15 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:famedlysdk/src/client.dart';
|
|
||||||
import 'dart:core';
|
import 'dart:core';
|
||||||
|
|
||||||
|
import '../client.dart';
|
||||||
|
|
||||||
extension MxcUriExtension on Uri {
|
extension MxcUriExtension on Uri {
|
||||||
/// Returns a download Link to this content.
|
/// Returns a download Link to this content.
|
||||||
String getDownloadLink(Client matrix) => isScheme('mxc')
|
String getDownloadLink(Client matrix) => isScheme('mxc')
|
||||||
? matrix.api.homeserver != null
|
? matrix.homeserver != null
|
||||||
? '${matrix.api.homeserver.toString()}/_matrix/media/r0/download/$host$path'
|
? '${matrix.homeserver.toString()}/_matrix/media/r0/download/$host$path'
|
||||||
: ''
|
: ''
|
||||||
: toString();
|
: toString();
|
||||||
|
|
||||||
|
@ -36,8 +37,8 @@ extension MxcUriExtension on Uri {
|
||||||
final methodStr = method.toString().split('.').last;
|
final methodStr = method.toString().split('.').last;
|
||||||
width = width.round();
|
width = width.round();
|
||||||
height = height.round();
|
height = height.round();
|
||||||
return matrix.api.homeserver != null
|
return matrix.homeserver != null
|
||||||
? '${matrix.api.homeserver.toString()}/_matrix/media/r0/thumbnail/$host$path?width=$width&height=$height&method=$methodStr'
|
? '${matrix.homeserver.toString()}/_matrix/media/r0/thumbnail/$host$path?width=$width&height=$height&method=$methodStr'
|
||||||
: '';
|
: '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit efd17631b16d1271a029e0af8f7d8e5ae795cc5d
|
663
pubspec.lock
663
pubspec.lock
|
@ -1,663 +0,0 @@
|
||||||
# Generated by pub
|
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
|
||||||
packages:
|
|
||||||
_fe_analyzer_shared:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: _fe_analyzer_shared
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.0"
|
|
||||||
analyzer:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: analyzer
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.39.8"
|
|
||||||
analyzer_plugin_fork:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: analyzer_plugin_fork
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.2"
|
|
||||||
args:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: args
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.6.0"
|
|
||||||
asn1lib:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: asn1lib
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.6.4"
|
|
||||||
async:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: async
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.4.1"
|
|
||||||
base58check:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: base58check
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.1"
|
|
||||||
boolean_selector:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: boolean_selector
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.0"
|
|
||||||
build:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: build
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.3.0"
|
|
||||||
build_config:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: build_config
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.4.2"
|
|
||||||
build_daemon:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: build_daemon
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.4"
|
|
||||||
build_resolvers:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: build_resolvers
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.3.9"
|
|
||||||
build_runner:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: build_runner
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.10.0"
|
|
||||||
build_runner_core:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: build_runner_core
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "5.2.0"
|
|
||||||
built_collection:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: built_collection
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "4.3.2"
|
|
||||||
built_value:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: built_value
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "7.1.0"
|
|
||||||
canonical_json:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: canonical_json
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.0"
|
|
||||||
charcode:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: charcode
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.3"
|
|
||||||
checked_yaml:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: checked_yaml
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.2"
|
|
||||||
cli_util:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: cli_util
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.1.4"
|
|
||||||
clock:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: clock
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.1"
|
|
||||||
code_builder:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: code_builder
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.2.1"
|
|
||||||
collection:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: collection
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.14.12"
|
|
||||||
convert:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: convert
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.1"
|
|
||||||
coverage:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: coverage
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.13.9"
|
|
||||||
crypto:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: crypto
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.4"
|
|
||||||
csslib:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: csslib
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.16.1"
|
|
||||||
dart_style:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: dart_style
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.3.6"
|
|
||||||
encrypt:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: encrypt
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "4.0.2"
|
|
||||||
ffi:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: ffi
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.1.3"
|
|
||||||
fixnum:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: fixnum
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.10.11"
|
|
||||||
glob:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: glob
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.2.0"
|
|
||||||
graphs:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: graphs
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.0"
|
|
||||||
html:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: html
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.14.0+3"
|
|
||||||
html_unescape:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: html_unescape
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.1+3"
|
|
||||||
http:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: http
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.12.1"
|
|
||||||
http_multi_server:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: http_multi_server
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.2.0"
|
|
||||||
http_parser:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: http_parser
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.1.4"
|
|
||||||
io:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: io
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.3.4"
|
|
||||||
js:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: js
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.6.1+1"
|
|
||||||
json_annotation:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: json_annotation
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.1"
|
|
||||||
lcov:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: lcov
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "5.7.0"
|
|
||||||
logging:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: logging
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.11.4"
|
|
||||||
markdown:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: markdown
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.3"
|
|
||||||
matcher:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: matcher
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.12.6"
|
|
||||||
matrix_file_e2ee:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: matrix_file_e2ee
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.4"
|
|
||||||
meta:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: meta
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.8"
|
|
||||||
mime:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: mime
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.9.6+3"
|
|
||||||
moor:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: moor
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.2"
|
|
||||||
moor_ffi:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: moor_ffi
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.5.0"
|
|
||||||
moor_generator:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: moor_generator
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.0"
|
|
||||||
multi_server_socket:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: multi_server_socket
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.2"
|
|
||||||
node_interop:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: node_interop
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.1"
|
|
||||||
node_io:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: node_io
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.1"
|
|
||||||
node_preamble:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: node_preamble
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.8"
|
|
||||||
olm:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: olm
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.2.1"
|
|
||||||
package_config:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: package_config
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.9.3"
|
|
||||||
password_hash:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: password_hash
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.0"
|
|
||||||
path:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: path
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.7.0"
|
|
||||||
pedantic:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: pedantic
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.9.0"
|
|
||||||
pointycastle:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: pointycastle
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.2"
|
|
||||||
pool:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: pool
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.0"
|
|
||||||
pub_semver:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: pub_semver
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.4"
|
|
||||||
pubspec_parse:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: pubspec_parse
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.1.5"
|
|
||||||
quiver:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: quiver
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.3"
|
|
||||||
random_string:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: random_string
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.1"
|
|
||||||
recase:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: recase
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.0"
|
|
||||||
shelf:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shelf
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.7.5"
|
|
||||||
shelf_packages_handler:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shelf_packages_handler
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.0"
|
|
||||||
shelf_static:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shelf_static
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.8"
|
|
||||||
shelf_web_socket:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shelf_web_socket
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.3"
|
|
||||||
source_gen:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: source_gen
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.9.5"
|
|
||||||
source_map_stack_trace:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: source_map_stack_trace
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.0"
|
|
||||||
source_maps:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: source_maps
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.10.9"
|
|
||||||
source_span:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: source_span
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.7.0"
|
|
||||||
sqlparser:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: sqlparser
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.8.1"
|
|
||||||
stack_trace:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: stack_trace
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.9.3"
|
|
||||||
stream_channel:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: stream_channel
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.0"
|
|
||||||
stream_transform:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: stream_transform
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.2.0"
|
|
||||||
string_scanner:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: string_scanner
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.5"
|
|
||||||
synchronized:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: synchronized
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.2.0"
|
|
||||||
term_glyph:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: term_glyph
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.0"
|
|
||||||
test:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: test
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.14.3"
|
|
||||||
test_api:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: test_api
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.15"
|
|
||||||
test_core:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: test_core
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.3.4"
|
|
||||||
test_coverage:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: test_coverage
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.4.1"
|
|
||||||
timing:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: timing
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.1.1+2"
|
|
||||||
typed_data:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: typed_data
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.6"
|
|
||||||
unorm_dart:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: unorm_dart
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.1.2"
|
|
||||||
vm_service:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: vm_service
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "4.0.4"
|
|
||||||
watcher:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: watcher
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.9.7+15"
|
|
||||||
web_socket_channel:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: web_socket_channel
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.0"
|
|
||||||
webkit_inspection_protocol:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: webkit_inspection_protocol
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.5.4"
|
|
||||||
yaml:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: yaml
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.2.1"
|
|
||||||
sdks:
|
|
||||||
dart: ">=2.7.0 <3.0.0"
|
|
|
@ -21,10 +21,12 @@ dependencies:
|
||||||
password_hash: ^2.0.0
|
password_hash: ^2.0.0
|
||||||
olm: ^1.2.1
|
olm: ^1.2.1
|
||||||
matrix_file_e2ee: ^1.0.4
|
matrix_file_e2ee: ^1.0.4
|
||||||
|
ansicolor: ^1.0.2
|
||||||
|
isolate: ^2.0.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
test: ^1.0.0
|
test: ^1.0.0
|
||||||
test_coverage: ^0.4.1
|
test_coverage: ^0.4.3
|
||||||
moor_generator: ^3.0.0
|
moor_generator: ^3.0.0
|
||||||
build_runner: ^1.5.2
|
build_runner: ^1.5.2
|
||||||
pedantic: ^1.9.0
|
pedantic: ^1.9.0
|
||||||
|
|
4
test.sh
4
test.sh
|
@ -1,6 +1,6 @@
|
||||||
#!/bin/sh -e
|
#!/bin/sh -e
|
||||||
pub run test -p vm
|
# pub run test -p vm
|
||||||
pub run test_coverage
|
pub run test_coverage --print-test-output
|
||||||
pub global activate remove_from_coverage
|
pub global activate remove_from_coverage
|
||||||
pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '\.g\.dart$'
|
pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '\.g\.dart$'
|
||||||
genhtml -o coverage coverage/lcov.info || true
|
genhtml -o coverage coverage/lcov.info || true
|
||||||
|
|
|
@ -23,6 +23,7 @@ import 'package:famedlysdk/famedlysdk.dart';
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
import 'package:famedlysdk/matrix_api.dart';
|
||||||
import 'package:famedlysdk/src/client.dart';
|
import 'package:famedlysdk/src/client.dart';
|
||||||
import 'package:famedlysdk/src/utils/event_update.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/room_update.dart';
|
||||||
import 'package:famedlysdk/src/utils/matrix_file.dart';
|
import 'package:famedlysdk/src/utils/matrix_file.dart';
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
@ -45,10 +46,10 @@ void main() {
|
||||||
const fingerprintKey = 'gjL//fyaFHADt9KBADGag8g7F8Up78B/K1zXeiEPLJo';
|
const fingerprintKey = 'gjL//fyaFHADt9KBADGag8g7F8Up78B/K1zXeiEPLJo';
|
||||||
|
|
||||||
/// All Tests related to the Login
|
/// All Tests related to the Login
|
||||||
group('FluffyMatrix', () {
|
group('Client', () {
|
||||||
/// Check if all Elements get created
|
/// Check if all Elements get created
|
||||||
|
|
||||||
matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
matrix = Client('testclient', httpClient: FakeMatrixApi());
|
||||||
|
|
||||||
roomUpdateListFuture = matrix.onRoomUpdate.stream.toList();
|
roomUpdateListFuture = matrix.onRoomUpdate.stream.toList();
|
||||||
eventUpdateListFuture = matrix.onEvent.stream.toList();
|
eventUpdateListFuture = matrix.onEvent.stream.toList();
|
||||||
|
@ -59,9 +60,9 @@ void main() {
|
||||||
olm.Account();
|
olm.Account();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
olmEnabled = false;
|
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 {
|
test('Login', () async {
|
||||||
var presenceCounter = 0;
|
var presenceCounter = 0;
|
||||||
|
@ -73,7 +74,7 @@ void main() {
|
||||||
accountDataCounter++;
|
accountDataCounter++;
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(matrix.api.homeserver, null);
|
expect(matrix.homeserver, null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await matrix.checkServer('https://fakeserver.wrongaddress');
|
await matrix.checkServer('https://fakeserver.wrongaddress');
|
||||||
|
@ -81,17 +82,9 @@ void main() {
|
||||||
expect(exception != null, true);
|
expect(exception != null, true);
|
||||||
}
|
}
|
||||||
await matrix.checkServer('https://fakeserver.notexisting');
|
await matrix.checkServer('https://fakeserver.notexisting');
|
||||||
expect(
|
expect(matrix.homeserver.toString(), 'https://fakeserver.notexisting');
|
||||||
matrix.api.homeserver.toString(), 'https://fakeserver.notexisting');
|
|
||||||
|
|
||||||
final resp = await matrix.api.login(
|
final available = await matrix.usernameAvailable('testuser');
|
||||||
type: 'm.login.password',
|
|
||||||
user: 'test',
|
|
||||||
password: '1234',
|
|
||||||
initialDeviceDisplayName: 'Fluffy Matrix Client',
|
|
||||||
);
|
|
||||||
|
|
||||||
final available = await matrix.api.usernameAvailable('testuser');
|
|
||||||
expect(available, true);
|
expect(available, true);
|
||||||
|
|
||||||
var loginStateFuture = matrix.onLoginStateChanged.stream.first;
|
var loginStateFuture = matrix.onLoginStateChanged.stream.first;
|
||||||
|
@ -99,21 +92,16 @@ void main() {
|
||||||
var syncFuture = matrix.onSync.stream.first;
|
var syncFuture = matrix.onSync.stream.first;
|
||||||
|
|
||||||
matrix.connect(
|
matrix.connect(
|
||||||
newToken: resp.accessToken,
|
newToken: 'abcd',
|
||||||
newUserID: resp.userId,
|
newUserID: '@test:fakeServer.notExisting',
|
||||||
newHomeserver: matrix.api.homeserver,
|
newHomeserver: matrix.homeserver,
|
||||||
newDeviceName: 'Text Matrix Client',
|
newDeviceName: 'Text Matrix Client',
|
||||||
newDeviceID: resp.deviceId,
|
newDeviceID: 'GHTYAJCE',
|
||||||
newOlmAccount: pickledOlmAccount,
|
newOlmAccount: pickledOlmAccount,
|
||||||
);
|
);
|
||||||
|
|
||||||
await Future.delayed(Duration(milliseconds: 50));
|
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 loginState = await loginStateFuture;
|
||||||
var firstSync = await firstSyncFuture;
|
var firstSync = await firstSyncFuture;
|
||||||
var sync = await syncFuture;
|
var sync = await syncFuture;
|
||||||
|
@ -207,14 +195,11 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Logout', () async {
|
test('Logout', () async {
|
||||||
await matrix.api.logout();
|
|
||||||
|
|
||||||
var loginStateFuture = matrix.onLoginStateChanged.stream.first;
|
var loginStateFuture = matrix.onLoginStateChanged.stream.first;
|
||||||
|
await matrix.logout();
|
||||||
|
|
||||||
matrix.clear();
|
expect(matrix.accessToken == null, true);
|
||||||
|
expect(matrix.homeserver == null, true);
|
||||||
expect(matrix.api.accessToken == null, true);
|
|
||||||
expect(matrix.api.homeserver == null, true);
|
|
||||||
expect(matrix.userID == null, true);
|
expect(matrix.userID == null, true);
|
||||||
expect(matrix.deviceID == null, true);
|
expect(matrix.deviceID == null, true);
|
||||||
expect(matrix.deviceName == null, true);
|
expect(matrix.deviceName == null, true);
|
||||||
|
@ -322,17 +307,17 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Login', () async {
|
test('Login', () async {
|
||||||
matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
matrix = Client('testclient', httpClient: FakeMatrixApi());
|
||||||
|
|
||||||
roomUpdateListFuture = matrix.onRoomUpdate.stream.toList();
|
roomUpdateListFuture = matrix.onRoomUpdate.stream.toList();
|
||||||
eventUpdateListFuture = matrix.onEvent.stream.toList();
|
eventUpdateListFuture = matrix.onEvent.stream.toList();
|
||||||
final checkResp =
|
final checkResp =
|
||||||
await matrix.checkServer('https://fakeServer.notExisting');
|
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(checkResp, true);
|
||||||
expect(loginResp, true);
|
expect(loginResp != null, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('setAvatar', () async {
|
test('setAvatar', () async {
|
||||||
|
@ -385,8 +370,8 @@ void main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, matrix);
|
}, matrix);
|
||||||
test('sendToDevice', () async {
|
test('sendToDeviceEncrypted', () async {
|
||||||
await matrix.sendToDevice(
|
await matrix.sendToDeviceEncrypted(
|
||||||
[deviceKeys],
|
[deviceKeys],
|
||||||
'm.message',
|
'm.message',
|
||||||
{
|
{
|
||||||
|
@ -395,8 +380,7 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('Test the fake store api', () async {
|
test('Test the fake store api', () async {
|
||||||
var client1 =
|
var client1 = Client('testclient', httpClient: FakeMatrixApi());
|
||||||
Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
|
||||||
client1.database = getDatabase();
|
client1.database = getDatabase();
|
||||||
|
|
||||||
client1.connect(
|
client1.connect(
|
||||||
|
@ -413,17 +397,16 @@ void main() {
|
||||||
expect(client1.isLogged(), true);
|
expect(client1.isLogged(), true);
|
||||||
expect(client1.rooms.length, 2);
|
expect(client1.rooms.length, 2);
|
||||||
|
|
||||||
var client2 =
|
var client2 = Client('testclient', httpClient: FakeMatrixApi());
|
||||||
Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
|
||||||
client2.database = client1.database;
|
client2.database = client1.database;
|
||||||
|
|
||||||
client2.connect();
|
client2.connect();
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
|
|
||||||
expect(client2.isLogged(), true);
|
expect(client2.isLogged(), true);
|
||||||
expect(client2.api.accessToken, client1.api.accessToken);
|
expect(client2.accessToken, client1.accessToken);
|
||||||
expect(client2.userID, client1.userID);
|
expect(client2.userID, client1.userID);
|
||||||
expect(client2.api.homeserver, client1.api.homeserver);
|
expect(client2.homeserver, client1.homeserver);
|
||||||
expect(client2.deviceID, client1.deviceID);
|
expect(client2.deviceID, client1.deviceID);
|
||||||
expect(client2.deviceName, client1.deviceName);
|
expect(client2.deviceName, client1.deviceName);
|
||||||
if (client2.encryptionEnabled) {
|
if (client2.encryptionEnabled) {
|
||||||
|
@ -438,6 +421,20 @@ void main() {
|
||||||
test('changePassword', () async {
|
test('changePassword', () async {
|
||||||
await matrix.changePassword('1234', oldPassword: '123456');
|
await matrix.changePassword('1234', oldPassword: '123456');
|
||||||
});
|
});
|
||||||
|
test('ignoredUsers', () async {
|
||||||
|
expect(matrix.ignoredUsers, []);
|
||||||
|
matrix.accountData['m.ignored_user_list'] =
|
||||||
|
BasicEvent(type: 'm.ignored_user_list', content: {
|
||||||
|
'ignored_users': {
|
||||||
|
'@charley:stupid.abc': {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(matrix.ignoredUsers, ['@charley:stupid.abc']);
|
||||||
|
});
|
||||||
|
test('ignoredUsers', () async {
|
||||||
|
await matrix.ignoreUser('@charley2:stupid.abc');
|
||||||
|
await matrix.unignoreUser('@charley:stupid.abc');
|
||||||
|
});
|
||||||
|
|
||||||
test('dispose', () async {
|
test('dispose', () async {
|
||||||
await matrix.dispose(closeDatabase: true);
|
await matrix.dispose(closeDatabase: true);
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:famedlysdk/src/utils/logs.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
|
@ -74,9 +75,9 @@ void main() {
|
||||||
olm.Account();
|
olm.Account();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
olmEnabled = false;
|
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;
|
if (!olmEnabled) return;
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:famedlysdk/src/utils/logs.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
|
@ -33,9 +34,9 @@ void main() {
|
||||||
olm.Account();
|
olm.Account();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
olmEnabled = false;
|
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;
|
if (!olmEnabled) return;
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:famedlysdk/src/utils/logs.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
|
@ -30,9 +31,9 @@ void main() {
|
||||||
olm.Account();
|
olm.Account();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
olmEnabled = false;
|
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;
|
if (!olmEnabled) return;
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:famedlysdk/src/utils/logs.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
|
@ -35,15 +36,14 @@ void main() {
|
||||||
olm.Account();
|
olm.Account();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
olmEnabled = false;
|
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;
|
if (!olmEnabled) return;
|
||||||
|
|
||||||
Client client;
|
Client client;
|
||||||
var otherClient =
|
var otherClient = Client('othertestclient', httpClient: FakeMatrixApi());
|
||||||
Client('othertestclient', debug: true, httpClient: FakeMatrixApi());
|
|
||||||
DeviceKeys device;
|
DeviceKeys device;
|
||||||
Map<String, dynamic> payload;
|
Map<String, dynamic> payload;
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ void main() {
|
||||||
otherClient.connect(
|
otherClient.connect(
|
||||||
newToken: 'abc',
|
newToken: 'abc',
|
||||||
newUserID: '@othertest:fakeServer.notExisting',
|
newUserID: '@othertest:fakeServer.notExisting',
|
||||||
newHomeserver: otherClient.api.homeserver,
|
newHomeserver: otherClient.homeserver,
|
||||||
newDeviceName: 'Text Matrix Client',
|
newDeviceName: 'Text Matrix Client',
|
||||||
newDeviceID: 'FOXDEVICE',
|
newDeviceID: 'FOXDEVICE',
|
||||||
newOlmAccount: otherPickledOlmAccount,
|
newOlmAccount: otherPickledOlmAccount,
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:famedlysdk/src/utils/logs.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
|
@ -30,9 +31,9 @@ void main() {
|
||||||
olm.Account();
|
olm.Account();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
olmEnabled = false;
|
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;
|
if (!olmEnabled) return;
|
||||||
|
|
||||||
|
@ -59,7 +60,7 @@ void main() {
|
||||||
'session_key': sessionKey,
|
'session_key': sessionKey,
|
||||||
},
|
},
|
||||||
encryptedContent: {
|
encryptedContent: {
|
||||||
'sender_key': validSessionId,
|
'sender_key': validSenderKey,
|
||||||
});
|
});
|
||||||
await client.encryption.keyManager.handleToDeviceEvent(event);
|
await client.encryption.keyManager.handleToDeviceEvent(event);
|
||||||
expect(
|
expect(
|
||||||
|
@ -184,6 +185,11 @@ void main() {
|
||||||
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
||||||
null,
|
null,
|
||||||
true);
|
true);
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager
|
||||||
|
.getInboundGroupSession(roomId, sessionId, 'invalid') !=
|
||||||
|
null,
|
||||||
|
false);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
client.encryption.keyManager
|
client.encryption.keyManager
|
||||||
|
@ -195,6 +201,11 @@ void main() {
|
||||||
.getInboundGroupSession('otherroom', sessionId, senderKey) !=
|
.getInboundGroupSession('otherroom', sessionId, senderKey) !=
|
||||||
null,
|
null,
|
||||||
true);
|
true);
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager
|
||||||
|
.getInboundGroupSession('otherroom', sessionId, 'invalid') !=
|
||||||
|
null,
|
||||||
|
false);
|
||||||
expect(
|
expect(
|
||||||
client.encryption.keyManager
|
client.encryption.keyManager
|
||||||
.getInboundGroupSession('otherroom', 'invalid', senderKey) !=
|
.getInboundGroupSession('otherroom', 'invalid', senderKey) !=
|
||||||
|
@ -214,6 +225,20 @@ void main() {
|
||||||
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
||||||
null,
|
null,
|
||||||
true);
|
true);
|
||||||
|
|
||||||
|
client.encryption.keyManager.clearInboundGroupSessions();
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager
|
||||||
|
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
||||||
|
null,
|
||||||
|
false);
|
||||||
|
await client.encryption.keyManager
|
||||||
|
.loadInboundGroupSession(roomId, sessionId, 'invalid');
|
||||||
|
expect(
|
||||||
|
client.encryption.keyManager
|
||||||
|
.getInboundGroupSession(roomId, sessionId, 'invalid') !=
|
||||||
|
null,
|
||||||
|
false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('setInboundGroupSession', () async {
|
test('setInboundGroupSession', () async {
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:famedlysdk/src/utils/logs.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
|
@ -45,14 +46,14 @@ void main() {
|
||||||
olm.Account();
|
olm.Account();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
olmEnabled = false;
|
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;
|
if (!olmEnabled) return;
|
||||||
|
|
||||||
final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
|
final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
|
||||||
final validSenderKey = '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI';
|
final validSenderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg';
|
||||||
test('Create Request', () async {
|
test('Create Request', () async {
|
||||||
var matrix = await getClient();
|
var matrix = await getClient();
|
||||||
final requestRoom = matrix.getRoomById('!726s6s6q:example.com');
|
final requestRoom = matrix.getRoomById('!726s6s6q:example.com');
|
||||||
|
@ -106,7 +107,7 @@ void main() {
|
||||||
'requesting_device_id': 'OTHERDEVICE',
|
'requesting_device_id': 'OTHERDEVICE',
|
||||||
});
|
});
|
||||||
await matrix.encryption.keyManager.handleToDeviceEvent(event);
|
await matrix.encryption.keyManager.handleToDeviceEvent(event);
|
||||||
print(FakeMatrixApi.calledEndpoints.keys.toString());
|
Logs.info(FakeMatrixApi.calledEndpoints.keys.toString());
|
||||||
expect(
|
expect(
|
||||||
FakeMatrixApi.calledEndpoints.keys.any(
|
FakeMatrixApi.calledEndpoints.keys.any(
|
||||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||||
|
|
|
@ -20,6 +20,7 @@ import 'dart:convert';
|
||||||
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
import 'package:famedlysdk/encryption.dart';
|
import 'package:famedlysdk/encryption.dart';
|
||||||
|
import 'package:famedlysdk/src/utils/logs.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
|
@ -31,7 +32,7 @@ class MockSSSS extends SSSS {
|
||||||
|
|
||||||
bool requestedSecrets = false;
|
bool requestedSecrets = false;
|
||||||
@override
|
@override
|
||||||
Future<void> maybeRequestAll(List<DeviceKeys> devices) async {
|
Future<void> maybeRequestAll([List<DeviceKeys> devices]) async {
|
||||||
requestedSecrets = true;
|
requestedSecrets = true;
|
||||||
final handle = open();
|
final handle = open();
|
||||||
handle.unlock(recoveryKey: SSSS_KEY);
|
handle.unlock(recoveryKey: SSSS_KEY);
|
||||||
|
@ -67,9 +68,9 @@ void main() {
|
||||||
olm.Account();
|
olm.Account();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
olmEnabled = false;
|
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;
|
if (!olmEnabled) return;
|
||||||
|
|
||||||
|
@ -82,14 +83,13 @@ void main() {
|
||||||
|
|
||||||
test('setupClient', () async {
|
test('setupClient', () async {
|
||||||
client1 = await getClient();
|
client1 = await getClient();
|
||||||
client2 =
|
client2 = Client('othertestclient', httpClient: FakeMatrixApi());
|
||||||
Client('othertestclient', debug: true, httpClient: FakeMatrixApi());
|
|
||||||
client2.database = client1.database;
|
client2.database = client1.database;
|
||||||
await client2.checkServer('https://fakeServer.notExisting');
|
await client2.checkServer('https://fakeServer.notExisting');
|
||||||
client2.connect(
|
client2.connect(
|
||||||
newToken: 'abc',
|
newToken: 'abc',
|
||||||
newUserID: '@othertest:fakeServer.notExisting',
|
newUserID: '@othertest:fakeServer.notExisting',
|
||||||
newHomeserver: client2.api.homeserver,
|
newHomeserver: client2.homeserver,
|
||||||
newDeviceName: 'Text Matrix Client',
|
newDeviceName: 'Text Matrix Client',
|
||||||
newDeviceID: 'FOXDEVICE',
|
newDeviceID: 'FOXDEVICE',
|
||||||
newOlmAccount: otherPickledOlmAccount,
|
newOlmAccount: otherPickledOlmAccount,
|
||||||
|
@ -207,7 +207,7 @@ void main() {
|
||||||
|
|
||||||
test('ask SSSS start', () async {
|
test('ask SSSS start', () async {
|
||||||
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(true);
|
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(true);
|
||||||
await client1.database.clearSSSSCache(client1.id);
|
await client1.encryption.ssss.clearCache();
|
||||||
final req1 =
|
final req1 =
|
||||||
await client1.userDeviceKeys[client2.userID].startVerification();
|
await client1.userDeviceKeys[client2.userID].startVerification();
|
||||||
expect(req1.state, KeyVerificationState.askSSSS);
|
expect(req1.state, KeyVerificationState.askSSSS);
|
||||||
|
@ -288,7 +288,7 @@ void main() {
|
||||||
|
|
||||||
// alright, they match
|
// alright, they match
|
||||||
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(true);
|
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(true);
|
||||||
await client1.database.clearSSSSCache(client1.id);
|
await client1.encryption.ssss.clearCache();
|
||||||
|
|
||||||
// send mac
|
// send mac
|
||||||
FakeMatrixApi.calledEndpoints.clear();
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
@ -312,7 +312,7 @@ void main() {
|
||||||
|
|
||||||
client1.encryption.ssss = MockSSSS(client1.encryption);
|
client1.encryption.ssss = MockSSSS(client1.encryption);
|
||||||
(client1.encryption.ssss as MockSSSS).requestedSecrets = false;
|
(client1.encryption.ssss as MockSSSS).requestedSecrets = false;
|
||||||
await client1.database.clearSSSSCache(client1.id);
|
await client1.encryption.ssss.clearCache();
|
||||||
await req1.maybeRequestSSSSSecrets();
|
await req1.maybeRequestSSSSSecrets();
|
||||||
await Future.delayed(Duration(milliseconds: 10));
|
await Future.delayed(Duration(milliseconds: 10));
|
||||||
expect((client1.encryption.ssss as MockSSSS).requestedSecrets, true);
|
expect((client1.encryption.ssss as MockSSSS).requestedSecrets, true);
|
||||||
|
|
|
@ -18,8 +18,10 @@
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:famedlysdk/src/utils/logs.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
import 'package:famedlysdk/encryption/utils/json_signature_check_extension.dart';
|
||||||
|
|
||||||
import '../fake_client.dart';
|
import '../fake_client.dart';
|
||||||
import '../fake_matrix_api.dart';
|
import '../fake_matrix_api.dart';
|
||||||
|
@ -32,9 +34,9 @@ void main() {
|
||||||
olm.Account();
|
olm.Account();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
olmEnabled = false;
|
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;
|
if (!olmEnabled) return;
|
||||||
|
|
||||||
|
@ -50,13 +52,9 @@ void main() {
|
||||||
};
|
};
|
||||||
final signedPayload = client.encryption.olmManager.signJson(payload);
|
final signedPayload = client.encryption.olmManager.signJson(payload);
|
||||||
expect(
|
expect(
|
||||||
client.encryption.olmManager.checkJsonSignature(client.fingerprintKey,
|
signedPayload.checkJsonSignature(
|
||||||
signedPayload, client.userID, client.deviceID),
|
client.fingerprintKey, client.userID, client.deviceID),
|
||||||
true);
|
true);
|
||||||
expect(
|
|
||||||
client.encryption.olmManager.checkJsonSignature(
|
|
||||||
client.fingerprintKey, payload, client.userID, client.deviceID),
|
|
||||||
false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('uploadKeys', () async {
|
test('uploadKeys', () async {
|
||||||
|
|
|
@ -16,11 +16,16 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:famedlysdk/src/utils/logs.dart';
|
||||||
|
import 'package:famedlysdk/matrix_api.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
import '../fake_client.dart';
|
import '../fake_client.dart';
|
||||||
|
import '../fake_matrix_api.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('Online Key Backup', () {
|
group('Online Key Backup', () {
|
||||||
|
@ -30,9 +35,9 @@ void main() {
|
||||||
olm.Account();
|
olm.Account();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
olmEnabled = false;
|
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;
|
if (!olmEnabled) return;
|
||||||
|
|
||||||
|
@ -66,6 +71,49 @@ void main() {
|
||||||
true);
|
true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('upload key', () async {
|
||||||
|
final session = olm.OutboundGroupSession();
|
||||||
|
session.create();
|
||||||
|
final inbound = olm.InboundGroupSession();
|
||||||
|
inbound.create(session.session_key());
|
||||||
|
final senderKey = client.identityKey;
|
||||||
|
final roomId = '!someroom:example.org';
|
||||||
|
final sessionId = inbound.session_id();
|
||||||
|
// set a payload...
|
||||||
|
var sessionPayload = <String, dynamic>{
|
||||||
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||||
|
'room_id': roomId,
|
||||||
|
'forwarding_curve25519_key_chain': [client.identityKey],
|
||||||
|
'session_id': sessionId,
|
||||||
|
'session_key': inbound.export_session(1),
|
||||||
|
'sender_key': senderKey,
|
||||||
|
'sender_claimed_ed25519_key': client.fingerprintKey,
|
||||||
|
};
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
client.encryption.keyManager.setInboundGroupSession(
|
||||||
|
roomId, sessionId, senderKey, sessionPayload,
|
||||||
|
forwarded: true);
|
||||||
|
var dbSessions =
|
||||||
|
await client.database.getInboundGroupSessionsToUpload().get();
|
||||||
|
expect(dbSessions.isNotEmpty, true);
|
||||||
|
await client.encryption.keyManager.backgroundTasks();
|
||||||
|
final payload = FakeMatrixApi
|
||||||
|
.calledEndpoints['/client/unstable/room_keys/keys?version=5'].first;
|
||||||
|
dbSessions =
|
||||||
|
await client.database.getInboundGroupSessionsToUpload().get();
|
||||||
|
expect(dbSessions.isEmpty, true);
|
||||||
|
|
||||||
|
final onlineKeys = RoomKeys.fromJson(json.decode(payload));
|
||||||
|
client.encryption.keyManager.clearInboundGroupSessions();
|
||||||
|
var ret = client.encryption.keyManager
|
||||||
|
.getInboundGroupSession(roomId, sessionId, senderKey);
|
||||||
|
expect(ret, null);
|
||||||
|
await client.encryption.keyManager.loadFromResponse(onlineKeys);
|
||||||
|
ret = client.encryption.keyManager
|
||||||
|
.getInboundGroupSession(roomId, sessionId, senderKey);
|
||||||
|
expect(ret != null, true);
|
||||||
|
});
|
||||||
|
|
||||||
test('dispose client', () async {
|
test('dispose client', () async {
|
||||||
await client.dispose(closeDatabase: true);
|
await client.dispose(closeDatabase: true);
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,6 +22,7 @@ import 'dart:convert';
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
import 'package:famedlysdk/matrix_api.dart';
|
||||||
import 'package:famedlysdk/encryption.dart';
|
import 'package:famedlysdk/encryption.dart';
|
||||||
|
import 'package:famedlysdk/src/utils/logs.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:encrypt/encrypt.dart';
|
import 'package:encrypt/encrypt.dart';
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
@ -29,6 +30,19 @@ import 'package:olm/olm.dart' as olm;
|
||||||
import '../fake_client.dart';
|
import '../fake_client.dart';
|
||||||
import '../fake_matrix_api.dart';
|
import '../fake_matrix_api.dart';
|
||||||
|
|
||||||
|
class MockSSSS extends SSSS {
|
||||||
|
MockSSSS(Encryption encryption) : super(encryption);
|
||||||
|
|
||||||
|
bool requestedSecrets = false;
|
||||||
|
@override
|
||||||
|
Future<void> maybeRequestAll([List<DeviceKeys> devices]) async {
|
||||||
|
requestedSecrets = true;
|
||||||
|
final handle = open();
|
||||||
|
handle.unlock(recoveryKey: SSSS_KEY);
|
||||||
|
await handle.maybeCacheAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('SSSS', () {
|
group('SSSS', () {
|
||||||
var olmEnabled = true;
|
var olmEnabled = true;
|
||||||
|
@ -37,9 +51,9 @@ void main() {
|
||||||
olm.Account();
|
olm.Account();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
olmEnabled = false;
|
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;
|
if (!olmEnabled) return;
|
||||||
|
|
||||||
|
@ -89,7 +103,7 @@ void main() {
|
||||||
// account_data for this test
|
// account_data for this test
|
||||||
final content = FakeMatrixApi
|
final content = FakeMatrixApi
|
||||||
.calledEndpoints[
|
.calledEndpoints[
|
||||||
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best+animal']
|
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best%20animal']
|
||||||
.first;
|
.first;
|
||||||
client.accountData['best animal'] = BasicEvent.fromJson({
|
client.accountData['best animal'] = BasicEvent.fromJson({
|
||||||
'type': 'best animal',
|
'type': 'best animal',
|
||||||
|
@ -247,7 +261,7 @@ void main() {
|
||||||
client.encryption.ssss.open('m.cross_signing.self_signing');
|
client.encryption.ssss.open('m.cross_signing.self_signing');
|
||||||
handle.unlock(recoveryKey: SSSS_KEY);
|
handle.unlock(recoveryKey: SSSS_KEY);
|
||||||
|
|
||||||
await client.database.clearSSSSCache(client.id);
|
await client.encryption.ssss.clearCache();
|
||||||
client.encryption.ssss.pendingShareRequests.clear();
|
client.encryption.ssss.pendingShareRequests.clear();
|
||||||
await client.encryption.ssss.request('best animal', [key]);
|
await client.encryption.ssss.request('best animal', [key]);
|
||||||
var event = ToDeviceEvent(
|
var event = ToDeviceEvent(
|
||||||
|
@ -271,7 +285,7 @@ void main() {
|
||||||
'm.megolm_backup.v1'
|
'm.megolm_backup.v1'
|
||||||
]) {
|
]) {
|
||||||
final secret = await handle.getStored(type);
|
final secret = await handle.getStored(type);
|
||||||
await client.database.clearSSSSCache(client.id);
|
await client.encryption.ssss.clearCache();
|
||||||
client.encryption.ssss.pendingShareRequests.clear();
|
client.encryption.ssss.pendingShareRequests.clear();
|
||||||
await client.encryption.ssss.request(type, [key]);
|
await client.encryption.ssss.request(type, [key]);
|
||||||
event = ToDeviceEvent(
|
event = ToDeviceEvent(
|
||||||
|
@ -293,7 +307,7 @@ void main() {
|
||||||
// test different fail scenarios
|
// test different fail scenarios
|
||||||
|
|
||||||
// not encrypted
|
// not encrypted
|
||||||
await client.database.clearSSSSCache(client.id);
|
await client.encryption.ssss.clearCache();
|
||||||
client.encryption.ssss.pendingShareRequests.clear();
|
client.encryption.ssss.pendingShareRequests.clear();
|
||||||
await client.encryption.ssss.request('best animal', [key]);
|
await client.encryption.ssss.request('best animal', [key]);
|
||||||
event = ToDeviceEvent(
|
event = ToDeviceEvent(
|
||||||
|
@ -308,7 +322,7 @@ void main() {
|
||||||
expect(await client.encryption.ssss.getCached('best animal'), null);
|
expect(await client.encryption.ssss.getCached('best animal'), null);
|
||||||
|
|
||||||
// unknown request id
|
// unknown request id
|
||||||
await client.database.clearSSSSCache(client.id);
|
await client.encryption.ssss.clearCache();
|
||||||
client.encryption.ssss.pendingShareRequests.clear();
|
client.encryption.ssss.pendingShareRequests.clear();
|
||||||
await client.encryption.ssss.request('best animal', [key]);
|
await client.encryption.ssss.request('best animal', [key]);
|
||||||
event = ToDeviceEvent(
|
event = ToDeviceEvent(
|
||||||
|
@ -326,7 +340,7 @@ void main() {
|
||||||
expect(await client.encryption.ssss.getCached('best animal'), null);
|
expect(await client.encryption.ssss.getCached('best animal'), null);
|
||||||
|
|
||||||
// not from a device we sent the request to
|
// not from a device we sent the request to
|
||||||
await client.database.clearSSSSCache(client.id);
|
await client.encryption.ssss.clearCache();
|
||||||
client.encryption.ssss.pendingShareRequests.clear();
|
client.encryption.ssss.pendingShareRequests.clear();
|
||||||
await client.encryption.ssss.request('best animal', [key]);
|
await client.encryption.ssss.request('best animal', [key]);
|
||||||
event = ToDeviceEvent(
|
event = ToDeviceEvent(
|
||||||
|
@ -344,7 +358,7 @@ void main() {
|
||||||
expect(await client.encryption.ssss.getCached('best animal'), null);
|
expect(await client.encryption.ssss.getCached('best animal'), null);
|
||||||
|
|
||||||
// secret not a string
|
// secret not a string
|
||||||
await client.database.clearSSSSCache(client.id);
|
await client.encryption.ssss.clearCache();
|
||||||
client.encryption.ssss.pendingShareRequests.clear();
|
client.encryption.ssss.pendingShareRequests.clear();
|
||||||
await client.encryption.ssss.request('best animal', [key]);
|
await client.encryption.ssss.request('best animal', [key]);
|
||||||
event = ToDeviceEvent(
|
event = ToDeviceEvent(
|
||||||
|
@ -362,7 +376,7 @@ void main() {
|
||||||
expect(await client.encryption.ssss.getCached('best animal'), null);
|
expect(await client.encryption.ssss.getCached('best animal'), null);
|
||||||
|
|
||||||
// validator doesn't check out
|
// validator doesn't check out
|
||||||
await client.database.clearSSSSCache(client.id);
|
await client.encryption.ssss.clearCache();
|
||||||
client.encryption.ssss.pendingShareRequests.clear();
|
client.encryption.ssss.pendingShareRequests.clear();
|
||||||
await client.encryption.ssss.request('m.megolm_backup.v1', [key]);
|
await client.encryption.ssss.request('m.megolm_backup.v1', [key]);
|
||||||
event = ToDeviceEvent(
|
event = ToDeviceEvent(
|
||||||
|
@ -385,12 +399,24 @@ void main() {
|
||||||
final key =
|
final key =
|
||||||
client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE'];
|
client.userDeviceKeys[client.userID].deviceKeys['OTHERDEVICE'];
|
||||||
key.setDirectVerified(true);
|
key.setDirectVerified(true);
|
||||||
await client.database.clearSSSSCache(client.id);
|
await client.encryption.ssss.clearCache();
|
||||||
client.encryption.ssss.pendingShareRequests.clear();
|
client.encryption.ssss.pendingShareRequests.clear();
|
||||||
await client.encryption.ssss.maybeRequestAll([key]);
|
await client.encryption.ssss.maybeRequestAll([key]);
|
||||||
expect(client.encryption.ssss.pendingShareRequests.length, 3);
|
expect(client.encryption.ssss.pendingShareRequests.length, 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('periodicallyRequestMissingCache', () async {
|
||||||
|
client.userDeviceKeys[client.userID].masterKey.setDirectVerified(true);
|
||||||
|
client.encryption.ssss = MockSSSS(client.encryption);
|
||||||
|
(client.encryption.ssss as MockSSSS).requestedSecrets = false;
|
||||||
|
await client.encryption.ssss.periodicallyRequestMissingCache();
|
||||||
|
expect((client.encryption.ssss as MockSSSS).requestedSecrets, true);
|
||||||
|
// it should only retry once every 15 min
|
||||||
|
(client.encryption.ssss as MockSSSS).requestedSecrets = false;
|
||||||
|
await client.encryption.ssss.periodicallyRequestMissingCache();
|
||||||
|
expect((client.encryption.ssss as MockSSSS).requestedSecrets, false);
|
||||||
|
});
|
||||||
|
|
||||||
test('dispose client', () async {
|
test('dispose client', () async {
|
||||||
await client.dispose(closeDatabase: true);
|
await client.dispose(closeDatabase: true);
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,19 +17,33 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
import 'package:famedlysdk/matrix_api.dart';
|
||||||
import 'package:famedlysdk/encryption.dart';
|
import 'package:famedlysdk/encryption.dart';
|
||||||
import 'package:famedlysdk/src/event.dart';
|
import 'package:famedlysdk/src/event.dart';
|
||||||
|
import 'package:famedlysdk/src/utils/logs.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
|
import 'fake_client.dart';
|
||||||
import 'fake_matrix_api.dart';
|
import 'fake_matrix_api.dart';
|
||||||
import 'fake_matrix_localizations.dart';
|
import 'fake_matrix_localizations.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
/// All Tests related to the Event
|
/// All Tests related to the Event
|
||||||
group('Event', () {
|
group('Event', () {
|
||||||
|
var olmEnabled = true;
|
||||||
|
try {
|
||||||
|
olm.init();
|
||||||
|
olm.Account();
|
||||||
|
} catch (_) {
|
||||||
|
olmEnabled = false;
|
||||||
|
Logs.warning('[LibOlm] Failed to load LibOlm: ' + _.toString());
|
||||||
|
}
|
||||||
|
Logs.success('[LibOlm] Enabled: $olmEnabled');
|
||||||
|
|
||||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
final id = '!4fsdfjisjf:server.abc';
|
final id = '!4fsdfjisjf:server.abc';
|
||||||
final senderID = '@alice:server.abc';
|
final senderID = '@alice:server.abc';
|
||||||
|
@ -50,7 +64,7 @@ void main() {
|
||||||
'status': 2,
|
'status': 2,
|
||||||
'content': contentJson,
|
'content': contentJson,
|
||||||
};
|
};
|
||||||
var client = Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
var client = Client('testclient', httpClient: FakeMatrixApi());
|
||||||
var event = Event.fromJson(
|
var event = Event.fromJson(
|
||||||
jsonObj, Room(id: '!localpart:server.abc', client: client));
|
jsonObj, Room(id: '!localpart:server.abc', client: client));
|
||||||
|
|
||||||
|
@ -67,7 +81,7 @@ void main() {
|
||||||
expect(event.formattedText, formatted_body);
|
expect(event.formattedText, formatted_body);
|
||||||
expect(event.body, body);
|
expect(event.body, body);
|
||||||
expect(event.type, EventTypes.Message);
|
expect(event.type, EventTypes.Message);
|
||||||
expect(event.isReply, true);
|
expect(event.relationshipType, RelationshipTypes.Reply);
|
||||||
jsonObj['state_key'] = '';
|
jsonObj['state_key'] = '';
|
||||||
var state = Event.fromJson(jsonObj, null);
|
var state = Event.fromJson(jsonObj, null);
|
||||||
expect(state.eventId, id);
|
expect(state.eventId, id);
|
||||||
|
@ -153,6 +167,11 @@ void main() {
|
||||||
event = Event.fromJson(jsonObj, null);
|
event = Event.fromJson(jsonObj, null);
|
||||||
expect(event.messageType, MessageTypes.Location);
|
expect(event.messageType, MessageTypes.Location);
|
||||||
|
|
||||||
|
jsonObj['type'] = 'm.sticker';
|
||||||
|
jsonObj['content']['msgtype'] = null;
|
||||||
|
event = Event.fromJson(jsonObj, null);
|
||||||
|
expect(event.messageType, MessageTypes.Sticker);
|
||||||
|
|
||||||
jsonObj['type'] = 'm.room.message';
|
jsonObj['type'] = 'm.room.message';
|
||||||
jsonObj['content']['msgtype'] = 'm.text';
|
jsonObj['content']['msgtype'] = 'm.text';
|
||||||
jsonObj['content']['m.relates_to'] = {};
|
jsonObj['content']['m.relates_to'] = {};
|
||||||
|
@ -160,7 +179,43 @@ void main() {
|
||||||
'event_id': '1234',
|
'event_id': '1234',
|
||||||
};
|
};
|
||||||
event = Event.fromJson(jsonObj, null);
|
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 {
|
test('redact', () async {
|
||||||
|
@ -175,8 +230,7 @@ void main() {
|
||||||
];
|
];
|
||||||
for (final testType in testTypes) {
|
for (final testType in testTypes) {
|
||||||
redactJsonObj['type'] = testType;
|
redactJsonObj['type'] = testType;
|
||||||
final room =
|
final room = Room(id: '1234', client: Client('testclient'));
|
||||||
Room(id: '1234', client: Client('testclient', debug: true));
|
|
||||||
final redactionEventJson = {
|
final redactionEventJson = {
|
||||||
'content': {'reason': 'Spamming'},
|
'content': {'reason': 'Spamming'},
|
||||||
'event_id': '143273582443PhrSn:example.org',
|
'event_id': '143273582443PhrSn:example.org',
|
||||||
|
@ -200,7 +254,7 @@ void main() {
|
||||||
|
|
||||||
test('remove', () async {
|
test('remove', () async {
|
||||||
var event = Event.fromJson(
|
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();
|
final removed1 = await event.remove();
|
||||||
event.status = 0;
|
event.status = 0;
|
||||||
final removed2 = await event.remove();
|
final removed2 = await event.remove();
|
||||||
|
@ -209,10 +263,9 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sendAgain', () async {
|
test('sendAgain', () async {
|
||||||
var matrix =
|
var matrix = Client('testclient', httpClient: FakeMatrixApi());
|
||||||
Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
|
||||||
await matrix.checkServer('https://fakeServer.notExisting');
|
await matrix.checkServer('https://fakeServer.notExisting');
|
||||||
await matrix.login('test', '1234');
|
await matrix.login(user: 'test', password: '1234');
|
||||||
|
|
||||||
var event = Event.fromJson(
|
var event = Event.fromJson(
|
||||||
jsonObj, Room(id: '!1234:example.com', client: matrix));
|
jsonObj, Room(id: '!1234:example.com', client: matrix));
|
||||||
|
@ -226,10 +279,9 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('requestKey', () async {
|
test('requestKey', () async {
|
||||||
var matrix =
|
var matrix = Client('testclient', httpClient: FakeMatrixApi());
|
||||||
Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
|
||||||
await matrix.checkServer('https://fakeServer.notExisting');
|
await matrix.checkServer('https://fakeServer.notExisting');
|
||||||
await matrix.login('test', '1234');
|
await matrix.login(user: 'test', password: '1234');
|
||||||
|
|
||||||
var event = Event.fromJson(
|
var event = Event.fromJson(
|
||||||
jsonObj, Room(id: '!1234:example.com', client: matrix));
|
jsonObj, Room(id: '!1234:example.com', client: matrix));
|
||||||
|
@ -274,8 +326,7 @@ void main() {
|
||||||
expect(event.canRedact, true);
|
expect(event.canRedact, true);
|
||||||
});
|
});
|
||||||
test('getLocalizedBody', () async {
|
test('getLocalizedBody', () async {
|
||||||
final matrix =
|
final matrix = Client('testclient', httpClient: FakeMatrixApi());
|
||||||
Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
|
||||||
final room = Room(id: '!1234:example.com', client: matrix);
|
final room = Room(id: '!1234:example.com', client: matrix);
|
||||||
var event = Event.fromJson({
|
var event = Event.fromJson({
|
||||||
'content': {
|
'content': {
|
||||||
|
@ -790,5 +841,444 @@ void main() {
|
||||||
}, room);
|
}, room);
|
||||||
expect(event.getLocalizedBody(FakeMatrixLocalizations()), null);
|
expect(event.getLocalizedBody(FakeMatrixLocalizations()), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('aggregations', () {
|
||||||
|
var event = Event.fromJson({
|
||||||
|
'content': {
|
||||||
|
'body': 'blah',
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
},
|
||||||
|
'event_id': '\$source',
|
||||||
|
}, null);
|
||||||
|
var edit1 = Event.fromJson({
|
||||||
|
'content': {
|
||||||
|
'body': 'blah',
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'm.relates_to': {
|
||||||
|
'event_id': '\$source',
|
||||||
|
'rel_type': RelationshipTypes.Edit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'event_id': '\$edit1',
|
||||||
|
}, null);
|
||||||
|
var edit2 = Event.fromJson({
|
||||||
|
'content': {
|
||||||
|
'body': 'blah',
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'm.relates_to': {
|
||||||
|
'event_id': '\$source',
|
||||||
|
'rel_type': RelationshipTypes.Edit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'event_id': '\$edit2',
|
||||||
|
}, null);
|
||||||
|
var room = Room(client: client);
|
||||||
|
var timeline = Timeline(events: <Event>[event, edit1, edit2], room: room);
|
||||||
|
expect(event.hasAggregatedEvents(timeline, RelationshipTypes.Edit), true);
|
||||||
|
expect(event.aggregatedEvents(timeline, RelationshipTypes.Edit),
|
||||||
|
{edit1, edit2});
|
||||||
|
expect(event.aggregatedEvents(timeline, RelationshipTypes.Reaction),
|
||||||
|
<Event>{});
|
||||||
|
expect(event.hasAggregatedEvents(timeline, RelationshipTypes.Reaction),
|
||||||
|
false);
|
||||||
|
|
||||||
|
timeline.removeAggregatedEvent(edit2);
|
||||||
|
expect(event.aggregatedEvents(timeline, RelationshipTypes.Edit), {edit1});
|
||||||
|
timeline.addAggregatedEvent(edit2);
|
||||||
|
expect(event.aggregatedEvents(timeline, RelationshipTypes.Edit),
|
||||||
|
{edit1, edit2});
|
||||||
|
|
||||||
|
timeline.removeAggregatedEvent(event);
|
||||||
|
expect(
|
||||||
|
event.aggregatedEvents(timeline, RelationshipTypes.Edit), <Event>{});
|
||||||
|
});
|
||||||
|
test('getDisplayEvent', () {
|
||||||
|
var event = Event.fromJson({
|
||||||
|
'type': EventTypes.Message,
|
||||||
|
'content': {
|
||||||
|
'body': 'blah',
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
},
|
||||||
|
'event_id': '\$source',
|
||||||
|
'sender': '@alice:example.org',
|
||||||
|
}, null);
|
||||||
|
event.sortOrder = 0;
|
||||||
|
var edit1 = Event.fromJson({
|
||||||
|
'type': EventTypes.Message,
|
||||||
|
'content': {
|
||||||
|
'body': '* edit 1',
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'm.new_content': {
|
||||||
|
'body': 'edit 1',
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
},
|
||||||
|
'm.relates_to': {
|
||||||
|
'event_id': '\$source',
|
||||||
|
'rel_type': RelationshipTypes.Edit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'event_id': '\$edit1',
|
||||||
|
'sender': '@alice:example.org',
|
||||||
|
}, null);
|
||||||
|
edit1.sortOrder = 1;
|
||||||
|
var edit2 = Event.fromJson({
|
||||||
|
'type': EventTypes.Message,
|
||||||
|
'content': {
|
||||||
|
'body': '* edit 2',
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'm.new_content': {
|
||||||
|
'body': 'edit 2',
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
},
|
||||||
|
'm.relates_to': {
|
||||||
|
'event_id': '\$source',
|
||||||
|
'rel_type': RelationshipTypes.Edit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'event_id': '\$edit2',
|
||||||
|
'sender': '@alice:example.org',
|
||||||
|
}, null);
|
||||||
|
edit2.sortOrder = 2;
|
||||||
|
var edit3 = Event.fromJson({
|
||||||
|
'type': EventTypes.Message,
|
||||||
|
'content': {
|
||||||
|
'body': '* edit 3',
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'm.new_content': {
|
||||||
|
'body': 'edit 3',
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
},
|
||||||
|
'm.relates_to': {
|
||||||
|
'event_id': '\$source',
|
||||||
|
'rel_type': RelationshipTypes.Edit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'event_id': '\$edit3',
|
||||||
|
'sender': '@bob:example.org',
|
||||||
|
}, null);
|
||||||
|
edit3.sortOrder = 3;
|
||||||
|
var room = Room(client: client);
|
||||||
|
// no edits
|
||||||
|
var displayEvent =
|
||||||
|
event.getDisplayEvent(Timeline(events: <Event>[event], room: room));
|
||||||
|
expect(displayEvent.body, 'blah');
|
||||||
|
// one edit
|
||||||
|
displayEvent = event
|
||||||
|
.getDisplayEvent(Timeline(events: <Event>[event, edit1], room: room));
|
||||||
|
expect(displayEvent.body, 'edit 1');
|
||||||
|
// two edits
|
||||||
|
displayEvent = event.getDisplayEvent(
|
||||||
|
Timeline(events: <Event>[event, edit1, edit2], room: room));
|
||||||
|
expect(displayEvent.body, 'edit 2');
|
||||||
|
// foreign edit
|
||||||
|
displayEvent = event
|
||||||
|
.getDisplayEvent(Timeline(events: <Event>[event, edit3], room: room));
|
||||||
|
expect(displayEvent.body, 'blah');
|
||||||
|
// mixed foreign and non-foreign
|
||||||
|
displayEvent = event.getDisplayEvent(
|
||||||
|
Timeline(events: <Event>[event, edit1, edit2, edit3], room: room));
|
||||||
|
expect(displayEvent.body, 'edit 2');
|
||||||
|
});
|
||||||
|
test('downloadAndDecryptAttachment', () async {
|
||||||
|
final FILE_BUFF = Uint8List.fromList([0]);
|
||||||
|
final THUMBNAIL_BUFF = Uint8List.fromList([2]);
|
||||||
|
final downloadCallback = (String url) async {
|
||||||
|
return {
|
||||||
|
'https://fakeserver.notexisting/_matrix/media/r0/download/example.org/file':
|
||||||
|
FILE_BUFF,
|
||||||
|
'https://fakeserver.notexisting/_matrix/media/r0/download/example.org/thumb':
|
||||||
|
THUMBNAIL_BUFF,
|
||||||
|
}[url];
|
||||||
|
};
|
||||||
|
await client.checkServer('https://fakeServer.notExisting');
|
||||||
|
final room = Room(id: '!localpart:server.abc', client: client);
|
||||||
|
var event = Event.fromJson({
|
||||||
|
'type': EventTypes.Message,
|
||||||
|
'content': {
|
||||||
|
'body': 'image',
|
||||||
|
'msgtype': 'm.image',
|
||||||
|
'url': 'mxc://example.org/file',
|
||||||
|
},
|
||||||
|
'event_id': '\$edit2',
|
||||||
|
'sender': '@alice:example.org',
|
||||||
|
}, room);
|
||||||
|
var buffer = await event.downloadAndDecryptAttachment(
|
||||||
|
downloadCallback: downloadCallback);
|
||||||
|
expect(buffer.bytes, FILE_BUFF);
|
||||||
|
|
||||||
|
event = Event.fromJson({
|
||||||
|
'type': EventTypes.Message,
|
||||||
|
'content': {
|
||||||
|
'body': 'image',
|
||||||
|
'msgtype': 'm.image',
|
||||||
|
'url': 'mxc://example.org/file',
|
||||||
|
'info': {
|
||||||
|
'thumbnail_url': 'mxc://example.org/thumb',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'event_id': '\$edit2',
|
||||||
|
'sender': '@alice:example.org',
|
||||||
|
}, room);
|
||||||
|
buffer = await event.downloadAndDecryptAttachment(
|
||||||
|
downloadCallback: downloadCallback);
|
||||||
|
expect(buffer.bytes, FILE_BUFF);
|
||||||
|
|
||||||
|
buffer = await event.downloadAndDecryptAttachment(
|
||||||
|
getThumbnail: true, downloadCallback: downloadCallback);
|
||||||
|
expect(buffer.bytes, THUMBNAIL_BUFF);
|
||||||
|
});
|
||||||
|
test('downloadAndDecryptAttachment encrypted', () async {
|
||||||
|
if (!olmEnabled) return;
|
||||||
|
|
||||||
|
final FILE_BUFF_ENC = Uint8List.fromList([0x3B, 0x6B, 0xB2, 0x8C, 0xAF]);
|
||||||
|
final FILE_BUFF_DEC = Uint8List.fromList([0x74, 0x65, 0x73, 0x74, 0x0A]);
|
||||||
|
final THUMB_BUFF_ENC =
|
||||||
|
Uint8List.fromList([0x55, 0xD7, 0xEB, 0x72, 0x05, 0x13]);
|
||||||
|
final THUMB_BUFF_DEC =
|
||||||
|
Uint8List.fromList([0x74, 0x68, 0x75, 0x6D, 0x62, 0x0A]);
|
||||||
|
final downloadCallback = (String url) async {
|
||||||
|
return {
|
||||||
|
'https://fakeserver.notexisting/_matrix/media/r0/download/example.com/file':
|
||||||
|
FILE_BUFF_ENC,
|
||||||
|
'https://fakeserver.notexisting/_matrix/media/r0/download/example.com/thumb':
|
||||||
|
THUMB_BUFF_ENC,
|
||||||
|
}[url];
|
||||||
|
};
|
||||||
|
final room = Room(id: '!localpart:server.abc', client: await getClient());
|
||||||
|
var event = Event.fromJson({
|
||||||
|
'type': EventTypes.Message,
|
||||||
|
'content': {
|
||||||
|
'body': 'image',
|
||||||
|
'msgtype': 'm.image',
|
||||||
|
'file': {
|
||||||
|
'v': 'v2',
|
||||||
|
'key': {
|
||||||
|
'alg': 'A256CTR',
|
||||||
|
'ext': true,
|
||||||
|
'k': '7aPRNIDPeUAUqD6SPR3vVX5W9liyMG98NexVJ9udnCc',
|
||||||
|
'key_ops': ['encrypt', 'decrypt'],
|
||||||
|
'kty': 'oct'
|
||||||
|
},
|
||||||
|
'iv': 'Wdsf+tnOHIoAAAAAAAAAAA',
|
||||||
|
'hashes': {'sha256': 'WgC7fw2alBC5t+xDx+PFlZxfFJXtIstQCg+j0WDaXxE'},
|
||||||
|
'url': 'mxc://example.com/file',
|
||||||
|
'mimetype': 'text/plain'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'event_id': '\$edit2',
|
||||||
|
'sender': '@alice:example.org',
|
||||||
|
}, room);
|
||||||
|
var buffer = await event.downloadAndDecryptAttachment(
|
||||||
|
downloadCallback: downloadCallback);
|
||||||
|
expect(buffer.bytes, FILE_BUFF_DEC);
|
||||||
|
|
||||||
|
event = Event.fromJson({
|
||||||
|
'type': EventTypes.Message,
|
||||||
|
'content': {
|
||||||
|
'body': 'image',
|
||||||
|
'msgtype': 'm.image',
|
||||||
|
'file': {
|
||||||
|
'v': 'v2',
|
||||||
|
'key': {
|
||||||
|
'alg': 'A256CTR',
|
||||||
|
'ext': true,
|
||||||
|
'k': '7aPRNIDPeUAUqD6SPR3vVX5W9liyMG98NexVJ9udnCc',
|
||||||
|
'key_ops': ['encrypt', 'decrypt'],
|
||||||
|
'kty': 'oct'
|
||||||
|
},
|
||||||
|
'iv': 'Wdsf+tnOHIoAAAAAAAAAAA',
|
||||||
|
'hashes': {'sha256': 'WgC7fw2alBC5t+xDx+PFlZxfFJXtIstQCg+j0WDaXxE'},
|
||||||
|
'url': 'mxc://example.com/file',
|
||||||
|
'mimetype': 'text/plain'
|
||||||
|
},
|
||||||
|
'info': {
|
||||||
|
'thumbnail_file': {
|
||||||
|
'v': 'v2',
|
||||||
|
'key': {
|
||||||
|
'alg': 'A256CTR',
|
||||||
|
'ext': true,
|
||||||
|
'k': 'TmF-rZYetZbxpL5yjDPE21UALQJcpEE6X-nvUDD5rA0',
|
||||||
|
'key_ops': ['encrypt', 'decrypt'],
|
||||||
|
'kty': 'oct'
|
||||||
|
},
|
||||||
|
'iv': '41ZqNRZSLFUAAAAAAAAAAA',
|
||||||
|
'hashes': {
|
||||||
|
'sha256': 'zccOwXiOTAYhGXyk0Fra7CRreBF6itjiCKdd+ov8mO4'
|
||||||
|
},
|
||||||
|
'url': 'mxc://example.com/thumb',
|
||||||
|
'mimetype': 'text/plain'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'event_id': '\$edit2',
|
||||||
|
'sender': '@alice:example.org',
|
||||||
|
}, room);
|
||||||
|
buffer = await event.downloadAndDecryptAttachment(
|
||||||
|
downloadCallback: downloadCallback);
|
||||||
|
expect(buffer.bytes, FILE_BUFF_DEC);
|
||||||
|
|
||||||
|
buffer = await event.downloadAndDecryptAttachment(
|
||||||
|
getThumbnail: true, downloadCallback: downloadCallback);
|
||||||
|
expect(buffer.bytes, THUMB_BUFF_DEC);
|
||||||
|
|
||||||
|
await room.client.dispose(closeDatabase: true);
|
||||||
|
});
|
||||||
|
test('downloadAndDecryptAttachment store', () async {
|
||||||
|
final FILE_BUFF = Uint8List.fromList([0]);
|
||||||
|
var serverHits = 0;
|
||||||
|
final downloadCallback = (String url) async {
|
||||||
|
serverHits++;
|
||||||
|
return {
|
||||||
|
'https://fakeserver.notexisting/_matrix/media/r0/download/example.org/newfile':
|
||||||
|
FILE_BUFF,
|
||||||
|
}[url];
|
||||||
|
};
|
||||||
|
await client.checkServer('https://fakeServer.notExisting');
|
||||||
|
final room = Room(id: '!localpart:server.abc', client: await getClient());
|
||||||
|
var event = Event.fromJson({
|
||||||
|
'type': EventTypes.Message,
|
||||||
|
'content': {
|
||||||
|
'body': 'image',
|
||||||
|
'msgtype': 'm.image',
|
||||||
|
'url': 'mxc://example.org/newfile',
|
||||||
|
'info': {
|
||||||
|
'size': 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'event_id': '\$edit2',
|
||||||
|
'sender': '@alice:example.org',
|
||||||
|
}, room);
|
||||||
|
var buffer = await event.downloadAndDecryptAttachment(
|
||||||
|
downloadCallback: downloadCallback);
|
||||||
|
expect(buffer.bytes, FILE_BUFF);
|
||||||
|
expect(serverHits, 1);
|
||||||
|
buffer = await event.downloadAndDecryptAttachment(
|
||||||
|
downloadCallback: downloadCallback);
|
||||||
|
expect(buffer.bytes, FILE_BUFF);
|
||||||
|
expect(serverHits, 1);
|
||||||
|
|
||||||
|
await room.client.dispose(closeDatabase: true);
|
||||||
|
});
|
||||||
|
test('emote detection', () async {
|
||||||
|
var event = Event.fromJson({
|
||||||
|
'type': EventTypes.Message,
|
||||||
|
'content': {
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'body': 'normal message',
|
||||||
|
},
|
||||||
|
'event_id': '\$edit2',
|
||||||
|
'sender': '@alice:example.org',
|
||||||
|
}, null);
|
||||||
|
expect(event.onlyEmotes, false);
|
||||||
|
expect(event.numberEmotes, 0);
|
||||||
|
event = Event.fromJson({
|
||||||
|
'type': EventTypes.Message,
|
||||||
|
'content': {
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'body': 'normal message\n\nvery normal',
|
||||||
|
},
|
||||||
|
'event_id': '\$edit2',
|
||||||
|
'sender': '@alice:example.org',
|
||||||
|
}, null);
|
||||||
|
expect(event.onlyEmotes, false);
|
||||||
|
expect(event.numberEmotes, 0);
|
||||||
|
event = Event.fromJson({
|
||||||
|
'type': EventTypes.Message,
|
||||||
|
'content': {
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'body': 'normal message with emoji 🦊',
|
||||||
|
},
|
||||||
|
'event_id': '\$edit2',
|
||||||
|
'sender': '@alice:example.org',
|
||||||
|
}, null);
|
||||||
|
expect(event.onlyEmotes, false);
|
||||||
|
expect(event.numberEmotes, 1);
|
||||||
|
event = Event.fromJson({
|
||||||
|
'type': EventTypes.Message,
|
||||||
|
'content': {
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'body': '🦊',
|
||||||
|
},
|
||||||
|
'event_id': '\$edit2',
|
||||||
|
'sender': '@alice:example.org',
|
||||||
|
}, null);
|
||||||
|
expect(event.onlyEmotes, true);
|
||||||
|
expect(event.numberEmotes, 1);
|
||||||
|
event = Event.fromJson({
|
||||||
|
'type': EventTypes.Message,
|
||||||
|
'content': {
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'body': '🦊🦊 🦊\n🦊🦊',
|
||||||
|
},
|
||||||
|
'event_id': '\$edit2',
|
||||||
|
'sender': '@alice:example.org',
|
||||||
|
}, null);
|
||||||
|
expect(event.onlyEmotes, true);
|
||||||
|
expect(event.numberEmotes, 5);
|
||||||
|
event = Event.fromJson({
|
||||||
|
'type': EventTypes.Message,
|
||||||
|
'content': {
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'body': 'rich message',
|
||||||
|
'format': 'org.matrix.custom.html',
|
||||||
|
'formatted_body': 'rich message'
|
||||||
|
},
|
||||||
|
'event_id': '\$edit2',
|
||||||
|
'sender': '@alice:example.org',
|
||||||
|
}, null);
|
||||||
|
expect(event.onlyEmotes, false);
|
||||||
|
expect(event.numberEmotes, 0);
|
||||||
|
event = Event.fromJson({
|
||||||
|
'type': EventTypes.Message,
|
||||||
|
'content': {
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'body': '🦊',
|
||||||
|
'format': 'org.matrix.custom.html',
|
||||||
|
'formatted_body': '🦊'
|
||||||
|
},
|
||||||
|
'event_id': '\$edit2',
|
||||||
|
'sender': '@alice:example.org',
|
||||||
|
}, null);
|
||||||
|
expect(event.onlyEmotes, true);
|
||||||
|
expect(event.numberEmotes, 1);
|
||||||
|
event = Event.fromJson({
|
||||||
|
'type': EventTypes.Message,
|
||||||
|
'content': {
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'body': ':blah:',
|
||||||
|
'format': 'org.matrix.custom.html',
|
||||||
|
'formatted_body': '<img data-mx-emoticon src="mxc://blah/blubb">'
|
||||||
|
},
|
||||||
|
'event_id': '\$edit2',
|
||||||
|
'sender': '@alice:example.org',
|
||||||
|
}, null);
|
||||||
|
expect(event.onlyEmotes, true);
|
||||||
|
expect(event.numberEmotes, 1);
|
||||||
|
event = Event.fromJson({
|
||||||
|
'type': EventTypes.Message,
|
||||||
|
'content': {
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'body': '🦊 :blah:',
|
||||||
|
'format': 'org.matrix.custom.html',
|
||||||
|
'formatted_body': '🦊 <img data-mx-emoticon src="mxc://blah/blubb">'
|
||||||
|
},
|
||||||
|
'event_id': '\$edit2',
|
||||||
|
'sender': '@alice:example.org',
|
||||||
|
}, null);
|
||||||
|
expect(event.onlyEmotes, true);
|
||||||
|
expect(event.numberEmotes, 2);
|
||||||
|
// with variant selector
|
||||||
|
event = Event.fromJson({
|
||||||
|
'type': EventTypes.Message,
|
||||||
|
'content': {
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'body': '❤️',
|
||||||
|
},
|
||||||
|
'event_id': '\$edit2',
|
||||||
|
'sender': '@alice:example.org',
|
||||||
|
}, null);
|
||||||
|
expect(event.onlyEmotes, true);
|
||||||
|
expect(event.numberEmotes, 1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,21 +29,15 @@ const pickledOlmAccount =
|
||||||
'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw';
|
'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw';
|
||||||
|
|
||||||
Future<Client> getClient() async {
|
Future<Client> getClient() async {
|
||||||
final client = Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
final client = Client('testclient', httpClient: FakeMatrixApi());
|
||||||
client.database = getDatabase();
|
client.database = getDatabase();
|
||||||
await client.checkServer('https://fakeServer.notExisting');
|
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(
|
client.connect(
|
||||||
newToken: resp.accessToken,
|
newToken: 'abcd',
|
||||||
newUserID: resp.userId,
|
newUserID: '@test:fakeServer.notExisting',
|
||||||
newHomeserver: client.api.homeserver,
|
newHomeserver: client.homeserver,
|
||||||
newDeviceName: 'Text Matrix Client',
|
newDeviceName: 'Text Matrix Client',
|
||||||
newDeviceID: resp.deviceId,
|
newDeviceID: 'GHTYAJCE',
|
||||||
newOlmAccount: pickledOlmAccount,
|
newOlmAccount: pickledOlmAccount,
|
||||||
);
|
);
|
||||||
await Future.delayed(Duration(milliseconds: 10));
|
await Future.delayed(Duration(milliseconds: 10));
|
||||||
|
|
|
@ -80,8 +80,12 @@ class FakeMatrixApi extends MockClient {
|
||||||
res = {'displayname': ''};
|
res = {'displayname': ''};
|
||||||
} else if (method == 'PUT' &&
|
} else if (method == 'PUT' &&
|
||||||
action.contains(
|
action.contains(
|
||||||
'/client/r0/rooms/%211234%3AfakeServer.notExisting/send/')) {
|
'/client/r0/rooms/!1234%3AfakeServer.notExisting/send/')) {
|
||||||
res = {'event_id': '\$event${FakeMatrixApi.eventCounter++}'};
|
res = {'event_id': '\$event${FakeMatrixApi.eventCounter++}'};
|
||||||
|
} else if (action.contains('/client/r0/sync')) {
|
||||||
|
res = {
|
||||||
|
'next_batch': DateTime.now().millisecondsSinceEpoch.toString
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
res = {
|
res = {
|
||||||
'errcode': 'M_UNRECOGNIZED',
|
'errcode': 'M_UNRECOGNIZED',
|
||||||
|
@ -748,7 +752,7 @@ class FakeMatrixApi extends MockClient {
|
||||||
'app_url': 'https://custom.app.example.org'
|
'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) => {
|
(var req) => {
|
||||||
'tags': {
|
'tags': {
|
||||||
'm.favourite': {'order': 0.1},
|
'm.favourite': {'order': 0.1},
|
||||||
|
@ -1978,25 +1982,27 @@ class FakeMatrixApi extends MockClient {
|
||||||
'/client/unstable/room_keys/version': (var reqI) => {'version': '5'},
|
'/client/unstable/room_keys/version': (var reqI) => {'version': '5'},
|
||||||
},
|
},
|
||||||
'PUT': {
|
'PUT': {
|
||||||
|
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/m.ignored_user_list':
|
||||||
|
(var req) => {},
|
||||||
'/client/r0/presence/${Uri.encodeComponent('@alice:example.com')}/status':
|
'/client/r0/presence/${Uri.encodeComponent('@alice:example.com')}/status':
|
||||||
(var req) => {},
|
(var req) => {},
|
||||||
'/client/r0/pushrules/global/content/nocake/enabled': (var req) => {},
|
'/client/r0/pushrules/global/content/nocake/enabled': (var req) => {},
|
||||||
'/client/r0/pushrules/global/content/nocake/actions': (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) => {},
|
(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) => {},
|
(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) => {},
|
(var req) => {},
|
||||||
'/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.call.invite/1234':
|
'/client/r0/rooms/!localpart%3Aserver.abc/send/m.call.invite/1234':
|
||||||
(var req) => {},
|
(var req) => {},
|
||||||
'/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.call.answer/1234':
|
'/client/r0/rooms/!localpart%3Aserver.abc/send/m.call.answer/1234':
|
||||||
(var req) => {},
|
(var req) => {},
|
||||||
'/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.call.candidates/1234':
|
'/client/r0/rooms/!localpart%3Aserver.abc/send/m.call.candidates/1234':
|
||||||
(var req) => {},
|
(var req) => {},
|
||||||
'/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.call.hangup/1234':
|
'/client/r0/rooms/!localpart%3Aserver.abc/send/m.call.hangup/1234':
|
||||||
(var req) => {},
|
(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'},
|
(var req) => {'event_id': '1234'},
|
||||||
'/client/r0/pushrules/global/room/!localpart%3Aserver.abc': (var req) =>
|
'/client/r0/pushrules/global/room/!localpart%3Aserver.abc': (var req) =>
|
||||||
{},
|
{},
|
||||||
|
@ -2006,23 +2012,31 @@ class FakeMatrixApi extends MockClient {
|
||||||
(var req) => {},
|
(var req) => {},
|
||||||
'/client/r0/devices/QBUAZIFURK': (var req) => {},
|
'/client/r0/devices/QBUAZIFURK': (var req) => {},
|
||||||
'/client/r0/directory/room/%23testalias%3Aexample.com': (var reqI) => {},
|
'/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) => {
|
(var reqI) => {
|
||||||
'event_id': '\$event${FakeMatrixApi.eventCounter++}',
|
'event_id': '\$event${FakeMatrixApi.eventCounter++}',
|
||||||
},
|
},
|
||||||
'/client/r0/rooms/!localpart%3Aexample.com/typing/%40alice%3Aexample.com':
|
'/client/r0/rooms/!localpart%3Aexample.com/typing/%40alice%3Aexample.com':
|
||||||
(var req) => {},
|
(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) => {
|
(var reqI) => {
|
||||||
'event_id': '\$event${FakeMatrixApi.eventCounter++}',
|
'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) => {},
|
(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) => {},
|
(var req) => {},
|
||||||
'/client/r0/user/%40alice%3Aexample.com/account_data/test.account.data':
|
'/client/r0/user/%40alice%3Aexample.com/account_data/test.account.data':
|
||||||
(var req) => {},
|
(var req) => {},
|
||||||
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best+animal':
|
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best%20animal':
|
||||||
(var req) => {},
|
(var req) => {},
|
||||||
'/client/r0/user/%40alice%3Aexample.com/rooms/1234/account_data/test.account.data':
|
'/client/r0/user/%40alice%3Aexample.com/rooms/1234/account_data/test.account.data':
|
||||||
(var req) => {},
|
(var req) => {},
|
||||||
|
@ -2034,27 +2048,27 @@ class FakeMatrixApi extends MockClient {
|
||||||
'/client/r0/profile/%40alice%3Aexample.com/avatar_url': (var reqI) => {},
|
'/client/r0/profile/%40alice%3Aexample.com/avatar_url': (var reqI) => {},
|
||||||
'/client/r0/profile/%40test%3AfakeServer.notExisting/avatar_url':
|
'/client/r0/profile/%40test%3AfakeServer.notExisting/avatar_url':
|
||||||
(var reqI) => {},
|
(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'},
|
(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'},
|
(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'},
|
(var reqI) => {'event_id': 'YUwRidLecu:example.com'},
|
||||||
'/client/r0/rooms/%21localpart%3Aserver.abc/redact/1234/1234':
|
'/client/r0/rooms/!localpart%3Aserver.abc/redact/1234/1234': (var reqI) =>
|
||||||
(var reqI) => {'event_id': 'YUwRidLecu:example.com'},
|
{'event_id': 'YUwRidLecu:example.com'},
|
||||||
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.name':
|
'/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.name':
|
||||||
(var reqI) => {
|
(var reqI) => {
|
||||||
'event_id': '42',
|
'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) => {
|
(var reqI) => {
|
||||||
'event_id': '42',
|
'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) => {
|
(var reqI) => {
|
||||||
'event_id': '42',
|
'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) => {
|
(var reqI) => {
|
||||||
'event_id': '42',
|
'event_id': '42',
|
||||||
},
|
},
|
||||||
|
@ -2083,9 +2097,9 @@ class FakeMatrixApi extends MockClient {
|
||||||
'/client/r0/pushrules/global/content/nocake': (var req) => {},
|
'/client/r0/pushrules/global/content/nocake': (var req) => {},
|
||||||
'/client/r0/pushrules/global/override/!localpart%3Aserver.abc':
|
'/client/r0/pushrules/global/override/!localpart%3Aserver.abc':
|
||||||
(var req) => {},
|
(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) => {},
|
(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) => {},
|
(var req) => {},
|
||||||
'/client/unstable/room_keys/version/5': (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':
|
'/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}/${Uri.encodeComponent('ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU')}?version=5':
|
||||||
|
|
|
@ -306,4 +306,28 @@ class FakeMatrixLocalizations extends MatrixLocalizations {
|
||||||
@override
|
@override
|
||||||
// TODO: implement you
|
// TODO: implement you
|
||||||
String get you => null;
|
String get you => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String answeredTheCall(String senderName) {
|
||||||
|
// TODO: implement answeredTheCall
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String endedTheCall(String senderName) {
|
||||||
|
// TODO: implement endedTheCall
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sentCallInformations(String senderName) {
|
||||||
|
// TODO: implement sentCallInformations
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String startedACall(String senderName) {
|
||||||
|
// TODO: implement startedACall
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,11 +54,11 @@ void main() {
|
||||||
});
|
});
|
||||||
test('emotes', () {
|
test('emotes', () {
|
||||||
expect(markdown(':fox:', emotePacks),
|
expect(markdown(':fox:', emotePacks),
|
||||||
'<img src="mxc://roomfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
|
'<img data-mx-emoticon="" src="mxc://roomfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
|
||||||
expect(markdown(':user~fox:', emotePacks),
|
expect(markdown(':user~fox:', emotePacks),
|
||||||
'<img src="mxc://userfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
|
'<img data-mx-emoticon="" src="mxc://userfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
|
||||||
expect(markdown(':raccoon:', emotePacks),
|
expect(markdown(':raccoon:', emotePacks),
|
||||||
'<img src="mxc://raccoon" alt=":raccoon:" title=":raccoon:" height="32" vertical-align="middle" />');
|
'<img data-mx-emoticon="" src="mxc://raccoon" alt=":raccoon:" title=":raccoon:" height="32" vertical-align="middle" />');
|
||||||
expect(markdown(':invalid:', emotePacks), ':invalid:');
|
expect(markdown(':invalid:', emotePacks), ':invalid:');
|
||||||
expect(markdown(':room~invalid:', emotePacks), ':room~invalid:');
|
expect(markdown(':room~invalid:', emotePacks), ':room~invalid:');
|
||||||
});
|
});
|
||||||
|
|
|
@ -33,7 +33,6 @@ void main() {
|
||||||
group('Matrix API', () {
|
group('Matrix API', () {
|
||||||
final matrixApi = MatrixApi(
|
final matrixApi = MatrixApi(
|
||||||
httpClient: FakeMatrixApi(),
|
httpClient: FakeMatrixApi(),
|
||||||
debug: true,
|
|
||||||
);
|
);
|
||||||
test('MatrixException test', () async {
|
test('MatrixException test', () async {
|
||||||
final exception = MatrixException.fromJson({
|
final exception = MatrixException.fromJson({
|
||||||
|
@ -1377,7 +1376,7 @@ void main() {
|
||||||
'@alice:example.com', '!localpart:example.com');
|
'@alice:example.com', '!localpart:example.com');
|
||||||
expect(
|
expect(
|
||||||
FakeMatrixApi.api['GET'][
|
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()))},
|
{'tags': response.map((k, v) => MapEntry(k, v.toJson()))},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,184 @@
|
||||||
|
/*
|
||||||
|
* Famedly Matrix SDK
|
||||||
|
* Copyright (C) 2020 Famedly GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'fake_database.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Databse', () {
|
||||||
|
final database = getDatabase();
|
||||||
|
var clientId = -1;
|
||||||
|
var room = Room(id: '!room:blubb');
|
||||||
|
test('setupDatabase', () async {
|
||||||
|
clientId = await database.insertClient(
|
||||||
|
'testclient',
|
||||||
|
'https://example.org',
|
||||||
|
'blubb',
|
||||||
|
'@test:example.org',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
});
|
||||||
|
test('storeEventUpdate', () async {
|
||||||
|
// store a simple update
|
||||||
|
var update = EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: room.id,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'origin_server_ts': 100,
|
||||||
|
'content': {'blah': 'blubb'},
|
||||||
|
'event_id': '\$event-1',
|
||||||
|
'sender': '@blah:blubb',
|
||||||
|
},
|
||||||
|
sortOrder: 0.0,
|
||||||
|
);
|
||||||
|
await database.storeEventUpdate(clientId, update);
|
||||||
|
var event = await database.getEventById(clientId, '\$event-1', room);
|
||||||
|
expect(event.eventId, '\$event-1');
|
||||||
|
|
||||||
|
// insert a transaction id
|
||||||
|
update = EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: room.id,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'origin_server_ts': 100,
|
||||||
|
'content': {'blah': 'blubb'},
|
||||||
|
'event_id': 'transaction-1',
|
||||||
|
'sender': '@blah:blubb',
|
||||||
|
'status': 0,
|
||||||
|
},
|
||||||
|
sortOrder: 0.0,
|
||||||
|
);
|
||||||
|
await database.storeEventUpdate(clientId, update);
|
||||||
|
event = await database.getEventById(clientId, 'transaction-1', room);
|
||||||
|
expect(event.eventId, 'transaction-1');
|
||||||
|
update = EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: room.id,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'origin_server_ts': 100,
|
||||||
|
'content': {'blah': 'blubb'},
|
||||||
|
'event_id': '\$event-2',
|
||||||
|
'sender': '@blah:blubb',
|
||||||
|
'unsigned': {
|
||||||
|
'transaction_id': 'transaction-1',
|
||||||
|
},
|
||||||
|
'status': 1,
|
||||||
|
},
|
||||||
|
sortOrder: 0.0,
|
||||||
|
);
|
||||||
|
await database.storeEventUpdate(clientId, update);
|
||||||
|
event = await database.getEventById(clientId, 'transaction-1', room);
|
||||||
|
expect(event, null);
|
||||||
|
event = await database.getEventById(clientId, '\$event-2', room);
|
||||||
|
|
||||||
|
// insert a transaction id if the event id for it already exists
|
||||||
|
update = EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: room.id,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'origin_server_ts': 100,
|
||||||
|
'content': {'blah': 'blubb'},
|
||||||
|
'event_id': '\$event-3',
|
||||||
|
'sender': '@blah:blubb',
|
||||||
|
'status': 0,
|
||||||
|
},
|
||||||
|
sortOrder: 0.0,
|
||||||
|
);
|
||||||
|
await database.storeEventUpdate(clientId, update);
|
||||||
|
event = await database.getEventById(clientId, '\$event-3', room);
|
||||||
|
expect(event.eventId, '\$event-3');
|
||||||
|
update = EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: room.id,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'origin_server_ts': 100,
|
||||||
|
'content': {'blah': 'blubb'},
|
||||||
|
'event_id': '\$event-3',
|
||||||
|
'sender': '@blah:blubb',
|
||||||
|
'status': 1,
|
||||||
|
'unsigned': {
|
||||||
|
'transaction_id': 'transaction-2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sortOrder: 0.0,
|
||||||
|
);
|
||||||
|
await database.storeEventUpdate(clientId, update);
|
||||||
|
event = await database.getEventById(clientId, '\$event-3', room);
|
||||||
|
expect(event.eventId, '\$event-3');
|
||||||
|
expect(event.status, 1);
|
||||||
|
event = await database.getEventById(clientId, 'transaction-2', room);
|
||||||
|
expect(event, null);
|
||||||
|
|
||||||
|
// insert transaction id and not update status
|
||||||
|
update = EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: room.id,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'origin_server_ts': 100,
|
||||||
|
'content': {'blah': 'blubb'},
|
||||||
|
'event_id': '\$event-4',
|
||||||
|
'sender': '@blah:blubb',
|
||||||
|
'status': 2,
|
||||||
|
},
|
||||||
|
sortOrder: 0.0,
|
||||||
|
);
|
||||||
|
await database.storeEventUpdate(clientId, update);
|
||||||
|
event = await database.getEventById(clientId, '\$event-4', room);
|
||||||
|
expect(event.eventId, '\$event-4');
|
||||||
|
update = EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: room.id,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'origin_server_ts': 100,
|
||||||
|
'content': {'blah': 'blubb'},
|
||||||
|
'event_id': '\$event-4',
|
||||||
|
'sender': '@blah:blubb',
|
||||||
|
'status': 1,
|
||||||
|
'unsigned': {
|
||||||
|
'transaction_id': 'transaction-3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sortOrder: 0.0,
|
||||||
|
);
|
||||||
|
await database.storeEventUpdate(clientId, update);
|
||||||
|
event = await database.getEventById(clientId, '\$event-4', room);
|
||||||
|
expect(event.eventId, '\$event-4');
|
||||||
|
expect(event.status, 2);
|
||||||
|
event = await database.getEventById(clientId, 'transaction-3', room);
|
||||||
|
expect(event, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -205,4 +205,24 @@ class MatrixDefaultLocalizations extends MatrixLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get you => 'You';
|
String get you => 'You';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String answeredTheCall(String senderName) {
|
||||||
|
return 'answeredTheCall';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String endedTheCall(String senderName) {
|
||||||
|
return 'endedTheCall';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sentCallInformations(String senderName) {
|
||||||
|
return 'sentCallInformations';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String startedACall(String senderName) {
|
||||||
|
return 'startedACall';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,9 +29,10 @@ void main() {
|
||||||
expect('!test:example.com'.isValidMatrixId, true);
|
expect('!test:example.com'.isValidMatrixId, true);
|
||||||
expect('+test:example.com'.isValidMatrixId, true);
|
expect('+test:example.com'.isValidMatrixId, true);
|
||||||
expect('\$test:example.com'.isValidMatrixId, true);
|
expect('\$test:example.com'.isValidMatrixId, true);
|
||||||
|
expect('\$testevent'.isValidMatrixId, true);
|
||||||
expect('test:example.com'.isValidMatrixId, false);
|
expect('test:example.com'.isValidMatrixId, false);
|
||||||
expect('@testexample.com'.isValidMatrixId, false);
|
expect('@testexample.com'.isValidMatrixId, false);
|
||||||
expect('@:example.com'.isValidMatrixId, false);
|
expect('@:example.com'.isValidMatrixId, true);
|
||||||
expect('@test:'.isValidMatrixId, false);
|
expect('@test:'.isValidMatrixId, false);
|
||||||
expect(mxId.sigil, '@');
|
expect(mxId.sigil, '@');
|
||||||
expect('#test:example.com'.sigil, '#');
|
expect('#test:example.com'.sigil, '#');
|
||||||
|
@ -42,6 +43,8 @@ void main() {
|
||||||
expect(mxId.domain, 'example.com');
|
expect(mxId.domain, 'example.com');
|
||||||
expect(mxId.equals('@Test:example.com'), true);
|
expect(mxId.equals('@Test:example.com'), true);
|
||||||
expect(mxId.equals('@test:example.org'), false);
|
expect(mxId.equals('@test:example.org'), false);
|
||||||
|
expect('@user:domain:8448'.localpart, 'user');
|
||||||
|
expect('@user:domain:8448'.domain, 'domain:8448');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,13 +33,13 @@ void main() {
|
||||||
expect(content.isScheme('mxc'), true);
|
expect(content.isScheme('mxc'), true);
|
||||||
|
|
||||||
expect(content.getDownloadLink(client),
|
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),
|
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(
|
expect(
|
||||||
content.getThumbnail(client,
|
content.getThumbnail(client,
|
||||||
width: 50, height: 50, method: ThumbnailMethod.scale),
|
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 'package:test/test.dart';
|
||||||
|
|
||||||
import 'fake_client.dart';
|
import 'fake_client.dart';
|
||||||
|
import 'fake_matrix_api.dart';
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
@ -181,10 +183,6 @@ void main() {
|
||||||
await room.sendReadReceipt('§1234:fakeServer.notExisting');
|
await room.sendReadReceipt('§1234:fakeServer.notExisting');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('enableEncryption', () async {
|
|
||||||
await room.enableEncryption();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('requestParticipants', () async {
|
test('requestParticipants', () async {
|
||||||
final participants = await room.requestParticipants();
|
final participants = await room.requestParticipants();
|
||||||
expect(participants.length, 1);
|
expect(participants.length, 1);
|
||||||
|
@ -349,9 +347,106 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sendEvent', () async {
|
test('sendEvent', () async {
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
final dynamic resp =
|
final dynamic resp =
|
||||||
await room.sendTextEvent('Hello world', txid: 'testtxid');
|
await room.sendTextEvent('Hello world', txid: 'testtxid');
|
||||||
expect(resp.startsWith('\$event'), true);
|
expect(resp.startsWith('\$event'), true);
|
||||||
|
final entry = FakeMatrixApi.calledEndpoints.entries
|
||||||
|
.firstWhere((p) => p.key.contains('/send/m.room.message/'));
|
||||||
|
final content = json.decode(entry.value.first);
|
||||||
|
expect(content, {
|
||||||
|
'body': 'Hello world',
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('send edit', () async {
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
final dynamic resp = await room.sendTextEvent('Hello world',
|
||||||
|
txid: 'testtxid', editEventId: '\$otherEvent');
|
||||||
|
expect(resp.startsWith('\$event'), true);
|
||||||
|
final entry = FakeMatrixApi.calledEndpoints.entries
|
||||||
|
.firstWhere((p) => p.key.contains('/send/m.room.message/'));
|
||||||
|
final content = json.decode(entry.value.first);
|
||||||
|
expect(content, {
|
||||||
|
'body': '* Hello world',
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'm.new_content': {
|
||||||
|
'body': 'Hello world',
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
},
|
||||||
|
'm.relates_to': {
|
||||||
|
'event_id': '\$otherEvent',
|
||||||
|
'rel_type': 'm.replace',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('send reply', () async {
|
||||||
|
var event = Event.fromJson({
|
||||||
|
'event_id': '\$replyEvent',
|
||||||
|
'content': {
|
||||||
|
'body': 'Blah',
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
},
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'sender': '@alice:example.org',
|
||||||
|
}, room);
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
final dynamic resp = await room.sendTextEvent('Hello world',
|
||||||
|
txid: 'testtxid', inReplyTo: event);
|
||||||
|
expect(resp.startsWith('\$event'), true);
|
||||||
|
final entry = FakeMatrixApi.calledEndpoints.entries
|
||||||
|
.firstWhere((p) => p.key.contains('/send/m.room.message/'));
|
||||||
|
final content = json.decode(entry.value.first);
|
||||||
|
expect(content, {
|
||||||
|
'body': '> <@alice:example.org> Blah\n\nHello world',
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'format': 'org.matrix.custom.html',
|
||||||
|
'formatted_body':
|
||||||
|
'<mx-reply><blockquote><a href="https://matrix.to/#/!localpart:server.abc/\$replyEvent">In reply to</a> <a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a><br>Blah</blockquote></mx-reply>Hello world',
|
||||||
|
'm.relates_to': {
|
||||||
|
'm.in_reply_to': {
|
||||||
|
'event_id': '\$replyEvent',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('send reaction', () async {
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
final dynamic resp =
|
||||||
|
await room.sendReaction('\$otherEvent', '🦊', txid: 'testtxid');
|
||||||
|
expect(resp.startsWith('\$event'), true);
|
||||||
|
final entry = FakeMatrixApi.calledEndpoints.entries
|
||||||
|
.firstWhere((p) => p.key.contains('/send/m.reaction/'));
|
||||||
|
final content = json.decode(entry.value.first);
|
||||||
|
expect(content, {
|
||||||
|
'm.relates_to': {
|
||||||
|
'event_id': '\$otherEvent',
|
||||||
|
'rel_type': 'm.annotation',
|
||||||
|
'key': '🦊',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('send location', () async {
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
|
||||||
|
final body = 'Middle of the ocean';
|
||||||
|
final geoUri = 'geo:0.0,0.0';
|
||||||
|
final dynamic resp =
|
||||||
|
await room.sendLocation(body, geoUri, txid: 'testtxid');
|
||||||
|
expect(resp.startsWith('\$event'), true);
|
||||||
|
|
||||||
|
final entry = FakeMatrixApi.calledEndpoints.entries
|
||||||
|
.firstWhere((p) => p.key.contains('/send/m.room.message/'));
|
||||||
|
final content = json.decode(entry.value.first);
|
||||||
|
expect(content, {
|
||||||
|
'msgtype': 'm.location',
|
||||||
|
'body': body,
|
||||||
|
'geo_uri': geoUri,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Not working because there is no real file to test it...
|
// Not working because there is no real file to test it...
|
||||||
|
@ -375,6 +470,17 @@ void main() {
|
||||||
expect(room.pushRuleState, PushRuleState.dont_notify);
|
expect(room.pushRuleState, PushRuleState.dont_notify);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Test call methods', () async {
|
||||||
|
await room.inviteToCall('1234', 1234, 'sdp', txid: '1234');
|
||||||
|
await room.answerCall('1234', 'sdp', txid: '1234');
|
||||||
|
await room.hangupCall('1234', txid: '1234');
|
||||||
|
await room.sendCallCandidates('1234', [], txid: '1234');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('enableEncryption', () async {
|
||||||
|
await room.enableEncryption();
|
||||||
|
});
|
||||||
|
|
||||||
test('Enable encryption', () async {
|
test('Enable encryption', () async {
|
||||||
room.setState(
|
room.setState(
|
||||||
Event(
|
Event(
|
||||||
|
@ -402,13 +508,6 @@ void main() {
|
||||||
await room.setPushRuleState(PushRuleState.notify);
|
await room.setPushRuleState(PushRuleState.notify);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Test call methods', () async {
|
|
||||||
await room.inviteToCall('1234', 1234, 'sdp', txid: '1234');
|
|
||||||
await room.answerCall('1234', 'sdp', txid: '1234');
|
|
||||||
await room.hangupCall('1234', txid: '1234');
|
|
||||||
await room.sendCallCandidates('1234', [], txid: '1234');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Test tag methods', () async {
|
test('Test tag methods', () async {
|
||||||
await room.addTag(TagType.Favourite, order: 0.1);
|
await room.addTag(TagType.Favourite, order: 0.1);
|
||||||
await room.removeTag(TagType.Favourite);
|
await room.removeTag(TagType.Favourite);
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
/*
|
||||||
|
* Ansible inventory script used at Famedly GmbH for managing many hosts
|
||||||
|
* Copyright (C) 2020 Famedly GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
const UPDATES = {
|
||||||
|
'empty': {
|
||||||
|
'next_batch': 'blah',
|
||||||
|
'account_data': {
|
||||||
|
'events': [],
|
||||||
|
},
|
||||||
|
'presences': {
|
||||||
|
'events': [],
|
||||||
|
},
|
||||||
|
'rooms': {
|
||||||
|
'join': {},
|
||||||
|
'leave': {},
|
||||||
|
'invite': {},
|
||||||
|
},
|
||||||
|
'to_device': {
|
||||||
|
'events': [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'presence': {
|
||||||
|
'next_batch': 'blah',
|
||||||
|
'presence': {
|
||||||
|
'events': [
|
||||||
|
{
|
||||||
|
'content': {
|
||||||
|
'avatar_url': 'mxc://localhost:wefuiwegh8742w',
|
||||||
|
'last_active_ago': 2478593,
|
||||||
|
'presence': 'online',
|
||||||
|
'currently_active': false,
|
||||||
|
'status_msg': 'Making cupcakes'
|
||||||
|
},
|
||||||
|
'type': 'm.presence',
|
||||||
|
'sender': '@example:localhost',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'account_data': {
|
||||||
|
'next_batch': 'blah',
|
||||||
|
'account_data': {
|
||||||
|
'events': [
|
||||||
|
{
|
||||||
|
'type': 'blah',
|
||||||
|
'content': {
|
||||||
|
'beep': 'boop',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'invite': {
|
||||||
|
'next_batch': 'blah',
|
||||||
|
'rooms': {
|
||||||
|
'invite': {
|
||||||
|
'!room': {
|
||||||
|
'invite_state': {
|
||||||
|
'events': [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'leave': {
|
||||||
|
'next_batch': 'blah',
|
||||||
|
'rooms': {
|
||||||
|
'leave': {
|
||||||
|
'!room': <String, dynamic>{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'join': {
|
||||||
|
'next_batch': 'blah',
|
||||||
|
'rooms': {
|
||||||
|
'join': {
|
||||||
|
'!room': {
|
||||||
|
'timeline': {
|
||||||
|
'events': [],
|
||||||
|
},
|
||||||
|
'state': {
|
||||||
|
'events': [],
|
||||||
|
},
|
||||||
|
'account_data': {
|
||||||
|
'events': [],
|
||||||
|
},
|
||||||
|
'ephemeral': {
|
||||||
|
'events': [],
|
||||||
|
},
|
||||||
|
'unread_notifications': <String, dynamic>{},
|
||||||
|
'summary': <String, dynamic>{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'to_device': {
|
||||||
|
'next_batch': 'blah',
|
||||||
|
'to_device': {
|
||||||
|
'events': [
|
||||||
|
{
|
||||||
|
'type': 'beep',
|
||||||
|
'content': {
|
||||||
|
'blah': 'blubb',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
void testUpdates(bool Function(SyncUpdate s) test, Map<String, bool> expected) {
|
||||||
|
for (final update in UPDATES.entries) {
|
||||||
|
var sync = SyncUpdate.fromJson(update.value);
|
||||||
|
expect(test(sync), expected[update.key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Sync Filters', () {
|
||||||
|
test('room update', () {
|
||||||
|
var testFn = (SyncUpdate s) => s.hasRoomUpdate;
|
||||||
|
final expected = {
|
||||||
|
'empty': false,
|
||||||
|
'presence': false,
|
||||||
|
'account_data': true,
|
||||||
|
'invite': true,
|
||||||
|
'leave': true,
|
||||||
|
'join': true,
|
||||||
|
'to_device': true,
|
||||||
|
};
|
||||||
|
testUpdates(testFn, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('presence update', () {
|
||||||
|
var testFn = (SyncUpdate s) => s.hasPresenceUpdate;
|
||||||
|
final expected = {
|
||||||
|
'empty': false,
|
||||||
|
'presence': true,
|
||||||
|
'account_data': false,
|
||||||
|
'invite': false,
|
||||||
|
'leave': false,
|
||||||
|
'join': false,
|
||||||
|
'to_device': false,
|
||||||
|
};
|
||||||
|
testUpdates(testFn, expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -33,7 +33,8 @@ void main() {
|
||||||
var updateCount = 0;
|
var updateCount = 0;
|
||||||
var insertList = <int>[];
|
var insertList = <int>[];
|
||||||
|
|
||||||
var client = Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
var client = Client('testclient',
|
||||||
|
httpClient: FakeMatrixApi(), sendMessageTimeoutSeconds: 5);
|
||||||
|
|
||||||
var room = Room(
|
var room = Room(
|
||||||
id: roomID, client: client, prev_batch: '1234', roomAccountData: {});
|
id: roomID, client: client, prev_batch: '1234', roomAccountData: {});
|
||||||
|
@ -186,8 +187,12 @@ void main() {
|
||||||
},
|
},
|
||||||
sortOrder: room.newSortOrder));
|
sortOrder: room.newSortOrder));
|
||||||
await Future.delayed(Duration(milliseconds: 50));
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
|
||||||
|
expect(updateCount, 7);
|
||||||
await room.sendTextEvent('test', txid: 'errortxid');
|
await room.sendTextEvent('test', txid: 'errortxid');
|
||||||
await Future.delayed(Duration(milliseconds: 50));
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
|
||||||
|
expect(updateCount, 9);
|
||||||
await room.sendTextEvent('test', txid: 'errortxid2');
|
await room.sendTextEvent('test', txid: 'errortxid2');
|
||||||
await Future.delayed(Duration(milliseconds: 50));
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
await room.sendTextEvent('test', txid: 'errortxid3');
|
await room.sendTextEvent('test', txid: 'errortxid3');
|
||||||
|
@ -214,29 +219,47 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Resend message', () async {
|
test('Resend message', () async {
|
||||||
await timeline.events[0].sendAgain(txid: '1234');
|
timeline.events.clear();
|
||||||
|
client.onEvent.add(EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: roomID,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||||
|
'sender': '@alice:example.com',
|
||||||
|
'status': -1,
|
||||||
|
'event_id': 'new-test-event',
|
||||||
|
'origin_server_ts': testTimeStamp,
|
||||||
|
'unsigned': {'transaction_id': 'newresend'},
|
||||||
|
},
|
||||||
|
sortOrder: room.newSortOrder));
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(timeline.events[0].status, -1);
|
||||||
|
await timeline.events[0].sendAgain();
|
||||||
|
|
||||||
await Future.delayed(Duration(milliseconds: 50));
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
|
||||||
expect(updateCount, 17);
|
expect(updateCount, 17);
|
||||||
|
|
||||||
expect(insertList, [0, 0, 0, 0, 0, 0, 0, 0]);
|
expect(insertList, [0, 0, 0, 0, 0, 0, 0, 0]);
|
||||||
expect(timeline.events.length, 6);
|
expect(timeline.events.length, 1);
|
||||||
expect(timeline.events[0].status, 1);
|
expect(timeline.events[0].status, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Request history', () async {
|
test('Request history', () async {
|
||||||
|
timeline.events.clear();
|
||||||
await room.requestHistory();
|
await room.requestHistory();
|
||||||
|
|
||||||
await Future.delayed(Duration(milliseconds: 50));
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
|
||||||
expect(updateCount, 20);
|
expect(updateCount, 20);
|
||||||
expect(timeline.events.length, 9);
|
expect(timeline.events.length, 3);
|
||||||
expect(timeline.events[6].eventId, '3143273582443PhrSn:example.org');
|
expect(timeline.events[0].eventId, '3143273582443PhrSn:example.org');
|
||||||
expect(timeline.events[7].eventId, '2143273582443PhrSn:example.org');
|
expect(timeline.events[1].eventId, '2143273582443PhrSn:example.org');
|
||||||
expect(timeline.events[8].eventId, '1143273582443PhrSn:example.org');
|
expect(timeline.events[2].eventId, '1143273582443PhrSn:example.org');
|
||||||
expect(room.prev_batch, 't47409-4357353_219380_26003_2265');
|
expect(room.prev_batch, 't47409-4357353_219380_26003_2265');
|
||||||
await timeline.events[8].redact(reason: 'test', txid: '1234');
|
await timeline.events[2].redact(reason: 'test', txid: '1234');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Clear cache on limited timeline', () async {
|
test('Clear cache on limited timeline', () async {
|
||||||
|
@ -251,5 +274,293 @@ void main() {
|
||||||
await Future.delayed(Duration(milliseconds: 50));
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
expect(timeline.events.isEmpty, true);
|
expect(timeline.events.isEmpty, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('sort errors on top', () async {
|
||||||
|
timeline.events.clear();
|
||||||
|
client.onEvent.add(EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: roomID,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||||
|
'sender': '@alice:example.com',
|
||||||
|
'status': -1,
|
||||||
|
'event_id': 'abc',
|
||||||
|
'origin_server_ts': testTimeStamp
|
||||||
|
},
|
||||||
|
sortOrder: room.newSortOrder));
|
||||||
|
client.onEvent.add(EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: roomID,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||||
|
'sender': '@alice:example.com',
|
||||||
|
'status': 2,
|
||||||
|
'event_id': 'def',
|
||||||
|
'origin_server_ts': testTimeStamp + 5
|
||||||
|
},
|
||||||
|
sortOrder: room.newSortOrder));
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(timeline.events[0].status, -1);
|
||||||
|
expect(timeline.events[1].status, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sending event to failed update', () async {
|
||||||
|
timeline.events.clear();
|
||||||
|
client.onEvent.add(EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: roomID,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||||
|
'sender': '@alice:example.com',
|
||||||
|
'status': 0,
|
||||||
|
'event_id': 'will-fail',
|
||||||
|
'origin_server_ts': testTimeStamp
|
||||||
|
},
|
||||||
|
sortOrder: room.newSortOrder));
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(timeline.events[0].status, 0);
|
||||||
|
expect(timeline.events.length, 1);
|
||||||
|
client.onEvent.add(EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: roomID,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||||
|
'sender': '@alice:example.com',
|
||||||
|
'status': -1,
|
||||||
|
'event_id': 'will-fail',
|
||||||
|
'origin_server_ts': testTimeStamp
|
||||||
|
},
|
||||||
|
sortOrder: room.newSortOrder));
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(timeline.events[0].status, -1);
|
||||||
|
expect(timeline.events.length, 1);
|
||||||
|
});
|
||||||
|
test('sending an event and the http request finishes first, 0 -> 1 -> 2',
|
||||||
|
() async {
|
||||||
|
timeline.events.clear();
|
||||||
|
client.onEvent.add(EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: roomID,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||||
|
'sender': '@alice:example.com',
|
||||||
|
'status': 0,
|
||||||
|
'event_id': 'transaction',
|
||||||
|
'origin_server_ts': testTimeStamp
|
||||||
|
},
|
||||||
|
sortOrder: room.newSortOrder));
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(timeline.events[0].status, 0);
|
||||||
|
expect(timeline.events.length, 1);
|
||||||
|
client.onEvent.add(EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: roomID,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||||
|
'sender': '@alice:example.com',
|
||||||
|
'status': 1,
|
||||||
|
'event_id': '\$event',
|
||||||
|
'origin_server_ts': testTimeStamp,
|
||||||
|
'unsigned': {'transaction_id': 'transaction'}
|
||||||
|
},
|
||||||
|
sortOrder: room.newSortOrder));
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(timeline.events[0].status, 1);
|
||||||
|
expect(timeline.events.length, 1);
|
||||||
|
client.onEvent.add(EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: roomID,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||||
|
'sender': '@alice:example.com',
|
||||||
|
'status': 2,
|
||||||
|
'event_id': '\$event',
|
||||||
|
'origin_server_ts': testTimeStamp,
|
||||||
|
'unsigned': {'transaction_id': 'transaction'}
|
||||||
|
},
|
||||||
|
sortOrder: room.newSortOrder));
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(timeline.events[0].status, 2);
|
||||||
|
expect(timeline.events.length, 1);
|
||||||
|
});
|
||||||
|
test('sending an event where the sync reply arrives first, 0 -> 2 -> 1',
|
||||||
|
() async {
|
||||||
|
timeline.events.clear();
|
||||||
|
client.onEvent.add(EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: roomID,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||||
|
'sender': '@alice:example.com',
|
||||||
|
'event_id': 'transaction',
|
||||||
|
'origin_server_ts': testTimeStamp,
|
||||||
|
'unsigned': {
|
||||||
|
MessageSendingStatusKey: 0,
|
||||||
|
'transaction_id': 'transaction',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sortOrder: room.newSortOrder));
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(timeline.events[0].status, 0);
|
||||||
|
expect(timeline.events.length, 1);
|
||||||
|
client.onEvent.add(EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: roomID,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||||
|
'sender': '@alice:example.com',
|
||||||
|
'event_id': '\$event',
|
||||||
|
'origin_server_ts': testTimeStamp,
|
||||||
|
'unsigned': {
|
||||||
|
'transaction_id': 'transaction',
|
||||||
|
MessageSendingStatusKey: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sortOrder: room.newSortOrder));
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(timeline.events[0].status, 2);
|
||||||
|
expect(timeline.events.length, 1);
|
||||||
|
client.onEvent.add(EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: roomID,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||||
|
'sender': '@alice:example.com',
|
||||||
|
'event_id': '\$event',
|
||||||
|
'origin_server_ts': testTimeStamp,
|
||||||
|
'unsigned': {
|
||||||
|
'transaction_id': 'transaction',
|
||||||
|
MessageSendingStatusKey: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sortOrder: room.newSortOrder));
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(timeline.events[0].status, 2);
|
||||||
|
expect(timeline.events.length, 1);
|
||||||
|
});
|
||||||
|
test('sending an event 0 -> -1 -> 2', () async {
|
||||||
|
timeline.events.clear();
|
||||||
|
client.onEvent.add(EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: roomID,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||||
|
'sender': '@alice:example.com',
|
||||||
|
'status': 0,
|
||||||
|
'event_id': 'transaction',
|
||||||
|
'origin_server_ts': testTimeStamp
|
||||||
|
},
|
||||||
|
sortOrder: room.newSortOrder));
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(timeline.events[0].status, 0);
|
||||||
|
expect(timeline.events.length, 1);
|
||||||
|
client.onEvent.add(EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: roomID,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||||
|
'sender': '@alice:example.com',
|
||||||
|
'status': -1,
|
||||||
|
'origin_server_ts': testTimeStamp,
|
||||||
|
'unsigned': {'transaction_id': 'transaction'},
|
||||||
|
},
|
||||||
|
sortOrder: room.newSortOrder));
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(timeline.events[0].status, -1);
|
||||||
|
expect(timeline.events.length, 1);
|
||||||
|
client.onEvent.add(EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: roomID,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||||
|
'sender': '@alice:example.com',
|
||||||
|
'status': 2,
|
||||||
|
'event_id': '\$event',
|
||||||
|
'origin_server_ts': testTimeStamp,
|
||||||
|
'unsigned': {'transaction_id': 'transaction'},
|
||||||
|
},
|
||||||
|
sortOrder: room.newSortOrder));
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(timeline.events[0].status, 2);
|
||||||
|
expect(timeline.events.length, 1);
|
||||||
|
});
|
||||||
|
test('sending an event 0 -> 2 -> -1', () async {
|
||||||
|
timeline.events.clear();
|
||||||
|
client.onEvent.add(EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: roomID,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||||
|
'sender': '@alice:example.com',
|
||||||
|
'status': 0,
|
||||||
|
'event_id': 'transaction',
|
||||||
|
'origin_server_ts': testTimeStamp
|
||||||
|
},
|
||||||
|
sortOrder: room.newSortOrder));
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(timeline.events[0].status, 0);
|
||||||
|
expect(timeline.events.length, 1);
|
||||||
|
client.onEvent.add(EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: roomID,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||||
|
'sender': '@alice:example.com',
|
||||||
|
'status': 2,
|
||||||
|
'event_id': '\$event',
|
||||||
|
'origin_server_ts': testTimeStamp,
|
||||||
|
'unsigned': {'transaction_id': 'transaction'},
|
||||||
|
},
|
||||||
|
sortOrder: room.newSortOrder));
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(timeline.events[0].status, 2);
|
||||||
|
expect(timeline.events.length, 1);
|
||||||
|
client.onEvent.add(EventUpdate(
|
||||||
|
type: 'timeline',
|
||||||
|
roomID: roomID,
|
||||||
|
eventType: 'm.room.message',
|
||||||
|
content: {
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
|
||||||
|
'sender': '@alice:example.com',
|
||||||
|
'status': -1,
|
||||||
|
'origin_server_ts': testTimeStamp,
|
||||||
|
'unsigned': {'transaction_id': 'transaction'},
|
||||||
|
},
|
||||||
|
sortOrder: room.newSortOrder));
|
||||||
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
|
expect(timeline.events[0].status, 2);
|
||||||
|
expect(timeline.events.length, 1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ import 'fake_matrix_api.dart';
|
||||||
void main() {
|
void main() {
|
||||||
/// All Tests related to the Event
|
/// All Tests related to the Event
|
||||||
group('User', () {
|
group('User', () {
|
||||||
var client = Client('testclient', debug: true, httpClient: FakeMatrixApi());
|
var client = Client('testclient', httpClient: FakeMatrixApi());
|
||||||
final user1 = User(
|
final user1 = User(
|
||||||
'@alice:example.com',
|
'@alice:example.com',
|
||||||
membership: 'join',
|
membership: 'join',
|
||||||
|
@ -102,7 +102,7 @@ void main() {
|
||||||
});
|
});
|
||||||
test('startDirectChat', () async {
|
test('startDirectChat', () async {
|
||||||
await client.checkServer('https://fakeserver.notexisting');
|
await client.checkServer('https://fakeserver.notexisting');
|
||||||
await client.login('test', '1234');
|
await client.login(user: 'test', password: '1234');
|
||||||
await user1.startDirectChat();
|
await user1.startDirectChat();
|
||||||
});
|
});
|
||||||
test('getPresence', () async {
|
test('getPresence', () async {
|
||||||
|
@ -132,6 +132,8 @@ void main() {
|
||||||
await client.checkServer('https://fakeserver.notexisting');
|
await client.checkServer('https://fakeserver.notexisting');
|
||||||
expect(user1.canChangePowerLevel, false);
|
expect(user1.canChangePowerLevel, false);
|
||||||
});
|
});
|
||||||
client.dispose();
|
test('dispose client', () async {
|
||||||
|
await client.dispose();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh -e
|
||||||
|
pub run test_driver/famedlysdk_test.dart -p vm
|
|
@ -1,14 +1,11 @@
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
import 'package:famedlysdk/matrix_api.dart';
|
import 'package:famedlysdk/matrix_api.dart';
|
||||||
|
import 'package:famedlysdk/src/utils/logs.dart';
|
||||||
import '../test/fake_database.dart';
|
import '../test/fake_database.dart';
|
||||||
|
import 'test_config.dart';
|
||||||
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
void main() => test();
|
void main() => test();
|
||||||
|
|
||||||
const String homeserver = 'https://matrix.test.famedly.de';
|
|
||||||
const String testUserA = '@tick:test.famedly.de';
|
|
||||||
const String testPasswordA = 'test';
|
|
||||||
const String testUserB = '@trick:test.famedly.de';
|
|
||||||
const String testPasswordB = 'test';
|
|
||||||
const String testMessage = 'Hello world';
|
const String testMessage = 'Hello world';
|
||||||
const String testMessage2 = 'Hello moon';
|
const String testMessage2 = 'Hello moon';
|
||||||
const String testMessage3 = 'Hello sun';
|
const String testMessage3 = 'Hello sun';
|
||||||
|
@ -17,186 +14,198 @@ const String testMessage5 = 'Hello earth';
|
||||||
const String testMessage6 = 'Hello mars';
|
const String testMessage6 = 'Hello mars';
|
||||||
|
|
||||||
void test() async {
|
void test() async {
|
||||||
print('++++ Login $testUserA ++++');
|
Client testClientA, testClientB;
|
||||||
var testClientA = Client('TestClientA', debug: false);
|
|
||||||
testClientA.database = getDatabase();
|
|
||||||
await testClientA.checkServer(homeserver);
|
|
||||||
await testClientA.login(testUserA, testPasswordA);
|
|
||||||
assert(testClientA.encryptionEnabled);
|
|
||||||
|
|
||||||
print('++++ Login $testUserB ++++');
|
try {
|
||||||
var testClientB = Client('TestClientB', debug: false);
|
await olm.init();
|
||||||
testClientB.database = getDatabase();
|
olm.Account();
|
||||||
await testClientB.checkServer(homeserver);
|
Logs.success('[LibOlm] Enabled');
|
||||||
await testClientB.login(testUserB, testPasswordA);
|
|
||||||
assert(testClientB.encryptionEnabled);
|
|
||||||
|
|
||||||
print('++++ ($testUserA) Leave all rooms ++++');
|
Logs.success('++++ Login Alice at ++++');
|
||||||
while (testClientA.rooms.isNotEmpty) {
|
testClientA = Client('TestClientA');
|
||||||
var room = testClientA.rooms.first;
|
testClientA.database = getDatabase();
|
||||||
if (room.canonicalAlias?.isNotEmpty ?? false) {
|
await testClientA.checkServer(TestUser.homeserver);
|
||||||
break;
|
await testClientA.login(
|
||||||
}
|
user: TestUser.username, password: TestUser.password);
|
||||||
try {
|
assert(testClientA.encryptionEnabled);
|
||||||
await room.leave();
|
|
||||||
await room.forget();
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
print('++++ ($testUserB) Leave all rooms ++++');
|
Logs.success('++++ Login Bob ++++');
|
||||||
for (var i = 0; i < 3; i++) {
|
testClientB = Client('TestClientB');
|
||||||
if (testClientB.rooms.isNotEmpty) {
|
testClientB.database = getDatabase();
|
||||||
var room = testClientB.rooms.first;
|
await testClientB.checkServer(TestUser.homeserver);
|
||||||
|
await testClientB.login(
|
||||||
|
user: TestUser.username2, password: TestUser.password);
|
||||||
|
assert(testClientB.encryptionEnabled);
|
||||||
|
|
||||||
|
Logs.success('++++ (Alice) Leave all rooms ++++');
|
||||||
|
while (testClientA.rooms.isNotEmpty) {
|
||||||
|
var room = testClientA.rooms.first;
|
||||||
|
if (room.canonicalAlias?.isNotEmpty ?? false) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await room.leave();
|
await room.leave();
|
||||||
await room.forget();
|
await room.forget();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
print('++++ Check if own olm device is verified by default ++++');
|
Logs.success('++++ (Bob) Leave all rooms ++++');
|
||||||
assert(testClientA.userDeviceKeys.containsKey(testUserA));
|
for (var i = 0; i < 3; i++) {
|
||||||
assert(testClientA.userDeviceKeys[testUserA].deviceKeys
|
if (testClientB.rooms.isNotEmpty) {
|
||||||
.containsKey(testClientA.deviceID));
|
var room = testClientB.rooms.first;
|
||||||
assert(testClientA
|
try {
|
||||||
.userDeviceKeys[testUserA].deviceKeys[testClientA.deviceID].verified);
|
await room.leave();
|
||||||
assert(!testClientA
|
await room.forget();
|
||||||
.userDeviceKeys[testUserA].deviceKeys[testClientA.deviceID].blocked);
|
} catch (_) {}
|
||||||
assert(testClientB.userDeviceKeys.containsKey(testUserB));
|
}
|
||||||
assert(testClientB.userDeviceKeys[testUserB].deviceKeys
|
}
|
||||||
.containsKey(testClientB.deviceID));
|
|
||||||
assert(testClientB
|
|
||||||
.userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID].verified);
|
|
||||||
assert(!testClientB
|
|
||||||
.userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID].blocked);
|
|
||||||
|
|
||||||
print('++++ ($testUserA) Create room and invite $testUserB ++++');
|
Logs.success('++++ Check if own olm device is verified by default ++++');
|
||||||
await testClientA.api.createRoom(invite: [testUserB]);
|
assert(testClientA.userDeviceKeys.containsKey(TestUser.username));
|
||||||
await Future.delayed(Duration(seconds: 1));
|
assert(testClientA.userDeviceKeys[TestUser.username].deviceKeys
|
||||||
var room = testClientA.rooms.first;
|
.containsKey(testClientA.deviceID));
|
||||||
assert(room != null);
|
assert(testClientA.userDeviceKeys[TestUser.username]
|
||||||
final roomId = room.id;
|
.deviceKeys[testClientA.deviceID].verified);
|
||||||
|
assert(!testClientA.userDeviceKeys[TestUser.username]
|
||||||
|
.deviceKeys[testClientA.deviceID].blocked);
|
||||||
|
assert(testClientB.userDeviceKeys.containsKey(TestUser.username2));
|
||||||
|
assert(testClientB.userDeviceKeys[TestUser.username2].deviceKeys
|
||||||
|
.containsKey(testClientB.deviceID));
|
||||||
|
assert(testClientB.userDeviceKeys[TestUser.username2]
|
||||||
|
.deviceKeys[testClientB.deviceID].verified);
|
||||||
|
assert(!testClientB.userDeviceKeys[TestUser.username2]
|
||||||
|
.deviceKeys[testClientB.deviceID].blocked);
|
||||||
|
|
||||||
print('++++ ($testUserB) Join room ++++');
|
Logs.success('++++ (Alice) Create room and invite Bob ++++');
|
||||||
var inviteRoom = testClientB.getRoomById(roomId);
|
await testClientA.createRoom(invite: [TestUser.username2]);
|
||||||
await inviteRoom.join();
|
await Future.delayed(Duration(seconds: 1));
|
||||||
await Future.delayed(Duration(seconds: 1));
|
var room = testClientA.rooms.first;
|
||||||
assert(inviteRoom.membership == Membership.join);
|
assert(room != null);
|
||||||
|
final roomId = room.id;
|
||||||
|
|
||||||
print('++++ ($testUserA) Enable encryption ++++');
|
Logs.success('++++ (Bob) Join room ++++');
|
||||||
assert(room.encrypted == false);
|
var inviteRoom = testClientB.getRoomById(roomId);
|
||||||
await room.enableEncryption();
|
await inviteRoom.join();
|
||||||
await Future.delayed(Duration(seconds: 5));
|
await Future.delayed(Duration(seconds: 1));
|
||||||
assert(room.encrypted == true);
|
assert(inviteRoom.membership == Membership.join);
|
||||||
assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) ==
|
|
||||||
null);
|
|
||||||
|
|
||||||
print('++++ ($testUserA) Check known olm devices ++++');
|
Logs.success('++++ (Alice) Enable encryption ++++');
|
||||||
assert(testClientA.userDeviceKeys.containsKey(testUserB));
|
assert(room.encrypted == false);
|
||||||
assert(testClientA.userDeviceKeys[testUserB].deviceKeys
|
await room.enableEncryption();
|
||||||
.containsKey(testClientB.deviceID));
|
await Future.delayed(Duration(seconds: 5));
|
||||||
assert(!testClientA
|
assert(room.encrypted == true);
|
||||||
.userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID].verified);
|
assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) ==
|
||||||
assert(!testClientA
|
null);
|
||||||
.userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID].blocked);
|
|
||||||
assert(testClientB.userDeviceKeys.containsKey(testUserA));
|
|
||||||
assert(testClientB.userDeviceKeys[testUserA].deviceKeys
|
|
||||||
.containsKey(testClientA.deviceID));
|
|
||||||
assert(!testClientB
|
|
||||||
.userDeviceKeys[testUserA].deviceKeys[testClientA.deviceID].verified);
|
|
||||||
assert(!testClientB
|
|
||||||
.userDeviceKeys[testUserA].deviceKeys[testClientA.deviceID].blocked);
|
|
||||||
await testClientA.userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID]
|
|
||||||
.setVerified(true);
|
|
||||||
|
|
||||||
print('++++ Check if own olm device is verified by default ++++');
|
Logs.success('++++ (Alice) Check known olm devices ++++');
|
||||||
assert(testClientA.userDeviceKeys.containsKey(testUserA));
|
assert(testClientA.userDeviceKeys.containsKey(TestUser.username2));
|
||||||
assert(testClientA.userDeviceKeys[testUserA].deviceKeys
|
assert(testClientA.userDeviceKeys[TestUser.username2].deviceKeys
|
||||||
.containsKey(testClientA.deviceID));
|
.containsKey(testClientB.deviceID));
|
||||||
assert(testClientA
|
assert(!testClientA.userDeviceKeys[TestUser.username2]
|
||||||
.userDeviceKeys[testUserA].deviceKeys[testClientA.deviceID].verified);
|
.deviceKeys[testClientB.deviceID].verified);
|
||||||
assert(testClientB.userDeviceKeys.containsKey(testUserB));
|
assert(!testClientA.userDeviceKeys[TestUser.username2]
|
||||||
assert(testClientB.userDeviceKeys[testUserB].deviceKeys
|
.deviceKeys[testClientB.deviceID].blocked);
|
||||||
.containsKey(testClientB.deviceID));
|
assert(testClientB.userDeviceKeys.containsKey(TestUser.username));
|
||||||
assert(testClientB
|
assert(testClientB.userDeviceKeys[TestUser.username].deviceKeys
|
||||||
.userDeviceKeys[testUserB].deviceKeys[testClientB.deviceID].verified);
|
.containsKey(testClientA.deviceID));
|
||||||
|
assert(!testClientB.userDeviceKeys[TestUser.username]
|
||||||
|
.deviceKeys[testClientA.deviceID].verified);
|
||||||
|
assert(!testClientB.userDeviceKeys[TestUser.username]
|
||||||
|
.deviceKeys[testClientA.deviceID].blocked);
|
||||||
|
await testClientA
|
||||||
|
.userDeviceKeys[TestUser.username2].deviceKeys[testClientB.deviceID]
|
||||||
|
.setVerified(true);
|
||||||
|
|
||||||
print("++++ ($testUserA) Send encrypted message: '$testMessage' ++++");
|
Logs.success('++++ Check if own olm device is verified by default ++++');
|
||||||
await room.sendTextEvent(testMessage);
|
assert(testClientA.userDeviceKeys.containsKey(TestUser.username));
|
||||||
await Future.delayed(Duration(seconds: 5));
|
assert(testClientA.userDeviceKeys[TestUser.username].deviceKeys
|
||||||
assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) !=
|
.containsKey(testClientA.deviceID));
|
||||||
null);
|
assert(testClientA.userDeviceKeys[TestUser.username]
|
||||||
var currentSessionIdA = room.client.encryption.keyManager
|
.deviceKeys[testClientA.deviceID].verified);
|
||||||
.getOutboundGroupSession(room.id)
|
assert(testClientB.userDeviceKeys.containsKey(TestUser.username2));
|
||||||
.outboundGroupSession
|
assert(testClientB.userDeviceKeys[TestUser.username2].deviceKeys
|
||||||
.session_id();
|
.containsKey(testClientB.deviceID));
|
||||||
assert(room.client.encryption.keyManager
|
assert(testClientB.userDeviceKeys[TestUser.username2]
|
||||||
|
.deviceKeys[testClientB.deviceID].verified);
|
||||||
|
|
||||||
|
Logs.success("++++ (Alice) Send encrypted message: '$testMessage' ++++");
|
||||||
|
await room.sendTextEvent(testMessage);
|
||||||
|
await Future.delayed(Duration(seconds: 5));
|
||||||
|
assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) !=
|
||||||
|
null);
|
||||||
|
var currentSessionIdA = room.client.encryption.keyManager
|
||||||
|
.getOutboundGroupSession(room.id)
|
||||||
|
.outboundGroupSession
|
||||||
|
.session_id();
|
||||||
|
/*assert(room.client.encryption.keyManager
|
||||||
.getInboundGroupSession(room.id, currentSessionIdA, '') !=
|
.getInboundGroupSession(room.id, currentSessionIdA, '') !=
|
||||||
null);
|
null);*/
|
||||||
assert(testClientA
|
assert(testClientA.encryption.olmManager
|
||||||
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
.olmSessions[testClientB.identityKey].length ==
|
||||||
1);
|
1);
|
||||||
assert(testClientB
|
assert(testClientB.encryption.olmManager
|
||||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
.olmSessions[testClientA.identityKey].length ==
|
||||||
1);
|
1);
|
||||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey]
|
assert(testClientA.encryption.olmManager
|
||||||
.first.sessionId ==
|
.olmSessions[testClientB.identityKey].first.sessionId ==
|
||||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||||
.first.sessionId);
|
.first.sessionId);
|
||||||
assert(inviteRoom.client.encryption.keyManager
|
/*assert(inviteRoom.client.encryption.keyManager
|
||||||
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
|
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
|
||||||
null);
|
null);*/
|
||||||
assert(room.lastMessage == testMessage);
|
assert(room.lastMessage == testMessage);
|
||||||
assert(inviteRoom.lastMessage == testMessage);
|
assert(inviteRoom.lastMessage == testMessage);
|
||||||
print(
|
Logs.success(
|
||||||
"++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
|
"++++ (Bob) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
|
||||||
|
|
||||||
print("++++ ($testUserA) Send again encrypted message: '$testMessage2' ++++");
|
Logs.success(
|
||||||
await room.sendTextEvent(testMessage2);
|
"++++ (Alice) Send again encrypted message: '$testMessage2' ++++");
|
||||||
await Future.delayed(Duration(seconds: 5));
|
await room.sendTextEvent(testMessage2);
|
||||||
assert(testClientA
|
await Future.delayed(Duration(seconds: 5));
|
||||||
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
assert(testClientA.encryption.olmManager
|
||||||
1);
|
.olmSessions[testClientB.identityKey].length ==
|
||||||
assert(testClientB
|
1);
|
||||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
assert(testClientB.encryption.olmManager
|
||||||
1);
|
.olmSessions[testClientA.identityKey].length ==
|
||||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey]
|
1);
|
||||||
.first.sessionId ==
|
assert(testClientA.encryption.olmManager
|
||||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
.olmSessions[testClientB.identityKey].first.sessionId ==
|
||||||
.first.sessionId);
|
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||||
|
.first.sessionId);
|
||||||
|
|
||||||
assert(room.client.encryption.keyManager
|
assert(room.client.encryption.keyManager
|
||||||
.getOutboundGroupSession(room.id)
|
.getOutboundGroupSession(room.id)
|
||||||
.outboundGroupSession
|
.outboundGroupSession
|
||||||
.session_id() ==
|
.session_id() ==
|
||||||
currentSessionIdA);
|
currentSessionIdA);
|
||||||
assert(room.client.encryption.keyManager
|
/*assert(room.client.encryption.keyManager
|
||||||
.getInboundGroupSession(room.id, currentSessionIdA, '') !=
|
.getInboundGroupSession(room.id, currentSessionIdA, '') !=
|
||||||
null);
|
null);*/
|
||||||
assert(room.lastMessage == testMessage2);
|
assert(room.lastMessage == testMessage2);
|
||||||
assert(inviteRoom.lastMessage == testMessage2);
|
assert(inviteRoom.lastMessage == testMessage2);
|
||||||
print(
|
Logs.success(
|
||||||
"++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
|
"++++ (Bob) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
|
||||||
|
|
||||||
print("++++ ($testUserB) Send again encrypted message: '$testMessage3' ++++");
|
Logs.success(
|
||||||
await inviteRoom.sendTextEvent(testMessage3);
|
"++++ (Bob) Send again encrypted message: '$testMessage3' ++++");
|
||||||
await Future.delayed(Duration(seconds: 5));
|
await inviteRoom.sendTextEvent(testMessage3);
|
||||||
assert(testClientA
|
await Future.delayed(Duration(seconds: 5));
|
||||||
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
assert(testClientA.encryption.olmManager
|
||||||
1);
|
.olmSessions[testClientB.identityKey].length ==
|
||||||
assert(testClientB
|
1);
|
||||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
assert(testClientB.encryption.olmManager
|
||||||
1);
|
.olmSessions[testClientA.identityKey].length ==
|
||||||
assert(room.client.encryption.keyManager
|
1);
|
||||||
.getOutboundGroupSession(room.id)
|
assert(room.client.encryption.keyManager
|
||||||
.outboundGroupSession
|
.getOutboundGroupSession(room.id)
|
||||||
.session_id() ==
|
.outboundGroupSession
|
||||||
currentSessionIdA);
|
.session_id() ==
|
||||||
var inviteRoomOutboundGroupSession = inviteRoom.client.encryption.keyManager
|
currentSessionIdA);
|
||||||
.getOutboundGroupSession(inviteRoom.id);
|
var inviteRoomOutboundGroupSession = inviteRoom.client.encryption.keyManager
|
||||||
|
.getOutboundGroupSession(inviteRoom.id);
|
||||||
|
|
||||||
assert(inviteRoomOutboundGroupSession != null);
|
assert(inviteRoomOutboundGroupSession != null);
|
||||||
assert(inviteRoom.client.encryption.keyManager.getInboundGroupSession(
|
/*assert(inviteRoom.client.encryption.keyManager.getInboundGroupSession(
|
||||||
inviteRoom.id,
|
inviteRoom.id,
|
||||||
inviteRoomOutboundGroupSession.outboundGroupSession.session_id(),
|
inviteRoomOutboundGroupSession.outboundGroupSession.session_id(),
|
||||||
'') !=
|
'') !=
|
||||||
|
@ -205,146 +214,113 @@ void test() async {
|
||||||
room.id,
|
room.id,
|
||||||
inviteRoomOutboundGroupSession.outboundGroupSession.session_id(),
|
inviteRoomOutboundGroupSession.outboundGroupSession.session_id(),
|
||||||
'') !=
|
'') !=
|
||||||
null);
|
null);*/
|
||||||
assert(inviteRoom.lastMessage == testMessage3);
|
assert(inviteRoom.lastMessage == testMessage3);
|
||||||
assert(room.lastMessage == testMessage3);
|
assert(room.lastMessage == testMessage3);
|
||||||
print(
|
Logs.success(
|
||||||
"++++ ($testUserA) Received decrypted message: '${room.lastMessage}' ++++");
|
"++++ (Alice) Received decrypted message: '${room.lastMessage}' ++++");
|
||||||
|
|
||||||
print('++++ Login $testUserB in another client ++++');
|
Logs.success('++++ Login Bob in another client ++++');
|
||||||
var testClientC =
|
var testClientC = Client('TestClientC', database: getDatabase());
|
||||||
Client('TestClientC', debug: false, database: getDatabase());
|
await testClientC.checkServer(TestUser.homeserver);
|
||||||
await testClientC.checkServer(homeserver);
|
await testClientC.login(
|
||||||
await testClientC.login(testUserB, testPasswordA);
|
user: TestUser.username2, password: TestUser.password);
|
||||||
await Future.delayed(Duration(seconds: 3));
|
await Future.delayed(Duration(seconds: 3));
|
||||||
|
|
||||||
print("++++ ($testUserA) Send again encrypted message: '$testMessage4' ++++");
|
Logs.success(
|
||||||
await room.sendTextEvent(testMessage4);
|
"++++ (Alice) Send again encrypted message: '$testMessage4' ++++");
|
||||||
await Future.delayed(Duration(seconds: 5));
|
await room.sendTextEvent(testMessage4);
|
||||||
assert(testClientA
|
await Future.delayed(Duration(seconds: 5));
|
||||||
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
assert(testClientA.encryption.olmManager
|
||||||
1);
|
.olmSessions[testClientB.identityKey].length ==
|
||||||
assert(testClientB
|
1);
|
||||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
assert(testClientB.encryption.olmManager
|
||||||
1);
|
.olmSessions[testClientA.identityKey].length ==
|
||||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey]
|
1);
|
||||||
.first.sessionId ==
|
assert(testClientA.encryption.olmManager
|
||||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
.olmSessions[testClientB.identityKey].first.sessionId ==
|
||||||
.first.sessionId);
|
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||||
assert(testClientA
|
.first.sessionId);
|
||||||
.encryption.olmManager.olmSessions[testClientC.identityKey].length ==
|
assert(testClientA.encryption.olmManager
|
||||||
1);
|
.olmSessions[testClientC.identityKey].length ==
|
||||||
assert(testClientC
|
1);
|
||||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
assert(testClientC.encryption.olmManager
|
||||||
1);
|
.olmSessions[testClientA.identityKey].length ==
|
||||||
assert(testClientA.encryption.olmManager.olmSessions[testClientC.identityKey]
|
1);
|
||||||
.first.sessionId ==
|
assert(testClientA.encryption.olmManager
|
||||||
testClientC.encryption.olmManager.olmSessions[testClientA.identityKey]
|
.olmSessions[testClientC.identityKey].first.sessionId ==
|
||||||
.first.sessionId);
|
testClientC.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||||
assert(room.client.encryption.keyManager
|
.first.sessionId);
|
||||||
.getOutboundGroupSession(room.id)
|
assert(room.client.encryption.keyManager
|
||||||
.outboundGroupSession
|
.getOutboundGroupSession(room.id)
|
||||||
.session_id() !=
|
.outboundGroupSession
|
||||||
currentSessionIdA);
|
.session_id() !=
|
||||||
currentSessionIdA = room.client.encryption.keyManager
|
currentSessionIdA);
|
||||||
.getOutboundGroupSession(room.id)
|
currentSessionIdA = room.client.encryption.keyManager
|
||||||
.outboundGroupSession
|
.getOutboundGroupSession(room.id)
|
||||||
.session_id();
|
.outboundGroupSession
|
||||||
assert(inviteRoom.client.encryption.keyManager
|
.session_id();
|
||||||
|
/*assert(inviteRoom.client.encryption.keyManager
|
||||||
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
|
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
|
||||||
null);
|
null);*/
|
||||||
assert(room.lastMessage == testMessage4);
|
assert(room.lastMessage == testMessage4);
|
||||||
assert(inviteRoom.lastMessage == testMessage4);
|
assert(inviteRoom.lastMessage == testMessage4);
|
||||||
print(
|
Logs.success(
|
||||||
"++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
|
"++++ (Bob) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
|
||||||
|
|
||||||
print('++++ Logout $testUserB another client ++++');
|
Logs.success('++++ Logout Bob another client ++++');
|
||||||
await testClientC.dispose();
|
await testClientC.dispose();
|
||||||
await testClientC.logout();
|
await testClientC.logout();
|
||||||
testClientC = null;
|
testClientC = null;
|
||||||
await Future.delayed(Duration(seconds: 5));
|
await Future.delayed(Duration(seconds: 5));
|
||||||
|
|
||||||
print("++++ ($testUserA) Send again encrypted message: '$testMessage6' ++++");
|
Logs.success(
|
||||||
await room.sendTextEvent(testMessage6);
|
"++++ (Alice) Send again encrypted message: '$testMessage6' ++++");
|
||||||
await Future.delayed(Duration(seconds: 5));
|
await room.sendTextEvent(testMessage6);
|
||||||
assert(testClientA
|
await Future.delayed(Duration(seconds: 5));
|
||||||
.encryption.olmManager.olmSessions[testClientB.identityKey].length ==
|
assert(testClientA.encryption.olmManager
|
||||||
1);
|
.olmSessions[testClientB.identityKey].length ==
|
||||||
assert(testClientB
|
1);
|
||||||
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
|
assert(testClientB.encryption.olmManager
|
||||||
1);
|
.olmSessions[testClientA.identityKey].length ==
|
||||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey]
|
1);
|
||||||
.first.sessionId ==
|
assert(testClientA.encryption.olmManager
|
||||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
.olmSessions[testClientB.identityKey].first.sessionId ==
|
||||||
.first.sessionId);
|
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
|
||||||
assert(room.client.encryption.keyManager
|
.first.sessionId);
|
||||||
.getOutboundGroupSession(room.id)
|
assert(room.client.encryption.keyManager
|
||||||
.outboundGroupSession
|
.getOutboundGroupSession(room.id)
|
||||||
.session_id() !=
|
.outboundGroupSession
|
||||||
currentSessionIdA);
|
.session_id() !=
|
||||||
currentSessionIdA = room.client.encryption.keyManager
|
currentSessionIdA);
|
||||||
.getOutboundGroupSession(room.id)
|
currentSessionIdA = room.client.encryption.keyManager
|
||||||
.outboundGroupSession
|
.getOutboundGroupSession(room.id)
|
||||||
.session_id();
|
.outboundGroupSession
|
||||||
assert(inviteRoom.client.encryption.keyManager
|
.session_id();
|
||||||
|
/*assert(inviteRoom.client.encryption.keyManager
|
||||||
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
|
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
|
||||||
null);
|
null);*/
|
||||||
assert(room.lastMessage == testMessage6);
|
assert(room.lastMessage == testMessage6);
|
||||||
assert(inviteRoom.lastMessage == testMessage6);
|
assert(inviteRoom.lastMessage == testMessage6);
|
||||||
print(
|
Logs.success(
|
||||||
"++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
|
"++++ (Bob) Received decrypted message: '${inviteRoom.lastMessage}' ++++");
|
||||||
|
|
||||||
/* print('++++ ($testUserA) Restore user ++++');
|
await room.leave();
|
||||||
await testClientA.dispose();
|
await room.forget();
|
||||||
testClientA = null;
|
await inviteRoom.leave();
|
||||||
testClientA = Client(
|
await inviteRoom.forget();
|
||||||
'TestClientA',
|
await Future.delayed(Duration(seconds: 1));
|
||||||
debug: false,
|
} catch (e, s) {
|
||||||
database: getDatabase(),
|
Logs.error('Test failed: ${e.toString()}', s);
|
||||||
);
|
rethrow;
|
||||||
testClientA.connect();
|
} finally {
|
||||||
await Future.delayed(Duration(seconds: 3));
|
Logs.success('++++ Logout Alice and Bob ++++');
|
||||||
var restoredRoom = testClientA.rooms.first;
|
if (testClientA?.isLogged() ?? false) await testClientA.logoutAll();
|
||||||
assert(room != null);
|
if (testClientA?.isLogged() ?? false) await testClientB.logoutAll();
|
||||||
assert(restoredRoom.id == room.id);
|
await testClientA?.dispose();
|
||||||
assert(restoredRoom.outboundGroupSession.session_id() ==
|
await testClientB?.dispose();
|
||||||
room.outboundGroupSession.session_id());
|
testClientA = null;
|
||||||
assert(restoredRoom.inboundGroupSessions.length == 4);
|
testClientB = null;
|
||||||
assert(restoredRoom.inboundGroupSessions.length ==
|
|
||||||
room.inboundGroupSessions.length);
|
|
||||||
for (var i = 0; i < restoredRoom.inboundGroupSessions.length; i++) {
|
|
||||||
assert(restoredRoom.inboundGroupSessions.keys.toList()[i] ==
|
|
||||||
room.inboundGroupSessions.keys.toList()[i]);
|
|
||||||
}
|
}
|
||||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].length == 1);
|
|
||||||
assert(testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].length == 1);
|
|
||||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].first.session_id() ==
|
|
||||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].first.session_id());
|
|
||||||
|
|
||||||
print("++++ ($testUserA) Send again encrypted message: '$testMessage5' ++++");
|
|
||||||
await restoredRoom.sendTextEvent(testMessage5);
|
|
||||||
await Future.delayed(Duration(seconds: 5));
|
|
||||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].length == 1);
|
|
||||||
assert(testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].length == 1);
|
|
||||||
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].first.session_id() ==
|
|
||||||
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].first.session_id());
|
|
||||||
assert(restoredRoom.lastMessage == testMessage5);
|
|
||||||
assert(inviteRoom.lastMessage == testMessage5);
|
|
||||||
assert(testClientB.getRoomById(roomId).lastMessage == testMessage5);
|
|
||||||
print(
|
|
||||||
"++++ ($testUserB) Received decrypted message: '${inviteRoom.lastMessage}' ++++");*/
|
|
||||||
|
|
||||||
print('++++ Logout $testUserA and $testUserB ++++');
|
|
||||||
await room.leave();
|
|
||||||
await room.forget();
|
|
||||||
await inviteRoom.leave();
|
|
||||||
await inviteRoom.forget();
|
|
||||||
await Future.delayed(Duration(seconds: 1));
|
|
||||||
await testClientA.dispose();
|
|
||||||
await testClientB.dispose();
|
|
||||||
await testClientA.api.logoutAll();
|
|
||||||
await testClientB.api.logoutAll();
|
|
||||||
testClientA = null;
|
|
||||||
testClientB = null;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
class TestUser {
|
||||||
|
static const String homeserver = 'https://enter-your-server.here';
|
||||||
|
static const String username = 'alice';
|
||||||
|
static const String username2 = 'bob';
|
||||||
|
static const String password = '1234';
|
||||||
|
}
|
Loading…
Reference in New Issue