2019-06-09 11:57:33 +00:00
|
|
|
|
/*
|
|
|
|
|
* Copyright (c) 2019 Zender & Kurtz GbR.
|
|
|
|
|
*
|
|
|
|
|
* Authors:
|
|
|
|
|
* Christian Pauly <krille@famedly.com>
|
|
|
|
|
* Marcel Radzio <mtrnord@famedly.com>
|
|
|
|
|
*
|
|
|
|
|
* This file is part of famedlysdk.
|
|
|
|
|
*
|
|
|
|
|
* famedlysdk is free software: you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* famedlysdk 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 General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License
|
2019-06-21 07:46:53 +00:00
|
|
|
|
* along with famedlysdk. If not, see <http://www.gnu.org/licenses/>.
|
2019-06-09 11:57:33 +00:00
|
|
|
|
*/
|
|
|
|
|
|
2020-01-04 10:29:38 +00:00
|
|
|
|
import 'dart:async';
|
2020-02-15 07:48:41 +00:00
|
|
|
|
import 'dart:convert';
|
2020-01-04 10:29:38 +00:00
|
|
|
|
|
2020-02-04 13:41:13 +00:00
|
|
|
|
import 'package:famedlysdk/famedlysdk.dart';
|
Update lib/src/client.dart, lib/src/user.dart, lib/src/timeline.dart, lib/src/room.dart, lib/src/presence.dart, lib/src/event.dart, lib/src/utils/profile.dart, lib/src/utils/receipt.dart, test/client_test.dart, test/event_test.dart, test/presence_test.dart, test/room_test.dart, test/timeline_test.dart, test/user_test.dart files
2020-01-04 17:56:17 +00:00
|
|
|
|
import 'package:famedlysdk/src/client.dart';
|
|
|
|
|
import 'package:famedlysdk/src/event.dart';
|
|
|
|
|
import 'package:famedlysdk/src/room_account_data.dart';
|
|
|
|
|
import 'package:famedlysdk/src/sync/event_update.dart';
|
|
|
|
|
import 'package:famedlysdk/src/sync/room_update.dart';
|
|
|
|
|
import 'package:famedlysdk/src/utils/matrix_exception.dart';
|
|
|
|
|
import 'package:famedlysdk/src/utils/matrix_file.dart';
|
2020-02-15 07:48:41 +00:00
|
|
|
|
import 'package:famedlysdk/src/utils/session_key.dart';
|
2020-04-17 14:11:13 +00:00
|
|
|
|
import 'package:image/image.dart';
|
2020-03-16 10:38:03 +00:00
|
|
|
|
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
2019-09-09 13:22:02 +00:00
|
|
|
|
import 'package:mime_type/mime_type.dart';
|
2020-02-15 07:48:41 +00:00
|
|
|
|
import 'package:olm/olm.dart' as olm;
|
2020-05-09 14:00:46 +00:00
|
|
|
|
import 'package:html_unescape/html_unescape.dart';
|
2019-07-12 09:26:07 +00:00
|
|
|
|
|
Update lib/src/client.dart, lib/src/user.dart, lib/src/timeline.dart, lib/src/room.dart, lib/src/presence.dart, lib/src/event.dart, lib/src/utils/profile.dart, lib/src/utils/receipt.dart, test/client_test.dart, test/event_test.dart, test/presence_test.dart, test/room_test.dart, test/timeline_test.dart, test/user_test.dart files
2020-01-04 17:56:17 +00:00
|
|
|
|
import './user.dart';
|
|
|
|
|
import 'timeline.dart';
|
2020-05-06 10:13:30 +00:00
|
|
|
|
import 'utils/matrix_localizations.dart';
|
Update lib/src/client.dart, lib/src/user.dart, lib/src/timeline.dart, lib/src/room.dart, lib/src/presence.dart, lib/src/event.dart, lib/src/utils/profile.dart, lib/src/utils/receipt.dart, test/client_test.dart, test/event_test.dart, test/presence_test.dart, test/room_test.dart, test/timeline_test.dart, test/user_test.dart files
2020-01-04 17:56:17 +00:00
|
|
|
|
import 'utils/states_map.dart';
|
2020-05-09 14:00:46 +00:00
|
|
|
|
import './utils/markdown.dart';
|
2020-05-15 18:40:17 +00:00
|
|
|
|
import './database/database.dart' show DbRoom;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
|
2020-01-18 14:49:15 +00:00
|
|
|
|
enum PushRuleState { notify, mentions_only, dont_notify }
|
|
|
|
|
enum JoinRules { public, knock, invite, private }
|
|
|
|
|
enum GuestAccess { can_join, forbidden }
|
|
|
|
|
enum HistoryVisibility { invited, joined, shared, world_readable }
|
|
|
|
|
|
2019-06-09 12:33:25 +00:00
|
|
|
|
/// Represents a Matrix room.
|
2019-06-09 10:16:48 +00:00
|
|
|
|
class Room {
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// The full qualified Matrix ID for the room in the format '!localid:server.abc'.
|
|
|
|
|
final String id;
|
|
|
|
|
|
|
|
|
|
/// Membership status of the user for this room.
|
2019-07-12 09:26:07 +00:00
|
|
|
|
Membership membership;
|
2019-06-11 08:51:45 +00:00
|
|
|
|
|
|
|
|
|
/// The count of unread notifications.
|
2019-06-09 10:16:48 +00:00
|
|
|
|
int notificationCount;
|
2019-06-11 08:51:45 +00:00
|
|
|
|
|
|
|
|
|
/// The count of highlighted notifications.
|
2019-06-09 10:16:48 +00:00
|
|
|
|
int highlightCount;
|
2019-06-11 08:51:45 +00:00
|
|
|
|
|
2019-09-03 14:34:38 +00:00
|
|
|
|
/// A token that can be supplied to the from parameter of the rooms/{roomId}/messages endpoint.
|
2019-06-11 08:51:45 +00:00
|
|
|
|
String prev_batch;
|
|
|
|
|
|
2019-09-03 14:34:38 +00:00
|
|
|
|
/// The users which can be used to generate a room name if the room does not have one.
|
|
|
|
|
/// Required if the room's m.room.name or m.room.canonical_alias state events are unset or empty.
|
2019-08-08 12:31:47 +00:00
|
|
|
|
List<String> mHeroes = [];
|
2019-09-03 14:34:38 +00:00
|
|
|
|
|
|
|
|
|
/// The number of users with membership of join, including the client's own user ID.
|
2019-08-07 08:17:03 +00:00
|
|
|
|
int mJoinedMemberCount;
|
2019-09-03 14:34:38 +00:00
|
|
|
|
|
|
|
|
|
/// The number of users with membership of invite.
|
2019-08-07 08:17:03 +00:00
|
|
|
|
int mInvitedMemberCount;
|
|
|
|
|
|
2019-11-20 13:02:23 +00:00
|
|
|
|
StatesMap states = StatesMap();
|
2019-06-11 08:51:45 +00:00
|
|
|
|
|
2019-10-20 09:44:14 +00:00
|
|
|
|
/// Key-Value store for ephemerals.
|
|
|
|
|
Map<String, RoomAccountData> ephemerals = {};
|
|
|
|
|
|
2019-09-03 14:34:38 +00:00
|
|
|
|
/// Key-Value store for private account data only visible for this user.
|
2019-08-07 10:27:02 +00:00
|
|
|
|
Map<String, RoomAccountData> roomAccountData = {};
|
2019-08-07 08:32:18 +00:00
|
|
|
|
|
2020-02-15 07:48:41 +00:00
|
|
|
|
olm.OutboundGroupSession get outboundGroupSession => _outboundGroupSession;
|
|
|
|
|
olm.OutboundGroupSession _outboundGroupSession;
|
|
|
|
|
|
2020-02-27 08:41:49 +00:00
|
|
|
|
List<String> _outboundGroupSessionDevices;
|
|
|
|
|
|
2020-05-15 18:40:17 +00:00
|
|
|
|
double _newestSortOrder;
|
|
|
|
|
double _oldestSortOrder;
|
|
|
|
|
|
|
|
|
|
double get newSortOrder {
|
|
|
|
|
_newestSortOrder++;
|
|
|
|
|
return _newestSortOrder;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double get oldSortOrder {
|
|
|
|
|
_oldestSortOrder--;
|
|
|
|
|
return _oldestSortOrder;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void resetSortOrder() {
|
|
|
|
|
_oldestSortOrder = _newestSortOrder = 0.0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> updateSortOrder() async {
|
|
|
|
|
await client.database?.updateRoomSortOrder(_oldestSortOrder, _newestSortOrder, client.id, id);
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-15 07:48:41 +00:00
|
|
|
|
/// Clears the existing outboundGroupSession, tries to create a new one and
|
|
|
|
|
/// stores it as an ingoingGroupSession in the [sessionKeys]. Then sends the
|
|
|
|
|
/// new session encrypted with olm to all non-blocked devices using
|
|
|
|
|
/// to-device-messaging.
|
|
|
|
|
Future<void> createOutboundGroupSession() async {
|
2020-02-27 08:41:49 +00:00
|
|
|
|
await clearOutboundGroupSession(wipe: true);
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var deviceKeys = await getUserDeviceKeys();
|
2020-03-13 10:09:49 +00:00
|
|
|
|
olm.OutboundGroupSession outboundGroupSession;
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var outboundGroupSessionDevices = <String>[];
|
|
|
|
|
for (var keys in deviceKeys) {
|
2020-03-13 10:09:49 +00:00
|
|
|
|
if (!keys.blocked) outboundGroupSessionDevices.add(keys.deviceId);
|
2020-02-27 08:41:49 +00:00
|
|
|
|
}
|
2020-03-13 10:09:49 +00:00
|
|
|
|
outboundGroupSessionDevices.sort();
|
2020-02-15 07:48:41 +00:00
|
|
|
|
try {
|
2020-03-13 10:09:49 +00:00
|
|
|
|
outboundGroupSession = olm.OutboundGroupSession();
|
|
|
|
|
outboundGroupSession.create();
|
2020-02-15 07:48:41 +00:00
|
|
|
|
} catch (e) {
|
2020-03-13 10:09:49 +00:00
|
|
|
|
outboundGroupSession = null;
|
2020-03-30 09:08:38 +00:00
|
|
|
|
print('[LibOlm] Unable to create new outboundGroupSession: ' +
|
2020-02-15 07:48:41 +00:00
|
|
|
|
e.toString());
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-13 10:09:49 +00:00
|
|
|
|
if (outboundGroupSession == null) return;
|
2020-02-15 07:48:41 +00:00
|
|
|
|
// Add as an inboundSession to the [sessionKeys].
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var rawSession = <String, dynamic>{
|
|
|
|
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
|
|
|
|
'room_id': id,
|
|
|
|
|
'session_id': outboundGroupSession.session_id(),
|
|
|
|
|
'session_key': outboundGroupSession.session_key(),
|
2020-02-15 07:48:41 +00:00
|
|
|
|
};
|
2020-05-15 18:40:17 +00:00
|
|
|
|
setInboundGroupSession(rawSession['session_id'], rawSession);
|
2020-02-15 07:48:41 +00:00
|
|
|
|
try {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
await client.sendToDevice(deviceKeys, 'm.room_key', rawSession);
|
2020-03-13 10:09:49 +00:00
|
|
|
|
_outboundGroupSession = outboundGroupSession;
|
|
|
|
|
_outboundGroupSessionDevices = outboundGroupSessionDevices;
|
|
|
|
|
await _storeOutboundGroupSession();
|
2020-02-15 07:48:41 +00:00
|
|
|
|
} catch (e) {
|
|
|
|
|
print(
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'[LibOlm] Unable to send the session key to the participating devices: ' +
|
2020-02-15 07:48:41 +00:00
|
|
|
|
e.toString());
|
2020-03-30 09:08:38 +00:00
|
|
|
|
await clearOutboundGroupSession();
|
2020-02-15 07:48:41 +00:00
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-18 07:42:52 +00:00
|
|
|
|
Future<void> _storeOutboundGroupSession() async {
|
2020-02-18 09:23:55 +00:00
|
|
|
|
if (_outboundGroupSession == null) return;
|
2020-05-15 18:40:17 +00:00
|
|
|
|
await client.database?.storeOutboundGroupSession(
|
|
|
|
|
client.id, id, _outboundGroupSession.pickle(client.userID),
|
|
|
|
|
json.encode(_outboundGroupSessionDevices),
|
|
|
|
|
);
|
2020-02-18 07:42:52 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-27 08:41:49 +00:00
|
|
|
|
/// Clears the existing outboundGroupSession but first checks if the participating
|
|
|
|
|
/// devices have been changed. Returns false if the session has not been cleared because
|
|
|
|
|
/// it wasn't necessary.
|
|
|
|
|
Future<bool> clearOutboundGroupSession({bool wipe = false}) async {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (!wipe && _outboundGroupSessionDevices != null) {
|
|
|
|
|
var deviceKeys = await getUserDeviceKeys();
|
|
|
|
|
var outboundGroupSessionDevices = <String>[];
|
|
|
|
|
for (var keys in deviceKeys) {
|
2020-02-27 08:52:45 +00:00
|
|
|
|
if (!keys.blocked) outboundGroupSessionDevices.add(keys.deviceId);
|
2020-02-27 08:41:49 +00:00
|
|
|
|
}
|
|
|
|
|
outboundGroupSessionDevices.sort();
|
|
|
|
|
if (outboundGroupSessionDevices.toString() ==
|
2020-03-30 09:08:38 +00:00
|
|
|
|
_outboundGroupSessionDevices.toString()) {
|
2020-02-27 08:41:49 +00:00
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-15 18:40:17 +00:00
|
|
|
|
if (!wipe && _outboundGroupSessionDevices == null && _outboundGroupSession == null) {
|
|
|
|
|
return true; // let's just short-circuit out of here, no need to do DB stuff
|
|
|
|
|
}
|
|
|
|
|
_outboundGroupSessionDevices = null;
|
|
|
|
|
await client.database?.removeOutboundGroupSession(client.id, id);
|
2020-03-30 09:08:38 +00:00
|
|
|
|
_outboundGroupSession?.free();
|
|
|
|
|
_outboundGroupSession = null;
|
2020-02-27 08:41:49 +00:00
|
|
|
|
return true;
|
2020-02-15 07:48:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Key-Value store of session ids to the session keys. Only m.megolm.v1.aes-sha2
|
|
|
|
|
/// session keys are supported. They are stored as a Map with the following keys:
|
|
|
|
|
/// {
|
|
|
|
|
/// "algorithm": "m.megolm.v1.aes-sha2",
|
|
|
|
|
/// "room_id": "!Cuyf34gef24t:localhost",
|
|
|
|
|
/// "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ",
|
|
|
|
|
/// "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..."
|
|
|
|
|
/// }
|
2020-05-15 18:40:17 +00:00
|
|
|
|
Map<String, SessionKey> get inboundGroupSessions => _inboundGroupSessions;
|
2020-05-17 07:54:34 +00:00
|
|
|
|
final _inboundGroupSessions = <String, SessionKey>{};
|
2020-02-15 07:48:41 +00:00
|
|
|
|
|
|
|
|
|
/// Add a new session key to the [sessionKeys].
|
2020-05-15 18:40:17 +00:00
|
|
|
|
void setInboundGroupSession(String sessionId, Map<String, dynamic> content,
|
2020-02-21 15:05:19 +00:00
|
|
|
|
{bool forwarded = false}) {
|
2020-05-15 18:40:17 +00:00
|
|
|
|
if (inboundGroupSessions.containsKey(sessionId)) return;
|
2020-02-15 07:48:41 +00:00
|
|
|
|
olm.InboundGroupSession inboundGroupSession;
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (content['algorithm'] == 'm.megolm.v1.aes-sha2') {
|
2020-02-15 07:48:41 +00:00
|
|
|
|
try {
|
|
|
|
|
inboundGroupSession = olm.InboundGroupSession();
|
2020-02-21 15:05:19 +00:00
|
|
|
|
if (forwarded) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
inboundGroupSession.import_session(content['session_key']);
|
2020-02-21 15:05:19 +00:00
|
|
|
|
} else {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
inboundGroupSession.create(content['session_key']);
|
2020-02-21 15:05:19 +00:00
|
|
|
|
}
|
2020-02-15 07:48:41 +00:00
|
|
|
|
} catch (e) {
|
|
|
|
|
inboundGroupSession = null;
|
2020-03-30 09:08:38 +00:00
|
|
|
|
print('[LibOlm] Could not create new InboundGroupSession: ' +
|
2020-02-15 07:48:41 +00:00
|
|
|
|
e.toString());
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-15 18:40:17 +00:00
|
|
|
|
_inboundGroupSessions[sessionId] = SessionKey(
|
2020-02-15 07:48:41 +00:00
|
|
|
|
content: content,
|
|
|
|
|
inboundGroupSession: inboundGroupSession,
|
|
|
|
|
indexes: {},
|
|
|
|
|
key: client.userID,
|
|
|
|
|
);
|
2020-05-17 07:54:34 +00:00
|
|
|
|
client.database?.storeInboundGroupSession(client.id, id, sessionId,
|
|
|
|
|
inboundGroupSession.pickle(client.userID), json.encode(content),
|
|
|
|
|
json.encode({}),
|
|
|
|
|
);
|
2020-03-13 12:18:24 +00:00
|
|
|
|
_tryAgainDecryptLastMessage();
|
|
|
|
|
onSessionKeyReceived.add(sessionId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _tryAgainDecryptLastMessage() {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (getState('m.room.encrypted') != null) {
|
|
|
|
|
final decrypted = getState('m.room.encrypted').decrypted;
|
2020-02-24 08:10:35 +00:00
|
|
|
|
if (decrypted.type != EventTypes.Encrypted) {
|
|
|
|
|
setState(decrypted);
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-02-15 07:48:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-02 14:09:49 +00:00
|
|
|
|
/// Returns the [Event] for the given [typeKey] and optional [stateKey].
|
2019-11-20 13:42:08 +00:00
|
|
|
|
/// If no [stateKey] is provided, it defaults to an empty string.
|
2020-03-30 09:08:38 +00:00
|
|
|
|
Event getState(String typeKey, [String stateKey = '']) =>
|
2019-11-20 13:42:08 +00:00
|
|
|
|
states.states[typeKey] != null ? states.states[typeKey][stateKey] : null;
|
|
|
|
|
|
2019-11-21 14:10:24 +00:00
|
|
|
|
/// Adds the [state] to this room and overwrites a state with the same
|
|
|
|
|
/// typeKey/stateKey key pair if there is one.
|
2020-01-02 14:09:49 +00:00
|
|
|
|
void setState(Event state) {
|
2020-02-21 08:44:05 +00:00
|
|
|
|
// Decrypt if necessary
|
|
|
|
|
if (state.type == EventTypes.Encrypted) {
|
|
|
|
|
try {
|
|
|
|
|
state = decryptGroupMessage(state);
|
|
|
|
|
} catch (e) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
print('[LibOlm] Could not decrypt room state: ' + e.toString());
|
2020-02-21 08:44:05 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-02-15 07:48:41 +00:00
|
|
|
|
// Check if this is a member change and we need to clear the outboundGroupSession.
|
|
|
|
|
if (encrypted &&
|
|
|
|
|
outboundGroupSession != null &&
|
|
|
|
|
state.type == EventTypes.RoomMember) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var newUser = state.asUser;
|
|
|
|
|
var oldUser = getState('m.room.member', newUser.id)?.asUser;
|
2020-02-15 07:48:41 +00:00
|
|
|
|
if (oldUser == null || oldUser.membership != newUser.membership) {
|
|
|
|
|
clearOutboundGroupSession();
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-02-24 08:10:35 +00:00
|
|
|
|
if ((getState(state.typeKey)?.time?.millisecondsSinceEpoch ?? 0) >
|
2020-02-24 09:34:28 +00:00
|
|
|
|
(state.time?.millisecondsSinceEpoch ?? 1)) {
|
2020-02-24 08:10:35 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
2020-01-02 14:33:26 +00:00
|
|
|
|
if (!states.states.containsKey(state.typeKey)) {
|
2019-11-21 14:10:24 +00:00
|
|
|
|
states.states[state.typeKey] = {};
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
2020-03-30 09:08:38 +00:00
|
|
|
|
states.states[state.typeKey][state.stateKey ?? ''] = state;
|
2019-11-20 13:42:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// ID of the fully read marker event.
|
2020-03-30 09:08:38 +00:00
|
|
|
|
String get fullyRead => roomAccountData['m.fully_read'] != null
|
|
|
|
|
? roomAccountData['m.fully_read'].content['event_id']
|
|
|
|
|
: '';
|
2019-06-11 08:51:45 +00:00
|
|
|
|
|
2020-01-04 10:29:38 +00:00
|
|
|
|
/// If something changes, this callback will be triggered. Will return the
|
|
|
|
|
/// room id.
|
|
|
|
|
final StreamController<String> onUpdate = StreamController.broadcast();
|
2019-09-03 11:24:44 +00:00
|
|
|
|
|
2020-02-21 15:05:19 +00:00
|
|
|
|
/// If there is a new session key received, this will be triggered with
|
|
|
|
|
/// the session ID.
|
|
|
|
|
final StreamController<String> onSessionKeyReceived =
|
|
|
|
|
StreamController.broadcast();
|
|
|
|
|
|
2019-08-07 08:17:03 +00:00
|
|
|
|
/// The name of the room if set by a participant.
|
2020-03-30 09:08:38 +00:00
|
|
|
|
String get name => states['m.room.name'] != null
|
|
|
|
|
? states['m.room.name'].content['name']
|
|
|
|
|
: '';
|
2019-08-07 08:17:03 +00:00
|
|
|
|
|
2020-05-06 10:13:30 +00:00
|
|
|
|
/// Returns a localized displayname for this server. If the room is a groupchat
|
|
|
|
|
/// without a name, then it will return the localized version of 'Group with Alice' instead
|
|
|
|
|
/// of just 'Alice' to make it different to a direct chat.
|
|
|
|
|
/// Empty chats will become the localized version of 'Empty Chat'.
|
|
|
|
|
/// This method requires a localization class which implements [MatrixLocalizations]
|
|
|
|
|
String getLocalizedDisplayname(MatrixLocalizations i18n) {
|
|
|
|
|
if ((name?.isEmpty ?? true) &&
|
|
|
|
|
(canonicalAlias?.isEmpty ?? true) &&
|
|
|
|
|
!isDirectChat &&
|
|
|
|
|
(mHeroes != null && mHeroes.isNotEmpty)) {
|
|
|
|
|
return i18n.groupWith(displayname);
|
|
|
|
|
}
|
2020-05-14 07:21:52 +00:00
|
|
|
|
if (displayname?.isNotEmpty ?? false) {
|
|
|
|
|
return displayname;
|
2020-05-06 10:13:30 +00:00
|
|
|
|
}
|
2020-05-14 07:21:52 +00:00
|
|
|
|
return i18n.emptyChat;
|
2020-05-06 10:13:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-07 08:17:03 +00:00
|
|
|
|
/// The topic of the room if set by a participant.
|
2020-03-30 09:08:38 +00:00
|
|
|
|
String get topic => states['m.room.topic'] != null
|
|
|
|
|
? states['m.room.topic'].content['topic']
|
|
|
|
|
: '';
|
2019-08-07 08:17:03 +00:00
|
|
|
|
|
|
|
|
|
/// The avatar of the room if set by a participant.
|
2020-04-24 07:24:06 +00:00
|
|
|
|
Uri get avatar {
|
|
|
|
|
if (states['m.room.avatar'] != null &&
|
|
|
|
|
states['m.room.avatar'].content['url'] != null) {
|
|
|
|
|
return Uri.parse(states['m.room.avatar'].content['url']);
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
|
|
|
|
if (mHeroes != null && mHeroes.length == 1 && states[mHeroes[0]] != null) {
|
2019-08-08 09:41:42 +00:00
|
|
|
|
return states[mHeroes[0]].asUser.avatarUrl;
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
2019-11-26 12:46:46 +00:00
|
|
|
|
if (membership == Membership.invite &&
|
2020-03-30 09:08:38 +00:00
|
|
|
|
getState('m.room.member', client.userID) != null) {
|
|
|
|
|
return getState('m.room.member', client.userID).sender.avatarUrl;
|
2019-09-30 08:19:28 +00:00
|
|
|
|
}
|
2020-04-24 07:24:06 +00:00
|
|
|
|
return null;
|
2019-08-07 08:17:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// The address in the format: #roomname:homeserver.org.
|
2020-03-30 09:08:38 +00:00
|
|
|
|
String get canonicalAlias => states['m.room.canonical_alias'] != null
|
|
|
|
|
? states['m.room.canonical_alias'].content['alias']
|
|
|
|
|
: '';
|
2019-06-11 08:51:45 +00:00
|
|
|
|
|
2019-08-08 08:31:39 +00:00
|
|
|
|
/// If this room is a direct chat, this is the matrix ID of the user.
|
|
|
|
|
/// Returns null otherwise.
|
|
|
|
|
String get directChatMatrixID {
|
2020-01-02 14:33:26 +00:00
|
|
|
|
String returnUserId;
|
2019-08-08 08:31:39 +00:00
|
|
|
|
if (client.directChats is Map<String, dynamic>) {
|
|
|
|
|
client.directChats.forEach((String userId, dynamic roomIds) {
|
|
|
|
|
if (roomIds is List<dynamic>) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
for (var i = 0; i < roomIds.length; i++) {
|
|
|
|
|
if (roomIds[i] == id) {
|
2019-08-08 08:31:39 +00:00
|
|
|
|
returnUserId = userId;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
2019-08-08 08:31:39 +00:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return returnUserId;
|
|
|
|
|
}
|
2019-06-11 08:51:45 +00:00
|
|
|
|
|
2019-08-29 08:49:07 +00:00
|
|
|
|
/// Wheither this is a direct chat or not
|
|
|
|
|
bool get isDirectChat => directChatMatrixID != null;
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// Must be one of [all, mention]
|
|
|
|
|
String notificationSettings;
|
|
|
|
|
|
2019-08-29 07:50:04 +00:00
|
|
|
|
Event get lastEvent {
|
2020-05-18 10:56:24 +00:00
|
|
|
|
var lastSortOrder = -1e32; // this bound to be small enough
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var lastEvent = getState('m.room.message');
|
2020-01-02 14:33:26 +00:00
|
|
|
|
if (lastEvent == null) {
|
2019-11-20 13:02:23 +00:00
|
|
|
|
states.forEach((final String key, final entry) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (!entry.containsKey('')) return;
|
|
|
|
|
final Event state = entry[''];
|
2020-05-18 10:56:24 +00:00
|
|
|
|
if (state.sortOrder != null &&
|
|
|
|
|
state.sortOrder > lastSortOrder) {
|
|
|
|
|
lastSortOrder = state.sortOrder;
|
2020-01-14 11:27:26 +00:00
|
|
|
|
lastEvent = state;
|
2019-11-13 14:08:27 +00:00
|
|
|
|
}
|
2019-11-20 13:02:23 +00:00
|
|
|
|
});
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
2019-08-29 07:50:04 +00:00
|
|
|
|
return lastEvent;
|
|
|
|
|
}
|
2019-06-11 08:51:45 +00:00
|
|
|
|
|
2019-10-20 09:44:14 +00:00
|
|
|
|
/// Returns a list of all current typing users.
|
|
|
|
|
List<User> get typingUsers {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (!ephemerals.containsKey('m.typing')) return [];
|
|
|
|
|
List<dynamic> typingMxid = ephemerals['m.typing'].content['user_ids'];
|
|
|
|
|
var typingUsers = <User>[];
|
|
|
|
|
for (var i = 0; i < typingMxid.length; i++) {
|
2019-11-15 11:08:43 +00:00
|
|
|
|
typingUsers.add(getUserByMXIDSync(typingMxid[i]));
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
2019-10-20 09:44:14 +00:00
|
|
|
|
return typingUsers;
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// Your current client instance.
|
|
|
|
|
final Client client;
|
|
|
|
|
|
2019-06-09 10:16:48 +00:00
|
|
|
|
Room({
|
2019-06-11 08:51:45 +00:00
|
|
|
|
this.id,
|
2019-08-08 11:00:56 +00:00
|
|
|
|
this.membership = Membership.join,
|
|
|
|
|
this.notificationCount = 0,
|
|
|
|
|
this.highlightCount = 0,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
this.prev_batch = '',
|
2019-06-11 08:51:45 +00:00
|
|
|
|
this.client,
|
2019-08-07 08:17:03 +00:00
|
|
|
|
this.notificationSettings,
|
2019-08-08 11:00:56 +00:00
|
|
|
|
this.mHeroes = const [],
|
|
|
|
|
this.mInvitedMemberCount = 0,
|
|
|
|
|
this.mJoinedMemberCount = 0,
|
|
|
|
|
this.roomAccountData = const {},
|
2020-05-15 18:40:17 +00:00
|
|
|
|
double newestSortOrder = 0.0,
|
|
|
|
|
double oldestSortOrder = 0.0,
|
|
|
|
|
}) : _newestSortOrder = newestSortOrder, _oldestSortOrder = oldestSortOrder;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
|
2019-09-26 09:30:07 +00:00
|
|
|
|
/// The default count of how much events should be requested when requesting the
|
|
|
|
|
/// history of this room.
|
|
|
|
|
static const int DefaultHistoryCount = 100;
|
|
|
|
|
|
2019-08-06 09:47:09 +00:00
|
|
|
|
/// Calculates the displayname. First checks if there is a name, then checks for a canonical alias and
|
|
|
|
|
/// then generates a name from the heroes.
|
|
|
|
|
String get displayname {
|
2019-11-26 12:46:46 +00:00
|
|
|
|
if (name != null && name.isNotEmpty) return name;
|
2019-08-06 09:47:09 +00:00
|
|
|
|
if (canonicalAlias != null &&
|
2020-01-02 14:33:26 +00:00
|
|
|
|
canonicalAlias.isNotEmpty &&
|
|
|
|
|
canonicalAlias.length > 3) {
|
2020-02-10 11:33:18 +00:00
|
|
|
|
return canonicalAlias.localpart;
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var heroes = <String>[];
|
2019-11-29 16:19:32 +00:00
|
|
|
|
if (mHeroes != null &&
|
2020-01-02 14:33:26 +00:00
|
|
|
|
mHeroes.isNotEmpty &&
|
2019-11-29 16:19:32 +00:00
|
|
|
|
mHeroes.any((h) => h.isNotEmpty)) {
|
|
|
|
|
heroes = mHeroes;
|
|
|
|
|
} else {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (states['m.room.member'] is Map<String, dynamic>) {
|
|
|
|
|
for (var entry in states['m.room.member'].entries) {
|
2020-01-02 14:09:49 +00:00
|
|
|
|
Event state = entry.value;
|
2019-11-29 16:19:32 +00:00
|
|
|
|
if (state.type == EventTypes.RoomMember &&
|
|
|
|
|
state.stateKey != client?.userID) heroes.add(state.stateKey);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-01-02 14:33:26 +00:00
|
|
|
|
if (heroes.isNotEmpty) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var displayname = '';
|
|
|
|
|
for (var i = 0; i < heroes.length; i++) {
|
2019-11-29 16:19:32 +00:00
|
|
|
|
if (heroes[i].isEmpty) continue;
|
2020-03-30 09:08:38 +00:00
|
|
|
|
displayname += getUserByMXIDSync(heroes[i]).calcDisplayname() + ', ';
|
2019-11-26 12:46:46 +00:00
|
|
|
|
}
|
2019-08-06 09:47:09 +00:00
|
|
|
|
return displayname.substring(0, displayname.length - 2);
|
|
|
|
|
}
|
2019-11-26 12:46:46 +00:00
|
|
|
|
if (membership == Membership.invite &&
|
2020-03-30 09:08:38 +00:00
|
|
|
|
getState('m.room.member', client.userID) != null) {
|
|
|
|
|
return getState('m.room.member', client.userID).sender.calcDisplayname();
|
2019-11-26 12:46:46 +00:00
|
|
|
|
}
|
2020-03-30 09:08:38 +00:00
|
|
|
|
return 'Empty chat';
|
2019-08-06 09:47:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// The last message sent to this room.
|
|
|
|
|
String get lastMessage {
|
2020-01-02 14:33:26 +00:00
|
|
|
|
if (lastEvent != null) {
|
2020-01-14 11:27:26 +00:00
|
|
|
|
return lastEvent.body;
|
2020-01-02 14:33:26 +00:00
|
|
|
|
} else {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
return '';
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
2019-06-11 08:51:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// When the last message received.
|
2020-01-02 14:09:49 +00:00
|
|
|
|
DateTime get timeCreated {
|
2020-01-02 14:33:26 +00:00
|
|
|
|
if (lastEvent != null) {
|
2019-06-21 10:18:54 +00:00
|
|
|
|
return lastEvent.time;
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
|
|
|
|
return DateTime.now();
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-12-29 10:28:33 +00:00
|
|
|
|
/// Call the Matrix API to change the name of this room. Returns the event ID of the
|
|
|
|
|
/// new m.room.name event.
|
|
|
|
|
Future<String> setName(String newName) async {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
final resp = await client.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/${id}/state/m.room.name',
|
|
|
|
|
data: {'name': newName});
|
|
|
|
|
return resp['event_id'];
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// Call the Matrix API to change the topic of this room.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Future<String> setDescription(String newName) async {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
final resp = await client.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/${id}/state/m.room.topic',
|
|
|
|
|
data: {'topic': newName});
|
|
|
|
|
return resp['event_id'];
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-15 19:05:28 +00:00
|
|
|
|
/// return all current emote packs for this room
|
|
|
|
|
Map<String, Map<String, String>> get emotePacks {
|
|
|
|
|
final packs = <String, Map<String, String>>{};
|
|
|
|
|
final addEmotePack = (String packName, Map<String, dynamic> content) {
|
|
|
|
|
packs[packName] = <String, String>{};
|
|
|
|
|
content.forEach((key, value) {
|
|
|
|
|
if (key is String && value is String && value.startsWith('mxc://')) {
|
|
|
|
|
packs[packName][key] = value;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
final roomEmotes = getState('im.ponies.room_emotes');
|
|
|
|
|
final userEmotes = client.accountData['im.ponies.user_emotes'];
|
|
|
|
|
if (roomEmotes != null && roomEmotes.content['short'] is Map) {
|
|
|
|
|
addEmotePack('room', roomEmotes.content['short']);
|
|
|
|
|
}
|
|
|
|
|
if (userEmotes != null && userEmotes.content['short'] is Map) {
|
|
|
|
|
addEmotePack('user', userEmotes.content['short']);
|
|
|
|
|
}
|
|
|
|
|
return packs;
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-17 07:55:25 +00:00
|
|
|
|
/// Sends a normal text message to this room. Returns the event ID generated
|
|
|
|
|
/// by the server for this message.
|
2020-05-15 19:05:28 +00:00
|
|
|
|
Future<String> sendTextEvent(String message, {String txid, Event inReplyTo, bool parseMarkdown = true, Map<String, Map<String, String>> emotePacks}) {
|
2020-05-09 14:00:46 +00:00
|
|
|
|
final event = <String, dynamic>{
|
|
|
|
|
'msgtype': 'm.text',
|
|
|
|
|
'body': message,
|
|
|
|
|
};
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (message.startsWith('/me ')) {
|
2020-05-15 16:44:59 +00:00
|
|
|
|
event['msgtype'] = 'm.emote';
|
2020-05-09 14:00:46 +00:00
|
|
|
|
event['body'] = message.substring(4);
|
|
|
|
|
}
|
|
|
|
|
if (parseMarkdown) {
|
2020-05-15 19:05:28 +00:00
|
|
|
|
final html = markdown(event['body'], emotePacks ?? this.emotePacks);
|
2020-05-09 14:00:46 +00:00
|
|
|
|
// if the decoded html is the same as the body, there is no need in sending a formatted message
|
|
|
|
|
if (HtmlUnescape().convert(html) != event['body']) {
|
|
|
|
|
event['format'] = 'org.matrix.custom.html';
|
|
|
|
|
event['formatted_body'] = html;
|
|
|
|
|
}
|
2020-03-16 10:38:03 +00:00
|
|
|
|
}
|
2020-05-09 14:00:46 +00:00
|
|
|
|
return sendEvent(event, txid: txid, inReplyTo: inReplyTo);
|
2020-03-16 10:38:03 +00:00
|
|
|
|
}
|
2019-09-09 13:22:02 +00:00
|
|
|
|
|
2019-12-18 11:46:25 +00:00
|
|
|
|
/// Sends a [file] to this room after uploading it. The [msgType] is optional
|
2020-03-17 07:55:25 +00:00
|
|
|
|
/// and will be detected by the mimetype of the file. Returns the mxc uri of
|
2020-03-30 09:59:24 +00:00
|
|
|
|
/// the uploaded file. If [waitUntilSent] is true, the future will wait until
|
|
|
|
|
/// the message event has received the server. Otherwise the future will only
|
|
|
|
|
/// wait until the file has been uploaded.
|
|
|
|
|
Future<String> sendFileEvent(
|
|
|
|
|
MatrixFile file, {
|
|
|
|
|
String msgType,
|
|
|
|
|
String txid,
|
|
|
|
|
Event inReplyTo,
|
|
|
|
|
Map<String, dynamic> info,
|
|
|
|
|
bool waitUntilSent = false,
|
2020-05-04 14:03:07 +00:00
|
|
|
|
MatrixFile thumbnail,
|
2020-03-30 09:59:24 +00:00
|
|
|
|
}) async {
|
2020-04-17 14:11:13 +00:00
|
|
|
|
Image fileImage;
|
|
|
|
|
Image thumbnailImage;
|
|
|
|
|
EncryptedFile encryptedThumbnail;
|
|
|
|
|
String thumbnailUploadResp;
|
2019-09-09 13:22:02 +00:00
|
|
|
|
|
2020-04-17 14:11:13 +00:00
|
|
|
|
var fileName = file.path.split('/').last;
|
2020-03-30 09:59:24 +00:00
|
|
|
|
final mimeType = mime(file.path) ?? '';
|
|
|
|
|
if (msgType == null) {
|
|
|
|
|
final metaType = (mimeType).split('/')[0];
|
|
|
|
|
switch (metaType) {
|
|
|
|
|
case 'image':
|
|
|
|
|
case 'audio':
|
|
|
|
|
case 'video':
|
|
|
|
|
msgType = 'm.$metaType';
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
msgType = 'm.file';
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-17 14:11:13 +00:00
|
|
|
|
if (msgType == 'm.image') {
|
|
|
|
|
fileImage = decodeImage(file.bytes.toList());
|
2020-05-04 14:03:07 +00:00
|
|
|
|
if (thumbnail != null) {
|
|
|
|
|
thumbnailImage = decodeImage(thumbnail.bytes.toList());
|
|
|
|
|
}
|
2020-04-17 14:11:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final sendEncrypted = encrypted && client.fileEncryptionEnabled;
|
|
|
|
|
EncryptedFile encryptedFile;
|
|
|
|
|
if (sendEncrypted) {
|
|
|
|
|
encryptedFile = await file.encrypt();
|
|
|
|
|
if (thumbnail != null) {
|
|
|
|
|
encryptedThumbnail = await thumbnail.encrypt();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
final uploadResp = await client.upload(
|
|
|
|
|
file,
|
|
|
|
|
contentType: sendEncrypted ? 'application/octet-stream' : null,
|
|
|
|
|
);
|
|
|
|
|
if (thumbnail != null) {
|
|
|
|
|
thumbnailUploadResp = await client.upload(
|
|
|
|
|
thumbnail,
|
|
|
|
|
contentType: sendEncrypted ? 'application/octet-stream' : null,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-09 13:22:02 +00:00
|
|
|
|
// Send event
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var content = <String, dynamic>{
|
|
|
|
|
'msgtype': msgType,
|
|
|
|
|
'body': fileName,
|
|
|
|
|
'filename': fileName,
|
|
|
|
|
if (!sendEncrypted) 'url': uploadResp,
|
2020-03-16 10:38:03 +00:00
|
|
|
|
if (sendEncrypted)
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'file': {
|
|
|
|
|
'url': uploadResp,
|
2020-03-30 09:59:24 +00:00
|
|
|
|
'mimetype': mimeType,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'v': 'v2',
|
|
|
|
|
'key': {
|
|
|
|
|
'alg': 'A256CTR',
|
|
|
|
|
'ext': true,
|
|
|
|
|
'k': encryptedFile.k,
|
|
|
|
|
'key_ops': ['encrypt', 'decrypt'],
|
|
|
|
|
'kty': 'oct'
|
2020-03-16 10:38:03 +00:00
|
|
|
|
},
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'iv': encryptedFile.iv,
|
|
|
|
|
'hashes': {'sha256': encryptedFile.sha256}
|
2020-03-16 10:38:03 +00:00
|
|
|
|
},
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'info': info ??
|
|
|
|
|
{
|
2020-03-30 09:59:24 +00:00
|
|
|
|
'mimetype': mimeType,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'size': file.size,
|
2020-04-17 14:11:13 +00:00
|
|
|
|
if (fileImage != null) 'h': fileImage.height,
|
|
|
|
|
if (fileImage != null) 'w': fileImage.width,
|
|
|
|
|
if (thumbnailUploadResp != null && !sendEncrypted)
|
|
|
|
|
'thumbnail_url': thumbnailUploadResp,
|
|
|
|
|
if (thumbnailUploadResp != null && sendEncrypted)
|
|
|
|
|
'thumbnail_file': {
|
|
|
|
|
'url': thumbnailUploadResp,
|
|
|
|
|
'mimetype': mimeType,
|
|
|
|
|
'v': 'v2',
|
|
|
|
|
'key': {
|
|
|
|
|
'alg': 'A256CTR',
|
|
|
|
|
'ext': true,
|
|
|
|
|
'k': encryptedThumbnail.k,
|
|
|
|
|
'key_ops': ['encrypt', 'decrypt'],
|
|
|
|
|
'kty': 'oct'
|
|
|
|
|
},
|
|
|
|
|
'iv': encryptedThumbnail.iv,
|
|
|
|
|
'hashes': {'sha256': encryptedThumbnail.sha256}
|
|
|
|
|
},
|
|
|
|
|
if (thumbnailImage != null)
|
|
|
|
|
'thumbnail_info': {
|
|
|
|
|
'h': thumbnailImage.height,
|
|
|
|
|
'mimetype': mimeType,
|
2020-04-17 14:51:01 +00:00
|
|
|
|
'size': thumbnail.size,
|
2020-04-17 14:11:13 +00:00
|
|
|
|
'w': thumbnailImage.width,
|
|
|
|
|
}
|
2020-03-30 09:08:38 +00:00
|
|
|
|
}
|
2019-12-18 11:46:25 +00:00
|
|
|
|
};
|
2020-03-30 09:59:24 +00:00
|
|
|
|
final sendResponse = sendEvent(
|
|
|
|
|
content,
|
|
|
|
|
txid: txid,
|
|
|
|
|
inReplyTo: inReplyTo,
|
|
|
|
|
);
|
|
|
|
|
if (waitUntilSent) {
|
|
|
|
|
await sendResponse;
|
|
|
|
|
}
|
2020-03-17 07:55:25 +00:00
|
|
|
|
return uploadResp;
|
2019-12-18 11:46:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-17 07:55:25 +00:00
|
|
|
|
/// Sends an audio file to this room and returns the mxc uri.
|
2019-12-18 11:46:25 +00:00
|
|
|
|
Future<String> sendAudioEvent(MatrixFile file,
|
2020-03-16 10:38:03 +00:00
|
|
|
|
{String txid, Event inReplyTo}) async {
|
|
|
|
|
return await sendFileEvent(file,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
msgType: 'm.audio', txid: txid, inReplyTo: inReplyTo);
|
2019-09-09 13:22:02 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-17 07:55:25 +00:00
|
|
|
|
/// Sends an image to this room and returns the mxc uri.
|
2019-10-18 11:05:07 +00:00
|
|
|
|
Future<String> sendImageEvent(MatrixFile file,
|
2020-02-11 11:06:54 +00:00
|
|
|
|
{String txid, int width, int height, Event inReplyTo}) async {
|
2020-03-16 10:38:03 +00:00
|
|
|
|
return await sendFileEvent(file,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
msgType: 'm.image',
|
2020-03-16 10:38:03 +00:00
|
|
|
|
txid: txid,
|
|
|
|
|
inReplyTo: inReplyTo,
|
|
|
|
|
info: {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'size': file.size,
|
|
|
|
|
'mimetype': mime(file.path.split('/').last),
|
|
|
|
|
'w': width,
|
|
|
|
|
'h': height,
|
2020-03-16 10:38:03 +00:00
|
|
|
|
});
|
2019-09-09 13:22:02 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-17 07:55:25 +00:00
|
|
|
|
/// Sends an video to this room and returns the mxc uri.
|
2019-12-18 11:46:25 +00:00
|
|
|
|
Future<String> sendVideoEvent(MatrixFile file,
|
2020-01-02 14:33:26 +00:00
|
|
|
|
{String txid,
|
2019-12-18 11:46:25 +00:00
|
|
|
|
int videoWidth,
|
|
|
|
|
int videoHeight,
|
|
|
|
|
int duration,
|
|
|
|
|
MatrixFile thumbnail,
|
|
|
|
|
int thumbnailWidth,
|
2020-02-11 11:06:54 +00:00
|
|
|
|
int thumbnailHeight,
|
|
|
|
|
Event inReplyTo}) async {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var fileName = file.path.split('/').last;
|
|
|
|
|
var info = <String, dynamic>{
|
|
|
|
|
'size': file.size,
|
|
|
|
|
'mimetype': mime(fileName),
|
2019-12-18 11:46:25 +00:00
|
|
|
|
};
|
|
|
|
|
if (videoWidth != null) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
info['w'] = videoWidth;
|
2019-12-18 11:46:25 +00:00
|
|
|
|
}
|
|
|
|
|
if (thumbnailHeight != null) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
info['h'] = thumbnailHeight;
|
2019-12-18 11:46:25 +00:00
|
|
|
|
}
|
|
|
|
|
if (duration != null) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
info['duration'] = duration;
|
2019-12-18 11:46:25 +00:00
|
|
|
|
}
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (thumbnail != null && !(encrypted && client.encryptionEnabled)) {
|
|
|
|
|
var thumbnailName = file.path.split('/').last;
|
|
|
|
|
final thumbnailUploadResp = await client.upload(thumbnail);
|
|
|
|
|
info['thumbnail_url'] = thumbnailUploadResp;
|
|
|
|
|
info['thumbnail_info'] = {
|
|
|
|
|
'size': thumbnail.size,
|
|
|
|
|
'mimetype': mime(thumbnailName),
|
2019-12-18 11:46:25 +00:00
|
|
|
|
};
|
|
|
|
|
if (thumbnailWidth != null) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
info['thumbnail_info']['w'] = thumbnailWidth;
|
2019-12-18 11:46:25 +00:00
|
|
|
|
}
|
|
|
|
|
if (thumbnailHeight != null) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
info['thumbnail_info']['h'] = thumbnailHeight;
|
2019-12-18 11:46:25 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-03-16 10:38:03 +00:00
|
|
|
|
|
2020-03-17 07:55:25 +00:00
|
|
|
|
return await sendFileEvent(
|
|
|
|
|
file,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
msgType: 'm.video',
|
2020-03-17 07:55:25 +00:00
|
|
|
|
txid: txid,
|
|
|
|
|
inReplyTo: inReplyTo,
|
|
|
|
|
info: info,
|
|
|
|
|
);
|
2019-12-18 11:46:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-17 07:55:25 +00:00
|
|
|
|
/// Sends an event to this room with this json as a content. Returns the
|
|
|
|
|
/// event ID generated from the server.
|
2020-02-11 11:06:54 +00:00
|
|
|
|
Future<String> sendEvent(Map<String, dynamic> content,
|
2020-05-17 13:25:42 +00:00
|
|
|
|
{String type, String txid, Event inReplyTo}) async {
|
|
|
|
|
type = type ?? 'm.room.message';
|
2020-03-30 09:08:38 +00:00
|
|
|
|
final sendType =
|
|
|
|
|
(encrypted && client.encryptionEnabled) ? 'm.room.encrypted' : type;
|
2019-06-27 07:25:25 +00:00
|
|
|
|
|
|
|
|
|
// Create new transaction id
|
2019-06-26 14:36:34 +00:00
|
|
|
|
String messageID;
|
2020-03-30 09:08:38 +00:00
|
|
|
|
final now = DateTime.now().millisecondsSinceEpoch;
|
2019-06-26 14:36:34 +00:00
|
|
|
|
if (txid == null) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
messageID = 'msg$now';
|
2020-01-02 14:33:26 +00:00
|
|
|
|
} else {
|
2019-06-26 14:36:34 +00:00
|
|
|
|
messageID = txid;
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
2019-06-11 15:16:01 +00:00
|
|
|
|
|
2020-02-11 11:06:54 +00:00
|
|
|
|
if (inReplyTo != null) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var replyText = '<${inReplyTo.senderId}> ' + inReplyTo.body;
|
|
|
|
|
var replyTextLines = replyText.split('\n');
|
|
|
|
|
for (var i = 0; i < replyTextLines.length; i++) {
|
|
|
|
|
replyTextLines[i] = '> ' + replyTextLines[i];
|
2020-02-11 11:06:54 +00:00
|
|
|
|
}
|
2020-03-30 09:08:38 +00:00
|
|
|
|
replyText = replyTextLines.join('\n');
|
|
|
|
|
content['format'] = 'org.matrix.custom.html';
|
|
|
|
|
content['formatted_body'] =
|
2020-02-15 07:48:41 +00:00
|
|
|
|
'<mx-reply><blockquote><a href="https://matrix.to/#/${inReplyTo.room.id}/${inReplyTo.eventId}">In reply to</a> <a href="https://matrix.to/#/${inReplyTo.senderId}">${inReplyTo.senderId}</a><br>${inReplyTo.body}</blockquote></mx-reply>${content["formatted_body"] ?? content["body"]}';
|
2020-03-30 09:08:38 +00:00
|
|
|
|
content['body'] = replyText + "\n\n${content["body"] ?? ""}";
|
|
|
|
|
content['m.relates_to'] = {
|
|
|
|
|
'm.in_reply_to': {
|
|
|
|
|
'event_id': inReplyTo.eventId,
|
2020-02-11 11:06:54 +00:00
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-15 18:40:17 +00:00
|
|
|
|
final sortOrder = newSortOrder;
|
2019-06-26 14:36:34 +00:00
|
|
|
|
// Display a *sending* event and store it.
|
2020-05-15 18:40:17 +00:00
|
|
|
|
var eventUpdate = EventUpdate(type: 'timeline', roomID: id, eventType: type, sortOrder: sortOrder,
|
|
|
|
|
content: {
|
|
|
|
|
'type': type,
|
|
|
|
|
'event_id': messageID,
|
|
|
|
|
'sender': client.userID,
|
|
|
|
|
'status': 0,
|
|
|
|
|
'origin_server_ts': now,
|
|
|
|
|
'content': content
|
|
|
|
|
},
|
|
|
|
|
);
|
2020-01-02 14:09:49 +00:00
|
|
|
|
client.onEvent.add(eventUpdate);
|
2020-05-15 18:40:17 +00:00
|
|
|
|
await client.database?.transaction(() async {
|
|
|
|
|
await client.database.storeEventUpdate(client.id, eventUpdate);
|
|
|
|
|
await updateSortOrder();
|
2019-06-11 15:16:01 +00:00
|
|
|
|
});
|
2019-06-26 14:36:34 +00:00
|
|
|
|
|
|
|
|
|
// Send the text and on success, store and display a *sent* event.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
try {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
final response = await client.jsonRequest(
|
2020-02-15 07:48:41 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/${id}/send/$sendType/$messageID',
|
2020-05-15 18:40:17 +00:00
|
|
|
|
data: encrypted && client.encryptionEnabled
|
2020-05-17 13:25:42 +00:00
|
|
|
|
? await encryptGroupMessagePayload(content, type: type)
|
2020-02-18 10:49:02 +00:00
|
|
|
|
: content);
|
2020-03-30 09:08:38 +00:00
|
|
|
|
final String res = response['event_id'];
|
|
|
|
|
eventUpdate.content['status'] = 1;
|
|
|
|
|
eventUpdate.content['unsigned'] = {'transaction_id': messageID};
|
|
|
|
|
eventUpdate.content['event_id'] = res;
|
2020-01-02 14:09:49 +00:00
|
|
|
|
client.onEvent.add(eventUpdate);
|
2020-05-15 18:40:17 +00:00
|
|
|
|
await client.database?.transaction(() async {
|
|
|
|
|
await client.database.storeEventUpdate(client.id, eventUpdate);
|
2019-06-26 18:03:20 +00:00
|
|
|
|
});
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return res;
|
|
|
|
|
} catch (exception) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
print('[Client] Error while sending: ' + exception.toString());
|
2019-12-29 10:28:33 +00:00
|
|
|
|
// On error, set status to -1
|
2020-03-30 09:08:38 +00:00
|
|
|
|
eventUpdate.content['status'] = -1;
|
|
|
|
|
eventUpdate.content['unsigned'] = {'transaction_id': messageID};
|
2020-01-02 14:09:49 +00:00
|
|
|
|
client.onEvent.add(eventUpdate);
|
2020-05-15 18:40:17 +00:00
|
|
|
|
await client.database?.transaction(() async {
|
|
|
|
|
await client.database.storeEventUpdate(client.id, eventUpdate);
|
2019-06-26 18:03:20 +00:00
|
|
|
|
});
|
2019-06-11 15:16:01 +00:00
|
|
|
|
}
|
|
|
|
|
return null;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-30 08:19:28 +00:00
|
|
|
|
/// Call the Matrix API to join this room if the user is not already a member.
|
|
|
|
|
/// If this room is intended to be a direct chat, the direct chat flag will
|
|
|
|
|
/// automatically be set.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Future<void> join() async {
|
|
|
|
|
try {
|
2020-01-02 14:09:49 +00:00
|
|
|
|
await client.jsonRequest(
|
2020-03-30 09:08:38 +00:00
|
|
|
|
type: HTTPType.POST, action: '/client/r0/rooms/${id}/join');
|
|
|
|
|
final invitation = getState('m.room.member', client.userID);
|
2020-01-28 08:15:53 +00:00
|
|
|
|
if (invitation != null &&
|
2020-03-30 09:08:38 +00:00
|
|
|
|
invitation.content['is_direct'] is bool &&
|
|
|
|
|
invitation.content['is_direct']) {
|
2020-01-28 08:15:53 +00:00
|
|
|
|
await addToDirectChat(invitation.sender.id);
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
2019-12-29 10:28:33 +00:00
|
|
|
|
} on MatrixException catch (exception) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (exception.errorMessage == 'No known servers') {
|
2020-05-15 18:40:17 +00:00
|
|
|
|
await client.database?.forgetRoom(client.id, id);
|
2020-01-02 14:09:49 +00:00
|
|
|
|
client.onRoomUpdate.add(
|
2019-11-13 13:56:20 +00:00
|
|
|
|
RoomUpdate(
|
|
|
|
|
id: id,
|
|
|
|
|
membership: Membership.leave,
|
|
|
|
|
notification_count: 0,
|
|
|
|
|
highlight_count: 0),
|
|
|
|
|
);
|
|
|
|
|
}
|
2019-12-29 10:28:33 +00:00
|
|
|
|
rethrow;
|
2019-11-13 13:56:20 +00:00
|
|
|
|
}
|
2019-09-30 08:19:28 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Call the Matrix API to leave this room. If this room is set as a direct
|
|
|
|
|
/// chat, this will be removed too.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Future<void> leave() async {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (directChatMatrixID != '') await removeFromDirectChat();
|
2020-01-02 14:09:49 +00:00
|
|
|
|
await client.jsonRequest(
|
2020-03-30 09:08:38 +00:00
|
|
|
|
type: HTTPType.POST, action: '/client/r0/rooms/${id}/leave');
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// Call the Matrix API to forget this room if you already left it.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Future<void> forget() async {
|
2020-05-15 18:40:17 +00:00
|
|
|
|
await client.database?.forgetRoom(client.id, id);
|
2020-01-02 14:09:49 +00:00
|
|
|
|
await client.jsonRequest(
|
2020-03-30 09:08:38 +00:00
|
|
|
|
type: HTTPType.POST, action: '/client/r0/rooms/${id}/forget');
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// Call the Matrix API to kick a user from this room.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Future<void> kick(String userID) async {
|
2020-01-02 14:09:49 +00:00
|
|
|
|
await client.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.POST,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/${id}/kick',
|
|
|
|
|
data: {'user_id': userID});
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// Call the Matrix API to ban a user from this room.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Future<void> ban(String userID) async {
|
2020-01-02 14:09:49 +00:00
|
|
|
|
await client.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.POST,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/${id}/ban',
|
|
|
|
|
data: {'user_id': userID});
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// Call the Matrix API to unban a banned user from this room.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Future<void> unban(String userID) async {
|
2020-01-02 14:09:49 +00:00
|
|
|
|
await client.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.POST,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/${id}/unban',
|
|
|
|
|
data: {'user_id': userID});
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-08 09:41:42 +00:00
|
|
|
|
/// Set the power level of the user with the [userID] to the value [power].
|
2019-12-29 10:28:33 +00:00
|
|
|
|
/// Returns the event ID of the new state event. If there is no known
|
|
|
|
|
/// power level event, there might something broken and this returns null.
|
|
|
|
|
Future<String> setPower(String userID, int power) async {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (states['m.room.power_levels'] == null) return null;
|
|
|
|
|
var powerMap = {}..addAll(states['m.room.power_levels'].content);
|
|
|
|
|
if (powerMap['users'] == null) powerMap['users'] = {};
|
|
|
|
|
powerMap['users'][userID] = power;
|
2019-06-11 11:32:14 +00:00
|
|
|
|
|
2020-03-30 09:08:38 +00:00
|
|
|
|
final resp = await client.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/$id/state/m.room.power_levels',
|
2019-12-05 09:06:23 +00:00
|
|
|
|
data: powerMap);
|
2020-03-30 09:08:38 +00:00
|
|
|
|
return resp['event_id'];
|
2019-06-11 11:32:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// Call the Matrix API to invite a user to this room.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Future<void> invite(String userID) async {
|
2020-01-02 14:09:49 +00:00
|
|
|
|
await client.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.POST,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/${id}/invite',
|
|
|
|
|
data: {'user_id': userID});
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-26 09:30:07 +00:00
|
|
|
|
/// Request more previous events from the server. [historyCount] defines how much events should
|
|
|
|
|
/// be received maximum. When the request is answered, [onHistoryReceived] will be triggered **before**
|
|
|
|
|
/// the historical events will be published in the onEvent stream.
|
|
|
|
|
Future<void> requestHistory(
|
|
|
|
|
{int historyCount = DefaultHistoryCount, onHistoryReceived}) async {
|
2020-01-02 14:09:49 +00:00
|
|
|
|
final dynamic resp = await client.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.GET,
|
2019-06-28 09:42:57 +00:00
|
|
|
|
action:
|
2020-05-02 05:02:11 +00:00
|
|
|
|
'/client/r0/rooms/$id/messages?from=${prev_batch}&dir=b&limit=$historyCount&filter=${Client.messagesFilters}');
|
2019-06-11 11:44:25 +00:00
|
|
|
|
|
2019-09-26 09:30:07 +00:00
|
|
|
|
if (onHistoryReceived != null) onHistoryReceived();
|
2020-03-30 09:08:38 +00:00
|
|
|
|
prev_batch = resp['end'];
|
2020-05-15 18:40:17 +00:00
|
|
|
|
|
|
|
|
|
final dbActions = <Future<dynamic> Function()>[];
|
|
|
|
|
if (client.database != null) {
|
|
|
|
|
dbActions.add(() => client.database.setRoomPrevBatch(prev_batch, client.id, id));
|
|
|
|
|
}
|
2019-06-28 10:32:33 +00:00
|
|
|
|
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (!(resp['chunk'] is List<dynamic> &&
|
|
|
|
|
resp['chunk'].length > 0 &&
|
|
|
|
|
resp['end'] is String)) return;
|
2019-06-11 11:44:25 +00:00
|
|
|
|
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (resp['state'] is List<dynamic>) {
|
2020-05-15 18:40:17 +00:00
|
|
|
|
for (final state in resp['state']) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var eventUpdate = EventUpdate(
|
2020-05-15 18:40:17 +00:00
|
|
|
|
type: 'state',
|
2019-06-11 11:44:25 +00:00
|
|
|
|
roomID: id,
|
2020-05-15 18:40:17 +00:00
|
|
|
|
eventType: state['type'],
|
|
|
|
|
content: state,
|
|
|
|
|
sortOrder: oldSortOrder,
|
2020-02-21 08:44:05 +00:00
|
|
|
|
).decrypt(this);
|
|
|
|
|
client.onEvent.add(eventUpdate);
|
2020-05-15 18:40:17 +00:00
|
|
|
|
if (client.database != null) {
|
|
|
|
|
dbActions.add(() => client.database.storeEventUpdate(client.id, eventUpdate));
|
|
|
|
|
}
|
2019-06-11 11:44:25 +00:00
|
|
|
|
}
|
2020-05-15 18:40:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<dynamic> history = resp['chunk'];
|
|
|
|
|
for (final hist in history) {
|
|
|
|
|
var eventUpdate = EventUpdate(
|
|
|
|
|
type: 'history',
|
|
|
|
|
roomID: id,
|
|
|
|
|
eventType: hist['type'],
|
|
|
|
|
content: hist,
|
|
|
|
|
sortOrder: oldSortOrder,
|
|
|
|
|
).decrypt(this);
|
|
|
|
|
client.onEvent.add(eventUpdate);
|
|
|
|
|
if (client.database != null) {
|
|
|
|
|
dbActions.add(() => client.database.storeEventUpdate(client.id, eventUpdate));
|
2019-06-28 06:39:43 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-15 18:40:17 +00:00
|
|
|
|
if (client.database != null) {
|
|
|
|
|
dbActions.add(() => client.database.setRoomPrevBatch(resp['end'], client.id, id));
|
|
|
|
|
}
|
|
|
|
|
await client.database?.transaction(() async {
|
|
|
|
|
for (final f in dbActions) {
|
|
|
|
|
await f();
|
|
|
|
|
}
|
|
|
|
|
await updateSortOrder();
|
|
|
|
|
});
|
2020-01-02 14:09:49 +00:00
|
|
|
|
client.onRoomUpdate.add(
|
2019-10-24 09:39:39 +00:00
|
|
|
|
RoomUpdate(
|
|
|
|
|
id: id,
|
|
|
|
|
membership: membership,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
prev_batch: resp['end'],
|
2019-10-24 09:39:39 +00:00
|
|
|
|
notification_count: notificationCount,
|
|
|
|
|
highlight_count: highlightCount,
|
|
|
|
|
),
|
|
|
|
|
);
|
2019-06-11 11:44:25 +00:00
|
|
|
|
}
|
2019-06-09 10:16:48 +00:00
|
|
|
|
|
2019-12-29 10:28:33 +00:00
|
|
|
|
/// Sets this room as a direct chat for this user if not already.
|
|
|
|
|
Future<void> addToDirectChat(String userID) async {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var directChats = client.directChats;
|
2020-01-02 14:33:26 +00:00
|
|
|
|
if (directChats.containsKey(userID)) {
|
|
|
|
|
if (!directChats[userID].contains(id)) {
|
|
|
|
|
directChats[userID].add(id);
|
|
|
|
|
} else {
|
|
|
|
|
return;
|
|
|
|
|
} // Is already in direct chats
|
|
|
|
|
} else {
|
2019-06-12 09:46:57 +00:00
|
|
|
|
directChats[userID] = [id];
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
2019-06-12 09:46:57 +00:00
|
|
|
|
|
2020-01-02 14:09:49 +00:00
|
|
|
|
await client.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/user/${client.userID}/account_data/m.direct',
|
2019-06-12 09:46:57 +00:00
|
|
|
|
data: directChats);
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return;
|
2019-06-12 09:46:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-12-29 10:28:33 +00:00
|
|
|
|
/// Removes this room from all direct chat tags.
|
|
|
|
|
Future<void> removeFromDirectChat() async {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var directChats = client.directChats;
|
2019-09-30 08:19:28 +00:00
|
|
|
|
if (directChats.containsKey(directChatMatrixID) &&
|
2020-01-02 14:33:26 +00:00
|
|
|
|
directChats[directChatMatrixID].contains(id)) {
|
2019-09-30 08:19:28 +00:00
|
|
|
|
directChats[directChatMatrixID].remove(id);
|
2020-01-02 14:33:26 +00:00
|
|
|
|
} else {
|
|
|
|
|
return;
|
|
|
|
|
} // Nothing to do here
|
2019-09-30 08:19:28 +00:00
|
|
|
|
|
2020-01-02 14:09:49 +00:00
|
|
|
|
await client.jsonRequest(
|
2019-09-30 08:19:28 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/user/${client.userID}/account_data/m.direct',
|
2019-09-30 08:19:28 +00:00
|
|
|
|
data: directChats);
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return;
|
2019-09-30 08:19:28 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-26 14:39:52 +00:00
|
|
|
|
/// Sends *m.fully_read* and *m.read* for the given event ID.
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Future<void> sendReadReceipt(String eventID) async {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
notificationCount = 0;
|
2020-05-15 18:40:17 +00:00
|
|
|
|
await client.database?.resetNotificationCount(client.id, id);
|
2020-01-02 14:33:26 +00:00
|
|
|
|
await client.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
|
type: HTTPType.POST,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/$id/read_markers',
|
2019-06-12 09:46:57 +00:00
|
|
|
|
data: {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'm.fully_read': eventID,
|
|
|
|
|
'm.read': eventID,
|
2019-06-12 09:46:57 +00:00
|
|
|
|
});
|
2019-12-29 10:28:33 +00:00
|
|
|
|
return;
|
2019-06-11 12:13:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-07 07:50:40 +00:00
|
|
|
|
/// Returns a Room from a json String which comes normally from the store. If the
|
|
|
|
|
/// state are also given, the method will await them.
|
2019-06-12 09:46:57 +00:00
|
|
|
|
static Future<Room> getRoomFromTableRow(
|
2020-05-15 18:40:17 +00:00
|
|
|
|
DbRoom row, // either Map<String, dynamic> or DbRoom
|
|
|
|
|
Client matrix,
|
|
|
|
|
{
|
|
|
|
|
dynamic states, // DbRoomState, as iterator and optionally as future
|
|
|
|
|
dynamic roomAccountData, // DbRoomAccountData, as iterator and optionally as future
|
|
|
|
|
}) async {
|
|
|
|
|
final newRoom = Room(
|
|
|
|
|
id: row.roomId,
|
2019-08-08 09:41:42 +00:00
|
|
|
|
membership: Membership.values
|
2020-05-15 18:40:17 +00:00
|
|
|
|
.firstWhere((e) => e.toString() == 'Membership.' + row.membership),
|
|
|
|
|
notificationCount: row.notificationCount,
|
|
|
|
|
highlightCount: row.highlightCount,
|
|
|
|
|
notificationSettings: 'mention', // TODO: do proper things
|
|
|
|
|
prev_batch: row.prevBatch,
|
|
|
|
|
mInvitedMemberCount: row.invitedMemberCount,
|
|
|
|
|
mJoinedMemberCount: row.joinedMemberCount,
|
|
|
|
|
mHeroes: row.heroes?.split(',') ?? [],
|
2019-06-11 08:51:45 +00:00
|
|
|
|
client: matrix,
|
2019-08-08 09:41:42 +00:00
|
|
|
|
roomAccountData: {},
|
2020-05-15 18:40:17 +00:00
|
|
|
|
newestSortOrder: row.newestSortOrder,
|
|
|
|
|
oldestSortOrder: row.oldestSortOrder,
|
2019-06-09 10:16:48 +00:00
|
|
|
|
);
|
2019-08-07 08:17:03 +00:00
|
|
|
|
|
|
|
|
|
if (states != null) {
|
2020-05-15 18:40:17 +00:00
|
|
|
|
var rawStates;
|
|
|
|
|
if (states is Future) {
|
|
|
|
|
rawStates = await states;
|
|
|
|
|
} else {
|
|
|
|
|
rawStates = states;
|
|
|
|
|
}
|
|
|
|
|
for (final rawState in rawStates) {
|
|
|
|
|
final newState = Event.fromDb(rawState, newRoom);;
|
2019-11-22 08:53:48 +00:00
|
|
|
|
newRoom.setState(newState);
|
2019-08-07 08:17:03 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var newRoomAccountData = <String, RoomAccountData>{};
|
2019-08-07 08:46:59 +00:00
|
|
|
|
if (roomAccountData != null) {
|
2020-05-15 18:40:17 +00:00
|
|
|
|
var rawRoomAccountData;
|
|
|
|
|
if (roomAccountData is Future) {
|
|
|
|
|
rawRoomAccountData = await roomAccountData;
|
|
|
|
|
} else {
|
|
|
|
|
rawRoomAccountData = roomAccountData;
|
|
|
|
|
}
|
|
|
|
|
for (final singleAccountData in rawRoomAccountData) {
|
|
|
|
|
final newData = RoomAccountData.fromDb(singleAccountData, newRoom);
|
2019-08-07 08:46:59 +00:00
|
|
|
|
newRoomAccountData[newData.typeKey] = newData;
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-15 18:40:17 +00:00
|
|
|
|
newRoom.roomAccountData = newRoomAccountData;
|
|
|
|
|
|
2019-08-07 08:46:59 +00:00
|
|
|
|
return newRoom;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-26 14:39:52 +00:00
|
|
|
|
/// Creates a timeline from the store. Returns a [Timeline] object.
|
2019-06-25 10:06:26 +00:00
|
|
|
|
Future<Timeline> getTimeline(
|
|
|
|
|
{onTimelineUpdateCallback onUpdate,
|
|
|
|
|
onTimelineInsertCallback onInsert}) async {
|
2020-05-15 18:40:17 +00:00
|
|
|
|
var events;
|
|
|
|
|
if (client.database != null) {
|
|
|
|
|
events = await client.database.getEventList(client.id, this);
|
|
|
|
|
} else {
|
|
|
|
|
events = <Event>[];
|
|
|
|
|
}
|
2020-02-21 08:44:05 +00:00
|
|
|
|
|
|
|
|
|
// Try again to decrypt encrypted events and update the database.
|
2020-05-15 18:40:17 +00:00
|
|
|
|
if (encrypted && client.database != null) {
|
|
|
|
|
await client.database.transaction(() async {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
for (var i = 0; i < events.length; i++) {
|
2020-02-21 08:56:40 +00:00
|
|
|
|
if (events[i].type == EventTypes.Encrypted &&
|
2020-03-30 09:08:38 +00:00
|
|
|
|
events[i].content['body'] == DecryptError.UNKNOWN_SESSION) {
|
2020-05-17 07:54:34 +00:00
|
|
|
|
await events[i].loadSession();
|
2020-02-21 08:44:05 +00:00
|
|
|
|
events[i] = events[i].decrypted;
|
2020-02-21 08:56:40 +00:00
|
|
|
|
if (events[i].type != EventTypes.Encrypted) {
|
2020-05-15 18:40:17 +00:00
|
|
|
|
await client.database.storeEventUpdate(client.id,
|
2020-02-21 08:44:05 +00:00
|
|
|
|
EventUpdate(
|
|
|
|
|
eventType: events[i].typeKey,
|
|
|
|
|
content: events[i].toJson(),
|
|
|
|
|
roomID: events[i].roomId,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
type: 'timeline',
|
2020-05-15 18:40:17 +00:00
|
|
|
|
sortOrder: events[i].sortOrder,
|
2020-02-21 08:44:05 +00:00
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-02-15 07:48:41 +00:00
|
|
|
|
}
|
2020-02-21 08:44:05 +00:00
|
|
|
|
});
|
2020-02-15 07:48:41 +00:00
|
|
|
|
}
|
2020-02-21 08:44:05 +00:00
|
|
|
|
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var timeline = Timeline(
|
2019-06-21 10:18:54 +00:00
|
|
|
|
room: this,
|
|
|
|
|
events: events,
|
|
|
|
|
onUpdate: onUpdate,
|
|
|
|
|
onInsert: onInsert,
|
|
|
|
|
);
|
2020-05-15 18:40:17 +00:00
|
|
|
|
if (client.database == null) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
prev_batch = '';
|
2020-03-20 08:59:29 +00:00
|
|
|
|
await requestHistory(historyCount: 10);
|
2020-01-23 10:43:01 +00:00
|
|
|
|
}
|
|
|
|
|
return timeline;
|
2019-06-21 10:18:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-02 10:09:30 +00:00
|
|
|
|
/// Returns all participants for this room. With lazy loading this
|
|
|
|
|
/// list may not be complete. User [requestParticipants] in this
|
|
|
|
|
/// case.
|
|
|
|
|
List<User> getParticipants() {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var userList = <User>[];
|
|
|
|
|
if (states['m.room.member'] is Map<String, dynamic>) {
|
|
|
|
|
for (var entry in states['m.room.member'].entries) {
|
2020-01-02 14:09:49 +00:00
|
|
|
|
Event state = entry.value;
|
2019-11-20 13:02:23 +00:00
|
|
|
|
if (state.type == EventTypes.RoomMember) userList.add(state.asUser);
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-09-02 10:09:30 +00:00
|
|
|
|
return userList;
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 08:51:45 +00:00
|
|
|
|
/// Request the full list of participants from the server. The local list
|
|
|
|
|
/// from the store is not complete if the client uses lazy loading.
|
2019-06-18 10:06:55 +00:00
|
|
|
|
Future<List<User>> requestParticipants() async {
|
2020-02-04 13:41:13 +00:00
|
|
|
|
if (participantListComplete) return getParticipants();
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var participants = <User>[];
|
2019-06-09 10:16:48 +00:00
|
|
|
|
|
2020-01-02 14:09:49 +00:00
|
|
|
|
dynamic res = await client.jsonRequest(
|
2020-03-30 09:08:38 +00:00
|
|
|
|
type: HTTPType.GET, action: '/client/r0/rooms/${id}/members');
|
2019-06-09 10:16:48 +00:00
|
|
|
|
|
2020-03-30 09:08:38 +00:00
|
|
|
|
for (num i = 0; i < res['chunk'].length; i++) {
|
|
|
|
|
var newUser = Event.fromJson(res['chunk'][i], this).asUser;
|
2020-02-04 13:41:13 +00:00
|
|
|
|
if (![Membership.leave, Membership.ban].contains(newUser.membership)) {
|
|
|
|
|
participants.add(newUser);
|
|
|
|
|
setState(newUser);
|
|
|
|
|
}
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-21 10:18:54 +00:00
|
|
|
|
return participants;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
}
|
2019-06-28 08:59:00 +00:00
|
|
|
|
|
2020-02-04 13:41:13 +00:00
|
|
|
|
/// Checks if the local participant list of joined and invited users is complete.
|
|
|
|
|
bool get participantListComplete {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var knownParticipants = getParticipants();
|
2020-02-04 13:41:13 +00:00
|
|
|
|
knownParticipants.removeWhere(
|
|
|
|
|
(u) => ![Membership.join, Membership.invite].contains(u.membership));
|
|
|
|
|
return knownParticipants.length ==
|
2020-03-30 09:08:38 +00:00
|
|
|
|
(mJoinedMemberCount ?? 0) + (mInvitedMemberCount ?? 0);
|
2020-02-04 13:41:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-15 11:08:43 +00:00
|
|
|
|
/// Returns the [User] object for the given [mxID] or requests it from
|
|
|
|
|
/// the homeserver and waits for a response.
|
2019-07-29 14:16:20 +00:00
|
|
|
|
Future<User> getUserByMXID(String mxID) async {
|
2019-08-08 09:41:42 +00:00
|
|
|
|
if (states[mxID] != null) return states[mxID].asUser;
|
2019-11-15 11:08:43 +00:00
|
|
|
|
return requestUser(mxID);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the [User] object for the given [mxID] or requests it from
|
|
|
|
|
/// the homeserver and returns a default [User] object while waiting.
|
|
|
|
|
User getUserByMXIDSync(String mxID) {
|
2020-01-02 14:33:26 +00:00
|
|
|
|
if (states[mxID] != null) {
|
2019-11-15 11:08:43 +00:00
|
|
|
|
return states[mxID].asUser;
|
2020-01-02 14:33:26 +00:00
|
|
|
|
} else {
|
2020-03-25 12:09:42 +00:00
|
|
|
|
requestUser(mxID, ignoreErrors: true);
|
2019-11-15 11:08:43 +00:00
|
|
|
|
return User(mxID, room: this);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-30 09:08:38 +00:00
|
|
|
|
final Set<String> _requestingMatrixIds = {};
|
2019-11-15 11:08:43 +00:00
|
|
|
|
|
|
|
|
|
/// Requests a missing [User] for this room. Important for clients using
|
|
|
|
|
/// lazy loading.
|
2020-03-25 12:09:42 +00:00
|
|
|
|
Future<User> requestUser(String mxID, {bool ignoreErrors = false}) async {
|
2020-05-15 18:40:17 +00:00
|
|
|
|
if (getState('m.room.member', mxID) != null) {
|
|
|
|
|
return getState('m.room.member', mxID).asUser;
|
|
|
|
|
}
|
2019-11-15 11:08:43 +00:00
|
|
|
|
if (mxID == null || !_requestingMatrixIds.add(mxID)) return null;
|
2019-12-29 10:28:33 +00:00
|
|
|
|
Map<String, dynamic> resp;
|
|
|
|
|
try {
|
2020-01-02 14:09:49 +00:00
|
|
|
|
resp = await client.jsonRequest(
|
2019-12-29 10:28:33 +00:00
|
|
|
|
type: HTTPType.GET,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/$id/state/m.room.member/$mxID');
|
2019-12-29 10:28:33 +00:00
|
|
|
|
} catch (exception) {
|
2019-11-15 11:08:43 +00:00
|
|
|
|
_requestingMatrixIds.remove(mxID);
|
2020-03-25 12:09:42 +00:00
|
|
|
|
if (!ignoreErrors) rethrow;
|
2019-11-15 11:08:43 +00:00
|
|
|
|
}
|
2020-05-15 18:40:17 +00:00
|
|
|
|
if (resp == null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2020-03-30 09:08:38 +00:00
|
|
|
|
final user = User(mxID,
|
|
|
|
|
displayName: resp['displayname'],
|
|
|
|
|
avatarUrl: resp['avatar_url'],
|
2019-09-17 12:21:16 +00:00
|
|
|
|
room: this);
|
2019-11-15 11:08:43 +00:00
|
|
|
|
states[mxID] = user;
|
2020-05-15 18:40:17 +00:00
|
|
|
|
await client.database?.transaction(() async {
|
|
|
|
|
final content = <String, dynamic>{
|
|
|
|
|
'sender': mxID,
|
|
|
|
|
'type': 'm.room.member',
|
|
|
|
|
'content': resp,
|
|
|
|
|
'state_key': mxID,
|
|
|
|
|
};
|
|
|
|
|
await client.database.storeEventUpdate(client.id,
|
|
|
|
|
EventUpdate(
|
|
|
|
|
content: content,
|
|
|
|
|
roomID: id,
|
|
|
|
|
type: 'state',
|
|
|
|
|
eventType: 'm.room.member',
|
|
|
|
|
sortOrder: 0.0),
|
|
|
|
|
);
|
|
|
|
|
});
|
2020-01-04 10:29:38 +00:00
|
|
|
|
if (onUpdate != null) onUpdate.add(id);
|
2019-11-15 11:08:43 +00:00
|
|
|
|
_requestingMatrixIds.remove(mxID);
|
|
|
|
|
return user;
|
2019-07-29 14:16:20 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-29 11:12:04 +00:00
|
|
|
|
/// Searches for the event on the server. Returns null if not found.
|
2019-06-28 08:59:00 +00:00
|
|
|
|
Future<Event> getEventById(String eventID) async {
|
2020-01-02 14:09:49 +00:00
|
|
|
|
final dynamic resp = await client.jsonRequest(
|
2020-03-30 09:08:38 +00:00
|
|
|
|
type: HTTPType.GET, action: '/client/r0/rooms/$id/event/$eventID');
|
2019-08-07 08:17:03 +00:00
|
|
|
|
return Event.fromJson(resp, this);
|
2019-06-28 08:59:00 +00:00
|
|
|
|
}
|
2019-08-07 09:23:57 +00:00
|
|
|
|
|
2019-09-03 15:57:27 +00:00
|
|
|
|
/// Returns the power level of the given user ID.
|
2019-08-08 09:41:42 +00:00
|
|
|
|
int getPowerLevelByUserId(String userId) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var powerLevel = 0;
|
|
|
|
|
Event powerLevelState = states['m.room.power_levels'];
|
2019-08-07 09:23:57 +00:00
|
|
|
|
if (powerLevelState == null) return powerLevel;
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (powerLevelState.content['users_default'] is int) {
|
|
|
|
|
powerLevel = powerLevelState.content['users_default'];
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (powerLevelState.content['users'] is Map<String, dynamic> &&
|
|
|
|
|
powerLevelState.content['users'][userId] != null) {
|
|
|
|
|
powerLevel = powerLevelState.content['users'][userId];
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
2019-08-07 09:23:57 +00:00
|
|
|
|
return powerLevel;
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-08 09:41:42 +00:00
|
|
|
|
/// Returns the user's own power level.
|
|
|
|
|
int get ownPowerLevel => getPowerLevelByUserId(client.userID);
|
|
|
|
|
|
2019-08-07 09:23:57 +00:00
|
|
|
|
/// Returns the power levels from all users for this room or null if not given.
|
|
|
|
|
Map<String, int> get powerLevels {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
Event powerLevelState = states['m.room.power_levels'];
|
|
|
|
|
if (powerLevelState.content['users'] is Map<String, int>) {
|
|
|
|
|
return powerLevelState.content['users'];
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
2019-08-07 09:23:57 +00:00
|
|
|
|
return null;
|
|
|
|
|
}
|
2019-09-09 13:22:02 +00:00
|
|
|
|
|
2019-12-29 10:28:33 +00:00
|
|
|
|
/// Uploads a new user avatar for this room. Returns the event ID of the new
|
|
|
|
|
/// m.room.avatar event.
|
|
|
|
|
Future<String> setAvatar(MatrixFile file) async {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
final uploadResp = await client.upload(file);
|
|
|
|
|
final setAvatarResp = await client.jsonRequest(
|
2020-01-02 14:09:49 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/$id/state/m.room.avatar/',
|
|
|
|
|
data: {'url': uploadResp});
|
|
|
|
|
return setAvatarResp['event_id'];
|
2019-09-09 13:22:02 +00:00
|
|
|
|
}
|
2019-11-26 06:38:44 +00:00
|
|
|
|
|
|
|
|
|
bool _hasPermissionFor(String action) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (getState('m.room.power_levels') == null ||
|
|
|
|
|
getState('m.room.power_levels').content[action] == null) return true;
|
|
|
|
|
return ownPowerLevel >= getState('m.room.power_levels').content[action];
|
2019-11-26 06:38:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The level required to ban a user.
|
2020-03-30 09:08:38 +00:00
|
|
|
|
bool get canBan => _hasPermissionFor('ban');
|
2019-11-26 06:38:44 +00:00
|
|
|
|
|
|
|
|
|
/// The default level required to send message events. Can be overridden by the events key.
|
2020-03-30 09:08:38 +00:00
|
|
|
|
bool get canSendDefaultMessages => _hasPermissionFor('events_default');
|
2019-11-26 06:38:44 +00:00
|
|
|
|
|
|
|
|
|
/// The level required to invite a user.
|
2020-03-30 09:08:38 +00:00
|
|
|
|
bool get canInvite => _hasPermissionFor('invite');
|
2019-11-26 06:38:44 +00:00
|
|
|
|
|
|
|
|
|
/// The level required to kick a user.
|
2020-03-30 09:08:38 +00:00
|
|
|
|
bool get canKick => _hasPermissionFor('kick');
|
2019-11-26 06:38:44 +00:00
|
|
|
|
|
|
|
|
|
/// The level required to redact an event.
|
2020-03-30 09:08:38 +00:00
|
|
|
|
bool get canRedact => _hasPermissionFor('redact');
|
2019-11-26 06:38:44 +00:00
|
|
|
|
|
|
|
|
|
/// The default level required to send state events. Can be overridden by the events key.
|
2020-03-30 09:08:38 +00:00
|
|
|
|
bool get canSendDefaultStates => _hasPermissionFor('state_default');
|
2019-11-26 06:38:44 +00:00
|
|
|
|
|
2020-03-30 09:08:38 +00:00
|
|
|
|
bool get canChangePowerLevel => canSendEvent('m.room.power_levels');
|
2019-11-26 06:38:44 +00:00
|
|
|
|
|
|
|
|
|
bool canSendEvent(String eventType) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (getState('m.room.power_levels') == null) return true;
|
|
|
|
|
if (getState('m.room.power_levels').content['events'] == null ||
|
|
|
|
|
getState('m.room.power_levels').content['events'][eventType] == null) {
|
|
|
|
|
return eventType == 'm.room.message'
|
2019-11-26 06:38:44 +00:00
|
|
|
|
? canSendDefaultMessages
|
|
|
|
|
: canSendDefaultStates;
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
2019-11-26 06:38:44 +00:00
|
|
|
|
return ownPowerLevel >=
|
2020-03-30 09:08:38 +00:00
|
|
|
|
getState('m.room.power_levels').content['events'][eventType];
|
2019-11-26 06:38:44 +00:00
|
|
|
|
}
|
2019-12-04 09:58:47 +00:00
|
|
|
|
|
|
|
|
|
/// Returns the [PushRuleState] for this room, based on the m.push_rules stored in
|
|
|
|
|
/// the account_data.
|
|
|
|
|
PushRuleState get pushRuleState {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (!client.accountData.containsKey('m.push_rules') ||
|
|
|
|
|
!(client.accountData['m.push_rules'].content['global'] is Map)) {
|
2019-12-04 09:58:47 +00:00
|
|
|
|
return PushRuleState.notify;
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
2019-12-04 09:58:47 +00:00
|
|
|
|
final Map<String, dynamic> globalPushRules =
|
2020-03-30 09:08:38 +00:00
|
|
|
|
client.accountData['m.push_rules'].content['global'];
|
2019-12-04 09:58:47 +00:00
|
|
|
|
if (globalPushRules == null) return PushRuleState.notify;
|
|
|
|
|
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (globalPushRules['override'] is List) {
|
|
|
|
|
for (var i = 0; i < globalPushRules['override'].length; i++) {
|
|
|
|
|
if (globalPushRules['override'][i]['rule_id'] == id) {
|
|
|
|
|
if (globalPushRules['override'][i]['actions']
|
|
|
|
|
.indexOf('dont_notify') !=
|
2019-12-04 09:58:47 +00:00
|
|
|
|
-1) {
|
|
|
|
|
return PushRuleState.dont_notify;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (globalPushRules['room'] is List) {
|
|
|
|
|
for (var i = 0; i < globalPushRules['room'].length; i++) {
|
|
|
|
|
if (globalPushRules['room'][i]['rule_id'] == id) {
|
|
|
|
|
if (globalPushRules['room'][i]['actions'].indexOf('dont_notify') !=
|
2019-12-04 09:58:47 +00:00
|
|
|
|
-1) {
|
|
|
|
|
return PushRuleState.mentions_only;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return PushRuleState.notify;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Sends a request to the homeserver to set the [PushRuleState] for this room.
|
|
|
|
|
/// Returns ErrorResponse if something goes wrong.
|
|
|
|
|
Future<dynamic> setPushRuleState(PushRuleState newState) async {
|
|
|
|
|
if (newState == pushRuleState) return null;
|
|
|
|
|
dynamic resp;
|
|
|
|
|
switch (newState) {
|
|
|
|
|
// All push notifications should be sent to the user
|
|
|
|
|
case PushRuleState.notify:
|
2020-01-02 14:33:26 +00:00
|
|
|
|
if (pushRuleState == PushRuleState.dont_notify) {
|
2020-01-02 14:09:49 +00:00
|
|
|
|
resp = await client.jsonRequest(
|
2019-12-04 09:58:47 +00:00
|
|
|
|
type: HTTPType.DELETE,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/pushrules/global/override/$id',
|
2019-12-04 09:58:47 +00:00
|
|
|
|
data: {});
|
2020-01-02 14:33:26 +00:00
|
|
|
|
} else if (pushRuleState == PushRuleState.mentions_only) {
|
2020-01-02 14:09:49 +00:00
|
|
|
|
resp = await client.jsonRequest(
|
2019-12-04 09:58:47 +00:00
|
|
|
|
type: HTTPType.DELETE,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/pushrules/global/room/$id',
|
2019-12-04 09:58:47 +00:00
|
|
|
|
data: {});
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
2019-12-04 09:58:47 +00:00
|
|
|
|
break;
|
|
|
|
|
// Only when someone mentions the user, a push notification should be sent
|
|
|
|
|
case PushRuleState.mentions_only:
|
|
|
|
|
if (pushRuleState == PushRuleState.dont_notify) {
|
2020-01-02 14:09:49 +00:00
|
|
|
|
resp = await client.jsonRequest(
|
2019-12-04 09:58:47 +00:00
|
|
|
|
type: HTTPType.DELETE,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/pushrules/global/override/$id',
|
2019-12-04 09:58:47 +00:00
|
|
|
|
data: {});
|
2020-01-02 14:09:49 +00:00
|
|
|
|
resp = await client.jsonRequest(
|
2019-12-04 09:58:47 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/pushrules/global/room/$id',
|
2019-12-04 09:58:47 +00:00
|
|
|
|
data: {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'actions': ['dont_notify']
|
2019-12-04 09:58:47 +00:00
|
|
|
|
});
|
2020-01-02 14:33:26 +00:00
|
|
|
|
} else if (pushRuleState == PushRuleState.notify) {
|
2020-01-02 14:09:49 +00:00
|
|
|
|
resp = await client.jsonRequest(
|
2019-12-04 09:58:47 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/pushrules/global/room/$id',
|
2019-12-04 09:58:47 +00:00
|
|
|
|
data: {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'actions': ['dont_notify']
|
2019-12-04 09:58:47 +00:00
|
|
|
|
});
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
2019-12-04 09:58:47 +00:00
|
|
|
|
break;
|
|
|
|
|
// No push notification should be ever sent for this room.
|
|
|
|
|
case PushRuleState.dont_notify:
|
|
|
|
|
if (pushRuleState == PushRuleState.mentions_only) {
|
2020-01-02 14:09:49 +00:00
|
|
|
|
resp = await client.jsonRequest(
|
2019-12-04 09:58:47 +00:00
|
|
|
|
type: HTTPType.DELETE,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/pushrules/global/room/$id',
|
2019-12-04 09:58:47 +00:00
|
|
|
|
data: {});
|
|
|
|
|
}
|
2020-01-02 14:09:49 +00:00
|
|
|
|
resp = await client.jsonRequest(
|
2019-12-04 09:58:47 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/pushrules/global/override/$id',
|
2019-12-04 09:58:47 +00:00
|
|
|
|
data: {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'actions': ['dont_notify'],
|
|
|
|
|
'conditions': [
|
|
|
|
|
{'key': 'room_id', 'kind': 'event_match', 'pattern': id}
|
2019-12-04 09:58:47 +00:00
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return resp;
|
|
|
|
|
}
|
2019-12-12 12:19:18 +00:00
|
|
|
|
|
|
|
|
|
/// Redacts this event. Returns [ErrorResponse] on error.
|
|
|
|
|
Future<dynamic> redactEvent(String eventId,
|
|
|
|
|
{String reason, String txid}) async {
|
|
|
|
|
// Create new transaction id
|
|
|
|
|
String messageID;
|
2020-03-30 09:08:38 +00:00
|
|
|
|
final now = DateTime.now().millisecondsSinceEpoch;
|
2019-12-12 12:19:18 +00:00
|
|
|
|
if (txid == null) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
messageID = 'msg$now';
|
2020-01-02 14:33:26 +00:00
|
|
|
|
} else {
|
2019-12-12 12:19:18 +00:00
|
|
|
|
messageID = txid;
|
2020-01-02 14:33:26 +00:00
|
|
|
|
}
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var data = <String, dynamic>{};
|
|
|
|
|
if (reason != null) data['reason'] = reason;
|
2020-01-02 14:09:49 +00:00
|
|
|
|
final dynamic resp = await client.jsonRequest(
|
2019-12-12 12:19:18 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/$id/redact/$eventId/$messageID',
|
2019-12-12 12:19:18 +00:00
|
|
|
|
data: data);
|
|
|
|
|
return resp;
|
|
|
|
|
}
|
2019-12-16 11:55:13 +00:00
|
|
|
|
|
|
|
|
|
Future<dynamic> sendTypingInfo(bool isTyping, {int timeout}) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var data = <String, dynamic>{
|
|
|
|
|
'typing': isTyping,
|
2019-12-16 11:55:13 +00:00
|
|
|
|
};
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (timeout != null) data['timeout'] = timeout;
|
2020-01-02 14:09:49 +00:00
|
|
|
|
return client.jsonRequest(
|
2019-12-16 11:55:13 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/${id}/typing/${client.userID}',
|
2019-12-16 11:55:13 +00:00
|
|
|
|
data: data,
|
|
|
|
|
);
|
|
|
|
|
}
|
2020-01-04 18:36:17 +00:00
|
|
|
|
|
|
|
|
|
/// This is sent by the caller when they wish to establish a call.
|
|
|
|
|
/// [callId] is a unique identifier for the call.
|
|
|
|
|
/// [version] is the version of the VoIP specification this message adheres to. This specification is version 0.
|
|
|
|
|
/// [lifetime] is the time in milliseconds that the invite is valid for. Once the invite age exceeds this value,
|
|
|
|
|
/// clients should discard it. They should also no longer show the call as awaiting an answer in the UI.
|
|
|
|
|
/// [type] The type of session description. Must be 'offer'.
|
|
|
|
|
/// [sdp] The SDP text of the session description.
|
|
|
|
|
Future<String> inviteToCall(String callId, int lifetime, String sdp,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
{String type = 'offer', int version = 0, String txid}) async {
|
|
|
|
|
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
|
|
|
|
|
final response = await client.jsonRequest(
|
2020-01-04 18:36:17 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/$id/send/m.call.invite/$txid',
|
2020-01-04 18:36:17 +00:00
|
|
|
|
data: {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'call_id': callId,
|
|
|
|
|
'lifetime': lifetime,
|
|
|
|
|
'offer': {'sdp': sdp, 'type': type},
|
|
|
|
|
'version': version,
|
2020-01-04 18:36:17 +00:00
|
|
|
|
},
|
|
|
|
|
);
|
2020-03-30 09:08:38 +00:00
|
|
|
|
return response['event_id'];
|
2020-01-04 18:36:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// This is sent by callers after sending an invite and by the callee after answering.
|
|
|
|
|
/// Its purpose is to give the other party additional ICE candidates to try using to communicate.
|
|
|
|
|
///
|
|
|
|
|
/// [callId] The ID of the call this event relates to.
|
|
|
|
|
///
|
|
|
|
|
/// [version] The version of the VoIP specification this messages adheres to. This specification is version 0.
|
|
|
|
|
///
|
|
|
|
|
/// [candidates] Array of objects describing the candidates. Example:
|
|
|
|
|
///
|
|
|
|
|
/// ```
|
|
|
|
|
/// [
|
|
|
|
|
/// {
|
|
|
|
|
/// "candidate": "candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0",
|
|
|
|
|
/// "sdpMLineIndex": 0,
|
|
|
|
|
/// "sdpMid": "audio"
|
|
|
|
|
/// }
|
|
|
|
|
/// ],
|
|
|
|
|
/// ```
|
|
|
|
|
Future<String> sendCallCandidates(
|
|
|
|
|
String callId,
|
|
|
|
|
List<Map<String, dynamic>> candidates, {
|
|
|
|
|
int version = 0,
|
|
|
|
|
String txid,
|
|
|
|
|
}) async {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
|
|
|
|
|
final response = await client.jsonRequest(
|
2020-01-04 18:36:17 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/$id/send/m.call.candidates/$txid',
|
2020-01-04 18:36:17 +00:00
|
|
|
|
data: {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'call_id': callId,
|
|
|
|
|
'candidates': candidates,
|
|
|
|
|
'version': version,
|
2020-01-04 18:36:17 +00:00
|
|
|
|
},
|
|
|
|
|
);
|
2020-03-30 09:08:38 +00:00
|
|
|
|
return response['event_id'];
|
2020-01-04 18:36:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// This event is sent by the callee when they wish to answer the call.
|
|
|
|
|
/// [callId] is a unique identifier for the call.
|
|
|
|
|
/// [version] is the version of the VoIP specification this message adheres to. This specification is version 0.
|
|
|
|
|
/// [type] The type of session description. Must be 'answer'.
|
|
|
|
|
/// [sdp] The SDP text of the session description.
|
|
|
|
|
Future<String> answerCall(String callId, String sdp,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
{String type = 'answer', int version = 0, String txid}) async {
|
|
|
|
|
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
|
|
|
|
|
final response = await client.jsonRequest(
|
2020-01-04 18:36:17 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/$id/send/m.call.answer/$txid',
|
2020-01-04 18:36:17 +00:00
|
|
|
|
data: {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'call_id': callId,
|
|
|
|
|
'answer': {'sdp': sdp, 'type': type},
|
|
|
|
|
'version': version,
|
2020-01-04 18:36:17 +00:00
|
|
|
|
},
|
|
|
|
|
);
|
2020-03-30 09:08:38 +00:00
|
|
|
|
return response['event_id'];
|
2020-01-04 18:36:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// This event is sent by the callee when they wish to answer the call.
|
|
|
|
|
/// [callId] The ID of the call this event relates to.
|
|
|
|
|
/// [version] is the version of the VoIP specification this message adheres to. This specification is version 0.
|
|
|
|
|
Future<String> hangupCall(String callId,
|
|
|
|
|
{int version = 0, String txid}) async {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
|
|
|
|
|
final response = await client.jsonRequest(
|
2020-01-04 18:36:17 +00:00
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/$id/send/m.call.hangup/$txid',
|
2020-01-04 18:36:17 +00:00
|
|
|
|
data: {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'call_id': callId,
|
|
|
|
|
'version': version,
|
2020-01-04 18:36:17 +00:00
|
|
|
|
},
|
|
|
|
|
);
|
2020-03-30 09:08:38 +00:00
|
|
|
|
return response['event_id'];
|
2020-01-04 18:36:17 +00:00
|
|
|
|
}
|
2019-12-04 09:58:47 +00:00
|
|
|
|
|
2020-01-18 14:49:15 +00:00
|
|
|
|
/// Returns all aliases for this room.
|
|
|
|
|
List<String> get aliases {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var aliases = <String>[];
|
|
|
|
|
for (var aliasEvent in states.states['m.room.aliases'].values) {
|
|
|
|
|
if (aliasEvent.content['aliases'] is List) {
|
|
|
|
|
aliases.addAll(aliasEvent.content['aliases']);
|
2020-01-18 14:49:15 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return aliases;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A room may be public meaning anyone can join the room without any prior action. Alternatively,
|
|
|
|
|
/// it can be invite meaning that a user who wishes to join the room must first receive an invite
|
|
|
|
|
/// to the room from someone already inside of the room. Currently, knock and private are reserved
|
|
|
|
|
/// keywords which are not implemented.
|
2020-03-30 09:08:38 +00:00
|
|
|
|
JoinRules get joinRules => getState('m.room.join_rules') != null
|
2020-01-18 14:49:15 +00:00
|
|
|
|
? JoinRules.values.firstWhere(
|
|
|
|
|
(r) =>
|
2020-03-30 09:08:38 +00:00
|
|
|
|
r.toString().replaceAll('JoinRules.', '') ==
|
|
|
|
|
getState('m.room.join_rules').content['join_rule'],
|
2020-01-18 14:49:15 +00:00
|
|
|
|
orElse: () => null)
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
/// Changes the join rules. You should check first if the user is able to change it.
|
|
|
|
|
Future<void> setJoinRules(JoinRules joinRules) async {
|
|
|
|
|
await client.jsonRequest(
|
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/$id/state/m.room.join_rules/',
|
2020-01-18 14:49:15 +00:00
|
|
|
|
data: {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'join_rule': joinRules.toString().replaceAll('JoinRules.', ''),
|
2020-01-18 14:49:15 +00:00
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Whether the user has the permission to change the join rules.
|
2020-03-30 09:08:38 +00:00
|
|
|
|
bool get canChangeJoinRules => canSendEvent('m.room.join_rules');
|
2020-01-18 14:49:15 +00:00
|
|
|
|
|
|
|
|
|
/// This event controls whether guest users are allowed to join rooms. If this event
|
|
|
|
|
/// is absent, servers should act as if it is present and has the guest_access value "forbidden".
|
2020-03-30 09:08:38 +00:00
|
|
|
|
GuestAccess get guestAccess => getState('m.room.guest_access') != null
|
2020-01-18 14:49:15 +00:00
|
|
|
|
? GuestAccess.values.firstWhere(
|
|
|
|
|
(r) =>
|
2020-03-30 09:08:38 +00:00
|
|
|
|
r.toString().replaceAll('GuestAccess.', '') ==
|
|
|
|
|
getState('m.room.guest_access').content['guest_access'],
|
2020-01-18 14:49:15 +00:00
|
|
|
|
orElse: () => GuestAccess.forbidden)
|
|
|
|
|
: GuestAccess.forbidden;
|
|
|
|
|
|
|
|
|
|
/// Changes the guest access. You should check first if the user is able to change it.
|
|
|
|
|
Future<void> setGuestAccess(GuestAccess guestAccess) async {
|
|
|
|
|
await client.jsonRequest(
|
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/$id/state/m.room.guest_access/',
|
2020-01-18 14:49:15 +00:00
|
|
|
|
data: {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'guest_access': guestAccess.toString().replaceAll('GuestAccess.', ''),
|
2020-01-18 14:49:15 +00:00
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Whether the user has the permission to change the guest access.
|
2020-03-30 09:08:38 +00:00
|
|
|
|
bool get canChangeGuestAccess => canSendEvent('m.room.guest_access');
|
2020-01-18 14:49:15 +00:00
|
|
|
|
|
|
|
|
|
/// This event controls whether a user can see the events that happened in a room from before they joined.
|
|
|
|
|
HistoryVisibility get historyVisibility =>
|
2020-03-30 09:08:38 +00:00
|
|
|
|
getState('m.room.history_visibility') != null
|
2020-01-18 14:49:15 +00:00
|
|
|
|
? HistoryVisibility.values.firstWhere(
|
|
|
|
|
(r) =>
|
2020-03-30 09:08:38 +00:00
|
|
|
|
r.toString().replaceAll('HistoryVisibility.', '') ==
|
|
|
|
|
getState('m.room.history_visibility')
|
|
|
|
|
.content['history_visibility'],
|
2020-01-18 14:49:15 +00:00
|
|
|
|
orElse: () => null)
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
/// Changes the history visibility. You should check first if the user is able to change it.
|
|
|
|
|
Future<void> setHistoryVisibility(HistoryVisibility historyVisibility) async {
|
|
|
|
|
await client.jsonRequest(
|
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/$id/state/m.room.history_visibility/',
|
2020-01-18 14:49:15 +00:00
|
|
|
|
data: {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'history_visibility':
|
|
|
|
|
historyVisibility.toString().replaceAll('HistoryVisibility.', ''),
|
2020-01-18 14:49:15 +00:00
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Whether the user has the permission to change the history visibility.
|
|
|
|
|
bool get canChangeHistoryVisibility =>
|
2020-03-30 09:08:38 +00:00
|
|
|
|
canSendEvent('m.room.history_visibility');
|
2020-02-04 13:41:13 +00:00
|
|
|
|
|
|
|
|
|
/// Returns the encryption algorithm. Currently only `m.megolm.v1.aes-sha2` is supported.
|
|
|
|
|
/// Returns null if there is no encryption algorithm.
|
2020-03-30 09:08:38 +00:00
|
|
|
|
String get encryptionAlgorithm => getState('m.room.encryption') != null
|
|
|
|
|
? getState('m.room.encryption').content['algorithm'].toString()
|
2020-02-04 13:41:13 +00:00
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
/// Checks if this room is encrypted.
|
|
|
|
|
bool get encrypted => encryptionAlgorithm != null;
|
|
|
|
|
|
|
|
|
|
Future<void> enableEncryption({int algorithmIndex = 0}) async {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (encrypted) throw ('Encryption is already enabled!');
|
|
|
|
|
final algorithm = Client.supportedGroupEncryptionAlgorithms[algorithmIndex];
|
2020-02-04 13:41:13 +00:00
|
|
|
|
await client.jsonRequest(
|
|
|
|
|
type: HTTPType.PUT,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
action: '/client/r0/rooms/$id/state/m.room.encryption/',
|
2020-02-04 13:41:13 +00:00
|
|
|
|
data: {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'algorithm': algorithm,
|
2020-02-04 13:41:13 +00:00
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-15 07:48:41 +00:00
|
|
|
|
/// Returns all known device keys for all participants in this room.
|
2020-02-04 13:41:13 +00:00
|
|
|
|
Future<List<DeviceKeys>> getUserDeviceKeys() async {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var deviceKeys = <DeviceKeys>[];
|
|
|
|
|
var users = await requestParticipants();
|
2020-05-15 18:40:17 +00:00
|
|
|
|
for (final user in users) {
|
|
|
|
|
if (client.userDeviceKeys.containsKey(user.id)) {
|
|
|
|
|
for (var deviceKeyEntry in client.userDeviceKeys[user.id].deviceKeys.values) {
|
|
|
|
|
deviceKeys.add(deviceKeyEntry);
|
|
|
|
|
}
|
2020-02-04 13:41:13 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return deviceKeys;
|
|
|
|
|
}
|
2020-05-17 07:54:34 +00:00
|
|
|
|
|
|
|
|
|
bool _restoredOutboundGroupSession = false;
|
|
|
|
|
|
|
|
|
|
Future<void> restoreOutboundGroupSession() async {
|
|
|
|
|
if (_restoredOutboundGroupSession || client.database == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
final outboundSession = await client.database.getDbOutboundGroupSession(client.id, id);
|
|
|
|
|
if (outboundSession != null) {
|
|
|
|
|
try {
|
|
|
|
|
_outboundGroupSession = olm.OutboundGroupSession();
|
|
|
|
|
_outboundGroupSession.unpickle(
|
|
|
|
|
client.userID, outboundSession.pickle);
|
|
|
|
|
_outboundGroupSessionDevices =
|
|
|
|
|
List<String>.from(json.decode(outboundSession.deviceIds));
|
|
|
|
|
} catch (e) {
|
|
|
|
|
_outboundGroupSession = null;
|
|
|
|
|
_outboundGroupSessionDevices = null;
|
|
|
|
|
print('[LibOlm] Unable to unpickle outboundGroupSession: ' +
|
|
|
|
|
e.toString());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_restoredOutboundGroupSession = true;
|
|
|
|
|
}
|
2020-02-15 07:48:41 +00:00
|
|
|
|
|
|
|
|
|
/// Encrypts the given json payload and creates a send-ready m.room.encrypted
|
|
|
|
|
/// payload. This will create a new outgoingGroupSession if necessary.
|
|
|
|
|
Future<Map<String, dynamic>> encryptGroupMessagePayload(
|
|
|
|
|
Map<String, dynamic> payload,
|
2020-03-30 09:08:38 +00:00
|
|
|
|
{String type = 'm.room.message'}) async {
|
|
|
|
|
if (!encrypted || !client.encryptionEnabled) return payload;
|
|
|
|
|
if (encryptionAlgorithm != 'm.megolm.v1.aes-sha2') {
|
|
|
|
|
throw ('Unknown encryption algorithm');
|
2020-02-15 07:48:41 +00:00
|
|
|
|
}
|
2020-05-17 07:54:34 +00:00
|
|
|
|
if (!_restoredOutboundGroupSession && client.database != null) {
|
|
|
|
|
// try to restore an outbound group session from the database
|
|
|
|
|
await restoreOutboundGroupSession();
|
|
|
|
|
}
|
|
|
|
|
// and clear the outbound session, if it needs clearing
|
|
|
|
|
await clearOutboundGroupSession();
|
|
|
|
|
// create a new one if none exists...
|
2020-02-15 07:48:41 +00:00
|
|
|
|
if (_outboundGroupSession == null) {
|
|
|
|
|
await createOutboundGroupSession();
|
|
|
|
|
}
|
2020-04-20 10:56:36 +00:00
|
|
|
|
final Map<String, dynamic> mRelatesTo = payload.remove('m.relates_to');
|
2020-03-30 09:08:38 +00:00
|
|
|
|
final payloadContent = {
|
|
|
|
|
'content': payload,
|
|
|
|
|
'type': type,
|
|
|
|
|
'room_id': id,
|
2020-02-15 07:48:41 +00:00
|
|
|
|
};
|
2020-03-30 09:08:38 +00:00
|
|
|
|
var encryptedPayload = <String, dynamic>{
|
|
|
|
|
'algorithm': 'm.megolm.v1.aes-sha2',
|
|
|
|
|
'ciphertext': _outboundGroupSession.encrypt(json.encode(payloadContent)),
|
|
|
|
|
'device_id': client.deviceID,
|
|
|
|
|
'sender_key': client.identityKey,
|
|
|
|
|
'session_id': _outboundGroupSession.session_id(),
|
2020-04-20 10:56:36 +00:00
|
|
|
|
if (mRelatesTo != null) 'm.relates_to': mRelatesTo,
|
2020-02-15 07:48:41 +00:00
|
|
|
|
};
|
2020-02-18 07:42:52 +00:00
|
|
|
|
await _storeOutboundGroupSession();
|
2020-02-15 07:48:41 +00:00
|
|
|
|
return encryptedPayload;
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-17 07:54:34 +00:00
|
|
|
|
Future<void> loadInboundGroupSessionKey(String sessionId) async {
|
|
|
|
|
if (sessionId == null || inboundGroupSessions.containsKey(sessionId)) return; // nothing to do
|
|
|
|
|
final session = await client.database.getDbInboundGroupSession(client.id, id, sessionId);
|
|
|
|
|
if (session == null) return; // no session found
|
|
|
|
|
try {
|
|
|
|
|
_inboundGroupSessions[sessionId] = SessionKey.fromDb(session, client.userID);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
print('[LibOlm] Could not unpickle inboundGroupSession: ' + e.toString());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> loadInboundGroupSessionKeyForEvent(Event event) async {
|
|
|
|
|
if (client.database == null) return; // nothing to do, no database
|
|
|
|
|
if (event.type != EventTypes.Encrypted) return;
|
|
|
|
|
if (!client.encryptionEnabled) {
|
|
|
|
|
throw (DecryptError.NOT_ENABLED);
|
|
|
|
|
}
|
|
|
|
|
if (event.content['algorithm'] != 'm.megolm.v1.aes-sha2') {
|
|
|
|
|
throw (DecryptError.UNKNOWN_ALGORITHM);
|
|
|
|
|
}
|
|
|
|
|
final String sessionId = event.content['session_id'];
|
|
|
|
|
return loadInboundGroupSessionKey(sessionId);
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-15 07:48:41 +00:00
|
|
|
|
/// Decrypts the given [event] with one of the available ingoingGroupSessions.
|
2020-02-21 08:44:05 +00:00
|
|
|
|
/// Returns a m.bad.encrypted event if it fails and does nothing if the event
|
|
|
|
|
/// was not encrypted.
|
2020-02-15 07:48:41 +00:00
|
|
|
|
Event decryptGroupMessage(Event event) {
|
2020-05-18 14:45:53 +00:00
|
|
|
|
if (event.type != EventTypes.Encrypted || event.content['ciphertext'] == null) return event;
|
2020-02-18 07:02:17 +00:00
|
|
|
|
Map<String, dynamic> decryptedPayload;
|
|
|
|
|
try {
|
2020-02-18 07:42:52 +00:00
|
|
|
|
if (!client.encryptionEnabled) {
|
2020-02-21 08:56:40 +00:00
|
|
|
|
throw (DecryptError.NOT_ENABLED);
|
2020-02-18 07:42:52 +00:00
|
|
|
|
}
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if (event.content['algorithm'] != 'm.megolm.v1.aes-sha2') {
|
2020-02-21 08:56:40 +00:00
|
|
|
|
throw (DecryptError.UNKNOWN_ALGORITHM);
|
2020-02-18 07:02:17 +00:00
|
|
|
|
}
|
2020-03-30 09:08:38 +00:00
|
|
|
|
final String sessionId = event.content['session_id'];
|
2020-05-15 18:40:17 +00:00
|
|
|
|
if (!inboundGroupSessions.containsKey(sessionId)) {
|
2020-02-21 08:56:40 +00:00
|
|
|
|
throw (DecryptError.UNKNOWN_SESSION);
|
2020-02-18 07:02:17 +00:00
|
|
|
|
}
|
2020-05-15 18:40:17 +00:00
|
|
|
|
final decryptResult = inboundGroupSessions[sessionId]
|
2020-02-18 07:02:17 +00:00
|
|
|
|
.inboundGroupSession
|
2020-03-30 09:08:38 +00:00
|
|
|
|
.decrypt(event.content['ciphertext']);
|
|
|
|
|
final messageIndexKey =
|
2020-02-18 07:02:17 +00:00
|
|
|
|
event.eventId + event.time.millisecondsSinceEpoch.toString();
|
2020-05-15 18:40:17 +00:00
|
|
|
|
if (inboundGroupSessions[sessionId].indexes.containsKey(messageIndexKey) &&
|
|
|
|
|
inboundGroupSessions[sessionId].indexes[messageIndexKey] !=
|
2020-02-18 07:02:17 +00:00
|
|
|
|
decryptResult.message_index) {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
if ((_outboundGroupSession?.session_id() ?? '') == sessionId) {
|
2020-02-18 07:42:52 +00:00
|
|
|
|
clearOutboundGroupSession();
|
|
|
|
|
}
|
2020-02-21 08:56:40 +00:00
|
|
|
|
throw (DecryptError.CHANNEL_CORRUPTED);
|
2020-02-18 07:02:17 +00:00
|
|
|
|
}
|
2020-05-15 18:40:17 +00:00
|
|
|
|
inboundGroupSessions[sessionId].indexes[messageIndexKey] =
|
2020-02-18 07:02:17 +00:00
|
|
|
|
decryptResult.message_index;
|
2020-02-18 07:42:52 +00:00
|
|
|
|
_storeOutboundGroupSession();
|
2020-02-18 07:02:17 +00:00
|
|
|
|
decryptedPayload = json.decode(decryptResult.plaintext);
|
|
|
|
|
} catch (exception) {
|
2020-05-18 15:31:40 +00:00
|
|
|
|
// alright, if this was actually by our own outbound group session, we might as well clear it
|
|
|
|
|
if ((_outboundGroupSession?.session_id() ?? '') == event.content['session_id']) {
|
|
|
|
|
clearOutboundGroupSession(wipe: true);
|
|
|
|
|
}
|
2020-02-21 08:56:40 +00:00
|
|
|
|
if (exception.toString() == DecryptError.UNKNOWN_SESSION) {
|
2020-02-21 08:44:05 +00:00
|
|
|
|
decryptedPayload = {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'content': event.content,
|
|
|
|
|
'type': 'm.room.encrypted',
|
2020-02-21 08:44:05 +00:00
|
|
|
|
};
|
2020-03-30 09:08:38 +00:00
|
|
|
|
decryptedPayload['content']['body'] = exception.toString();
|
|
|
|
|
decryptedPayload['content']['msgtype'] = 'm.bad.encrypted';
|
2020-02-21 08:44:05 +00:00
|
|
|
|
} else {
|
|
|
|
|
decryptedPayload = {
|
2020-05-18 14:45:53 +00:00
|
|
|
|
'content': <String, dynamic>{
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'msgtype': 'm.bad.encrypted',
|
|
|
|
|
'body': exception.toString(),
|
2020-02-21 08:44:05 +00:00
|
|
|
|
},
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'type': 'm.room.encrypted',
|
2020-02-21 08:44:05 +00:00
|
|
|
|
};
|
|
|
|
|
}
|
2020-02-18 07:02:17 +00:00
|
|
|
|
}
|
2020-04-20 10:56:36 +00:00
|
|
|
|
if (event.content['m.relates_to'] != null) {
|
|
|
|
|
decryptedPayload['content']['m.relates_to'] =
|
|
|
|
|
event.content['m.relates_to'];
|
|
|
|
|
}
|
2020-02-15 07:48:41 +00:00
|
|
|
|
return Event(
|
2020-03-30 09:08:38 +00:00
|
|
|
|
content: decryptedPayload['content'],
|
|
|
|
|
typeKey: decryptedPayload['type'],
|
2020-02-15 07:48:41 +00:00
|
|
|
|
senderId: event.senderId,
|
|
|
|
|
eventId: event.eventId,
|
|
|
|
|
roomId: event.roomId,
|
|
|
|
|
room: event.room,
|
|
|
|
|
time: event.time,
|
|
|
|
|
unsigned: event.unsigned,
|
|
|
|
|
stateKey: event.stateKey,
|
|
|
|
|
prevContent: event.prevContent,
|
|
|
|
|
status: event.status,
|
2020-05-15 18:40:17 +00:00
|
|
|
|
sortOrder: event.sortOrder,
|
2020-02-15 07:48:41 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
2020-01-18 14:49:15 +00:00
|
|
|
|
}
|
2020-02-21 08:56:40 +00:00
|
|
|
|
|
|
|
|
|
abstract class DecryptError {
|
2020-03-30 09:08:38 +00:00
|
|
|
|
static const String NOT_ENABLED = 'Encryption is not enabled in your client.';
|
|
|
|
|
static const String UNKNOWN_ALGORITHM = 'Unknown encryption algorithm.';
|
2020-02-21 08:56:40 +00:00
|
|
|
|
static const String UNKNOWN_SESSION =
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'The sender has not sent us the session key.';
|
2020-02-21 08:56:40 +00:00
|
|
|
|
static const String CHANNEL_CORRUPTED =
|
2020-03-30 09:08:38 +00:00
|
|
|
|
'The secure channel with the sender was corrupted.';
|
2020-02-21 08:56:40 +00:00
|
|
|
|
}
|