Initial commit version 0.8.13
This commit is contained in:
commit
9526dfa4f2
111 changed files with 35074 additions and 0 deletions
237
lib/src/utils/commands_extension.dart
Normal file
237
lib/src/utils/commands_extension.dart
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import '../../matrix.dart';
|
||||
|
||||
extension CommandsClientExtension on Client {
|
||||
/// Add a command to the command handler. `command` is its name, and `callback` is the
|
||||
/// callback to invoke
|
||||
void addCommand(
|
||||
String command, FutureOr<String?> Function(CommandArgs) callback) {
|
||||
commands[command.toLowerCase()] = callback;
|
||||
}
|
||||
|
||||
/// Parse and execute a string, `msg` is the input. Optionally `inReplyTo` is the event being
|
||||
/// replied to and `editEventId` is the eventId of the event being replied to
|
||||
Future<String?> parseAndRunCommand(Room room, String msg,
|
||||
{Event? inReplyTo, String? editEventId, String? txid}) async {
|
||||
final args = CommandArgs(
|
||||
inReplyTo: inReplyTo,
|
||||
editEventId: editEventId,
|
||||
msg: '',
|
||||
room: room,
|
||||
txid: txid,
|
||||
);
|
||||
if (!msg.startsWith('/')) {
|
||||
final sendCommand = commands['send'];
|
||||
if (sendCommand != null) {
|
||||
args.msg = msg;
|
||||
return await sendCommand(args);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// remove the /
|
||||
msg = msg.substring(1);
|
||||
var command = msg;
|
||||
if (msg.contains(' ')) {
|
||||
final idx = msg.indexOf(' ');
|
||||
command = msg.substring(0, idx).toLowerCase();
|
||||
args.msg = msg.substring(idx + 1);
|
||||
} else {
|
||||
command = msg.toLowerCase();
|
||||
}
|
||||
final commandOp = commands[command];
|
||||
if (commandOp != null) {
|
||||
return await commandOp(args);
|
||||
}
|
||||
if (msg.startsWith('/') && commands.containsKey('send')) {
|
||||
// re-set to include the "command"
|
||||
final sendCommand = commands['send'];
|
||||
if (sendCommand != null) {
|
||||
args.msg = msg;
|
||||
return await sendCommand(args);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Unregister all commands
|
||||
void unregisterAllCommands() {
|
||||
commands.clear();
|
||||
}
|
||||
|
||||
/// Register all default commands
|
||||
void registerDefaultCommands() {
|
||||
addCommand('send', (CommandArgs args) async {
|
||||
return await args.room.sendTextEvent(
|
||||
args.msg,
|
||||
inReplyTo: args.inReplyTo,
|
||||
editEventId: args.editEventId,
|
||||
parseCommands: false,
|
||||
txid: args.txid,
|
||||
);
|
||||
});
|
||||
addCommand('me', (CommandArgs args) async {
|
||||
return await args.room.sendTextEvent(
|
||||
args.msg,
|
||||
inReplyTo: args.inReplyTo,
|
||||
editEventId: args.editEventId,
|
||||
msgtype: MessageTypes.Emote,
|
||||
parseCommands: false,
|
||||
txid: args.txid,
|
||||
);
|
||||
});
|
||||
addCommand('dm', (CommandArgs args) async {
|
||||
final parts = args.msg.split(' ');
|
||||
return await args.room.client.startDirectChat(
|
||||
parts.first,
|
||||
enableEncryption: !parts.any((part) => part == '--no-encryption'),
|
||||
);
|
||||
});
|
||||
addCommand('create', (CommandArgs args) async {
|
||||
final parts = args.msg.split(' ');
|
||||
return await args.room.client.createGroupChat(
|
||||
enableEncryption: !parts.any((part) => part == '--no-encryption'),
|
||||
);
|
||||
});
|
||||
addCommand('plain', (CommandArgs args) async {
|
||||
return await args.room.sendTextEvent(
|
||||
args.msg,
|
||||
inReplyTo: args.inReplyTo,
|
||||
editEventId: args.editEventId,
|
||||
parseMarkdown: false,
|
||||
parseCommands: false,
|
||||
txid: args.txid,
|
||||
);
|
||||
});
|
||||
addCommand('html', (CommandArgs args) async {
|
||||
final event = <String, dynamic>{
|
||||
'msgtype': 'm.text',
|
||||
'body': args.msg,
|
||||
'format': 'org.matrix.custom.html',
|
||||
'formatted_body': args.msg,
|
||||
};
|
||||
return await args.room.sendEvent(
|
||||
event,
|
||||
inReplyTo: args.inReplyTo,
|
||||
editEventId: args.editEventId,
|
||||
txid: args.txid,
|
||||
);
|
||||
});
|
||||
addCommand('react', (CommandArgs args) async {
|
||||
final inReplyTo = args.inReplyTo;
|
||||
if (inReplyTo == null) {
|
||||
return null;
|
||||
}
|
||||
return await args.room.sendReaction(inReplyTo.eventId, args.msg);
|
||||
});
|
||||
addCommand('join', (CommandArgs args) async {
|
||||
await args.room.client.joinRoom(args.msg);
|
||||
return null;
|
||||
});
|
||||
addCommand('leave', (CommandArgs args) async {
|
||||
await args.room.leave();
|
||||
return '';
|
||||
});
|
||||
addCommand('op', (CommandArgs args) async {
|
||||
final parts = args.msg.split(' ');
|
||||
if (parts.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
int? pl;
|
||||
if (parts.length >= 2) {
|
||||
pl = int.tryParse(parts[1]);
|
||||
}
|
||||
final mxid = parts.first;
|
||||
return await args.room.setPower(mxid, pl ?? 50);
|
||||
});
|
||||
addCommand('kick', (CommandArgs args) async {
|
||||
final parts = args.msg.split(' ');
|
||||
await args.room.kick(parts.first);
|
||||
return '';
|
||||
});
|
||||
addCommand('ban', (CommandArgs args) async {
|
||||
final parts = args.msg.split(' ');
|
||||
await args.room.ban(parts.first);
|
||||
return '';
|
||||
});
|
||||
addCommand('unban', (CommandArgs args) async {
|
||||
final parts = args.msg.split(' ');
|
||||
await args.room.unban(parts.first);
|
||||
return '';
|
||||
});
|
||||
addCommand('invite', (CommandArgs args) async {
|
||||
final parts = args.msg.split(' ');
|
||||
await args.room.invite(parts.first);
|
||||
return '';
|
||||
});
|
||||
addCommand('myroomnick', (CommandArgs args) async {
|
||||
final currentEventJson = args.room
|
||||
.getState(EventTypes.RoomMember, args.room.client.userID!)
|
||||
?.content
|
||||
.copy() ??
|
||||
{};
|
||||
currentEventJson['displayname'] = args.msg;
|
||||
return await args.room.client.setRoomStateWithKey(
|
||||
args.room.id,
|
||||
EventTypes.RoomMember,
|
||||
args.room.client.userID!,
|
||||
currentEventJson,
|
||||
);
|
||||
});
|
||||
addCommand('myroomavatar', (CommandArgs args) async {
|
||||
final currentEventJson = args.room
|
||||
.getState(EventTypes.RoomMember, args.room.client.userID!)
|
||||
?.content
|
||||
.copy() ??
|
||||
{};
|
||||
currentEventJson['avatar_url'] = args.msg;
|
||||
return await args.room.client.setRoomStateWithKey(
|
||||
args.room.id,
|
||||
EventTypes.RoomMember,
|
||||
args.room.client.userID!,
|
||||
currentEventJson,
|
||||
);
|
||||
});
|
||||
addCommand('discardsession', (CommandArgs args) async {
|
||||
await encryption?.keyManager
|
||||
.clearOrUseOutboundGroupSession(args.room.id, wipe: true);
|
||||
return '';
|
||||
});
|
||||
addCommand('clearcache', (CommandArgs args) async {
|
||||
await clearCache();
|
||||
return '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CommandArgs {
|
||||
String msg;
|
||||
String? editEventId;
|
||||
Event? inReplyTo;
|
||||
Room room;
|
||||
String? txid;
|
||||
CommandArgs(
|
||||
{required this.msg,
|
||||
this.editEventId,
|
||||
this.inReplyTo,
|
||||
required this.room,
|
||||
this.txid});
|
||||
}
|
||||
29
lib/src/utils/crypto/crypto.dart
Normal file
29
lib/src/utils/crypto/crypto.dart
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2019, 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export 'native.dart' if (dart.library.js) 'js.dart';
|
||||
|
||||
import 'dart:typed_data';
|
||||
import 'dart:math';
|
||||
|
||||
Uint8List secureRandomBytes(int len) {
|
||||
final rng = Random.secure();
|
||||
final list = Uint8List(len);
|
||||
list.setAll(0, Iterable.generate(list.length, (i) => rng.nextInt(256)));
|
||||
return list;
|
||||
}
|
||||
60
lib/src/utils/crypto/encrypted_file.dart
Normal file
60
lib/src/utils/crypto/encrypted_file.dart
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2019, 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:typed_data';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:matrix/encryption/utils/base64_unpadded.dart';
|
||||
|
||||
import 'crypto.dart';
|
||||
|
||||
class EncryptedFile {
|
||||
EncryptedFile({
|
||||
required this.data,
|
||||
required this.k,
|
||||
required this.iv,
|
||||
required this.sha256,
|
||||
});
|
||||
Uint8List data;
|
||||
String k;
|
||||
String iv;
|
||||
String sha256;
|
||||
}
|
||||
|
||||
Future<EncryptedFile> encryptFile(Uint8List input) async {
|
||||
final key = secureRandomBytes(32);
|
||||
final iv = secureRandomBytes(16);
|
||||
final data = await aesCtr.encrypt(input, key, iv);
|
||||
final hash = await sha256(data);
|
||||
return EncryptedFile(
|
||||
data: data,
|
||||
k: base64Url.encode(key).replaceAll('=', ''),
|
||||
iv: base64.encode(iv).replaceAll('=', ''),
|
||||
sha256: base64.encode(hash).replaceAll('=', ''),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Uint8List?> decryptFile(EncryptedFile input) async {
|
||||
if (base64.encode(await sha256(input.data)) !=
|
||||
base64.normalize(input.sha256)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final key = base64decodeUnpadded(base64.normalize(input.k));
|
||||
final iv = base64decodeUnpadded(base64.normalize(input.iv));
|
||||
return await aesCtr.encrypt(input.data, key, iv);
|
||||
}
|
||||
123
lib/src/utils/crypto/ffi.dart
Normal file
123
lib/src/utils/crypto/ffi.dart
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2019, 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
|
||||
final libcrypto = Platform.isIOS
|
||||
? DynamicLibrary.process()
|
||||
: DynamicLibrary.open(Platform.isAndroid
|
||||
? 'libcrypto.so'
|
||||
: Platform.isWindows
|
||||
? 'libcrypto.dll'
|
||||
: Platform.isMacOS
|
||||
? 'libcrypto.1.1.dylib'
|
||||
: 'libcrypto.so.1.1');
|
||||
|
||||
final PKCS5_PBKDF2_HMAC = libcrypto.lookupFunction<
|
||||
IntPtr Function(
|
||||
Pointer<Uint8> pass,
|
||||
IntPtr passlen,
|
||||
Pointer<Uint8> salt,
|
||||
IntPtr saltlen,
|
||||
IntPtr iter,
|
||||
Pointer<NativeType> digest,
|
||||
IntPtr keylen,
|
||||
Pointer<Uint8> out),
|
||||
int Function(
|
||||
Pointer<Uint8> pass,
|
||||
int passlen,
|
||||
Pointer<Uint8> salt,
|
||||
int saltlen,
|
||||
int iter,
|
||||
Pointer<NativeType> digest,
|
||||
int keylen,
|
||||
Pointer<Uint8> out)>('PKCS5_PBKDF2_HMAC');
|
||||
|
||||
final EVP_sha1 = libcrypto.lookupFunction<Pointer<NativeType> Function(),
|
||||
Pointer<NativeType> Function()>('EVP_sha1');
|
||||
|
||||
final EVP_sha256 = libcrypto.lookupFunction<Pointer<NativeType> Function(),
|
||||
Pointer<NativeType> Function()>('EVP_sha256');
|
||||
|
||||
final EVP_sha512 = libcrypto.lookupFunction<Pointer<NativeType> Function(),
|
||||
Pointer<NativeType> Function()>('EVP_sha512');
|
||||
|
||||
final EVP_aes_128_ctr = libcrypto.lookupFunction<Pointer<NativeType> Function(),
|
||||
Pointer<NativeType> Function()>('EVP_aes_128_ctr');
|
||||
|
||||
final EVP_aes_256_ctr = libcrypto.lookupFunction<Pointer<NativeType> Function(),
|
||||
Pointer<NativeType> Function()>('EVP_aes_256_ctr');
|
||||
|
||||
final EVP_CIPHER_CTX_new = libcrypto.lookupFunction<
|
||||
Pointer<NativeType> Function(),
|
||||
Pointer<NativeType> Function()>('EVP_CIPHER_CTX_new');
|
||||
|
||||
final EVP_EncryptInit_ex = libcrypto.lookupFunction<
|
||||
Pointer<NativeType> Function(
|
||||
Pointer<NativeType> ctx,
|
||||
Pointer<NativeType> alg,
|
||||
Pointer<NativeType> some,
|
||||
Pointer<Uint8> key,
|
||||
Pointer<Uint8> iv),
|
||||
Pointer<NativeType> Function(
|
||||
Pointer<NativeType> ctx,
|
||||
Pointer<NativeType> alg,
|
||||
Pointer<NativeType> some,
|
||||
Pointer<Uint8> key,
|
||||
Pointer<Uint8> iv)>('EVP_EncryptInit_ex');
|
||||
|
||||
final EVP_EncryptUpdate = libcrypto.lookupFunction<
|
||||
Pointer<NativeType> Function(Pointer<NativeType> ctx, Pointer<Uint8> output,
|
||||
Pointer<IntPtr> outputLen, Pointer<Uint8> input, IntPtr inputLen),
|
||||
Pointer<NativeType> Function(
|
||||
Pointer<NativeType> ctx,
|
||||
Pointer<Uint8> output,
|
||||
Pointer<IntPtr> outputLen,
|
||||
Pointer<Uint8> input,
|
||||
int inputLen)>('EVP_EncryptUpdate');
|
||||
|
||||
final EVP_EncryptFinal_ex = libcrypto.lookupFunction<
|
||||
Pointer<NativeType> Function(
|
||||
Pointer<NativeType> ctx, Pointer<Uint8> data, Pointer<IntPtr> len),
|
||||
Pointer<NativeType> Function(Pointer<NativeType> ctx, Pointer<Uint8> data,
|
||||
Pointer<IntPtr> len)>('EVP_EncryptFinal_ex');
|
||||
|
||||
final EVP_CIPHER_CTX_free = libcrypto.lookupFunction<
|
||||
Pointer<NativeType> Function(Pointer<NativeType> ctx),
|
||||
Pointer<NativeType> Function(
|
||||
Pointer<NativeType> ctx)>('EVP_CIPHER_CTX_free');
|
||||
|
||||
final EVP_Digest = libcrypto.lookupFunction<
|
||||
IntPtr Function(
|
||||
Pointer<Uint8> data,
|
||||
IntPtr len,
|
||||
Pointer<Uint8> hash,
|
||||
Pointer<IntPtr> hsize,
|
||||
Pointer<NativeType> alg,
|
||||
Pointer<NativeType> engine),
|
||||
int Function(
|
||||
Pointer<Uint8> data,
|
||||
int len,
|
||||
Pointer<Uint8> hash,
|
||||
Pointer<IntPtr> hsize,
|
||||
Pointer<NativeType> alg,
|
||||
Pointer<NativeType> engine)>('EVP_Digest');
|
||||
|
||||
final EVP_MD_size = libcrypto.lookupFunction<
|
||||
IntPtr Function(Pointer<NativeType> ctx),
|
||||
int Function(Pointer<NativeType> ctx)>('EVP_MD_size');
|
||||
64
lib/src/utils/crypto/js.dart
Normal file
64
lib/src/utils/crypto/js.dart
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright (c) 2020 Famedly GmbH
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'subtle.dart';
|
||||
import 'subtle.dart' as subtle;
|
||||
|
||||
abstract class Hash {
|
||||
Hash._(this.name);
|
||||
String name;
|
||||
|
||||
Future<Uint8List> call(Uint8List input) async =>
|
||||
Uint8List.view(await digest(name, input));
|
||||
}
|
||||
|
||||
final Hash sha1 = _Sha1();
|
||||
final Hash sha256 = _Sha256();
|
||||
final Hash sha512 = _Sha512();
|
||||
|
||||
class _Sha1 extends Hash {
|
||||
_Sha1() : super._('SHA-1');
|
||||
}
|
||||
|
||||
class _Sha256 extends Hash {
|
||||
_Sha256() : super._('SHA-256');
|
||||
}
|
||||
|
||||
class _Sha512 extends Hash {
|
||||
_Sha512() : super._('SHA-512');
|
||||
}
|
||||
|
||||
abstract class Cipher {
|
||||
Cipher._(this.name);
|
||||
String name;
|
||||
Object params(Uint8List iv);
|
||||
Future<Uint8List> encrypt(
|
||||
Uint8List input, Uint8List key, Uint8List iv) async {
|
||||
final subtleKey = await importKey('raw', key, name, false, ['encrypt']);
|
||||
return (await subtle.encrypt(params(iv), subtleKey, input)).asUint8List();
|
||||
}
|
||||
}
|
||||
|
||||
final Cipher aesCtr = _AesCtr();
|
||||
|
||||
class _AesCtr extends Cipher {
|
||||
_AesCtr() : super._('AES-CTR');
|
||||
|
||||
@override
|
||||
Object params(Uint8List iv) =>
|
||||
AesCtrParams(name: name, counter: iv, length: 64);
|
||||
}
|
||||
|
||||
Future<Uint8List> pbkdf2(Uint8List passphrase, Uint8List salt, Hash hash,
|
||||
int iterations, int bits) async {
|
||||
final raw =
|
||||
await importKey('raw', passphrase, 'PBKDF2', false, ['deriveBits']);
|
||||
final res = await deriveBits(
|
||||
Pbkdf2Params(
|
||||
name: 'PBKDF2', hash: hash.name, salt: salt, iterations: iterations),
|
||||
raw,
|
||||
bits);
|
||||
return Uint8List.view(res);
|
||||
}
|
||||
102
lib/src/utils/crypto/native.dart
Normal file
102
lib/src/utils/crypto/native.dart
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ffi';
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
||||
import 'ffi.dart';
|
||||
|
||||
abstract class Hash {
|
||||
Hash._(this.ptr);
|
||||
Pointer<NativeType> ptr;
|
||||
|
||||
FutureOr<Uint8List> call(Uint8List data) {
|
||||
final outSize = EVP_MD_size(ptr);
|
||||
final mem = malloc.call<Uint8>(outSize + data.length);
|
||||
final dataMem = mem.elementAt(outSize);
|
||||
try {
|
||||
dataMem.asTypedList(data.length).setAll(0, data);
|
||||
EVP_Digest(dataMem, data.length, mem, nullptr, ptr, nullptr);
|
||||
return Uint8List.fromList(mem.asTypedList(outSize));
|
||||
} finally {
|
||||
malloc.free(mem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final Hash sha1 = _Sha1();
|
||||
final Hash sha256 = _Sha256();
|
||||
final Hash sha512 = _Sha512();
|
||||
|
||||
class _Sha1 extends Hash {
|
||||
_Sha1() : super._(EVP_sha1());
|
||||
}
|
||||
|
||||
class _Sha256 extends Hash {
|
||||
_Sha256() : super._(EVP_sha256());
|
||||
}
|
||||
|
||||
class _Sha512 extends Hash {
|
||||
_Sha512() : super._(EVP_sha512());
|
||||
}
|
||||
|
||||
abstract class Cipher {
|
||||
Cipher._();
|
||||
Pointer<NativeType> getAlg(int keysize);
|
||||
FutureOr<Uint8List> encrypt(Uint8List input, Uint8List key, Uint8List iv) {
|
||||
final alg = getAlg(key.length * 8);
|
||||
final mem = malloc
|
||||
.call<Uint8>(sizeOf<IntPtr>() + key.length + iv.length + input.length);
|
||||
final lenMem = mem.cast<IntPtr>();
|
||||
final keyMem = mem.elementAt(sizeOf<IntPtr>());
|
||||
final ivMem = keyMem.elementAt(key.length);
|
||||
final dataMem = ivMem.elementAt(iv.length);
|
||||
try {
|
||||
keyMem.asTypedList(key.length).setAll(0, key);
|
||||
ivMem.asTypedList(iv.length).setAll(0, iv);
|
||||
dataMem.asTypedList(input.length).setAll(0, input);
|
||||
final ctx = EVP_CIPHER_CTX_new();
|
||||
EVP_EncryptInit_ex(ctx, alg, nullptr, keyMem, ivMem);
|
||||
EVP_EncryptUpdate(ctx, dataMem, lenMem, dataMem, input.length);
|
||||
EVP_EncryptFinal_ex(ctx, dataMem.elementAt(lenMem.value), lenMem);
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
return Uint8List.fromList(dataMem.asTypedList(input.length));
|
||||
} finally {
|
||||
malloc.free(mem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final Cipher aesCtr = _AesCtr();
|
||||
|
||||
class _AesCtr extends Cipher {
|
||||
_AesCtr() : super._();
|
||||
|
||||
@override
|
||||
Pointer<NativeType> getAlg(int keysize) {
|
||||
switch (keysize) {
|
||||
case 128:
|
||||
return EVP_aes_128_ctr();
|
||||
case 256:
|
||||
return EVP_aes_256_ctr();
|
||||
default:
|
||||
throw ArgumentError('invalid key size');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<Uint8List> pbkdf2(
|
||||
Uint8List passphrase, Uint8List salt, Hash hash, int iterations, int bits) {
|
||||
final outLen = bits ~/ 8;
|
||||
final mem = malloc.call<Uint8>(passphrase.length + salt.length + outLen);
|
||||
final saltMem = mem.elementAt(passphrase.length);
|
||||
final outMem = saltMem.elementAt(salt.length);
|
||||
try {
|
||||
mem.asTypedList(passphrase.length).setAll(0, passphrase);
|
||||
saltMem.asTypedList(salt.length).setAll(0, salt);
|
||||
PKCS5_PBKDF2_HMAC(mem, passphrase.length, saltMem, salt.length, iterations,
|
||||
hash.ptr, outLen, outMem);
|
||||
return Uint8List.fromList(outMem.asTypedList(outLen));
|
||||
} finally {
|
||||
malloc.free(mem);
|
||||
}
|
||||
}
|
||||
93
lib/src/utils/crypto/subtle.dart
Normal file
93
lib/src/utils/crypto/subtle.dart
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// Copyright (c) 2020 Famedly GmbH
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
@JS()
|
||||
library subtle;
|
||||
|
||||
import 'package:js/js.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:js_util';
|
||||
import 'dart:typed_data';
|
||||
|
||||
@JS()
|
||||
@anonymous
|
||||
class Pbkdf2Params {
|
||||
external factory Pbkdf2Params({
|
||||
String name,
|
||||
String hash,
|
||||
Uint8List salt,
|
||||
int iterations,
|
||||
});
|
||||
String? name;
|
||||
String? hash;
|
||||
Uint8List? salt;
|
||||
int? iterations;
|
||||
}
|
||||
|
||||
@JS()
|
||||
@anonymous
|
||||
class AesCtrParams {
|
||||
external factory AesCtrParams({
|
||||
String name,
|
||||
Uint8List counter,
|
||||
int length,
|
||||
});
|
||||
String? name;
|
||||
Uint8List? counter;
|
||||
int? length;
|
||||
}
|
||||
|
||||
@JS('crypto.subtle.encrypt')
|
||||
external dynamic _encrypt(dynamic algorithm, dynamic key, Uint8List data);
|
||||
|
||||
Future<ByteBuffer> encrypt(dynamic algorithm, dynamic key, Uint8List data) {
|
||||
return promiseToFuture(_encrypt(algorithm, key, data));
|
||||
}
|
||||
|
||||
@JS('crypto.subtle.decrypt')
|
||||
external dynamic _decrypt(dynamic algorithm, dynamic key, Uint8List data);
|
||||
|
||||
Future<ByteBuffer> decrypt(dynamic algorithm, dynamic key, Uint8List data) {
|
||||
return promiseToFuture(_decrypt(algorithm, key, data));
|
||||
}
|
||||
|
||||
@JS('crypto.subtle.importKey')
|
||||
external dynamic _importKey(String format, dynamic keyData, dynamic algorithm,
|
||||
bool extractable, List<String> keyUsages);
|
||||
|
||||
Future<dynamic> importKey(String format, dynamic keyData, dynamic algorithm,
|
||||
bool extractable, List<String> keyUsages) {
|
||||
return promiseToFuture(
|
||||
_importKey(format, keyData, algorithm, extractable, keyUsages));
|
||||
}
|
||||
|
||||
@JS('crypto.subtle.exportKey')
|
||||
external dynamic _exportKey(String algorithm, dynamic key);
|
||||
|
||||
Future<dynamic> exportKey(String algorithm, dynamic key) {
|
||||
return promiseToFuture(_exportKey(algorithm, key));
|
||||
}
|
||||
|
||||
@JS('crypto.subtle.deriveKey')
|
||||
external dynamic _deriveKey(dynamic algorithm, dynamic baseKey,
|
||||
dynamic derivedKeyAlgorithm, bool extractable, List<String> keyUsages);
|
||||
|
||||
Future<ByteBuffer> deriveKey(dynamic algorithm, dynamic baseKey,
|
||||
dynamic derivedKeyAlgorithm, bool extractable, List<String> keyUsages) {
|
||||
return promiseToFuture(_deriveKey(
|
||||
algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages));
|
||||
}
|
||||
|
||||
@JS('crypto.subtle.deriveBits')
|
||||
external dynamic _deriveBits(dynamic algorithm, dynamic baseKey, int length);
|
||||
|
||||
Future<ByteBuffer> deriveBits(dynamic algorithm, dynamic baseKey, int length) {
|
||||
return promiseToFuture(_deriveBits(algorithm, baseKey, length));
|
||||
}
|
||||
|
||||
@JS('crypto.subtle.digest')
|
||||
external dynamic _digest(String algorithm, Uint8List data);
|
||||
|
||||
Future<ByteBuffer> digest(String algorithm, Uint8List data) {
|
||||
return promiseToFuture(_digest(algorithm, data));
|
||||
}
|
||||
515
lib/src/utils/device_keys_list.dart
Normal file
515
lib/src/utils/device_keys_list.dart
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:canonical_json/canonical_json.dart';
|
||||
import 'package:collection/collection.dart' show IterableExtension;
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import '../../encryption.dart';
|
||||
|
||||
enum UserVerifiedStatus { verified, unknown, unknownDevice }
|
||||
|
||||
class DeviceKeysList {
|
||||
Client client;
|
||||
String userId;
|
||||
bool outdated = true;
|
||||
Map<String, DeviceKeys> deviceKeys = {};
|
||||
Map<String, CrossSigningKey> crossSigningKeys = {};
|
||||
|
||||
SignableKey? getKey(String id) => deviceKeys[id] ?? crossSigningKeys[id];
|
||||
|
||||
CrossSigningKey? getCrossSigningKey(String type) =>
|
||||
crossSigningKeys.values.firstWhereOrNull((k) => k.usage.contains(type));
|
||||
|
||||
CrossSigningKey? get masterKey => getCrossSigningKey('master');
|
||||
CrossSigningKey? get selfSigningKey => getCrossSigningKey('self_signing');
|
||||
CrossSigningKey? get userSigningKey => getCrossSigningKey('user_signing');
|
||||
|
||||
UserVerifiedStatus get verified {
|
||||
if (masterKey == null) {
|
||||
return UserVerifiedStatus.unknown;
|
||||
}
|
||||
if (masterKey!.verified) {
|
||||
for (final key in deviceKeys.values) {
|
||||
if (!key.verified) {
|
||||
return UserVerifiedStatus.unknownDevice;
|
||||
}
|
||||
}
|
||||
return UserVerifiedStatus.verified;
|
||||
} else {
|
||||
for (final key in deviceKeys.values) {
|
||||
if (!key.verified) {
|
||||
return UserVerifiedStatus.unknown;
|
||||
}
|
||||
}
|
||||
return UserVerifiedStatus.verified;
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts a verification with this device. This might need to create a new
|
||||
/// direct chat to send the verification request over this room. For this you
|
||||
/// can set parameters here.
|
||||
Future<KeyVerification> startVerification({
|
||||
bool? newDirectChatEnableEncryption,
|
||||
List<StateEvent>? newDirectChatInitialState,
|
||||
}) async {
|
||||
final encryption = client.encryption;
|
||||
if (encryption == null) {
|
||||
throw Exception('Encryption not enabled');
|
||||
}
|
||||
if (userId != client.userID) {
|
||||
// in-room verification with someone else
|
||||
final roomId = await client.startDirectChat(
|
||||
userId,
|
||||
enableEncryption: newDirectChatEnableEncryption,
|
||||
initialState: newDirectChatInitialState,
|
||||
waitForSync: false,
|
||||
);
|
||||
|
||||
final room =
|
||||
client.getRoomById(roomId) ?? Room(id: roomId, client: client);
|
||||
final request =
|
||||
KeyVerification(encryption: encryption, room: room, userId: userId);
|
||||
await request.start();
|
||||
// no need to add to the request client object. As we are doing a room
|
||||
// verification request that'll happen automatically once we know the transaction id
|
||||
return request;
|
||||
} else {
|
||||
// broadcast self-verification
|
||||
final request = KeyVerification(
|
||||
encryption: encryption, userId: userId, deviceId: '*');
|
||||
await request.start();
|
||||
encryption.keyVerificationManager.addRequest(request);
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
DeviceKeysList.fromDbJson(
|
||||
Map<String, dynamic> dbEntry,
|
||||
List<Map<String, dynamic>> childEntries,
|
||||
List<Map<String, dynamic>> crossSigningEntries,
|
||||
Client cl)
|
||||
: client = cl,
|
||||
userId = dbEntry['user_id'] ?? '' {
|
||||
outdated = dbEntry['outdated'];
|
||||
deviceKeys = {};
|
||||
for (final childEntry in childEntries) {
|
||||
final entry = DeviceKeys.fromDb(childEntry, client);
|
||||
if (entry.isValid) {
|
||||
deviceKeys[childEntry['device_id']] = entry;
|
||||
} else {
|
||||
outdated = true;
|
||||
}
|
||||
}
|
||||
for (final crossSigningEntry in crossSigningEntries) {
|
||||
final entry = CrossSigningKey.fromDbJson(crossSigningEntry, client);
|
||||
if (entry.isValid) {
|
||||
crossSigningKeys[crossSigningEntry['public_key']] = entry;
|
||||
} else {
|
||||
outdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DeviceKeysList(this.userId, this.client);
|
||||
}
|
||||
|
||||
class SimpleSignableKey extends MatrixSignableKey {
|
||||
@override
|
||||
String? identifier;
|
||||
|
||||
SimpleSignableKey.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
}
|
||||
|
||||
abstract class SignableKey extends MatrixSignableKey {
|
||||
Client client;
|
||||
Map<String, dynamic>? validSignatures;
|
||||
bool? _verified;
|
||||
bool? _blocked;
|
||||
|
||||
String? get ed25519Key => keys['ed25519:$identifier'];
|
||||
bool get verified =>
|
||||
identifier != null && (directVerified || crossVerified) && !(blocked);
|
||||
bool get blocked => _blocked ?? false;
|
||||
set blocked(bool b) => _blocked = b;
|
||||
|
||||
bool get encryptToDevice =>
|
||||
!(blocked) &&
|
||||
identifier != null &&
|
||||
ed25519Key != null &&
|
||||
(client.userDeviceKeys[userId]?.masterKey?.verified ?? false
|
||||
? verified
|
||||
: true);
|
||||
|
||||
void setDirectVerified(bool v) {
|
||||
_verified = v;
|
||||
}
|
||||
|
||||
bool get directVerified => _verified ?? false;
|
||||
bool get crossVerified => hasValidSignatureChain();
|
||||
bool get signed => hasValidSignatureChain(verifiedOnly: false);
|
||||
|
||||
SignableKey.fromJson(Map<String, dynamic> json, Client cl)
|
||||
: client = cl,
|
||||
super.fromJson(json) {
|
||||
_verified = false;
|
||||
_blocked = false;
|
||||
}
|
||||
|
||||
SimpleSignableKey cloneForSigning() {
|
||||
final newKey = SimpleSignableKey.fromJson(toJson().copy());
|
||||
newKey.identifier = identifier;
|
||||
(newKey.signatures ??= {}).clear();
|
||||
return newKey;
|
||||
}
|
||||
|
||||
String get signingContent {
|
||||
final data = super.toJson().copy();
|
||||
// some old data might have the custom verified and blocked keys
|
||||
data.remove('verified');
|
||||
data.remove('blocked');
|
||||
// remove the keys not needed for signing
|
||||
data.remove('unsigned');
|
||||
data.remove('signatures');
|
||||
return String.fromCharCodes(canonicalJson.encode(data));
|
||||
}
|
||||
|
||||
bool _verifySignature(String pubKey, String signature,
|
||||
{bool isSignatureWithoutLibolmValid = false}) {
|
||||
olm.Utility olmutil;
|
||||
try {
|
||||
olmutil = olm.Utility();
|
||||
} catch (e) {
|
||||
// if no libolm is present we land in this catch block, and return the default
|
||||
// set if no libolm is there. Some signatures should be assumed-valid while others
|
||||
// should be assumed-invalid
|
||||
return isSignatureWithoutLibolmValid;
|
||||
}
|
||||
var valid = false;
|
||||
try {
|
||||
olmutil.ed25519_verify(pubKey, signingContent, signature);
|
||||
valid = true;
|
||||
} catch (_) {
|
||||
// bad signature
|
||||
valid = false;
|
||||
} finally {
|
||||
olmutil.free();
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
bool hasValidSignatureChain(
|
||||
{bool verifiedOnly = true,
|
||||
Set<String>? visited,
|
||||
Set<String>? onlyValidateUserIds}) {
|
||||
if (!client.encryptionEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final visited_ = visited ?? <String>{};
|
||||
final onlyValidateUserIds_ = onlyValidateUserIds ?? <String>{};
|
||||
|
||||
final setKey = '$userId;$identifier';
|
||||
if (visited_.contains(setKey) ||
|
||||
(onlyValidateUserIds_.isNotEmpty &&
|
||||
!onlyValidateUserIds_.contains(userId))) {
|
||||
return false; // prevent recursion & validate hasValidSignatureChain
|
||||
}
|
||||
visited_.add(setKey);
|
||||
|
||||
if (signatures == null) return false;
|
||||
|
||||
for (final signatureEntries in signatures!.entries) {
|
||||
final otherUserId = signatureEntries.key;
|
||||
if (!client.userDeviceKeys.containsKey(otherUserId)) {
|
||||
continue;
|
||||
}
|
||||
// we don't allow transitive trust unless it is for ourself
|
||||
if (otherUserId != userId && otherUserId != client.userID) {
|
||||
continue;
|
||||
}
|
||||
for (final signatureEntry in signatureEntries.value.entries) {
|
||||
final fullKeyId = signatureEntry.key;
|
||||
final signature = signatureEntry.value;
|
||||
final keyId = fullKeyId.substring('ed25519:'.length);
|
||||
// we ignore self-signatures here
|
||||
if (otherUserId == userId && keyId == identifier) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final key = client.userDeviceKeys[otherUserId]?.deviceKeys[keyId] ??
|
||||
client.userDeviceKeys[otherUserId]?.crossSigningKeys[keyId];
|
||||
if (key == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (onlyValidateUserIds_.isNotEmpty &&
|
||||
!onlyValidateUserIds_.contains(key.userId)) {
|
||||
// we don't want to verify keys from this user
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key.blocked) {
|
||||
continue; // we can't be bothered about this keys signatures
|
||||
}
|
||||
var haveValidSignature = false;
|
||||
var gotSignatureFromCache = false;
|
||||
final fullKeyIdBool = validSignatures
|
||||
?.tryGetMap<String, dynamic>(otherUserId)
|
||||
?.tryGet<bool>(fullKeyId);
|
||||
if (fullKeyIdBool == true) {
|
||||
haveValidSignature = true;
|
||||
gotSignatureFromCache = true;
|
||||
} else if (fullKeyIdBool == false) {
|
||||
haveValidSignature = false;
|
||||
gotSignatureFromCache = true;
|
||||
}
|
||||
|
||||
if (!gotSignatureFromCache && key.ed25519Key != null) {
|
||||
// validate the signature manually
|
||||
haveValidSignature = _verifySignature(key.ed25519Key!, signature);
|
||||
final validSignatures = this.validSignatures ??= <String, dynamic>{};
|
||||
if (!validSignatures.containsKey(otherUserId)) {
|
||||
validSignatures[otherUserId] = <String, dynamic>{};
|
||||
}
|
||||
validSignatures[otherUserId][fullKeyId] = haveValidSignature;
|
||||
}
|
||||
if (!haveValidSignature) {
|
||||
// no valid signature, this key is useless
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((verifiedOnly && key.directVerified) ||
|
||||
(key is CrossSigningKey &&
|
||||
key.usage.contains('master') &&
|
||||
key.directVerified &&
|
||||
key.userId == client.userID)) {
|
||||
return true; // we verified this key and it is valid...all checks out!
|
||||
}
|
||||
// or else we just recurse into that key and chack if it works out
|
||||
final haveChain = key.hasValidSignatureChain(
|
||||
verifiedOnly: verifiedOnly,
|
||||
visited: visited_,
|
||||
onlyValidateUserIds: onlyValidateUserIds);
|
||||
if (haveChain) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> setVerified(bool newVerified, [bool sign = true]) async {
|
||||
_verified = newVerified;
|
||||
final encryption = client.encryption;
|
||||
if (newVerified &&
|
||||
sign &&
|
||||
encryption != null &&
|
||||
client.encryptionEnabled &&
|
||||
encryption.crossSigning.signable([this])) {
|
||||
// sign the key!
|
||||
// ignore: unawaited_futures
|
||||
encryption.crossSigning.sign([this]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setBlocked(bool newBlocked);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = super.toJson().copy();
|
||||
// some old data may have the verified and blocked keys which are unneeded now
|
||||
data.remove('verified');
|
||||
data.remove('blocked');
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => json.encode(toJson());
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) => (other is SignableKey &&
|
||||
other.userId == userId &&
|
||||
other.identifier == identifier);
|
||||
}
|
||||
|
||||
class CrossSigningKey extends SignableKey {
|
||||
@override
|
||||
String? identifier;
|
||||
|
||||
String? get publicKey => identifier;
|
||||
late List<String> usage;
|
||||
|
||||
bool get isValid =>
|
||||
userId.isNotEmpty &&
|
||||
publicKey != null &&
|
||||
keys.isNotEmpty &&
|
||||
ed25519Key != null;
|
||||
|
||||
@override
|
||||
Future<void> setVerified(bool newVerified, [bool sign = true]) async {
|
||||
if (!isValid) {
|
||||
throw Exception('setVerified called on invalid key');
|
||||
}
|
||||
await super.setVerified(newVerified, sign);
|
||||
await client.database
|
||||
?.setVerifiedUserCrossSigningKey(newVerified, userId, publicKey!);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setBlocked(bool newBlocked) async {
|
||||
if (!isValid) {
|
||||
throw Exception('setBlocked called on invalid key');
|
||||
}
|
||||
_blocked = newBlocked;
|
||||
await client.database
|
||||
?.setBlockedUserCrossSigningKey(newBlocked, userId, publicKey!);
|
||||
}
|
||||
|
||||
CrossSigningKey.fromMatrixCrossSigningKey(MatrixCrossSigningKey k, Client cl)
|
||||
: super.fromJson(k.toJson().copy(), cl) {
|
||||
final json = toJson();
|
||||
identifier = k.publicKey;
|
||||
usage = json['usage'].cast<String>();
|
||||
}
|
||||
|
||||
CrossSigningKey.fromDbJson(Map<String, dynamic> dbEntry, Client cl)
|
||||
: super.fromJson(Event.getMapFromPayload(dbEntry['content']), cl) {
|
||||
final json = toJson();
|
||||
identifier = dbEntry['public_key'];
|
||||
usage = json['usage'].cast<String>();
|
||||
_verified = dbEntry['verified'];
|
||||
_blocked = dbEntry['blocked'];
|
||||
}
|
||||
|
||||
CrossSigningKey.fromJson(Map<String, dynamic> json, Client cl)
|
||||
: super.fromJson(json.copy(), cl) {
|
||||
final json = toJson();
|
||||
usage = json['usage'].cast<String>();
|
||||
if (keys.isNotEmpty) {
|
||||
identifier = keys.values.first;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DeviceKeys extends SignableKey {
|
||||
@override
|
||||
String? identifier;
|
||||
|
||||
String? get deviceId => identifier;
|
||||
late List<String> algorithms;
|
||||
late DateTime lastActive;
|
||||
|
||||
String? get curve25519Key => keys['curve25519:$deviceId'];
|
||||
String? get deviceDisplayName => unsigned?['device_display_name'];
|
||||
|
||||
bool? _validSelfSignature;
|
||||
bool get selfSigned =>
|
||||
_validSelfSignature ??
|
||||
(_validSelfSignature = (deviceId != null &&
|
||||
signatures
|
||||
?.tryGetMap<String, dynamic>(userId)
|
||||
?.tryGet<String>('ed25519:$deviceId') ==
|
||||
null
|
||||
? false
|
||||
// without libolm we still want to be able to add devices. In that case we ofc just can't
|
||||
// verify the signature
|
||||
: _verifySignature(
|
||||
ed25519Key!, signatures![userId]!['ed25519:$deviceId']!,
|
||||
isSignatureWithoutLibolmValid: true)));
|
||||
|
||||
@override
|
||||
bool get blocked => super.blocked || !selfSigned;
|
||||
|
||||
bool get isValid =>
|
||||
deviceId != null &&
|
||||
keys.isNotEmpty &&
|
||||
curve25519Key != null &&
|
||||
ed25519Key != null &&
|
||||
selfSigned;
|
||||
|
||||
@override
|
||||
Future<void> setVerified(bool newVerified, [bool sign = true]) async {
|
||||
if (!isValid) {
|
||||
//throw Exception('setVerified called on invalid key');
|
||||
return;
|
||||
}
|
||||
await super.setVerified(newVerified, sign);
|
||||
await client.database
|
||||
?.setVerifiedUserDeviceKey(newVerified, userId, deviceId!);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setBlocked(bool newBlocked) async {
|
||||
if (!isValid) {
|
||||
//throw Exception('setBlocked called on invalid key');
|
||||
return;
|
||||
}
|
||||
_blocked = newBlocked;
|
||||
await client.database
|
||||
?.setBlockedUserDeviceKey(newBlocked, userId, deviceId!);
|
||||
}
|
||||
|
||||
DeviceKeys.fromMatrixDeviceKeys(MatrixDeviceKeys k, Client cl,
|
||||
[DateTime? lastActiveTs])
|
||||
: super.fromJson(k.toJson().copy(), cl) {
|
||||
final json = toJson();
|
||||
identifier = k.deviceId;
|
||||
algorithms = json['algorithms'].cast<String>();
|
||||
lastActive = lastActiveTs ?? DateTime.now();
|
||||
}
|
||||
|
||||
DeviceKeys.fromDb(Map<String, dynamic> dbEntry, Client cl)
|
||||
: super.fromJson(Event.getMapFromPayload(dbEntry['content']), cl) {
|
||||
final json = toJson();
|
||||
identifier = dbEntry['device_id'];
|
||||
algorithms = json['algorithms'].cast<String>();
|
||||
_verified = dbEntry['verified'];
|
||||
_blocked = dbEntry['blocked'];
|
||||
lastActive =
|
||||
DateTime.fromMillisecondsSinceEpoch(dbEntry['last_active'] ?? 0);
|
||||
}
|
||||
|
||||
DeviceKeys.fromJson(Map<String, dynamic> json, Client cl)
|
||||
: super.fromJson(json.copy(), cl) {
|
||||
final json = toJson();
|
||||
identifier = json['device_id'];
|
||||
algorithms = json['algorithms'].cast<String>();
|
||||
lastActive = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
}
|
||||
|
||||
KeyVerification startVerification() {
|
||||
if (!isValid) {
|
||||
throw Exception('setVerification called on invalid key');
|
||||
}
|
||||
final encryption = client.encryption;
|
||||
if (encryption == null) {
|
||||
throw Exception('setVerification called with disabled encryption');
|
||||
}
|
||||
|
||||
final request = KeyVerification(
|
||||
encryption: encryption, userId: userId, deviceId: deviceId!);
|
||||
|
||||
request.start();
|
||||
encryption.keyVerificationManager.addRequest(request);
|
||||
return request;
|
||||
}
|
||||
}
|
||||
224
lib/src/utils/event_localizations.dart
Normal file
224
lib/src/utils/event_localizations.dart
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2019, 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import '../../encryption.dart';
|
||||
import '../../matrix.dart';
|
||||
|
||||
abstract class EventLocalizations {
|
||||
// As we need to create the localized body off of a different set of parameters, we
|
||||
// might create it with `event.plaintextBody`, maybe with `event.body`, maybe with the
|
||||
// reply fallback stripped, and maybe with the new body in `event.content['m.new_content']`.
|
||||
// Thus, it seems easier to offload that logic into `Event.getLocalizedBody()` and pass the
|
||||
// `body` variable around here.
|
||||
static String _localizedBodyNormalMessage(
|
||||
Event event, MatrixLocalizations i18n, String body) {
|
||||
switch (event.messageType) {
|
||||
case MessageTypes.Image:
|
||||
return i18n.sentAPicture(event.sender.calcDisplayname());
|
||||
case MessageTypes.File:
|
||||
return i18n.sentAFile(event.sender.calcDisplayname());
|
||||
case MessageTypes.Audio:
|
||||
return i18n.sentAnAudio(event.sender.calcDisplayname());
|
||||
case MessageTypes.Video:
|
||||
return i18n.sentAVideo(event.sender.calcDisplayname());
|
||||
case MessageTypes.Location:
|
||||
return i18n.sharedTheLocation(event.sender.calcDisplayname());
|
||||
case MessageTypes.Sticker:
|
||||
return i18n.sentASticker(event.sender.calcDisplayname());
|
||||
case MessageTypes.Emote:
|
||||
return '* $body';
|
||||
case MessageTypes.BadEncrypted:
|
||||
String errorText;
|
||||
switch (event.body) {
|
||||
case DecryptException.channelCorrupted:
|
||||
errorText = i18n.channelCorruptedDecryptError + '.';
|
||||
break;
|
||||
case DecryptException.notEnabled:
|
||||
errorText = i18n.encryptionNotEnabled + '.';
|
||||
break;
|
||||
case DecryptException.unknownAlgorithm:
|
||||
errorText = i18n.unknownEncryptionAlgorithm + '.';
|
||||
break;
|
||||
case DecryptException.unknownSession:
|
||||
errorText = i18n.noPermission + '.';
|
||||
break;
|
||||
default:
|
||||
errorText = body;
|
||||
break;
|
||||
}
|
||||
return i18n.couldNotDecryptMessage(errorText);
|
||||
case MessageTypes.Text:
|
||||
case MessageTypes.Notice:
|
||||
case MessageTypes.None:
|
||||
default:
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
// This map holds how to localize event types, and thus which event types exist.
|
||||
// If an event exists but it does not have a localized body, set its callback to null
|
||||
static final Map<String,
|
||||
String Function(Event event, MatrixLocalizations i18n, String body)?>
|
||||
localizationsMap = {
|
||||
EventTypes.Sticker: (event, i18n, body) =>
|
||||
i18n.sentASticker(event.sender.calcDisplayname()),
|
||||
EventTypes.Redaction: (event, i18n, body) =>
|
||||
i18n.redactedAnEvent(event.sender.calcDisplayname()),
|
||||
EventTypes.RoomAliases: (event, i18n, body) =>
|
||||
i18n.changedTheRoomAliases(event.sender.calcDisplayname()),
|
||||
EventTypes.RoomCanonicalAlias: (event, i18n, body) =>
|
||||
i18n.changedTheRoomInvitationLink(event.sender.calcDisplayname()),
|
||||
EventTypes.RoomCreate: (event, i18n, body) =>
|
||||
i18n.createdTheChat(event.sender.calcDisplayname()),
|
||||
EventTypes.RoomTombstone: (event, i18n, body) => i18n.roomHasBeenUpgraded,
|
||||
EventTypes.RoomJoinRules: (event, i18n, body) {
|
||||
final joinRules = JoinRules.values.firstWhereOrNull((r) =>
|
||||
r.toString().replaceAll('JoinRules.', '') ==
|
||||
event.content['join_rule']);
|
||||
if (joinRules == null) {
|
||||
return i18n.changedTheJoinRules(event.sender.calcDisplayname());
|
||||
} else {
|
||||
return i18n.changedTheJoinRulesTo(
|
||||
event.sender.calcDisplayname(), joinRules.getLocalizedString(i18n));
|
||||
}
|
||||
},
|
||||
EventTypes.RoomMember: (event, i18n, body) {
|
||||
var text = 'Failed to parse member event';
|
||||
final targetName = event.stateKeyUser?.calcDisplayname() ?? '';
|
||||
// Has the membership changed?
|
||||
final newMembership = event.content['membership'] ?? '';
|
||||
final oldMembership = event.prevContent?['membership'] ?? '';
|
||||
|
||||
if (newMembership != oldMembership) {
|
||||
if (oldMembership == 'invite' && newMembership == 'join') {
|
||||
text = i18n.acceptedTheInvitation(targetName);
|
||||
} else if (oldMembership == 'invite' && newMembership == 'leave') {
|
||||
if (event.stateKey == event.senderId) {
|
||||
text = i18n.rejectedTheInvitation(targetName);
|
||||
} else {
|
||||
text = i18n.hasWithdrawnTheInvitationFor(
|
||||
event.sender.calcDisplayname(), targetName);
|
||||
}
|
||||
} else if (oldMembership == 'leave' && newMembership == 'join') {
|
||||
text = i18n.joinedTheChat(targetName);
|
||||
} else if (oldMembership == 'join' && newMembership == 'ban') {
|
||||
text =
|
||||
i18n.kickedAndBanned(event.sender.calcDisplayname(), targetName);
|
||||
} else if (oldMembership == 'join' &&
|
||||
newMembership == 'leave' &&
|
||||
event.stateKey != event.senderId) {
|
||||
text = i18n.kicked(event.sender.calcDisplayname(), targetName);
|
||||
} else if (oldMembership == 'join' &&
|
||||
newMembership == 'leave' &&
|
||||
event.stateKey == event.senderId) {
|
||||
text = i18n.userLeftTheChat(targetName);
|
||||
} else if (oldMembership == 'invite' && newMembership == 'ban') {
|
||||
text = i18n.bannedUser(event.sender.calcDisplayname(), targetName);
|
||||
} else if (oldMembership == 'leave' && newMembership == 'ban') {
|
||||
text = i18n.bannedUser(event.sender.calcDisplayname(), targetName);
|
||||
} else if (oldMembership == 'ban' && newMembership == 'leave') {
|
||||
text = i18n.unbannedUser(event.sender.calcDisplayname(), targetName);
|
||||
} else if (newMembership == 'invite') {
|
||||
text = i18n.invitedUser(event.sender.calcDisplayname(), targetName);
|
||||
} else if (newMembership == 'join') {
|
||||
text = i18n.joinedTheChat(targetName);
|
||||
}
|
||||
} else if (newMembership == 'join') {
|
||||
final newAvatar = event.content.tryGet<String>('avatar_url') ?? '';
|
||||
final oldAvatar = event.prevContent?.tryGet<String>('avatar_url') ?? '';
|
||||
|
||||
final newDisplayname =
|
||||
event.content.tryGet<String>('displayname') ?? '';
|
||||
final oldDisplayname =
|
||||
event.prevContent?.tryGet<String>('displayname') ?? '';
|
||||
final stateKey = event.stateKey;
|
||||
|
||||
// Has the user avatar changed?
|
||||
if (newAvatar != oldAvatar) {
|
||||
text = i18n.changedTheProfileAvatar(targetName);
|
||||
}
|
||||
// Has the user displayname changed?
|
||||
else if (newDisplayname != oldDisplayname && stateKey != null) {
|
||||
text = i18n.changedTheDisplaynameTo(oldDisplayname, newDisplayname);
|
||||
}
|
||||
}
|
||||
return text;
|
||||
},
|
||||
EventTypes.RoomPowerLevels: (event, i18n, body) =>
|
||||
i18n.changedTheChatPermissions(event.sender.calcDisplayname()),
|
||||
EventTypes.RoomName: (event, i18n, body) => i18n.changedTheChatNameTo(
|
||||
event.sender.calcDisplayname(), event.content['name']),
|
||||
EventTypes.RoomTopic: (event, i18n, body) =>
|
||||
i18n.changedTheChatDescriptionTo(
|
||||
event.sender.calcDisplayname(), event.content['topic']),
|
||||
EventTypes.RoomAvatar: (event, i18n, body) =>
|
||||
i18n.changedTheChatAvatar(event.sender.calcDisplayname()),
|
||||
EventTypes.GuestAccess: (event, i18n, body) {
|
||||
final guestAccess = GuestAccess.values.firstWhereOrNull((r) =>
|
||||
r.toString().replaceAll('GuestAccess.', '') ==
|
||||
event.content['guest_access']);
|
||||
if (guestAccess == null) {
|
||||
return i18n.changedTheGuestAccessRules(event.sender.calcDisplayname());
|
||||
} else {
|
||||
return i18n.changedTheGuestAccessRulesTo(event.sender.calcDisplayname(),
|
||||
guestAccess.getLocalizedString(i18n));
|
||||
}
|
||||
},
|
||||
EventTypes.HistoryVisibility: (event, i18n, body) {
|
||||
final historyVisibility = HistoryVisibility.values.firstWhereOrNull((r) =>
|
||||
r.toString().replaceAll('HistoryVisibility.', '') ==
|
||||
event.content['history_visibility']);
|
||||
if (historyVisibility == null) {
|
||||
return i18n.changedTheHistoryVisibility(event.sender.calcDisplayname());
|
||||
} else {
|
||||
return i18n.changedTheHistoryVisibilityTo(
|
||||
event.sender.calcDisplayname(),
|
||||
historyVisibility.getLocalizedString(i18n));
|
||||
}
|
||||
},
|
||||
EventTypes.Encryption: (event, i18n, body) {
|
||||
var localizedBody =
|
||||
i18n.activatedEndToEndEncryption(event.sender.calcDisplayname());
|
||||
if (event.room.client.encryptionEnabled == false) {
|
||||
localizedBody += '. ' + i18n.needPantalaimonWarning;
|
||||
}
|
||||
return localizedBody;
|
||||
},
|
||||
EventTypes.CallAnswer: (event, i18n, body) =>
|
||||
i18n.answeredTheCall(event.sender.calcDisplayname()),
|
||||
EventTypes.CallHangup: (event, i18n, body) =>
|
||||
i18n.endedTheCall(event.sender.calcDisplayname()),
|
||||
EventTypes.CallInvite: (event, i18n, body) =>
|
||||
i18n.startedACall(event.sender.calcDisplayname()),
|
||||
EventTypes.CallCandidates: (event, i18n, body) =>
|
||||
i18n.sentCallInformations(event.sender.calcDisplayname()),
|
||||
EventTypes.Encrypted: (event, i18n, body) =>
|
||||
_localizedBodyNormalMessage(event, i18n, body),
|
||||
EventTypes.Message: (event, i18n, body) =>
|
||||
_localizedBodyNormalMessage(event, i18n, body),
|
||||
EventTypes.Reaction: (event, i18n, body) => i18n.sentReaction(
|
||||
event.sender.calcDisplayname(),
|
||||
event.content
|
||||
.tryGetMap<String, dynamic>('m.relates_to')
|
||||
?.tryGet<String>('key') ??
|
||||
body,
|
||||
),
|
||||
};
|
||||
}
|
||||
72
lib/src/utils/event_update.dart
Normal file
72
lib/src/utils/event_update.dart
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2019, 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import '../../matrix.dart';
|
||||
|
||||
enum EventUpdateType {
|
||||
timeline,
|
||||
state,
|
||||
history,
|
||||
accountData,
|
||||
ephemeral,
|
||||
inviteState
|
||||
}
|
||||
|
||||
/// Represents a new event (e.g. a message in a room) or an update for an
|
||||
/// already known event.
|
||||
class EventUpdate {
|
||||
/// Usually 'timeline', 'state' or whatever.
|
||||
final EventUpdateType type;
|
||||
|
||||
/// Most events belong to a room. If not, this equals to eventType.
|
||||
final String roomID;
|
||||
|
||||
@Deprecated("Use `content['type']` instead.")
|
||||
String get eventType => content['type'];
|
||||
|
||||
// The json payload of the content of this event.
|
||||
final Map<String, dynamic> content;
|
||||
|
||||
EventUpdate({
|
||||
required this.roomID,
|
||||
required this.type,
|
||||
required this.content,
|
||||
});
|
||||
|
||||
Future<EventUpdate> decrypt(Room room, {bool store = false}) async {
|
||||
final encryption = room.client.encryption;
|
||||
if (content['type'] != EventTypes.Encrypted ||
|
||||
!room.client.encryptionEnabled ||
|
||||
encryption == null) {
|
||||
return this;
|
||||
}
|
||||
try {
|
||||
final decrpytedEvent = await encryption.decryptRoomEvent(
|
||||
room.id, Event.fromJson(content, room),
|
||||
store: store, updateType: type);
|
||||
return EventUpdate(
|
||||
roomID: roomID,
|
||||
type: type,
|
||||
content: decrpytedEvent.toJson(),
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logs().e('[LibOlm] Could not decrypt megolm event', e, s);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
259
lib/src/utils/html_to_text.dart
Normal file
259
lib/src/utils/html_to_text.dart
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:html_unescape/html_unescape.dart';
|
||||
|
||||
class HtmlToText {
|
||||
/// Convert an HTML string to a pseudo-markdown plain text representation, with
|
||||
/// `data-mx-spoiler` spans redacted
|
||||
static String convert(String html) {
|
||||
// riot-web is notorious for creating bad reply fallback events from invalid messages which, if
|
||||
// not handled properly, can lead to impersonation. As such, we strip the entire `<mx-reply>` tags
|
||||
// here already, to prevent that from happening.
|
||||
// We do *not* do this in an AST and just with simple regex here, as riot-web tends to create
|
||||
// miss-matching tags, and this way we actually correctly identify what we want to strip and, well,
|
||||
// strip it.
|
||||
final renderHtml = html.replaceAll(
|
||||
RegExp('<mx-reply>.*<\/mx-reply>',
|
||||
caseSensitive: false, multiLine: false, dotAll: true),
|
||||
'');
|
||||
|
||||
final opts = _ConvertOpts();
|
||||
var reply = _walkNode(opts, parseFragment(renderHtml));
|
||||
reply = reply.replaceAll(RegExp(r'\s*$', multiLine: false), '');
|
||||
return reply;
|
||||
}
|
||||
|
||||
static String _parsePreContent(_ConvertOpts opts, Element node) {
|
||||
var text = node.innerHtml;
|
||||
final match =
|
||||
RegExp(r'^<code([^>]*)>', multiLine: false, caseSensitive: false)
|
||||
.firstMatch(text);
|
||||
if (match == null) {
|
||||
text = HtmlUnescape().convert(text);
|
||||
if (text.isNotEmpty) {
|
||||
if (text[0] != '\n') {
|
||||
text = '\n$text';
|
||||
}
|
||||
if (text[text.length - 1] != '\n') {
|
||||
text += '\n';
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
// remove <code> opening tag
|
||||
text = text.substring(match.end);
|
||||
// remove the </code> closing tag
|
||||
text = text.replaceAll(
|
||||
RegExp(r'</code>$', multiLine: false, caseSensitive: false), '');
|
||||
text = HtmlUnescape().convert(text);
|
||||
if (text.isNotEmpty) {
|
||||
if (text[0] != '\n') {
|
||||
text = '\n$text';
|
||||
}
|
||||
if (text[text.length - 1] != '\n') {
|
||||
text += '\n';
|
||||
}
|
||||
}
|
||||
final language =
|
||||
RegExp(r'language-(\w+)', multiLine: false, caseSensitive: false)
|
||||
.firstMatch(match.group(1)!);
|
||||
if (language != null) {
|
||||
text = language.group(1)! + text;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
static String _parseBlockquoteContent(_ConvertOpts opts, Element node) {
|
||||
final msg = _walkChildNodes(opts, node);
|
||||
return msg.split('\n').map((s) => '> $s').join('\n') + '\n';
|
||||
}
|
||||
|
||||
static String _parseSpanContent(_ConvertOpts opts, Element node) {
|
||||
final content = _walkChildNodes(opts, node);
|
||||
if (node.attributes['data-mx-spoiler'] is String) {
|
||||
var spoiler = '█' * content.length;
|
||||
final reason = node.attributes['data-mx-spoiler'];
|
||||
if (reason != '') {
|
||||
spoiler = '($reason) $spoiler';
|
||||
}
|
||||
return spoiler;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
static String _parseUlContent(_ConvertOpts opts, Element node) {
|
||||
opts.listDepth++;
|
||||
final entries = _listChildNodes(opts, node, {'li'});
|
||||
opts.listDepth--;
|
||||
final bulletPoint =
|
||||
_listBulletPoints[opts.listDepth % _listBulletPoints.length];
|
||||
|
||||
return entries
|
||||
.map((s) =>
|
||||
(' ' * opts.listDepth) +
|
||||
bulletPoint +
|
||||
' ' +
|
||||
s.replaceAll('\n', '\n' + (' ' * opts.listDepth) + ' '))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
static String _parseOlContent(_ConvertOpts opts, Element node) {
|
||||
opts.listDepth++;
|
||||
final entries = _listChildNodes(opts, node, {'li'});
|
||||
opts.listDepth--;
|
||||
final startStr = node.attributes['start'];
|
||||
final start = (startStr is String &&
|
||||
RegExp(r'^[0-9]+$', multiLine: false).hasMatch(startStr))
|
||||
? int.parse(startStr)
|
||||
: 1;
|
||||
|
||||
return entries
|
||||
.mapIndexed((index, s) =>
|
||||
(' ' * opts.listDepth) +
|
||||
'${start + index}. ' +
|
||||
s.replaceAll('\n', '\n' + (' ' * opts.listDepth) + ' '))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
static const _listBulletPoints = <String>['●', '○', '■', '‣'];
|
||||
|
||||
static List<String> _listChildNodes(_ConvertOpts opts, Element node,
|
||||
[Iterable<String>? types]) {
|
||||
final replies = <String>[];
|
||||
for (final child in node.nodes) {
|
||||
if (types != null &&
|
||||
types.isNotEmpty &&
|
||||
((child is Text) ||
|
||||
((child is Element) &&
|
||||
!types.contains(child.localName!.toLowerCase())))) {
|
||||
continue;
|
||||
}
|
||||
replies.add(_walkNode(opts, child));
|
||||
}
|
||||
return replies;
|
||||
}
|
||||
|
||||
static const _blockTags = <String>{
|
||||
'blockquote',
|
||||
'ul',
|
||||
'ol',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'pre',
|
||||
};
|
||||
|
||||
static String _walkChildNodes(_ConvertOpts opts, Node node) {
|
||||
var reply = '';
|
||||
var lastTag = '';
|
||||
for (final child in node.nodes) {
|
||||
final thisTag = child is Element ? child.localName!.toLowerCase() : '';
|
||||
if (thisTag == 'p' && lastTag == 'p') {
|
||||
reply += '\n\n';
|
||||
} else if (_blockTags.contains(thisTag) &&
|
||||
reply.isNotEmpty &&
|
||||
reply[reply.length - 1] != '\n') {
|
||||
reply += '\n';
|
||||
}
|
||||
reply += _walkNode(opts, child);
|
||||
if (thisTag.isNotEmpty) {
|
||||
lastTag = thisTag;
|
||||
}
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
static String _walkNode(_ConvertOpts opts, Node node) {
|
||||
if (node is Text) {
|
||||
// ignore \n between single nodes
|
||||
return node.text == '\n' ? '' : node.text;
|
||||
} else if (node is Element) {
|
||||
final tag = node.localName!.toLowerCase();
|
||||
switch (tag) {
|
||||
case 'em':
|
||||
case 'i':
|
||||
return '*${_walkChildNodes(opts, node)}*';
|
||||
case 'strong':
|
||||
case 'b':
|
||||
return '**${_walkChildNodes(opts, node)}**';
|
||||
case 'u':
|
||||
case 'ins':
|
||||
return '__${_walkChildNodes(opts, node)}__';
|
||||
case 'del':
|
||||
case 'strike':
|
||||
case 's':
|
||||
return '~~${_walkChildNodes(opts, node)}~~';
|
||||
case 'code':
|
||||
return '`${node.text}`';
|
||||
case 'pre':
|
||||
return '```${_parsePreContent(opts, node)}```\n';
|
||||
case 'a':
|
||||
final href = node.attributes['href'] ?? '';
|
||||
final content = _walkChildNodes(opts, node);
|
||||
if (href.toLowerCase().startsWith('https://matrix.to/#/') ||
|
||||
href.toLowerCase().startsWith('matrix:')) {
|
||||
return content;
|
||||
}
|
||||
return '🔗$content';
|
||||
case 'img':
|
||||
return node.attributes['alt'] ??
|
||||
node.attributes['title'] ??
|
||||
node.attributes['src'] ??
|
||||
'';
|
||||
case 'br':
|
||||
return '\n';
|
||||
case 'blockquote':
|
||||
return _parseBlockquoteContent(opts, node);
|
||||
case 'ul':
|
||||
return _parseUlContent(opts, node);
|
||||
case 'ol':
|
||||
return _parseOlContent(opts, node);
|
||||
case 'mx-reply':
|
||||
return '';
|
||||
case 'hr':
|
||||
return '\n----------\n';
|
||||
case 'h1':
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
case 'h4':
|
||||
case 'h5':
|
||||
case 'h6':
|
||||
final mark = '#' * int.parse(tag[1]);
|
||||
return '$mark ${_walkChildNodes(opts, node)}\n';
|
||||
case 'span':
|
||||
return _parseSpanContent(opts, node);
|
||||
default:
|
||||
return _walkChildNodes(opts, node);
|
||||
}
|
||||
} else {
|
||||
return _walkChildNodes(opts, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ConvertOpts {
|
||||
int listDepth = 0;
|
||||
}
|
||||
115
lib/src/utils/http_timeout.dart
Normal file
115
lib/src/utils/http_timeout.dart
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../matrix.dart';
|
||||
|
||||
/// Stream.timeout fails if no progress is made in timeLimit.
|
||||
/// In contrast, streamTotalTimeout fails if the stream isn't completed
|
||||
/// until timeoutFuture.
|
||||
Stream<T> streamTotalTimeout<T>(
|
||||
Stream<T> stream, Future<Never> timeoutFuture) async* {
|
||||
final si = StreamIterator(stream);
|
||||
while (await Future.any([si.moveNext(), timeoutFuture])) {
|
||||
yield si.current;
|
||||
}
|
||||
}
|
||||
|
||||
http.StreamedResponse replaceStream(
|
||||
http.StreamedResponse base, Stream<List<int>> stream) =>
|
||||
http.StreamedResponse(
|
||||
http.ByteStream(stream),
|
||||
base.statusCode,
|
||||
contentLength: base.contentLength,
|
||||
request: base.request,
|
||||
headers: base.headers,
|
||||
isRedirect: base.isRedirect,
|
||||
persistentConnection: base.persistentConnection,
|
||||
reasonPhrase: base.reasonPhrase,
|
||||
);
|
||||
|
||||
/// Http Client that enforces a timeout on requests.
|
||||
/// Timeout calculation is done in a subclass.
|
||||
abstract class TimeoutHttpClient extends http.BaseClient {
|
||||
TimeoutHttpClient(this.inner);
|
||||
|
||||
http.Client inner;
|
||||
|
||||
Duration get timeout;
|
||||
|
||||
@override
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
||||
final timeoutFuture = Completer<Never>().future.timeout(timeout);
|
||||
final response = await Future.any([inner.send(request), timeoutFuture]);
|
||||
return replaceStream(
|
||||
response, streamTotalTimeout(response.stream, timeoutFuture));
|
||||
}
|
||||
}
|
||||
|
||||
class FixedTimeoutHttpClient extends TimeoutHttpClient {
|
||||
FixedTimeoutHttpClient(http.Client inner, this.timeout) : super(inner);
|
||||
@override
|
||||
Duration timeout;
|
||||
|
||||
@override
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) =>
|
||||
super.send(request);
|
||||
}
|
||||
|
||||
class VariableTimeoutHttpClient extends TimeoutHttpClient {
|
||||
/// Matrix synchronisation is done with https long polling. This needs a
|
||||
/// timeout which is usually 30 seconds.
|
||||
int syncTimeoutSec;
|
||||
|
||||
int _timeoutFactor = 1;
|
||||
|
||||
@override
|
||||
Duration get timeout =>
|
||||
Duration(seconds: _timeoutFactor * syncTimeoutSec + 5);
|
||||
|
||||
VariableTimeoutHttpClient(http.Client inner, [this.syncTimeoutSec = 30])
|
||||
: super(inner);
|
||||
|
||||
@override
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
||||
try {
|
||||
final response = await super.send(request);
|
||||
return replaceStream(response, (() async* {
|
||||
try {
|
||||
await for (final chunk in response.stream) {
|
||||
yield chunk;
|
||||
}
|
||||
_timeoutFactor = 1;
|
||||
} on TimeoutException catch (e, s) {
|
||||
_timeoutFactor *= 2;
|
||||
throw MatrixConnectionException(e, s);
|
||||
} catch (e, s) {
|
||||
throw MatrixConnectionException(e, s);
|
||||
}
|
||||
})());
|
||||
} on TimeoutException catch (e, s) {
|
||||
_timeoutFactor *= 2;
|
||||
throw MatrixConnectionException(e, s);
|
||||
} catch (e, s) {
|
||||
throw MatrixConnectionException(e, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
95
lib/src/utils/image_pack_extension.dart
Normal file
95
lib/src/utils/image_pack_extension.dart
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:slugify/slugify.dart';
|
||||
import 'package:matrix_api_lite/matrix_api_lite.dart';
|
||||
|
||||
import '../room.dart';
|
||||
|
||||
extension ImagePackRoomExtension on Room {
|
||||
/// Get all the active image packs for the specified [usage], mapped by their slug
|
||||
Map<String, ImagePackContent> getImagePacks([ImagePackUsage? usage]) {
|
||||
final allMxcs = <Uri>{}; // used for easy deduplication
|
||||
final packs = <String, ImagePackContent>{};
|
||||
final addImagePack = (BasicEvent? event, {Room? room, String? slug}) {
|
||||
if (event == null) return;
|
||||
final imagePack = event.parsedImagePackContent;
|
||||
final finalSlug = slugify(slug ?? 'pack');
|
||||
for (final entry in imagePack.images.entries) {
|
||||
final image = entry.value;
|
||||
if (allMxcs.contains(image.url)) {
|
||||
continue;
|
||||
}
|
||||
final imageUsage = image.usage ?? imagePack.pack.usage;
|
||||
if (usage != null &&
|
||||
imageUsage != null &&
|
||||
!imageUsage.contains(usage)) {
|
||||
continue;
|
||||
}
|
||||
packs
|
||||
.putIfAbsent(
|
||||
finalSlug,
|
||||
() => ImagePackContent.fromJson({})
|
||||
..pack.displayName = imagePack.pack.displayName ??
|
||||
room?.displayname ??
|
||||
finalSlug
|
||||
..pack.avatarUrl = imagePack.pack.avatarUrl ?? room?.avatar
|
||||
..pack.attribution = imagePack.pack.attribution)
|
||||
.images[entry.key] = image;
|
||||
allMxcs.add(image.url);
|
||||
}
|
||||
};
|
||||
// first we add the user image pack
|
||||
addImagePack(client.accountData['im.ponies.user_emotes'], slug: 'user');
|
||||
// next we add all the external image packs
|
||||
final packRooms = client.accountData['im.ponies.emote_rooms'];
|
||||
if (packRooms != null && packRooms.content['rooms'] is Map) {
|
||||
for (final roomEntry in packRooms.content['rooms'].entries) {
|
||||
final roomId = roomEntry.key;
|
||||
final room = client.getRoomById(roomId);
|
||||
if (room != null && roomEntry.value is Map) {
|
||||
for (final stateKeyEntry in roomEntry.value.entries) {
|
||||
final stateKey = stateKeyEntry.key;
|
||||
final fallbackSlug =
|
||||
'${room.displayname}-${stateKey.isNotEmpty ? '$stateKey-' : ''}${room.id}';
|
||||
addImagePack(room.getState('im.ponies.room_emotes', stateKey),
|
||||
room: room, slug: fallbackSlug);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// finally we add all of this rooms state
|
||||
final allRoomEmotes = states['im.ponies.room_emotes'];
|
||||
if (allRoomEmotes != null) {
|
||||
for (final entry in allRoomEmotes.entries) {
|
||||
addImagePack(entry.value,
|
||||
room: this,
|
||||
slug: (entry.value.stateKey?.isNotEmpty == true)
|
||||
? entry.value.stateKey
|
||||
: 'room');
|
||||
}
|
||||
}
|
||||
return packs;
|
||||
}
|
||||
|
||||
/// Get a flat view of all the image packs of a specified [usage], that is a map of all
|
||||
/// slugs to a map of the image code to their mxc url
|
||||
Map<String, Map<String, String>> getImagePacksFlat([ImagePackUsage? usage]) =>
|
||||
getImagePacks(usage).map((k, v) =>
|
||||
MapEntry(k, v.images.map((k, v) => MapEntry(k, v.url.toString()))));
|
||||
}
|
||||
33
lib/src/utils/map_copy_extension.dart
Normal file
33
lib/src/utils/map_copy_extension.dart
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
extension MapCopyExtension on Map<String, dynamic> {
|
||||
/// Deep-copies a given json map
|
||||
Map<String, dynamic> copy() {
|
||||
final copy = Map<String, dynamic>.from(this);
|
||||
for (final entry in copy.entries) {
|
||||
if (entry.value is Map<String, dynamic>) {
|
||||
copy[entry.key] = (entry.value as Map<String, dynamic>).copy();
|
||||
}
|
||||
if (entry.value is List) {
|
||||
copy[entry.key] = List.from(entry.value);
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
256
lib/src/utils/markdown.dart
Normal file
256
lib/src/utils/markdown.dart
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:markdown/markdown.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
const htmlAttrEscape = HtmlEscape(HtmlEscapeMode.attribute);
|
||||
|
||||
class LinebreakSyntax extends InlineSyntax {
|
||||
LinebreakSyntax() : super(r'\n');
|
||||
|
||||
@override
|
||||
bool onMatch(InlineParser parser, Match match) {
|
||||
parser.addNode(Element.empty('br'));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class SpoilerSyntax extends TagSyntax {
|
||||
SpoilerSyntax() : super(r'\|\|', requiresDelimiterRun: true);
|
||||
|
||||
@override
|
||||
Node close(InlineParser parser, Delimiter opener, Delimiter closer,
|
||||
{required List<Node> Function() getChildren}) {
|
||||
final children = getChildren();
|
||||
final newChildren = <Node>[];
|
||||
var searchingForReason = true;
|
||||
var reason = '';
|
||||
for (final child in children) {
|
||||
// If we already found a reason, let's just use our child nodes as-is
|
||||
if (!searchingForReason) {
|
||||
newChildren.add(child);
|
||||
continue;
|
||||
}
|
||||
if (child is Text) {
|
||||
final ix = child.text.indexOf('|');
|
||||
if (ix > 0) {
|
||||
reason += child.text.substring(0, ix);
|
||||
newChildren.add(Text(child.text.substring(ix + 1)));
|
||||
searchingForReason = false;
|
||||
} else {
|
||||
reason += child.text;
|
||||
}
|
||||
} else {
|
||||
// if we don't have a text node as reason we just want to cancel this whole thing
|
||||
break;
|
||||
}
|
||||
}
|
||||
// if we were still searching for a reason that means there was none - use the original children!
|
||||
final element =
|
||||
Element('span', searchingForReason ? children : newChildren);
|
||||
element.attributes['data-mx-spoiler'] =
|
||||
searchingForReason ? '' : htmlAttrEscape.convert(reason);
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
class EmoteSyntax extends InlineSyntax {
|
||||
final Map<String, Map<String, String>> Function()? getEmotePacks;
|
||||
Map<String, Map<String, String>>? emotePacks;
|
||||
EmoteSyntax(this.getEmotePacks) : super(r':(?:([-\w]+)~)?([-\w]+):');
|
||||
|
||||
@override
|
||||
bool onMatch(InlineParser parser, Match match) {
|
||||
final emotePacks = this.emotePacks ??= getEmotePacks?.call() ?? {};
|
||||
final pack = match[1] ?? '';
|
||||
final emote = match[2];
|
||||
String? mxc;
|
||||
if (pack.isEmpty) {
|
||||
// search all packs
|
||||
for (final emotePack in emotePacks.values) {
|
||||
mxc = emotePack[emote];
|
||||
if (mxc != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mxc = emotePacks[pack]?[emote];
|
||||
}
|
||||
if (mxc == null) {
|
||||
// emote not found. Insert the whole thing as plain text
|
||||
parser.addNode(Text(match[0]!));
|
||||
return true;
|
||||
}
|
||||
final element = Element.empty('img');
|
||||
element.attributes['data-mx-emoticon'] = '';
|
||||
element.attributes['src'] = htmlAttrEscape.convert(mxc);
|
||||
element.attributes['alt'] = htmlAttrEscape.convert(':$emote:');
|
||||
element.attributes['title'] = htmlAttrEscape.convert(':$emote:');
|
||||
element.attributes['height'] = '32';
|
||||
element.attributes['vertical-align'] = 'middle';
|
||||
parser.addNode(element);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class InlineLatexSyntax extends TagSyntax {
|
||||
InlineLatexSyntax() : super(r'\$([^\s$]([^\$]*[^\s$])?)\$');
|
||||
|
||||
@override
|
||||
bool onMatch(InlineParser parser, Match match) {
|
||||
final element =
|
||||
Element('span', [Element.text('code', htmlEscape.convert(match[1]!))]);
|
||||
element.attributes['data-mx-maths'] = htmlAttrEscape.convert(match[1]!);
|
||||
parser.addNode(element);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// We also want to allow single-lines of like "$$latex$$"
|
||||
class BlockLatexSyntax extends BlockSyntax {
|
||||
@override
|
||||
RegExp get pattern => RegExp(r'^[ ]{0,3}\$\$(.*)$');
|
||||
|
||||
final endPattern = RegExp(r'^(.*)\$\$\s*$');
|
||||
|
||||
@override
|
||||
List<String> parseChildLines(BlockParser parser) {
|
||||
final childLines = <String>[];
|
||||
var first = true;
|
||||
while (!parser.isDone) {
|
||||
final match = endPattern.firstMatch(parser.current);
|
||||
if (match == null || (first && match[1]!.trim().isEmpty)) {
|
||||
childLines.add(parser.current);
|
||||
parser.advance();
|
||||
} else {
|
||||
childLines.add(match[1]!);
|
||||
parser.advance();
|
||||
break;
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
return childLines;
|
||||
}
|
||||
|
||||
@override
|
||||
Node parse(BlockParser parser) {
|
||||
final childLines = parseChildLines(parser);
|
||||
// we use .substring(2) as childLines will *always* contain the first two '$$'
|
||||
final latex = childLines.join('\n').trim().substring(2).trim();
|
||||
final element = Element('div', [
|
||||
Element('pre', [Element.text('code', htmlEscape.convert(latex))])
|
||||
]);
|
||||
element.attributes['data-mx-maths'] = htmlAttrEscape.convert(latex);
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
class PillSyntax extends InlineSyntax {
|
||||
PillSyntax()
|
||||
: super(
|
||||
r'([@#!][^\s:]*:(?:[^\s]+\.\w+|[\d\.]+|\[[a-fA-F0-9:]+\])(?::\d+)?)');
|
||||
|
||||
@override
|
||||
bool onMatch(InlineParser parser, Match match) {
|
||||
if (match.start > 0 &&
|
||||
!RegExp(r'[\s.!?:;\(]').hasMatch(match.input[match.start - 1])) {
|
||||
parser.addNode(Text(match[0]!));
|
||||
return true;
|
||||
}
|
||||
final identifier = match[1]!;
|
||||
final element = Element.text('a', htmlEscape.convert(identifier));
|
||||
element.attributes['href'] =
|
||||
htmlAttrEscape.convert('https://matrix.to/#/$identifier');
|
||||
parser.addNode(element);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class MentionSyntax extends InlineSyntax {
|
||||
final String? Function(String)? getMention;
|
||||
MentionSyntax(this.getMention) : super(r'(@(?:\[[^\]:]+\]|\w+)(?:#\w+)?)');
|
||||
|
||||
@override
|
||||
bool onMatch(InlineParser parser, Match match) {
|
||||
final mention = getMention?.call(match[1]!);
|
||||
if ((match.start > 0 &&
|
||||
!RegExp(r'[\s.!?:;\(]').hasMatch(match.input[match.start - 1])) ||
|
||||
mention == null) {
|
||||
parser.addNode(Text(match[0]!));
|
||||
return true;
|
||||
}
|
||||
final element = Element.text('a', htmlEscape.convert(match[1]!));
|
||||
element.attributes['href'] =
|
||||
htmlAttrEscape.convert('https://matrix.to/#/$mention');
|
||||
parser.addNode(element);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
String markdown(
|
||||
String text, {
|
||||
Map<String, Map<String, String>> Function()? getEmotePacks,
|
||||
String? Function(String)? getMention,
|
||||
}) {
|
||||
var ret = markdownToHtml(
|
||||
text,
|
||||
extensionSet: ExtensionSet.commonMark,
|
||||
blockSyntaxes: [
|
||||
BlockLatexSyntax(),
|
||||
],
|
||||
inlineSyntaxes: [
|
||||
StrikethroughSyntax(),
|
||||
LinebreakSyntax(),
|
||||
SpoilerSyntax(),
|
||||
EmoteSyntax(getEmotePacks),
|
||||
PillSyntax(),
|
||||
MentionSyntax(getMention),
|
||||
InlineLatexSyntax(),
|
||||
],
|
||||
);
|
||||
|
||||
var stripPTags = '<p>'.allMatches(ret).length <= 1;
|
||||
if (stripPTags) {
|
||||
const otherBlockTags = {
|
||||
'table',
|
||||
'pre',
|
||||
'ol',
|
||||
'ul',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'blockquote',
|
||||
'div',
|
||||
};
|
||||
for (final tag in otherBlockTags) {
|
||||
// we check for the close tag as the opening one might have attributes
|
||||
if (ret.contains('</$tag>')) {
|
||||
stripPTags = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stripPTags) {
|
||||
ret = ret.replaceAll('<p>', '').replaceAll('</p>', '');
|
||||
}
|
||||
return ret.trim().replaceAll(RegExp(r'(<br />)+$'), '');
|
||||
}
|
||||
34
lib/src/utils/marked_unread.dart
Normal file
34
lib/src/utils/marked_unread.dart
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2019, 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:matrix_api_lite/src/utils/try_get_map_extension.dart';
|
||||
|
||||
mixin EventType {
|
||||
static const String markedUnread = 'com.famedly.marked_unread';
|
||||
}
|
||||
|
||||
class MarkedUnread {
|
||||
final bool unread;
|
||||
|
||||
const MarkedUnread(this.unread);
|
||||
|
||||
MarkedUnread.fromJson(Map<String, dynamic> json)
|
||||
: unread = json.tryGet<bool>('unread') ?? false;
|
||||
|
||||
Map<String, dynamic> toJson() => {'unread': unread};
|
||||
}
|
||||
290
lib/src/utils/matrix_file.dart
Normal file
290
lib/src/utils/matrix_file.dart
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/// Workaround until [File] in dart:io and dart:html is unified
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:blurhash_dart/blurhash_dart.dart';
|
||||
import 'package:image/image.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
import '../../matrix.dart';
|
||||
|
||||
class MatrixFile {
|
||||
final Uint8List bytes;
|
||||
final String name;
|
||||
final String mimeType;
|
||||
|
||||
/// Encrypts this file and returns the
|
||||
/// encryption information as an [EncryptedFile].
|
||||
Future<EncryptedFile> encrypt() async {
|
||||
return await encryptFile(bytes);
|
||||
}
|
||||
|
||||
MatrixFile({required this.bytes, required String name, String? mimeType})
|
||||
: mimeType = mimeType ??
|
||||
lookupMimeType(name, headerBytes: bytes) ??
|
||||
'application/octet-stream',
|
||||
name = name.split('/').last.toLowerCase();
|
||||
|
||||
int get size => bytes.length;
|
||||
|
||||
String get msgType {
|
||||
if (mimeType.toLowerCase().startsWith('image/')) {
|
||||
return MessageTypes.Image;
|
||||
}
|
||||
if (mimeType.toLowerCase().startsWith('video/')) {
|
||||
return MessageTypes.Video;
|
||||
}
|
||||
if (mimeType.toLowerCase().startsWith('audio/')) {
|
||||
return MessageTypes.Audio;
|
||||
}
|
||||
return MessageTypes.File;
|
||||
}
|
||||
|
||||
Map<String, dynamic> get info => ({
|
||||
'mimetype': mimeType,
|
||||
'size': size,
|
||||
});
|
||||
}
|
||||
|
||||
class MatrixImageFile extends MatrixFile {
|
||||
MatrixImageFile({
|
||||
required Uint8List bytes,
|
||||
required String name,
|
||||
String? mimeType,
|
||||
this.width,
|
||||
this.height,
|
||||
this.blurhash,
|
||||
}) : super(bytes: bytes, name: name, mimeType: mimeType);
|
||||
|
||||
/// Creates a new image file and calculates the width, height and blurhash.
|
||||
static Future<MatrixImageFile> create(
|
||||
{required Uint8List bytes,
|
||||
required String name,
|
||||
String? mimeType,
|
||||
Future<T> Function<T, U>(FutureOr<T> Function(U arg) function, U arg)?
|
||||
compute}) async {
|
||||
final metaData = compute != null
|
||||
? await compute(_calcMetadata, bytes)
|
||||
: _calcMetadata(bytes);
|
||||
|
||||
return MatrixImageFile(
|
||||
bytes: metaData?.bytes ?? bytes,
|
||||
name: name,
|
||||
mimeType: mimeType,
|
||||
width: metaData?.width,
|
||||
height: metaData?.height,
|
||||
blurhash: metaData?.blurhash,
|
||||
);
|
||||
}
|
||||
|
||||
/// builds a [MatrixImageFile] and shrinks it in order to reduce traffic
|
||||
///
|
||||
/// in case shrinking does not work (e.g. for unsupported MIME types), the
|
||||
/// initial image is simply preserved
|
||||
static Future<MatrixImageFile> shrink(
|
||||
{required Uint8List bytes,
|
||||
required String name,
|
||||
int maxDimension = 1600,
|
||||
String? mimeType,
|
||||
Future<T> Function<T, U>(FutureOr<T> Function(U arg) function, U arg)?
|
||||
compute}) async {
|
||||
final arguments = _ResizeArguments(
|
||||
bytes: bytes,
|
||||
maxDimension: maxDimension,
|
||||
fileName: name,
|
||||
calcBlurhash: true,
|
||||
);
|
||||
final resizedData = compute != null
|
||||
? await compute(_resize, arguments)
|
||||
: _resize(arguments);
|
||||
|
||||
if (resizedData == null) {
|
||||
return MatrixImageFile(bytes: bytes, name: name, mimeType: mimeType);
|
||||
}
|
||||
|
||||
final thumbnailFile = MatrixImageFile(
|
||||
bytes: resizedData.bytes,
|
||||
name: name,
|
||||
mimeType: mimeType,
|
||||
width: resizedData.width,
|
||||
height: resizedData.height,
|
||||
blurhash: resizedData.blurhash,
|
||||
);
|
||||
return thumbnailFile;
|
||||
}
|
||||
|
||||
/// returns the width of the image
|
||||
final int? width;
|
||||
|
||||
/// returns the height of the image
|
||||
final int? height;
|
||||
|
||||
/// generates the blur hash for the image
|
||||
final String? blurhash;
|
||||
|
||||
@override
|
||||
String get msgType => 'm.image';
|
||||
@override
|
||||
Map<String, dynamic> get info => ({
|
||||
...super.info,
|
||||
if (width != null) 'w': width,
|
||||
if (height != null) 'h': height,
|
||||
if (blurhash != null) 'xyz.amorgan.blurhash': blurhash,
|
||||
});
|
||||
|
||||
/// computes a thumbnail for the image
|
||||
Future<MatrixImageFile?> generateThumbnail(
|
||||
{int dimension = Client.defaultThumbnailSize,
|
||||
Future<T> Function<T, U>(FutureOr<T> Function(U arg) function, U arg)?
|
||||
compute}) async {
|
||||
final thumbnailFile = await shrink(
|
||||
bytes: bytes,
|
||||
name: name,
|
||||
mimeType: mimeType,
|
||||
compute: compute,
|
||||
maxDimension: dimension,
|
||||
);
|
||||
// the thumbnail should rather return null than the unshrinked image
|
||||
if ((thumbnailFile.width ?? 0) > dimension ||
|
||||
(thumbnailFile.height ?? 0) > dimension) {
|
||||
return null;
|
||||
}
|
||||
return thumbnailFile;
|
||||
}
|
||||
|
||||
static _ResizedResponse? _calcMetadata(Uint8List bytes) {
|
||||
final image = decodeImage(bytes);
|
||||
if (image == null) return null;
|
||||
|
||||
return _ResizedResponse(
|
||||
bytes: bytes,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
blurhash: BlurHash.encode(
|
||||
image,
|
||||
numCompX: 4,
|
||||
numCompY: 3,
|
||||
).hash,
|
||||
);
|
||||
}
|
||||
|
||||
static _ResizedResponse? _resize(_ResizeArguments arguments) {
|
||||
final image = decodeImage(arguments.bytes);
|
||||
|
||||
final resized = copyResize(image!,
|
||||
height: image.height > image.width ? arguments.maxDimension : null,
|
||||
width: image.width >= image.height ? arguments.maxDimension : null);
|
||||
|
||||
final encoded = encodeNamedImage(resized, arguments.fileName);
|
||||
if (encoded == null) return null;
|
||||
final bytes = Uint8List.fromList(encoded);
|
||||
return _ResizedResponse(
|
||||
bytes: bytes,
|
||||
width: resized.width,
|
||||
height: resized.height,
|
||||
blurhash: arguments.calcBlurhash
|
||||
? BlurHash.encode(
|
||||
resized,
|
||||
numCompX: 4,
|
||||
numCompY: 3,
|
||||
).hash
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ResizedResponse {
|
||||
final Uint8List bytes;
|
||||
final int width;
|
||||
final int height;
|
||||
final String? blurhash;
|
||||
|
||||
const _ResizedResponse({
|
||||
required this.bytes,
|
||||
required this.width,
|
||||
required this.height,
|
||||
this.blurhash,
|
||||
});
|
||||
}
|
||||
|
||||
class _ResizeArguments {
|
||||
final Uint8List bytes;
|
||||
final int maxDimension;
|
||||
final String fileName;
|
||||
final bool calcBlurhash;
|
||||
|
||||
const _ResizeArguments({
|
||||
required this.bytes,
|
||||
required this.maxDimension,
|
||||
required this.fileName,
|
||||
required this.calcBlurhash,
|
||||
});
|
||||
}
|
||||
|
||||
class MatrixVideoFile extends MatrixFile {
|
||||
final int? width;
|
||||
final int? height;
|
||||
final int? duration;
|
||||
|
||||
MatrixVideoFile(
|
||||
{required Uint8List bytes,
|
||||
required String name,
|
||||
String? mimeType,
|
||||
this.width,
|
||||
this.height,
|
||||
this.duration})
|
||||
: super(bytes: bytes, name: name, mimeType: mimeType);
|
||||
@override
|
||||
String get msgType => 'm.video';
|
||||
@override
|
||||
Map<String, dynamic> get info => ({
|
||||
...super.info,
|
||||
if (width != null) 'w': width,
|
||||
if (height != null) 'h': height,
|
||||
if (duration != null) 'duration': duration,
|
||||
});
|
||||
}
|
||||
|
||||
class MatrixAudioFile extends MatrixFile {
|
||||
final int? duration;
|
||||
|
||||
MatrixAudioFile(
|
||||
{required Uint8List bytes,
|
||||
required String name,
|
||||
String? mimeType,
|
||||
this.duration})
|
||||
: super(bytes: bytes, name: name, mimeType: mimeType);
|
||||
@override
|
||||
String get msgType => 'm.audio';
|
||||
@override
|
||||
Map<String, dynamic> get info => ({
|
||||
...super.info,
|
||||
if (duration != null) 'duration': duration,
|
||||
});
|
||||
}
|
||||
|
||||
extension ToMatrixFile on EncryptedFile {
|
||||
MatrixFile toMatrixFile() {
|
||||
return MatrixFile(
|
||||
bytes: data, name: 'crypt', mimeType: 'application/octet-stream');
|
||||
}
|
||||
}
|
||||
136
lib/src/utils/matrix_id_string_extension.dart
Normal file
136
lib/src/utils/matrix_id_string_extension.dart
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const Set<String> validSigils = {'@', '!', '#', '\$', '+'};
|
||||
|
||||
const int maxLength = 255;
|
||||
|
||||
extension MatrixIdExtension on String {
|
||||
List<String> _getParts() {
|
||||
final s = substring(1);
|
||||
final ix = s.indexOf(':');
|
||||
if (ix == -1) {
|
||||
return [substring(1)];
|
||||
}
|
||||
return [s.substring(0, ix), s.substring(ix + 1)];
|
||||
}
|
||||
|
||||
bool get isValidMatrixId {
|
||||
if (isEmpty) return false;
|
||||
if (length > maxLength) return false;
|
||||
if (!validSigils.contains(substring(0, 1))) {
|
||||
return false;
|
||||
}
|
||||
// event IDs do not have to have a domain
|
||||
if (substring(0, 1) == '\$') {
|
||||
return true;
|
||||
}
|
||||
// all other matrix IDs have to have a domain
|
||||
final parts = _getParts();
|
||||
// the localpart can be an empty string, e.g. for aliases
|
||||
if (parts.length != 2 || parts[1].isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
String? get sigil => isValidMatrixId ? substring(0, 1) : null;
|
||||
|
||||
String? get localpart => isValidMatrixId ? _getParts().first : null;
|
||||
|
||||
String? get domain => isValidMatrixId ? _getParts().last : null;
|
||||
|
||||
bool equals(String? other) => toLowerCase() == other?.toLowerCase();
|
||||
|
||||
/// Parse a matrix identifier string into a Uri. Primary and secondary identifiers
|
||||
/// are stored in pathSegments. The query string is stored as such.
|
||||
Uri? _parseIdentifierIntoUri() {
|
||||
const matrixUriPrefix = 'matrix:';
|
||||
const matrixToPrefix = 'https://matrix.to/#/';
|
||||
if (toLowerCase().startsWith(matrixUriPrefix)) {
|
||||
final uri = Uri.tryParse(this);
|
||||
if (uri == null) return null;
|
||||
final pathSegments = uri.pathSegments;
|
||||
final identifiers = <String>[];
|
||||
for (var i = 0; i < pathSegments.length - 1; i += 2) {
|
||||
final thisSigil = {
|
||||
'u': '@',
|
||||
'roomid': '!',
|
||||
'r': '#',
|
||||
'e': '\$',
|
||||
}[pathSegments[i].toLowerCase()];
|
||||
if (thisSigil == null) {
|
||||
break;
|
||||
}
|
||||
identifiers.add(thisSigil + pathSegments[i + 1]);
|
||||
}
|
||||
return uri.replace(pathSegments: identifiers);
|
||||
} else if (toLowerCase().startsWith(matrixToPrefix)) {
|
||||
return Uri.tryParse('//' +
|
||||
substring(matrixToPrefix.length - 1)
|
||||
.replaceAllMapped(
|
||||
RegExp(r'(?<=/)[#!@+][^:]*:|(\?.*$)'),
|
||||
(m) => m[0]!.replaceAllMapped(
|
||||
RegExp(m.group(1) != null ? '' : '[/?]'),
|
||||
(m) => Uri.encodeComponent(m.group(0)!)))
|
||||
.replaceAll('#', '%23'));
|
||||
} else {
|
||||
return Uri(
|
||||
pathSegments: RegExp(r'/((?:[#!@+][^:]*:)?[^/?]*)(?:\?.*$)?')
|
||||
.allMatches('/$this')
|
||||
.map((m) => m[1]!),
|
||||
query: RegExp(r'(?:/(?:[#!@+][^:]*:)?[^/?]*)*\?(.*$)')
|
||||
.firstMatch('/$this')?[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Separate a matrix identifier string into a primary indentifier, a secondary identifier,
|
||||
/// a query string and already parsed `via` parameters. A matrix identifier string
|
||||
/// can be an mxid, a matrix.to-url or a matrix-uri.
|
||||
MatrixIdentifierStringExtensionResults? parseIdentifierIntoParts() {
|
||||
final uri = _parseIdentifierIntoUri();
|
||||
if (uri == null) return null;
|
||||
final primary = uri.pathSegments.isNotEmpty ? uri.pathSegments[0] : null;
|
||||
if (primary == null || !primary.isValidMatrixId) return null;
|
||||
final secondary = uri.pathSegments.length > 1 ? uri.pathSegments[1] : null;
|
||||
if (secondary != null && !secondary.isValidMatrixId) return null;
|
||||
|
||||
return MatrixIdentifierStringExtensionResults(
|
||||
primaryIdentifier: primary,
|
||||
secondaryIdentifier: secondary,
|
||||
queryString: uri.query.isNotEmpty ? uri.query : null,
|
||||
via: (uri.queryParametersAll['via'] ?? []).toSet(),
|
||||
action: uri.queryParameters['action'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MatrixIdentifierStringExtensionResults {
|
||||
final String primaryIdentifier;
|
||||
final String? secondaryIdentifier;
|
||||
final String? queryString;
|
||||
final Set<String> via;
|
||||
final String? action;
|
||||
|
||||
MatrixIdentifierStringExtensionResults(
|
||||
{required this.primaryIdentifier,
|
||||
this.secondaryIdentifier,
|
||||
this.queryString,
|
||||
this.via = const {},
|
||||
this.action});
|
||||
}
|
||||
179
lib/src/utils/matrix_localizations.dart
Normal file
179
lib/src/utils/matrix_localizations.dart
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import '../room.dart';
|
||||
|
||||
abstract class MatrixLocalizations {
|
||||
const MatrixLocalizations();
|
||||
String get emptyChat;
|
||||
|
||||
String get invitedUsersOnly;
|
||||
|
||||
String get fromTheInvitation;
|
||||
|
||||
String get fromJoining;
|
||||
|
||||
String get visibleForAllParticipants;
|
||||
|
||||
String get visibleForEveryone;
|
||||
|
||||
String get guestsCanJoin;
|
||||
|
||||
String get guestsAreForbidden;
|
||||
|
||||
String get anyoneCanJoin;
|
||||
|
||||
String get needPantalaimonWarning;
|
||||
|
||||
String get channelCorruptedDecryptError;
|
||||
|
||||
String get encryptionNotEnabled;
|
||||
|
||||
String get unknownEncryptionAlgorithm;
|
||||
|
||||
String get noPermission;
|
||||
|
||||
String get you;
|
||||
|
||||
String get roomHasBeenUpgraded;
|
||||
|
||||
String groupWith(String displayname);
|
||||
|
||||
String removedBy(String calcDisplayname);
|
||||
|
||||
String sentASticker(String senderName);
|
||||
|
||||
String redactedAnEvent(String senderName);
|
||||
|
||||
String changedTheRoomAliases(String senderName);
|
||||
|
||||
String changedTheRoomInvitationLink(String senderName);
|
||||
|
||||
String createdTheChat(String senderName);
|
||||
|
||||
String changedTheJoinRules(String senderName);
|
||||
|
||||
String changedTheJoinRulesTo(String senderName, String localizedString);
|
||||
|
||||
String acceptedTheInvitation(String targetName);
|
||||
|
||||
String rejectedTheInvitation(String targetName);
|
||||
|
||||
String hasWithdrawnTheInvitationFor(String senderName, String targetName);
|
||||
|
||||
String joinedTheChat(String targetName);
|
||||
|
||||
String kickedAndBanned(String senderName, String targetName);
|
||||
|
||||
String kicked(String senderName, String targetName);
|
||||
|
||||
String userLeftTheChat(String targetName);
|
||||
|
||||
String bannedUser(String senderName, String targetName);
|
||||
|
||||
String unbannedUser(String senderName, String targetName);
|
||||
|
||||
String invitedUser(String senderName, String targetName);
|
||||
|
||||
String changedTheProfileAvatar(String targetName);
|
||||
|
||||
String changedTheDisplaynameTo(String targetName, String newDisplayname);
|
||||
|
||||
String changedTheChatPermissions(String senderName);
|
||||
|
||||
String changedTheChatNameTo(String senderName, String content);
|
||||
|
||||
String changedTheChatDescriptionTo(String senderName, String content);
|
||||
|
||||
String changedTheChatAvatar(String senderName);
|
||||
|
||||
String changedTheGuestAccessRules(String senderName);
|
||||
|
||||
String changedTheGuestAccessRulesTo(
|
||||
String senderName, String localizedString);
|
||||
|
||||
String changedTheHistoryVisibility(String senderName);
|
||||
|
||||
String changedTheHistoryVisibilityTo(
|
||||
String senderName, String localizedString);
|
||||
|
||||
String activatedEndToEndEncryption(String senderName);
|
||||
|
||||
String sentAPicture(String senderName);
|
||||
|
||||
String sentAFile(String senderName);
|
||||
|
||||
String sentAnAudio(String senderName);
|
||||
|
||||
String sentAVideo(String senderName);
|
||||
|
||||
String sentReaction(String senderName, String reactionKey);
|
||||
|
||||
String sharedTheLocation(String senderName);
|
||||
|
||||
String couldNotDecryptMessage(String errorText);
|
||||
|
||||
String unknownEvent(String typeKey);
|
||||
|
||||
String startedACall(String senderName);
|
||||
|
||||
String endedTheCall(String senderName);
|
||||
|
||||
String answeredTheCall(String senderName);
|
||||
|
||||
String sentCallInformations(String senderName);
|
||||
}
|
||||
|
||||
extension HistoryVisibilityDisplayString on HistoryVisibility {
|
||||
String getLocalizedString(MatrixLocalizations i18n) {
|
||||
switch (this) {
|
||||
case HistoryVisibility.invited:
|
||||
return i18n.fromTheInvitation;
|
||||
case HistoryVisibility.joined:
|
||||
return i18n.fromJoining;
|
||||
case HistoryVisibility.shared:
|
||||
return i18n.visibleForAllParticipants;
|
||||
case HistoryVisibility.worldReadable:
|
||||
return i18n.visibleForEveryone;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension GuestAccessDisplayString on GuestAccess {
|
||||
String getLocalizedString(MatrixLocalizations i18n) {
|
||||
switch (this) {
|
||||
case GuestAccess.canJoin:
|
||||
return i18n.guestsCanJoin;
|
||||
case GuestAccess.forbidden:
|
||||
return i18n.guestsAreForbidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension JoinRulesDisplayString on JoinRules {
|
||||
String getLocalizedString(MatrixLocalizations i18n) {
|
||||
switch (this) {
|
||||
case JoinRules.public:
|
||||
return i18n.anyoneCanJoin;
|
||||
case JoinRules.invite:
|
||||
return i18n.invitedUsersOnly;
|
||||
default:
|
||||
return toString().replaceAll('JoinRules.', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
68
lib/src/utils/multilock.dart
Normal file
68
lib/src/utils/multilock.dart
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2019, 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:async';
|
||||
|
||||
/// Lock management class. It allows to lock and unlock multiple keys at once. The keys have
|
||||
/// the type [T]
|
||||
class MultiLock<T> {
|
||||
final Map<T, Completer<void>> _completers = {};
|
||||
|
||||
/// Set a number of [keys] locks, awaiting them to be released previously.
|
||||
Future<void> lock(Iterable<T> keys) async {
|
||||
// An iterable might have duplicate entries. A set is guaranteed not to, and we need
|
||||
// unique entries, as else a lot of things might go bad.
|
||||
final uniqueKeys = keys.toSet();
|
||||
// we want to make sure that there are no existing completers for any of the locks
|
||||
// we are trying to set. So, we await all the completers until they are all gone.
|
||||
// We can't just assume they are all gone after one go, due to rare race conditions
|
||||
// which could then result in a deadlock.
|
||||
while (_completers.keys.any((k) => uniqueKeys.contains(k))) {
|
||||
// Here we try to build all the futures to wait for single completers and then await
|
||||
// them at the same time, in parallel
|
||||
final futures = <Future<void>>[];
|
||||
for (final key in uniqueKeys) {
|
||||
if (_completers[key] != null) {
|
||||
futures.add(() async {
|
||||
while (_completers[key] != null) {
|
||||
await _completers[key]!.future;
|
||||
}
|
||||
}());
|
||||
}
|
||||
}
|
||||
await Future.wait(futures);
|
||||
}
|
||||
// And finally set all the completers
|
||||
for (final key in uniqueKeys) {
|
||||
_completers[key] = Completer<void>();
|
||||
}
|
||||
}
|
||||
|
||||
/// Unlock all [keys] locks. Typically these should be the same keys as called
|
||||
/// in `.lock(keys)``
|
||||
void unlock(Iterable<T> keys) {
|
||||
final uniqueKeys = keys.toSet();
|
||||
// we just have to simply unlock all the completers
|
||||
for (final key in uniqueKeys) {
|
||||
if (_completers[key] != null) {
|
||||
final completer = _completers[key]!;
|
||||
_completers.remove(key);
|
||||
completer.complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
lib/src/utils/queued_to_device_event.dart
Normal file
50
lib/src/utils/queued_to_device_event.dart
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2019, 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:convert';
|
||||
|
||||
class QueuedToDeviceEvent {
|
||||
final int id;
|
||||
final String type;
|
||||
final String txnId;
|
||||
final Map<String, dynamic> content;
|
||||
|
||||
QueuedToDeviceEvent({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.txnId,
|
||||
required this.content,
|
||||
});
|
||||
|
||||
factory QueuedToDeviceEvent.fromJson(Map<String, dynamic> json) =>
|
||||
QueuedToDeviceEvent(
|
||||
id: json['id'],
|
||||
type: json['type'],
|
||||
txnId: json['txn_id'],
|
||||
// Temporary fix to stay compatible to Moor AND a key value store
|
||||
content: json['content'] is String
|
||||
? jsonDecode(json['content'])
|
||||
: json['content'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'type': type,
|
||||
'txn_id': txnId,
|
||||
'content': content,
|
||||
};
|
||||
}
|
||||
33
lib/src/utils/receipt.dart
Normal file
33
lib/src/utils/receipt.dart
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import '../user.dart';
|
||||
|
||||
/// Represents a receipt.
|
||||
/// This [user] has read an event at the given [time].
|
||||
class Receipt {
|
||||
final User user;
|
||||
final DateTime time;
|
||||
|
||||
const Receipt(this.user, this.time);
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) => (other is Receipt &&
|
||||
other.user == user &&
|
||||
other.time.microsecondsSinceEpoch == time.microsecondsSinceEpoch);
|
||||
}
|
||||
43
lib/src/utils/run_benchmarked.dart
Normal file
43
lib/src/utils/run_benchmarked.dart
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2019, 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:matrix/matrix.dart';
|
||||
|
||||
/// Calculates some benchmarks for this function. Give it a [name] and a [func]
|
||||
/// to call and it will calculate the needed milliseconds. Give it an optional
|
||||
/// [itemCount] to let it also calculate the needed milliseconds per item.
|
||||
Future<T> runBenchmarked<T>(
|
||||
String name,
|
||||
Future<T> Function() func, [
|
||||
int? itemCount,
|
||||
]) async {
|
||||
if (Logs().level.index < Level.debug.index) {
|
||||
return func();
|
||||
}
|
||||
final start = DateTime.now();
|
||||
final result = await func();
|
||||
final milliseconds =
|
||||
DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch;
|
||||
var message = 'Benchmark: $name -> $milliseconds ms';
|
||||
if (itemCount != null) {
|
||||
message +=
|
||||
' ($itemCount items, ${itemCount > 0 ? milliseconds / itemCount : milliseconds} ms/item)';
|
||||
}
|
||||
Logs().d(message);
|
||||
return result;
|
||||
}
|
||||
32
lib/src/utils/run_in_root.dart
Normal file
32
lib/src/utils/run_in_root.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import '../../matrix.dart';
|
||||
|
||||
Future<T?> runInRoot<T>(FutureOr<T> Function() fn) async {
|
||||
return await Zone.root.run(() async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e, s) {
|
||||
Logs().e('Error thrown in root zone', e, s);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
47
lib/src/utils/space_child.dart
Normal file
47
lib/src/utils/space_child.dart
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2019, 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:matrix_api_lite/matrix_api_lite.dart';
|
||||
|
||||
import '../event.dart';
|
||||
|
||||
class SpaceChild {
|
||||
final String? roomId;
|
||||
final List<String>? via;
|
||||
final String order;
|
||||
final bool? suggested;
|
||||
|
||||
SpaceChild.fromState(Event state)
|
||||
: assert(state.type == EventTypes.spaceChild),
|
||||
roomId = state.stateKey,
|
||||
via = state.content.tryGetList<String>('via'),
|
||||
order = state.content.tryGet<String>('order') ?? '',
|
||||
suggested = state.content.tryGet<bool>('suggested');
|
||||
}
|
||||
|
||||
class SpaceParent {
|
||||
final String? roomId;
|
||||
final List<String>? via;
|
||||
final bool? canonical;
|
||||
|
||||
SpaceParent.fromState(Event state)
|
||||
: assert(state.type == EventTypes.spaceParent),
|
||||
roomId = state.stateKey,
|
||||
via = state.content.tryGetList<String>('via'),
|
||||
canonical = state.content.tryGet<bool>('canonical');
|
||||
}
|
||||
44
lib/src/utils/sync_update_extension.dart
Normal file
44
lib/src/utils/sync_update_extension.dart
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import '../../matrix.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?.isNotEmpty ?? false;
|
||||
}
|
||||
41
lib/src/utils/sync_update_item_count.dart
Normal file
41
lib/src/utils/sync_update_item_count.dart
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import 'package:matrix/matrix.dart';
|
||||
|
||||
extension SyncUpdateItemCount on SyncUpdate {
|
||||
int get itemCount {
|
||||
var count = 0;
|
||||
count += accountData?.length ?? 0;
|
||||
count += deviceLists?.changed?.length ?? 0;
|
||||
count += deviceLists?.left?.length ?? 0;
|
||||
count += toDevice?.length ?? 0;
|
||||
count += presence?.length ?? 0;
|
||||
count += _joinRoomsItemCount;
|
||||
count += _inviteRoomsItemCount;
|
||||
count += _leaveRoomsItemCount;
|
||||
return count;
|
||||
}
|
||||
|
||||
int get _joinRoomsItemCount =>
|
||||
rooms?.join?.values.fold<int>(
|
||||
0,
|
||||
(prev, room) =>
|
||||
prev +
|
||||
(room.accountData?.length ?? 0) +
|
||||
(room.state?.length ?? 0) +
|
||||
(room.timeline?.events?.length ?? 0)) ??
|
||||
0;
|
||||
|
||||
int get _inviteRoomsItemCount =>
|
||||
rooms?.invite?.values.fold<int>(
|
||||
0, (prev, room) => prev + (room.inviteState?.length ?? 0)) ??
|
||||
0;
|
||||
|
||||
int get _leaveRoomsItemCount =>
|
||||
rooms?.leave?.values.fold<int>(
|
||||
0,
|
||||
(prev, room) =>
|
||||
prev +
|
||||
(room.accountData?.length ?? 0) +
|
||||
(room.state?.length ?? 0) +
|
||||
(room.timeline?.events?.length ?? 0)) ??
|
||||
0;
|
||||
}
|
||||
53
lib/src/utils/to_device_event.dart
Normal file
53
lib/src/utils/to_device_event.dart
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import '../../matrix.dart';
|
||||
|
||||
class ToDeviceEvent extends BasicEventWithSender {
|
||||
Map<String, dynamic>? encryptedContent;
|
||||
|
||||
String get sender => senderId;
|
||||
set sender(String sender) => senderId = sender;
|
||||
|
||||
ToDeviceEvent({
|
||||
required String sender,
|
||||
required String type,
|
||||
required Map<String, dynamic> content,
|
||||
this.encryptedContent,
|
||||
}) : super(senderId: sender, type: type, content: content);
|
||||
|
||||
factory ToDeviceEvent.fromJson(Map<String, dynamic> json) {
|
||||
final event = BasicEventWithSender.fromJson(json);
|
||||
return ToDeviceEvent(
|
||||
sender: event.senderId, type: event.type, content: event.content);
|
||||
}
|
||||
}
|
||||
|
||||
class ToDeviceEventDecryptionError extends ToDeviceEvent {
|
||||
Exception exception;
|
||||
StackTrace? stackTrace;
|
||||
ToDeviceEventDecryptionError({
|
||||
required ToDeviceEvent toDeviceEvent,
|
||||
required this.exception,
|
||||
this.stackTrace,
|
||||
}) : super(
|
||||
sender: toDeviceEvent.senderId,
|
||||
content: toDeviceEvent.content,
|
||||
type: toDeviceEvent.type,
|
||||
);
|
||||
}
|
||||
120
lib/src/utils/uia_request.dart
Normal file
120
lib/src/utils/uia_request.dart
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import '../../matrix.dart';
|
||||
|
||||
enum UiaRequestState {
|
||||
/// The request is done
|
||||
done,
|
||||
|
||||
/// The request has failed
|
||||
fail,
|
||||
|
||||
/// The request is currently loading
|
||||
loading,
|
||||
|
||||
/// The request is waiting for user interaction
|
||||
waitForUser,
|
||||
}
|
||||
|
||||
/// Wrapper to handle User interactive authentication requests
|
||||
class UiaRequest<T> {
|
||||
void Function(UiaRequestState state)? onUpdate;
|
||||
final Future<T> Function(AuthenticationData? auth) request;
|
||||
String? session;
|
||||
UiaRequestState _state = UiaRequestState.loading;
|
||||
T? result;
|
||||
Exception? error;
|
||||
Set<String> nextStages = <String>{};
|
||||
Map<String, dynamic> params = <String, dynamic>{};
|
||||
|
||||
UiaRequestState get state => _state;
|
||||
|
||||
set state(UiaRequestState newState) {
|
||||
if (_state == newState) return;
|
||||
_state = newState;
|
||||
onUpdate?.call(newState);
|
||||
}
|
||||
|
||||
UiaRequest({this.onUpdate, required this.request}) {
|
||||
_run();
|
||||
}
|
||||
|
||||
Future<T?> _run([AuthenticationData? auth]) async {
|
||||
state = UiaRequestState.loading;
|
||||
try {
|
||||
final res = await request(auth);
|
||||
state = UiaRequestState.done;
|
||||
result = res;
|
||||
return res;
|
||||
} on MatrixException catch (err) {
|
||||
if (err.session == null) {
|
||||
error = err;
|
||||
state = UiaRequestState.fail;
|
||||
return null;
|
||||
}
|
||||
session ??= err.session;
|
||||
final completed = err.completedAuthenticationFlows;
|
||||
final flows = err.authenticationFlows ?? <AuthenticationFlow>[];
|
||||
params = err.authenticationParams ?? <String, dynamic>{};
|
||||
nextStages = getNextStages(flows, completed);
|
||||
if (nextStages.isEmpty) {
|
||||
error = err;
|
||||
state = UiaRequestState.fail;
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
} catch (err) {
|
||||
error = err is Exception ? err : Exception(err);
|
||||
state = UiaRequestState.fail;
|
||||
return null;
|
||||
} finally {
|
||||
if (state == UiaRequestState.loading) {
|
||||
state = UiaRequestState.waitForUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<T?> completeStage(AuthenticationData auth) => _run(auth);
|
||||
|
||||
/// Cancel this uia request for example if the app can not handle this stage.
|
||||
void cancel([Exception? err]) {
|
||||
error = err ?? Exception('Request has been canceled');
|
||||
state = UiaRequestState.fail;
|
||||
}
|
||||
|
||||
Set<String> getNextStages(
|
||||
List<AuthenticationFlow> flows, List<String> completed) {
|
||||
final nextStages = <String>{};
|
||||
for (final flow in flows) {
|
||||
final stages = flow.stages;
|
||||
final nextStage = stages[completed.length];
|
||||
var stagesValid = true;
|
||||
for (var i = 0; i < completed.length; i++) {
|
||||
if (stages[i] != completed[i]) {
|
||||
stagesValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (stagesValid) {
|
||||
nextStages.add(nextStage);
|
||||
}
|
||||
}
|
||||
return nextStages;
|
||||
}
|
||||
}
|
||||
63
lib/src/utils/uri_extension.dart
Normal file
63
lib/src/utils/uri_extension.dart
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2019, 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:core';
|
||||
|
||||
import '../client.dart';
|
||||
|
||||
extension MxcUriExtension on Uri {
|
||||
/// Returns a download Link to this content.
|
||||
Uri getDownloadLink(Client matrix) => isScheme('mxc')
|
||||
? matrix.homeserver != null
|
||||
? matrix.homeserver?.resolve(
|
||||
'_matrix/media/r0/download/$host${hasPort ? ':$port' : ''}$path') ??
|
||||
Uri()
|
||||
: Uri()
|
||||
: this;
|
||||
|
||||
/// Returns a scaled thumbnail link to this content with the given `width` and
|
||||
/// `height`. `method` can be `ThumbnailMethod.crop` or
|
||||
/// `ThumbnailMethod.scale` and defaults to `ThumbnailMethod.scale`.
|
||||
/// If `animated` (default false) is set to true, an animated thumbnail is requested
|
||||
/// as per MSC2705. Thumbnails only animate if the media repository supports that.
|
||||
Uri getThumbnail(Client matrix,
|
||||
{num? width,
|
||||
num? height,
|
||||
ThumbnailMethod? method = ThumbnailMethod.crop,
|
||||
bool? animated = false}) {
|
||||
if (!isScheme('mxc')) return this;
|
||||
final homeserver = matrix.homeserver;
|
||||
if (homeserver == null) {
|
||||
return Uri();
|
||||
}
|
||||
return Uri(
|
||||
scheme: homeserver.scheme,
|
||||
host: homeserver.host,
|
||||
path: '/_matrix/media/r0/thumbnail/$host${hasPort ? ':$port' : ''}$path',
|
||||
port: homeserver.port,
|
||||
queryParameters: {
|
||||
if (width != null) 'width': width.round().toString(),
|
||||
if (height != null) 'height': height.round().toString(),
|
||||
if (method != null) 'method': method.toString().split('.').last,
|
||||
if (animated != null) 'animated': animated.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ThumbnailMethod { crop, scale }
|
||||
Loading…
Add table
Add a link
Reference in a new issue