From 1f8285c3e7cc9a05730a9680eb39700544508673 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Tue, 2 Mar 2021 09:05:46 +0100 Subject: [PATCH] refactor: Room states --- lib/famedlysdk.dart | 1 - lib/src/client.dart | 10 +- lib/src/room.dart | 76 +++++----- lib/src/utils/states_map.dart | 62 -------- test/encryption/key_manager_test.dart | 40 ++--- test/room_test.dart | 208 ++++++++++++++------------ test/states_map_test.dart | 71 --------- 7 files changed, 185 insertions(+), 283 deletions(-) delete mode 100644 lib/src/utils/states_map.dart delete mode 100644 test/states_map_test.dart diff --git a/lib/famedlysdk.dart b/lib/famedlysdk.dart index aac105e0..1f7aba74 100644 --- a/lib/famedlysdk.dart +++ b/lib/famedlysdk.dart @@ -27,7 +27,6 @@ export 'src/utils/matrix_id_string_extension.dart'; export 'src/utils/uri_extension.dart'; export 'src/utils/matrix_localizations.dart'; export 'src/utils/receipt.dart'; -export 'src/utils/states_map.dart'; export 'src/utils/sync_update_extension.dart'; export 'src/utils/to_device_event.dart'; export 'src/utils/uia_request.dart'; diff --git a/lib/src/client.dart b/lib/src/client.dart index 57e1214d..ccb559a1 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -271,8 +271,12 @@ class Client extends MatrixApi { } for (var i = 0; i < rooms.length; i++) { if (rooms[i].membership == Membership.invite && - rooms[i].states[userID]?.senderId == userId && - rooms[i].states[userID].content['is_direct'] == true) { + rooms[i].getState(EventTypes.RoomMember, userID)?.senderId == + userId && + rooms[i] + .getState(EventTypes.RoomMember, userID) + .content['is_direct'] == + true) { return rooms[i].id; } } @@ -1412,7 +1416,7 @@ sort order of ${prevState.sortOrder}. This should never happen...'''); } if (stateEvent.type == EventTypes.Redaction) { final String redacts = eventUpdate.content['redacts']; - room.states.states.forEach( + room.states.forEach( (String key, Map states) => states.forEach( (String key, Event state) { if (state.eventId == redacts) { diff --git a/lib/src/room.dart b/lib/src/room.dart index 8018922e..e2e985d1 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -33,7 +33,6 @@ import 'utils/markdown.dart'; import 'utils/marked_unread.dart'; import 'utils/matrix_file.dart'; import 'utils/matrix_localizations.dart'; -import 'utils/states_map.dart'; enum PushRuleState { notify, mentions_only, dont_notify } enum JoinRules { public, knock, invite, private } @@ -70,7 +69,10 @@ class Room { /// The number of users with membership of invite. int mInvitedMemberCount; - StatesMap states = StatesMap(); + /// The room states are a key value store of the key ([type],[state_key]) => State(event). + /// In a lot of cases the [state_key] might be an empty string. You **should** use the + /// methods [getState] and [setState] to interact with the room states. + Map> states = {}; /// Key-Value store for ephemerals. Map ephemerals = {}; @@ -128,7 +130,7 @@ class Room { /// Returns the [Event] for the given [typeKey] and optional [stateKey]. /// If no [stateKey] is provided, it defaults to an empty string. Event getState(String typeKey, [String stateKey = '']) => - states.states[typeKey] != null ? states.states[typeKey][stateKey] : null; + states[typeKey] != null ? states[typeKey][stateKey] : null; /// Adds the [state] to this room and overwrites a state with the same /// typeKey/stateKey key pair if there is one. @@ -152,10 +154,10 @@ class Room { if (oldStateEvent != null && oldStateEvent.sortOrder >= state.sortOrder) { return; } - if (!states.states.containsKey(state.type)) { - states.states[state.type] = {}; + if (!states.containsKey(state.type)) { + states[state.type] = {}; } - states.states[state.type][state.stateKey ?? ''] = state; + states[state.type][state.stateKey ?? ''] = state; } /// ID of the fully read marker event. @@ -173,16 +175,17 @@ class Room { StreamController.broadcast(); /// The name of the room if set by a participant. - String get name => states[EventTypes.RoomName] != null && - states[EventTypes.RoomName].content['name'] is String - ? states[EventTypes.RoomName].content['name'] + String get name => getState(EventTypes.RoomName) != null && + getState(EventTypes.RoomName).content['name'] is String + ? getState(EventTypes.RoomName).content['name'] : ''; /// The pinned events for this room. If there are none this returns an empty /// list. - List get pinnedEventIds => states[EventTypes.RoomPinnedEvents] != null - ? (states[EventTypes.RoomPinnedEvents].content['pinned'] is List - ? states[EventTypes.RoomPinnedEvents].content['pinned'] + List get pinnedEventIds => getState(EventTypes.RoomPinnedEvents) != + null + ? (getState(EventTypes.RoomPinnedEvents).content['pinned'] is List + ? getState(EventTypes.RoomPinnedEvents).content['pinned'] : []) : []; @@ -205,19 +208,21 @@ class Room { } /// The topic of the room if set by a participant. - String get topic => states[EventTypes.RoomTopic] != null && - states[EventTypes.RoomTopic].content['topic'] is String - ? states[EventTypes.RoomTopic].content['topic'] + String get topic => getState(EventTypes.RoomTopic) != null && + getState(EventTypes.RoomTopic).content['topic'] is String + ? getState(EventTypes.RoomTopic).content['topic'] : ''; /// The avatar of the room if set by a participant. Uri get avatar { - if (states[EventTypes.RoomAvatar] != null && - states[EventTypes.RoomAvatar].content['url'] is String) { - return Uri.tryParse(states[EventTypes.RoomAvatar].content['url']); + if (getState(EventTypes.RoomAvatar) != null && + getState(EventTypes.RoomAvatar).content['url'] is String) { + return Uri.tryParse(getState(EventTypes.RoomAvatar).content['url']); } - if (mHeroes != null && mHeroes.length == 1 && states[mHeroes[0]] != null) { - return states[mHeroes[0]].asUser.avatarUrl; + if (mHeroes != null && + mHeroes.length == 1 && + getState(EventTypes.RoomMember, mHeroes.first) != null) { + return getState(EventTypes.RoomMember, mHeroes.first).asUser.avatarUrl; } if (membership == Membership.invite && getState(EventTypes.RoomMember, client.userID) != null) { @@ -227,10 +232,11 @@ class Room { } /// The address in the format: #roomname:homeserver.org. - String get canonicalAlias => states[EventTypes.RoomCanonicalAlias] != null && - states[EventTypes.RoomCanonicalAlias].content['alias'] is String - ? states[EventTypes.RoomCanonicalAlias].content['alias'] - : ''; + String get canonicalAlias => + getState(EventTypes.RoomCanonicalAlias) != null && + getState(EventTypes.RoomCanonicalAlias).content['alias'] is String + ? getState(EventTypes.RoomCanonicalAlias).content['alias'] + : ''; /// If this room is a direct chat, this is the matrix ID of the user. /// Returns null otherwise. @@ -289,7 +295,7 @@ class Room { if (lastEvent == null) { states.forEach((final String key, final entry) { if (!entry.containsKey('')) return; - final Event state = entry['']; + final state = entry['']; if (state.originServerTs != null && state.originServerTs.millisecondsSinceEpoch > lastTime.millisecondsSinceEpoch) { @@ -358,7 +364,7 @@ class Room { } else { if (states[EventTypes.RoomMember] is Map) { for (var entry in states[EventTypes.RoomMember].entries) { - Event state = entry.value; + final state = entry.value; if (state.type == EventTypes.RoomMember && state.stateKey != client?.userID) heroes.add(state.stateKey); } @@ -565,7 +571,7 @@ class Room { } } // finally add all the room emotes - final allRoomEmotes = states.states['im.ponies.room_emotes']; + final allRoomEmotes = states['im.ponies.room_emotes']; if (allRoomEmotes != null) { for (final entry in allRoomEmotes.entries) { final stateKey = entry.key; @@ -923,9 +929,9 @@ class Room { /// 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 setPower(String userID, int power) async { - if (states[EventTypes.RoomPowerLevels] == null) return null; + if (getState(EventTypes.RoomPowerLevels) == null) return null; final powerMap = {} - ..addAll(states[EventTypes.RoomPowerLevels].content); + ..addAll(getState(EventTypes.RoomPowerLevels).content); if (powerMap['users'] == null) powerMap['users'] = {}; powerMap['users'][userID] = power; @@ -1171,7 +1177,7 @@ class Room { var userList = []; if (states[EventTypes.RoomMember] is Map) { for (var entry in states[EventTypes.RoomMember].entries) { - Event state = entry.value; + final state = entry.value; if (state.type == EventTypes.RoomMember) userList.add(state.asUser); } } @@ -1218,7 +1224,9 @@ class Room { /// the homeserver and waits for a response. @Deprecated('Use [requestUser] instead') Future getUserByMXID(String mxID) async { - if (states[mxID] != null) return states[mxID].asUser; + if (getState(EventTypes.RoomMember, mxID) != null) { + return getState(EventTypes.RoomMember, mxID).asUser; + } return requestUser(mxID); } @@ -1351,7 +1359,7 @@ class Room { /// Returns the power level of the given user ID. int getPowerLevelByUserId(String userId) { var powerLevel = 0; - Event powerLevelState = states[EventTypes.RoomPowerLevels]; + final powerLevelState = getState(EventTypes.RoomPowerLevels); if (powerLevelState == null) return powerLevel; if (powerLevelState.content['users_default'] is int) { powerLevel = powerLevelState.content['users_default']; @@ -1370,7 +1378,7 @@ class Room { /// Returns the power levels from all users for this room or null if not given. Map get powerLevels { - Event powerLevelState = states[EventTypes.RoomPowerLevels]; + final powerLevelState = getState(EventTypes.RoomPowerLevels); if (powerLevelState.content['users'] is Map) { return powerLevelState.content['users']; } @@ -1653,7 +1661,7 @@ class Room { /// Returns all aliases for this room. List get aliases { var aliases = []; - for (var aliasEvent in states.states[EventTypes.RoomAliases].values) { + for (var aliasEvent in states[EventTypes.RoomAliases].values) { if (aliasEvent.content['aliases'] is List) { aliases.addAll(aliasEvent.content['aliases']); } diff --git a/lib/src/utils/states_map.dart b/lib/src/utils/states_map.dart deleted file mode 100644 index 30609edc..00000000 --- a/lib/src/utils/states_map.dart +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Famedly Matrix SDK - * Copyright (C) 2020, 2021 Famedly GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import '../../famedlysdk.dart'; - -/// Matrix room states are addressed by a tuple of the [type] and an -/// optional [stateKey]. -class StatesMap { - Map> states = {}; - - /// Returns either the [Event] or a map of state_keys to [Event] objects. - /// If you just enter a MatrixID, it will try to return the corresponding m.room.member event. - dynamic operator [](String key) { - //print("[Warning] This method will be depracated in the future!"); - if (key == null) return null; - if (key.startsWith('@') && key.contains(':')) { - if (!states.containsKey(EventTypes.RoomMember)) { - states[EventTypes.RoomMember] = {}; - } - return states[EventTypes.RoomMember][key]; - } - if (!states.containsKey(key)) states[key] = {}; - if (states[key][''] is Event) { - return states[key]['']; - } else if (states[key].isEmpty) { - return null; - } else { - return states[key]; - } - } - - void operator []=(String key, Event val) { - //print("[Warning] This method will be depracated in the future!"); - if (key.startsWith('@') && key.contains(':')) { - if (!states.containsKey(EventTypes.RoomMember)) { - states[EventTypes.RoomMember] = {}; - } - states[EventTypes.RoomMember][key] = val; - } - if (!states.containsKey(key)) states[key] = {}; - states[key][val.stateKey ?? ''] = val; - } - - bool containsKey(String key) => states.containsKey(key); - - void forEach(f) => states.forEach(f); -} diff --git a/test/encryption/key_manager_test.dart b/test/encryption/key_manager_test.dart index 32fe345e..bbd2b174 100644 --- a/test/encryption/key_manager_test.dart +++ b/test/encryption/key_manager_test.dart @@ -339,25 +339,27 @@ void main() { final room = Room(id: roomId, client: client); client.rooms.add(room); // we build up an encrypted message so that we can test if it successfully decrypted afterwards - room.states['m.room.encrypted'] = Event( - senderId: '@test:example.com', - type: 'm.room.encrypted', - roomId: room.id, - room: room, - eventId: '12345', - originServerTs: DateTime.now(), - content: { - 'algorithm': AlgorithmTypes.megolmV1AesSha2, - 'ciphertext': session.encrypt(json.encode({ - 'type': 'm.room.message', - 'content': {'msgtype': 'm.text', 'body': 'foxies'}, - })), - 'device_id': client.deviceID, - 'sender_key': client.identityKey, - 'session_id': sessionId, - }, - stateKey: '', - sortOrder: 42.0, + room.setState( + Event( + senderId: '@test:example.com', + type: 'm.room.encrypted', + roomId: room.id, + room: room, + eventId: '12345', + originServerTs: DateTime.now(), + content: { + 'algorithm': AlgorithmTypes.megolmV1AesSha2, + 'ciphertext': session.encrypt(json.encode({ + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'foxies'}, + })), + 'device_id': client.deviceID, + 'sender_key': client.identityKey, + 'session_id': sessionId, + }, + stateKey: '', + sortOrder: 42.0, + ), ); expect(room.lastEvent.type, 'm.room.encrypted'); // set a payload... diff --git a/test/room_test.dart b/test/room_test.dart index 7270b666..05554cbf 100644 --- a/test/room_test.dart +++ b/test/room_test.dart @@ -112,97 +112,113 @@ void main() { expect(room.getState('m.room.join_rules').content['join_rule'], 'public'); expect(room.roomAccountData['com.test.foo'].content['foo'], 'bar'); - room.states['m.room.canonical_alias'] = Event( - senderId: '@test:example.com', - type: 'm.room.canonical_alias', - roomId: room.id, - room: room, - eventId: '123', - content: {'alias': '#testalias:example.com'}, - stateKey: ''); + room.setState( + Event( + senderId: '@test:example.com', + type: 'm.room.canonical_alias', + roomId: room.id, + room: room, + eventId: '123', + content: {'alias': '#testalias:example.com'}, + stateKey: ''), + ); expect(room.displayname, 'testalias'); expect(room.canonicalAlias, '#testalias:example.com'); - room.states['m.room.name'] = Event( - senderId: '@test:example.com', - type: 'm.room.name', - roomId: room.id, - room: room, - eventId: '123', - content: {'name': 'testname'}, - stateKey: ''); + room.setState( + Event( + senderId: '@test:example.com', + type: 'm.room.name', + roomId: room.id, + room: room, + eventId: '123', + content: {'name': 'testname'}, + stateKey: ''), + ); expect(room.displayname, 'testname'); expect(room.topic, ''); - room.states['m.room.topic'] = Event( - senderId: '@test:example.com', - type: 'm.room.topic', - roomId: room.id, - room: room, - eventId: '123', - content: {'topic': 'testtopic'}, - stateKey: ''); + room.setState( + Event( + senderId: '@test:example.com', + type: 'm.room.topic', + roomId: room.id, + room: room, + eventId: '123', + content: {'topic': 'testtopic'}, + stateKey: ''), + ); expect(room.topic, 'testtopic'); expect(room.avatar, null); - room.states['m.room.avatar'] = Event( - senderId: '@test:example.com', - type: 'm.room.avatar', - roomId: room.id, - room: room, - eventId: '123', - content: {'url': 'mxc://testurl'}, - stateKey: ''); + room.setState( + Event( + senderId: '@test:example.com', + type: 'm.room.avatar', + roomId: room.id, + room: room, + eventId: '123', + content: {'url': 'mxc://testurl'}, + stateKey: ''), + ); expect(room.avatar.toString(), 'mxc://testurl'); expect(room.pinnedEventIds, []); - room.states['m.room.pinned_events'] = Event( - senderId: '@test:example.com', - type: 'm.room.pinned_events', - roomId: room.id, - room: room, - eventId: '123', - content: { - 'pinned': ['1234'] - }, - stateKey: ''); + room.setState( + Event( + senderId: '@test:example.com', + type: 'm.room.pinned_events', + roomId: room.id, + room: room, + eventId: '123', + content: { + 'pinned': ['1234'] + }, + stateKey: ''), + ); expect(room.pinnedEventIds.first, '1234'); - room.states['m.room.message'] = Event( - senderId: '@test:example.com', - type: 'm.room.message', - roomId: room.id, - room: room, - eventId: '12345', - originServerTs: DateTime.now(), - content: {'msgtype': 'm.text', 'body': 'test'}, - stateKey: ''); + room.setState( + Event( + senderId: '@test:example.com', + type: 'm.room.message', + roomId: room.id, + room: room, + eventId: '12345', + originServerTs: DateTime.now(), + content: {'msgtype': 'm.text', 'body': 'test'}, + stateKey: ''), + ); expect(room.lastEvent.eventId, '12345'); expect(room.lastEvent.body, 'test'); expect(room.timeCreated, room.lastEvent.originServerTs); }); test('multiple last event with same sort order', () { - room.states['m.room.encrypted'] = Event( - senderId: '@test:example.com', - type: 'm.room.encrypted', - roomId: room.id, - room: room, - eventId: '12345', - originServerTs: DateTime.now(), - content: {'msgtype': 'm.text', 'body': 'test'}, - stateKey: '', - sortOrder: 42.0); + room.setState( + Event( + senderId: '@test:example.com', + type: 'm.room.encrypted', + roomId: room.id, + room: room, + eventId: '12345', + originServerTs: DateTime.now(), + content: {'msgtype': 'm.text', 'body': 'test'}, + stateKey: '', + sortOrder: 42.0), + ); expect(room.lastEvent.type, 'm.room.encrypted'); - room.states['m.room.messge'] = Event( - senderId: '@test:example.com', - type: 'm.room.messge', - roomId: room.id, - room: room, - eventId: '12345', - originServerTs: DateTime.now(), - content: {'msgtype': 'm.text', 'body': 'test'}, - stateKey: '', - sortOrder: 42.0); + room.setState( + Event( + senderId: '@test:example.com', + type: 'm.room.messge', + roomId: room.id, + room: room, + eventId: '12345', + originServerTs: DateTime.now(), + content: {'msgtype': 'm.text', 'body': 'test'}, + stateKey: '', + sortOrder: 42.0), + ); expect(room.lastEvent.type, 'm.room.encrypted'); }); @@ -249,25 +265,27 @@ void main() { }); test('PowerLevels', () async { - room.states['m.room.power_levels'] = Event( - senderId: '@test:example.com', - type: 'm.room.power_levels', - roomId: room.id, - room: room, - eventId: '123', - content: { - 'ban': 50, - 'events': {'m.room.name': 100, 'm.room.power_levels': 100}, - 'events_default': 0, - 'invite': 50, - 'kick': 50, - 'notifications': {'room': 20}, - 'redact': 50, - 'state_default': 50, - 'users': {'@test:fakeServer.notExisting': 100}, - 'users_default': 10 - }, - stateKey: ''); + room.setState( + Event( + senderId: '@test:example.com', + type: 'm.room.power_levels', + roomId: room.id, + room: room, + eventId: '123', + content: { + 'ban': 50, + 'events': {'m.room.name': 100, 'm.room.power_levels': 100}, + 'events_default': 0, + 'invite': 50, + 'kick': 50, + 'notifications': {'room': 20}, + 'redact': 50, + 'state_default': 50, + 'users': {'@test:fakeServer.notExisting': 100}, + 'users_default': 10 + }, + stateKey: ''), + ); expect(room.ownPowerLevel, 100); expect(room.getPowerLevelByUserId(matrix.userID), room.ownPowerLevel); expect(room.getPowerLevelByUserId('@nouser:example.com'), 10); @@ -283,9 +301,10 @@ void main() { expect(room.canSendEvent('m.room.power_levels'), true); expect(room.canSendEvent('m.room.member'), true); expect(room.powerLevels, - room.states['m.room.power_levels'].content['users']); + room.getState('m.room.power_levels').content['users']); - room.states['m.room.power_levels'] = Event( + room.setState( + Event( senderId: '@test:example.com', type: 'm.room.power_levels', roomId: room.id, @@ -303,7 +322,10 @@ void main() { 'users': {}, 'users_default': 0 }, - stateKey: ''); + stateKey: '', + sortOrder: 1, + ), + ); expect(room.ownPowerLevel, 0); expect(room.canBan, false); expect(room.canInvite, false); diff --git a/test/states_map_test.dart b/test/states_map_test.dart deleted file mode 100644 index cae5826b..00000000 --- a/test/states_map_test.dart +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Famedly Matrix SDK - * Copyright (C) 2019, 2020 Famedly GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import 'package:famedlysdk/famedlysdk.dart'; -import 'package:logger/logger.dart'; -import 'package:test/test.dart'; -import 'package:famedlysdk/src/utils/states_map.dart'; - -void main() { - /// All Tests related to the ChatTime - group('StateKeys', () { - Logs().level = Level.error; - test('Operator overload', () async { - var states = StatesMap(); - states['m.room.name'] = Event( - eventId: '1', - content: {'name': 'test'}, - type: 'm.room.name', - stateKey: '', - roomId: '!test:test.test', - senderId: '@alice:test.test'); - - states['@alice:test.test'] = Event( - eventId: '2', - content: {'membership': 'join'}, - type: 'm.room.name', - stateKey: '@alice:test.test', - roomId: '!test:test.test', - senderId: '@alice:test.test'); - - states['m.room.member']['@bob:test.test'] = Event( - eventId: '3', - content: {'membership': 'join'}, - type: 'm.room.name', - stateKey: '@bob:test.test', - roomId: '!test:test.test', - senderId: '@bob:test.test'); - - states['com.test.custom'] = Event( - eventId: '4', - content: {'custom': 'stuff'}, - type: 'com.test.custom', - stateKey: 'customStateKey', - roomId: '!test:test.test', - senderId: '@bob:test.test'); - - expect(states['m.room.name'].eventId, '1'); - expect(states['@alice:test.test'].eventId, '2'); - expect(states['m.room.member']['@alice:test.test'].eventId, '2'); - expect(states['@bob:test.test'].eventId, '3'); - expect(states['m.room.member']['@bob:test.test'].eventId, '3'); - expect(states['m.room.member'].length, 2); - expect(states['com.test.custom']['customStateKey'].eventId, '4'); - }); - }); -}