Merge branch 'krille/refactor-room-states' into 'main'

refactor: Room states

Closes #143

See merge request famedly/famedlysdk!663
This commit is contained in:
Krille Fear 2021-03-09 18:12:23 +00:00
commit 4bd8359518
7 changed files with 185 additions and 283 deletions

View File

@ -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';

View File

@ -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<String, Event> states) => states.forEach(
(String key, Event state) {
if (state.eventId == redacts) {

View File

@ -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<String, Map<String, Event>> states = {};
/// Key-Value store for ephemerals.
Map<String, BasicRoomEvent> 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<String> get pinnedEventIds => states[EventTypes.RoomPinnedEvents] != null
? (states[EventTypes.RoomPinnedEvents].content['pinned'] is List<String>
? states[EventTypes.RoomPinnedEvents].content['pinned']
List<String> get pinnedEventIds => getState(EventTypes.RoomPinnedEvents) !=
null
? (getState(EventTypes.RoomPinnedEvents).content['pinned'] is List<String>
? getState(EventTypes.RoomPinnedEvents).content['pinned']
: <String>[])
: <String>[];
@ -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<String, dynamic>) {
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<String> setPower(String userID, int power) async {
if (states[EventTypes.RoomPowerLevels] == null) return null;
if (getState(EventTypes.RoomPowerLevels) == null) return null;
final powerMap = <String, dynamic>{}
..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 = <User>[];
if (states[EventTypes.RoomMember] is Map<String, dynamic>) {
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<User> 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<String, int> get powerLevels {
Event powerLevelState = states[EventTypes.RoomPowerLevels];
final powerLevelState = getState(EventTypes.RoomPowerLevels);
if (powerLevelState.content['users'] is Map<String, int>) {
return powerLevelState.content['users'];
}
@ -1653,7 +1661,7 @@ class Room {
/// Returns all aliases for this room.
List<String> get aliases {
var aliases = <String>[];
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']);
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
import '../../famedlysdk.dart';
/// Matrix room states are addressed by a tuple of the [type] and an
/// optional [stateKey].
class StatesMap {
Map<String, Map<String, Event>> 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);
}

View File

@ -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...

View File

@ -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, <String>[]);
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);

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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');
});
});
}