diff --git a/lib/famedlysdk.dart b/lib/famedlysdk.dart index 0636a0b3..a9a1a65b 100644 --- a/lib/famedlysdk.dart +++ b/lib/famedlysdk.dart @@ -35,6 +35,7 @@ export 'package:famedlysdk/src/Connection.dart'; export 'package:famedlysdk/src/Event.dart'; export 'package:famedlysdk/src/Room.dart'; export 'package:famedlysdk/src/RoomList.dart'; +export 'package:famedlysdk/src/RoomState.dart'; export 'package:famedlysdk/src/Store.dart'; export 'package:famedlysdk/src/Timeline.dart'; export 'package:famedlysdk/src/User.dart'; diff --git a/lib/src/AccountData.dart b/lib/src/AccountData.dart new file mode 100644 index 00000000..b3a0a96a --- /dev/null +++ b/lib/src/AccountData.dart @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019 Zender & Kurtz GbR. + * + * Authors: + * Christian Pauly + * Marcel Radzio + * + * 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 + * along with famedlysdk. If not, see . + */ + +import 'package:famedlysdk/src/RoomState.dart'; + +class AccountData { + /// The json payload of the content. The content highly depends on the type. + final Map content; + + /// The type String of this event. For example 'm.room.message'. + final String typeKey; + + AccountData({this.content, this.typeKey}); + + /// Get a State event from a table row or from the event stream. + factory AccountData.fromJson(Map jsonPayload) { + final Map content = + RoomState.getMapFromPayload(jsonPayload['content']); + return AccountData(content: content, typeKey: jsonPayload['type']); + } +} diff --git a/lib/src/Client.dart b/lib/src/Client.dart index 28dcb07c..305538e7 100644 --- a/lib/src/Client.dart +++ b/lib/src/Client.dart @@ -24,6 +24,10 @@ import 'dart:async'; import 'dart:core'; +import 'package:famedlysdk/src/AccountData.dart'; +import 'package:famedlysdk/src/Presence.dart'; +import 'package:famedlysdk/src/sync/UserUpdate.dart'; + import 'Connection.dart'; import 'Room.dart'; import 'RoomList.dart'; @@ -33,6 +37,9 @@ import 'requests/SetPushersRequest.dart'; import 'responses/ErrorResponse.dart'; import 'responses/PushrulesResponse.dart'; +typedef AccountDataEventCB = void Function(AccountData accountData); +typedef PresenceCB = void Function(Presence presence); + /// Represents a Matrix client to communicate with a /// [Matrix](https://matrix.org) homeserver and is the entry point for this /// SDK. @@ -86,6 +93,56 @@ class Client { /// Returns the current login state. bool isLogged() => accessToken != null; + /// A list of all rooms the user is participating or invited. + RoomList roomList; + + /// Key/Value store of account data. + Map accountData = {}; + + /// Presences of users by a given matrix ID + Map presences = {}; + + /// Callback will be called on account data updates. + AccountDataEventCB onAccountData; + + /// Callback will be called on presences. + PresenceCB onPresence; + + void handleUserUpdate(UserUpdate userUpdate) { + if (userUpdate.type == "account_data") { + AccountData newAccountData = AccountData.fromJson(userUpdate.content); + accountData[newAccountData.typeKey] = newAccountData; + if (onAccountData != null) onAccountData(newAccountData); + } + if (userUpdate.type == "presence") { + Presence newPresence = Presence.fromJson(userUpdate.content); + presences[newPresence.sender] = newPresence; + if (onPresence != null) onPresence(newPresence); + } + } + + Map get directChats => + accountData["m.direct"] != null ? accountData["m.direct"].content : {}; + + /// Returns the (first) room ID from the store which is a private chat with the user [userId]. + /// Returns null if there is none. + String getDirectChatFromUserId(String userId) { + if (accountData["m.direct"] != null && + accountData["m.direct"].content[userId] is List && + accountData["m.direct"].content[userId].length > 0) { + if (roomList.getRoomById(accountData["m.direct"].content[userId][0]) != + null) return accountData["m.direct"].content[userId][0]; + (accountData["m.direct"].content[userId] as List) + .remove(accountData["m.direct"].content[userId][0]); + connection.jsonRequest( + type: HTTPType.PUT, + action: "/client/r0/user/${userID}/account_data/m.direct", + data: directChats); + return getDirectChatFromUserId(userId); + } + return null; + } + /// Checks the supported versions of the Matrix protocol and the supported /// login types. Returns false if the server is not compatible with the /// client. Automatically sets [matrixVersions] and [lazyLoadMembers]. @@ -225,12 +282,12 @@ class Client { /// defined by the autojoin room feature in Synapse. Future> loadFamedlyContacts() async { List contacts = []; - Room contactDiscoveryRoom = await store + Room contactDiscoveryRoom = roomList .getRoomByAlias("#famedlyContactDiscovery:${userID.split(":")[1]}"); if (contactDiscoveryRoom != null) contacts = await contactDiscoveryRoom.requestParticipants(); else - contacts = await store.loadContacts(); + contacts = await store?.loadContacts(); return contacts; } diff --git a/lib/src/Connection.dart b/lib/src/Connection.dart index 68cdecfd..b7819398 100644 --- a/lib/src/Connection.dart +++ b/lib/src/Connection.dart @@ -25,6 +25,8 @@ import 'dart:async'; import 'dart:convert'; import 'dart:core'; +import 'package:famedlysdk/src/Room.dart'; +import 'package:famedlysdk/src/RoomList.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; @@ -50,9 +52,9 @@ class Connection { })); } - String get _syncFilters => '{"room":{"state":{"lazy_load_members":true}}}'; + static String syncFilters = '{"room":{"state":{"lazy_load_members":true}}}'; - String get _firstSyncFilters => + static String firstSyncFilters = '{"room":{"include_leave":true,"state":{"lazy_load_members":true}}}'; /// Handles the connection to the Matrix Homeserver. You can change this to a @@ -147,13 +149,34 @@ class Connection { client.lazyLoadMembers = newLazyLoadMembers; client.prevBatch = newPrevBatch; - client.store?.storeClient(); + List rooms = []; + if (client.store != null) { + client.store.storeClient(); + rooms = await client.store + .getRoomList(onlyLeft: false, onlyGroups: false, onlyDirect: false); + client.accountData = await client.store.getAccountData(); + client.presences = await client.store.getPresences(); + } + + client.roomList = RoomList( + client: client, + onlyLeft: false, + onlyDirect: false, + onlyGroups: false, + onUpdate: null, + onInsert: null, + onRemove: null, + rooms: rooms); + + _userEventSub ??= onUserEvent.stream.listen(client.handleUserUpdate); onLoginStateChanged.add(LoginState.logged); _sync(); } + StreamSubscription _userEventSub; + /// Resets all settings and stops the synchronisation. void clear() { client.store?.clear(); @@ -261,10 +284,10 @@ class Connection { Future _sync() async { if (client.isLogged() == false) return; - String action = "/client/r0/sync?filter=$_firstSyncFilters"; + String action = "/client/r0/sync?filter=$firstSyncFilters"; if (client.prevBatch != null) { - action = "/client/r0/sync?filter=$_syncFilters"; + action = "/client/r0/sync?filter=$syncFilters"; action += "&timeout=30000"; action += "&since=${client.prevBatch}"; } @@ -450,6 +473,8 @@ class Connection { } } +typedef _FutureVoidCallback = Future Function(); + class _LifecycleEventHandler extends WidgetsBindingObserver { _LifecycleEventHandler({this.resumeCallBack, this.suspendingCallBack}); @@ -471,6 +496,4 @@ class _LifecycleEventHandler extends WidgetsBindingObserver { } } -typedef _FutureVoidCallback = Future Function(); - enum LoginState { logged, loggedOut } diff --git a/lib/src/Event.dart b/lib/src/Event.dart index c5fb1ab6..25a9d869 100644 --- a/lib/src/Event.dart +++ b/lib/src/Event.dart @@ -21,37 +21,14 @@ * along with famedlysdk. If not, see . */ -import 'dart:convert'; - -import 'package:famedlysdk/src/Client.dart'; +import 'package:famedlysdk/src/RoomState.dart'; import 'package:famedlysdk/src/sync/EventUpdate.dart'; import 'package:famedlysdk/src/utils/ChatTime.dart'; import './Room.dart'; -import './User.dart'; - -/// A single Matrix event, e.g. a message in a chat. -class Event { - /// The Matrix ID for this event in the format '$localpart:server.abc'. - final String id; - - /// The room this event belongs to. - final Room room; - - /// The time this event has received at the server. - final ChatTime time; - - /// The user who has sent this event. - final User sender; - - /// The user who is the target of this event e.g. for a m.room.member event. - final User stateKey; - - /// The type of this event. Mostly this is 'timeline'. - final String environment; - - Event replyEvent; +/// Defines a timeline event for a room. +class Event extends RoomState { /// The status of this event. /// -1=ERROR /// 0=SENDING @@ -59,20 +36,53 @@ class Event { /// 2=RECEIVED int status; - /// The json payload of the content. The content highly depends on the type. - final Map content; + static const int defaultStatus = 2; Event( - this.id, - this.sender, - this.time, { - this.room, - this.stateKey, - this.status = 2, - this.environment, - this.content, - this.replyEvent, - }); + {this.status = defaultStatus, + dynamic content, + String typeKey, + String eventId, + String roomId, + String senderId, + ChatTime time, + dynamic unsigned, + dynamic prevContent, + String stateKey, + Room room}) + : super( + content: content, + typeKey: typeKey, + eventId: eventId, + roomId: roomId, + senderId: senderId, + time: time, + unsigned: unsigned, + prevContent: prevContent, + stateKey: stateKey, + room: room); + + /// Get a State event from a table row or from the event stream. + factory Event.fromJson(Map jsonPayload, Room room) { + final Map content = + RoomState.getMapFromPayload(jsonPayload['content']); + final Map unsigned = + RoomState.getMapFromPayload(jsonPayload['unsigned']); + final Map prevContent = + RoomState.getMapFromPayload(jsonPayload['prev_content']); + return Event( + status: jsonPayload['status'] ?? defaultStatus, + content: content, + typeKey: jsonPayload['type'], + eventId: jsonPayload['event_id'], + roomId: jsonPayload['room_id'], + senderId: jsonPayload['sender'], + time: ChatTime(jsonPayload['origin_server_ts']), + unsigned: unsigned, + prevContent: prevContent, + stateKey: jsonPayload['state_key'], + room: room); + } /// Returns the body of this event if it has a body. String get text => content["body"] ?? ""; @@ -84,89 +94,7 @@ class Event { String getBody() { if (text != "") return text; if (formattedText != "") return formattedText; - return "*** Unable to parse Content ***"; - } - - /// Get the real type. - EventTypes get type { - switch (environment) { - case "m.room.avatar": - return EventTypes.RoomAvatar; - case "m.room.name": - return EventTypes.RoomName; - case "m.room.topic": - return EventTypes.RoomTopic; - case "m.room.Aliases": - return EventTypes.RoomAliases; - case "m.room.canonical_alias": - return EventTypes.RoomCanonicalAlias; - case "m.room.create": - return EventTypes.RoomCreate; - case "m.room.join_rules": - return EventTypes.RoomJoinRules; - case "m.room.member": - return EventTypes.RoomMember; - case "m.room.power_levels": - return EventTypes.RoomPowerLevels; - case "m.room.guest_access": - return EventTypes.GuestAccess; - case "m.room.history_visibility": - return EventTypes.HistoryVisibility; - case "m.room.message": - switch (content["msgtype"] ?? "m.text") { - case "m.text": - if (content.containsKey("m.relates_to")) { - return EventTypes.Reply; - } - return EventTypes.Text; - case "m.notice": - return EventTypes.Notice; - case "m.emote": - return EventTypes.Emote; - case "m.image": - return EventTypes.Image; - case "m.video": - return EventTypes.Video; - case "m.audio": - return EventTypes.Audio; - case "m.file": - return EventTypes.File; - case "m.location": - return EventTypes.Location; - } - } - return EventTypes.Unknown; - } - - /// Generate a new Event object from a json string, mostly a table row. - static Event fromJson(Map jsonObj, Room room, - {User senderUser, User stateKeyUser}) { - Map content = jsonObj["content"]; - - if (content == null && jsonObj["content_json"] != null) - try { - content = json.decode(jsonObj["content_json"]); - } catch (e) { - if (room.client.debug) { - print("jsonObj decode of event content failed: ${e.toString()}"); - } - content = {}; - } - else if (content == null) content = {}; - - if (senderUser == null) senderUser = User.fromJson(jsonObj, room); - if (stateKeyUser == null) stateKeyUser = User(jsonObj["state_key"]); - - return Event( - jsonObj["event_id"] ?? jsonObj["id"], - senderUser, - ChatTime(jsonObj["origin_server_ts"]), - stateKey: stateKeyUser, - environment: jsonObj["type"], - status: jsonObj["status"] ?? 2, - content: content, - room: room, - ); + return "$type"; } /// Removes this event if the status is < 1. This event will just be removed @@ -175,14 +103,14 @@ class Event { if (status < 1) { if (room.client.store != null) await room.client.store.db - .rawDelete("DELETE FROM Events WHERE id=?", [id]); + .rawDelete("DELETE FROM Events WHERE id=?", [eventId]); room.client.connection.onEvent.add(EventUpdate( roomID: room.id, type: "timeline", - eventType: environment, + eventType: typeKey, content: { - "event_id": id, + "event_id": eventId, "status": -2, "content": {"body": "Removed..."} })); @@ -198,42 +126,4 @@ class Event { final String eventID = await room.sendTextEvent(text, txid: txid); return eventID; } - - @Deprecated("Use [client.store.getEventList(Room room)] instead!") - static Future> getEventList(Client matrix, Room room) async { - List eventList = await matrix.store.getEventList(room); - return eventList; - } } - -enum EventTypes { - Text, - Emote, - Notice, - Image, - Video, - Audio, - File, - Location, - Reply, - RoomAliases, - RoomCanonicalAlias, - RoomCreate, - RoomJoinRules, - RoomMember, - RoomPowerLevels, - RoomName, - RoomTopic, - RoomAvatar, - GuestAccess, - HistoryVisibility, - Unknown, -} - -final Map StatusTypes = { - "REMOVE": -2, - "ERROR": -1, - "SENDING": 0, - "SENT": 1, - "RECEIVED": 2, -}; diff --git a/lib/src/Presence.dart b/lib/src/Presence.dart new file mode 100644 index 00000000..124087a7 --- /dev/null +++ b/lib/src/Presence.dart @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019 Zender & Kurtz GbR. + * + * Authors: + * Christian Pauly + * Marcel Radzio + * + * 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 + * along with famedlysdk. If not, see . + */ + +import 'package:famedlysdk/src/AccountData.dart'; +import 'package:famedlysdk/src/RoomState.dart'; + +class Presence extends AccountData { + /// The user who has sent this event if it is not a global account data event. + final String sender; + + Presence({this.sender, Map content, String typeKey}) + : super(content: content, typeKey: typeKey); + + /// Get a State event from a table row or from the event stream. + factory Presence.fromJson(Map jsonPayload) { + final Map content = + RoomState.getMapFromPayload(jsonPayload['content']); + return Presence( + content: content, + typeKey: jsonPayload['type'], + sender: jsonPayload['sender']); + } +} diff --git a/lib/src/Room.dart b/lib/src/Room.dart index 0f265c56..bb198d00 100644 --- a/lib/src/Room.dart +++ b/lib/src/Room.dart @@ -23,6 +23,8 @@ import 'package:famedlysdk/src/Client.dart'; import 'package:famedlysdk/src/Event.dart'; +import 'package:famedlysdk/src/RoomAccountData.dart'; +import 'package:famedlysdk/src/RoomState.dart'; import 'package:famedlysdk/src/responses/ErrorResponse.dart'; import 'package:famedlysdk/src/sync/EventUpdate.dart'; import 'package:famedlysdk/src/utils/ChatTime.dart'; @@ -40,15 +42,6 @@ class Room { /// Membership status of the user for this room. Membership membership; - /// The name of the room if set by a participant. - String name; - - /// The topic of the room if set by a participant. - String topic; - - /// The avatar of the room if set by a participant. - MxContent avatar = MxContent(""); - /// The count of unread notifications. int notificationCount; @@ -57,7 +50,13 @@ class Room { String prev_batch; - String draft; + List mHeroes = []; + int mJoinedMemberCount; + int mInvitedMemberCount; + + Map states = {}; + + Map roomAccountData = {}; /// Time when the user has last read the chat. ChatTime unread; @@ -65,69 +64,97 @@ class Room { /// ID of the fully read marker event. String fullyRead; - /// The address in the format: #roomname:homeserver.org. - String canonicalAlias; + /// The name of the room if set by a participant. + String get name { + if (states["m.room.name"] != null && + !states["m.room.name"].content["name"].isEmpty) + return states["m.room.name"].content["name"]; + if (canonicalAlias != null && !canonicalAlias.isEmpty) + return canonicalAlias.substring(1, canonicalAlias.length).split(":")[0]; + if (mHeroes != null && mHeroes.length > 0) { + String displayname = ""; + for (int i = 0; i < mHeroes.length; i++) { + User hero = states[mHeroes[i]] != null + ? states[mHeroes[i]].asUser + : User(mHeroes[i]); + displayname += hero.calcDisplayname() + ", "; + } + return displayname.substring(0, displayname.length - 2); + } + return "Empty chat"; + } - /// If this room is a direct chat, this is the matrix ID of the user - String directChatMatrixID; + /// The topic of the room if set by a participant. + String get topic => states["m.room.topic"] != null + ? states["m.room.topic"].content["topic"] + : ""; + + /// The avatar of the room if set by a participant. + MxContent get avatar { + if (states["m.room.avatar"] != null) + return MxContent(states["m.room.avatar"].content["url"]); + if (mHeroes != null && mHeroes.length == 1 && states[mHeroes[0]] != null) + return states[mHeroes[0]].asUser.avatarUrl; + return MxContent(""); + } + + /// The address in the format: #roomname:homeserver.org. + String get canonicalAlias => states["m.room.canonical_alias"] != null + ? states["m.room.canonical_alias"].content["alias"] + : ""; + + /// If this room is a direct chat, this is the matrix ID of the user. + /// Returns null otherwise. + String get directChatMatrixID { + String returnUserId = null; + if (client.directChats is Map) { + client.directChats.forEach((String userId, dynamic roomIds) { + if (roomIds is List) { + for (int i = 0; i < roomIds.length; i++) + if (roomIds[i] == this.id) { + returnUserId = userId; + break; + } + } + }); + } + return returnUserId; + } + + /// Wheither this is a direct chat or not + bool get isDirectChat => directChatMatrixID != null; /// Must be one of [all, mention] String notificationSettings; - /// Are guest users allowed? - String guestAccess; - - /// Who can see the history of this room? - String historyVisibility; - - /// Who is allowed to join this room? - String joinRules; - - /// The needed power levels for all actions. - Map powerLevels = {}; - - List mHeroes; - int mJoinedMemberCount; - int mInvitedMemberCount; - - Event lastEvent; + Event get lastEvent { + ChatTime lastTime = ChatTime(0); + Event lastEvent = null; + states.forEach((String key, RoomState state) { + if (state.time != null && state.time > lastTime) { + lastTime = state.time; + lastEvent = state.timelineEvent; + } + }); + return lastEvent; + } /// Your current client instance. final Client client; - @Deprecated("Rooms.roomID is deprecated! Use Rooms.id instead!") - String get roomID => this.id; - - @Deprecated("Rooms.matrix is deprecated! Use Rooms.client instead!") - Client get matrix => this.client; - - @Deprecated("Rooms.status is deprecated! Use Rooms.membership instead!") - String get status => this.membership.toString().split('.').last; - Room({ this.id, - this.membership, - this.name, - this.topic, - this.avatar, - this.notificationCount, - this.highlightCount, + this.membership = Membership.join, + this.notificationCount = 0, + this.highlightCount = 0, this.prev_batch = "", - this.draft, - this.unread, - this.fullyRead, - this.canonicalAlias, - this.directChatMatrixID, - this.notificationSettings, - this.guestAccess, - this.historyVisibility, - this.joinRules, - this.powerLevels, - this.lastEvent, this.client, - this.mHeroes, - this.mInvitedMemberCount, - this.mJoinedMemberCount, + this.notificationSettings, + this.mHeroes = const [], + this.mInvitedMemberCount = 0, + this.mJoinedMemberCount = 0, + this.states = const {}, + this.roomAccountData = const {}, }); /// Calculates the displayname. First checks if there is a name, then checks for a canonical alias and @@ -297,9 +324,10 @@ class Room { return res; } - /// Call the Matrix API to unban a banned user from this room. + /// Set the power level of the user with the [userID] to the value [power]. Future setPower(String userID, int power) async { - Map powerMap = await client.store.getPowerLevels(id); + if (states["m.room.power_levels"] == null) return null; + Map powerMap = states["m.room.power_levels"].content["users"]; powerMap[userID] = power; dynamic res = await client.connection.jsonRequest( @@ -325,7 +353,7 @@ class Room { final dynamic resp = await client.connection.jsonRequest( type: HTTPType.GET, action: - "/client/r0/rooms/$id/messages?from=${prev_batch}&dir=b&limit=$historyCount"); + "/client/r0/rooms/$id/messages?from=${prev_batch}&dir=b&limit=$historyCount&filter=${Connection.syncFilters}"); if (resp is ErrorResponse) return; @@ -336,6 +364,33 @@ class Room { resp["chunk"].length > 0 && resp["end"] is String)) return; + if (resp["state"] is List) { + client.store?.transaction(() { + for (int i = 0; i < resp["state"].length; i++) { + EventUpdate eventUpdate = EventUpdate( + type: "state", + roomID: id, + eventType: resp["state"][i]["type"], + content: resp["state"][i], + ); + client.connection.onEvent.add(eventUpdate); + client.store.storeEventUpdate(eventUpdate); + } + return; + }); + if (client.store == null) { + for (int i = 0; i < resp["state"].length; i++) { + EventUpdate eventUpdate = EventUpdate( + type: "state", + roomID: id, + eventType: resp["state"][i]["type"], + content: resp["state"][i], + ); + client.connection.onEvent.add(eventUpdate); + } + } + } + List history = resp["chunk"]; client.store?.transaction(() { for (int i = 0; i < history.length; i++) { @@ -348,7 +403,7 @@ class Room { client.connection.onEvent.add(eventUpdate); client.store.storeEventUpdate(eventUpdate); client.store.txn.rawUpdate( - "UPDATE Rooms SET prev_batch=? WHERE id=?", [resp["end"], id]); + "UPDATE Rooms SET prev_batch=? WHERE room_id=?", [resp["end"], id]); } return; }); @@ -367,8 +422,7 @@ class Room { /// Sets this room as a direct chat for this user. Future addToDirectChat(String userID) async { - Map> directChats = - await client.store.getAccountDataDirectChats(); + Map directChats = client.directChats; if (directChats.containsKey(userID)) if (!directChats[userID].contains(id)) directChats[userID].add(id); else @@ -395,73 +449,58 @@ class Room { return resp; } - /// Returns a Room from a json String which comes normally from the store. + /// Returns a Room from a json String which comes normally from the store. If the + /// state are also given, the method will await them. static Future getRoomFromTableRow( - Map row, Client matrix) async { - String avatarUrl = row["avatar_url"]; - if (avatarUrl == "") - avatarUrl = await matrix.store?.getAvatarFromSingleChat(row["id"]) ?? ""; - - return Room( - id: row["id"], - name: row["topic"], + Map row, Client matrix, + {Future>> states, + Future>> roomAccountData}) async { + Room newRoom = Room( + id: row["room_id"], membership: Membership.values .firstWhere((e) => e.toString() == 'Membership.' + row["membership"]), - topic: row["description"], - avatar: MxContent(avatarUrl), notificationCount: row["notification_count"], highlightCount: row["highlight_count"], - unread: ChatTime(row["unread"]), - fullyRead: row["fully_read"], notificationSettings: row["notification_settings"], - directChatMatrixID: row["direct_chat_matrix_id"], - draft: row["draft"], prev_batch: row["prev_batch"], - guestAccess: row["guest_access"], - historyVisibility: row["history_visibility"], - joinRules: row["join_rules"], - canonicalAlias: row["canonical_alias"], mInvitedMemberCount: row["invited_member_count"], mJoinedMemberCount: row["joined_member_count"], mHeroes: row["heroes"]?.split(",") ?? [], - powerLevels: { - "power_events_default": row["power_events_default"], - "power_state_default": row["power_state_default"], - "power_redact": row["power_redact"], - "power_invite": row["power_invite"], - "power_ban": row["power_ban"], - "power_kick": row["power_kick"], - "power_user_default": row["power_user_default"], - "power_event_avatar": row["power_event_avatar"], - "power_event_history_visibility": row["power_event_history_visibility"], - "power_event_canonical_alias": row["power_event_canonical_alias"], - "power_event_aliases": row["power_event_aliases"], - "power_event_name": row["power_event_name"], - "power_event_power_levels": row["power_event_power_levels"], - }, - lastEvent: Event.fromJson(row, null), client: matrix, + states: {}, + roomAccountData: {}, ); - } - @Deprecated("Use client.store.getRoomById(String id) instead!") - static Future getRoomById(String id, Client matrix) async { - Room room = await matrix.store.getRoomById(id); - return room; - } + Map newStates = {}; + if (states != null) { + List> rawStates = await states; + for (int i = 0; i < rawStates.length; i++) { + RoomState newState = RoomState.fromJson(rawStates[i], newRoom); + newStates[newState.key] = newState; + } + newRoom.states = newStates; + } - /// Load a room from the store including all room events. - static Future loadRoomEvents(String id, Client matrix) async { - Room room = await matrix.store.getRoomById(id); - await room.loadEvents(); - return room; + Map newRoomAccountData = {}; + if (roomAccountData != null) { + List> rawRoomAccountData = await roomAccountData; + for (int i = 0; i < rawRoomAccountData.length; i++) { + RoomAccountData newData = + RoomAccountData.fromJson(rawRoomAccountData[i], newRoom); + newRoomAccountData[newData.typeKey] = newData; + } + newRoom.roomAccountData = newRoomAccountData; + } + + return newRoom; } /// Creates a timeline from the store. Returns a [Timeline] object. Future getTimeline( {onTimelineUpdateCallback onUpdate, onTimelineInsertCallback onInsert}) async { - List events = await loadEvents(); + List events = []; + if (client.store != null) events = await client.store.getEventList(this); return Timeline( room: this, events: events, @@ -470,17 +509,23 @@ class Room { ); } - /// Load all events for a given room from the store. This includes all - /// senders of those events, who will be added to the participants list. - Future> loadEvents() async { - return await client.store.getEventList(this); - } - /// Load all participants for a given room from the store. + @deprecated Future> loadParticipants() async { return await client.store.loadParticipants(this); } + /// Returns all participants for this room. With lazy loading this + /// list may not be complete. User [requestParticipants] in this + /// case. + List getParticipants() { + List userList = []; + for (var entry in states.entries) + if (entry.value.type == EventTypes.RoomMember) + userList.add(entry.value.asUser); + return userList; + } + /// Request the full list of participants from the server. The local list /// from the store is not complete if the client uses lazy loading. Future> requestParticipants() async { @@ -492,14 +537,7 @@ class Room { return participants; for (num i = 0; i < res["chunk"].length; i++) { - User newUser = User(res["chunk"][i]["state_key"], - displayName: res["chunk"][i]["content"]["displayname"] ?? "", - membership: Membership.values.firstWhere((e) => - e.toString() == - 'Membership.' + res["chunk"][i]["content"]["membership"] ?? - ""), - avatarUrl: MxContent(res["chunk"][i]["content"]["avatar_url"] ?? ""), - room: this); + User newUser = RoomState.fromJson(res["chunk"][i], this).asUser; if (newUser.membership != Membership.leave) participants.add(newUser); } @@ -507,18 +545,14 @@ class Room { } Future getUserByMXID(String mxID) async { - if (client.store != null) { - final User storeEvent = - await client.store.getUser(matrixID: mxID, room: this); - if (storeEvent != null) return storeEvent; - } + if (states[mxID] != null) return states[mxID].asUser; final dynamic resp = await client.connection.jsonRequest( type: HTTPType.GET, action: "/client/r0/rooms/$id/state/m.room.member/$mxID"); if (resp is ErrorResponse) return null; // Somehow we miss the mxid in the response and only get the content of the event. resp["matrix_id"] = mxID; - return User.fromJson(resp, this); + return RoomState.fromJson(resp, this).asUser; } /// Searches for the event in the store. If it isn't found, try to request it @@ -531,7 +565,30 @@ class Room { final dynamic resp = await client.connection.jsonRequest( type: HTTPType.GET, action: "/client/r0/rooms/$id/event/$eventID"); if (resp is ErrorResponse) return null; - return Event.fromJson(resp, this, - senderUser: (await getUserByMXID(resp["sender"]))); + return Event.fromJson(resp, this); + } + + /// Returns the user's own power level. + int getPowerLevelByUserId(String userId) { + int powerLevel = 0; + RoomState powerLevelState = states["m.room.power_levels"]; + if (powerLevelState == null) return powerLevel; + if (powerLevelState.content["users_default"] is int) + powerLevel = powerLevelState.content["users_default"]; + if (powerLevelState.content["users"] is Map && + powerLevelState.content["users"][userId] != null) + powerLevel = powerLevelState.content["users"][userId]; + return powerLevel; + } + + /// Returns the user's own power level. + int get ownPowerLevel => getPowerLevelByUserId(client.userID); + + /// Returns the power levels from all users for this room or null if not given. + Map get powerLevels { + RoomState powerLevelState = states["m.room.power_levels"]; + if (powerLevelState.content["users"] is Map) + return powerLevelState.content["users"]; + return null; } } diff --git a/lib/src/RoomAccountData.dart b/lib/src/RoomAccountData.dart new file mode 100644 index 00000000..919dcfa9 --- /dev/null +++ b/lib/src/RoomAccountData.dart @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019 Zender & Kurtz GbR. + * + * Authors: + * Christian Pauly + * Marcel Radzio + * + * 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 + * along with famedlysdk. If not, see . + */ + +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/src/AccountData.dart'; +import 'package:famedlysdk/src/RoomState.dart'; + +class RoomAccountData extends AccountData { + /// The user who has sent this event if it is not a global account data event. + final String roomId; + + final Room room; + + RoomAccountData( + {this.roomId, this.room, Map content, String typeKey}) + : super(content: content, typeKey: typeKey); + + /// Get a State event from a table row or from the event stream. + factory RoomAccountData.fromJson( + Map jsonPayload, Room room) { + final Map content = + RoomState.getMapFromPayload(jsonPayload['content']); + return RoomAccountData( + content: content, + typeKey: jsonPayload['type'], + roomId: jsonPayload['room_id'], + room: room); + } +} diff --git a/lib/src/RoomList.dart b/lib/src/RoomList.dart index c3d1f2db..a26819ba 100644 --- a/lib/src/RoomList.dart +++ b/lib/src/RoomList.dart @@ -24,14 +24,17 @@ import 'dart:async'; import 'dart:core'; +import 'package:famedlysdk/src/RoomState.dart'; + import 'Client.dart'; -import 'Event.dart'; import 'Room.dart'; import 'User.dart'; import 'sync/EventUpdate.dart'; import 'sync/RoomUpdate.dart'; -import 'utils/ChatTime.dart'; -import 'utils/MxContent.dart'; + +typedef onRoomListUpdateCallback = void Function(); +typedef onRoomListInsertCallback = void Function(int insertID); +typedef onRoomListRemoveCallback = void Function(int insertID); /// Represents a list of rooms for this client, which will automatically update /// itself and call the [onUpdate], [onInsert] and [onDelete] callbacks. To get @@ -69,6 +72,21 @@ class RoomList { this.onlyGroups = false}) { eventSub ??= client.connection.onEvent.stream.listen(_handleEventUpdate); roomSub ??= client.connection.onRoomUpdate.stream.listen(_handleRoomUpdate); + sort(); + } + + Room getRoomByAlias(String alias) { + for (int i = 0; i < rooms.length; i++) { + if (rooms[i].canonicalAlias == alias) return rooms[i]; + } + return null; + } + + Room getRoomById(String id) { + for (int j = 0; j < rooms.length; j++) { + if (rooms[j].id == id) return rooms[j]; + } + return null; } void _handleRoomUpdate(RoomUpdate chatUpdate) { @@ -87,7 +105,6 @@ class RoomList { // Add the new chat to the list Room newRoom = Room( id: chatUpdate.id, - name: "", membership: chatUpdate.membership, prev_batch: chatUpdate.prev_batch, highlightCount: chatUpdate.highlight_count, @@ -95,6 +112,9 @@ class RoomList { mHeroes: chatUpdate.summary?.mHeroes, mJoinedMemberCount: chatUpdate.summary?.mJoinedMemberCount, mInvitedMemberCount: chatUpdate.summary?.mInvitedMemberCount, + states: {}, + roomAccountData: {}, + client: client, ); rooms.insert(position, newRoom); if (onInsert != null) onInsert(position); @@ -125,11 +145,7 @@ class RoomList { } void _handleEventUpdate(EventUpdate eventUpdate) { - // Is the event necessary for the chat list? If not, then return - if (!(eventUpdate.type == "timeline" || - eventUpdate.eventType == "m.room.avatar" || - eventUpdate.eventType == "m.room.name")) return; - + if (eventUpdate.type != "timeline" && eventUpdate.type != "state") return; // Search the room in the rooms num j = 0; for (j = 0; j < rooms.length; j++) { @@ -138,44 +154,20 @@ class RoomList { final bool found = (j < rooms.length && rooms[j].id == eventUpdate.roomID); if (!found) return; - // Is this an old timeline event? Then stop here... - /*if (eventUpdate.type == "timeline" && - ChatTime(eventUpdate.content["origin_server_ts"]) <= - rooms[j].timeCreated) return;*/ - - if (eventUpdate.type == "timeline") { - User stateKey = null; - if (eventUpdate.content["state_key"] is String) - stateKey = User(eventUpdate.content["state_key"]); - // Update the last message preview - rooms[j].lastEvent = Event( - eventUpdate.content["id"], - User(eventUpdate.content["sender"]), - ChatTime(eventUpdate.content["origin_server_ts"]), - room: rooms[j], - stateKey: stateKey, - content: eventUpdate.content["content"], - environment: eventUpdate.eventType, - status: 2, - ); - } - if (eventUpdate.eventType == "m.room.name") { - // Update the room name - rooms[j].name = eventUpdate.content["content"]["name"]; - } else if (eventUpdate.eventType == "m.room.avatar") { - // Update the room avatar - rooms[j].avatar = MxContent(eventUpdate.content["content"]["url"]); - } + RoomState stateEvent = RoomState.fromJson(eventUpdate.content, rooms[j]); + if (rooms[j].states[stateEvent.key] != null && + rooms[j].states[stateEvent.key].time > stateEvent.time) return; + rooms[j].states[stateEvent.key] = stateEvent; sortAndUpdate(); } - sortAndUpdate() { + sort() { rooms?.sort((a, b) => b.timeCreated.toTimeStamp().compareTo(a.timeCreated.toTimeStamp())); + } + + sortAndUpdate() { + sort(); if (onUpdate != null) onUpdate(); } } - -typedef onRoomListUpdateCallback = void Function(); -typedef onRoomListInsertCallback = void Function(int insertID); -typedef onRoomListRemoveCallback = void Function(int insertID); diff --git a/lib/src/RoomState.dart b/lib/src/RoomState.dart new file mode 100644 index 00000000..105a1406 --- /dev/null +++ b/lib/src/RoomState.dart @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2019 Zender & Kurtz GbR. + * + * Authors: + * Christian Pauly + * Marcel Radzio + * + * 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 + * along with famedlysdk. If not, see . + */ + +import 'dart:convert'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/src/utils/ChatTime.dart'; +import './Room.dart'; + +class RoomState { + /// The Matrix ID for this event in the format '$localpart:server.abc'. Please not + /// that account data, presence and other events may not have an eventId. + final String eventId; + + /// The json payload of the content. The content highly depends on the type. + final Map content; + + /// The type String of this event. For example 'm.room.message'. + final String typeKey; + + /// The ID of the room this event belongs to. + final String roomId; + + /// The user who has sent this event if it is not a global account data event. + final String senderId; + + User get sender => room.states[senderId]?.asUser ?? User(senderId); + + /// The time this event has received at the server. May be null for events like + /// account data. + final ChatTime time; + + /// Optional additional content for this event. + final Map unsigned; + + /// The room this event belongs to. May be null. + final Room room; + + /// Optional. The previous content for this state. + /// This will be present only for state events appearing in the timeline. + /// If this is not a state event, or there is no previous content, this key will be null. + final Map prevContent; + + /// Optional. This key will only be present for state events. A unique key which defines + /// the overwriting semantics for this piece of room state. + final String stateKey; + + User get stateKeyUser => room.states[stateKey]?.asUser ?? User(stateKey); + + RoomState( + {this.content, + this.typeKey, + this.eventId, + this.roomId, + this.senderId, + this.time, + this.unsigned, + this.prevContent, + this.stateKey, + this.room}); + + static Map getMapFromPayload(dynamic payload) { + if (payload is String) + try { + return json.decode(payload); + } catch (e) { + return {}; + } + if (payload is Map) return payload; + return {}; + } + + /// Get a State event from a table row or from the event stream. + factory RoomState.fromJson(Map jsonPayload, Room room) { + final Map content = + RoomState.getMapFromPayload(jsonPayload['content']); + final Map unsigned = + RoomState.getMapFromPayload(jsonPayload['unsigned']); + final Map prevContent = + RoomState.getMapFromPayload(jsonPayload['prev_content']); + return RoomState( + stateKey: jsonPayload['state_key'], + prevContent: prevContent, + content: content, + typeKey: jsonPayload['type'], + eventId: jsonPayload['event_id'], + roomId: jsonPayload['room_id'], + senderId: jsonPayload['sender'], + time: ChatTime(jsonPayload['origin_server_ts']), + unsigned: unsigned, + room: room); + } + + Event get timelineEvent => Event( + content: content, + typeKey: typeKey, + eventId: eventId, + room: room, + roomId: roomId, + senderId: senderId, + time: time, + unsigned: unsigned, + status: 1, + ); + + /// The unique key of this event. For events with a [stateKey], it will be the + /// stateKey. Otherwise it will be the [type] as a string. + String get key => stateKey == null || stateKey.isEmpty ? typeKey : stateKey; + + User get asUser => User.fromState( + stateKey: stateKey, + prevContent: prevContent, + content: content, + typeKey: typeKey, + eventId: eventId, + roomId: roomId, + senderId: senderId, + time: time, + unsigned: unsigned, + room: room); + + /// Get the real type. + EventTypes get type { + switch (typeKey) { + case "m.room.avatar": + return EventTypes.RoomAvatar; + case "m.room.name": + return EventTypes.RoomName; + case "m.room.topic": + return EventTypes.RoomTopic; + case "m.room.Aliases": + return EventTypes.RoomAliases; + case "m.room.canonical_alias": + return EventTypes.RoomCanonicalAlias; + case "m.room.create": + return EventTypes.RoomCreate; + case "m.room.join_rules": + return EventTypes.RoomJoinRules; + case "m.room.member": + return EventTypes.RoomMember; + case "m.room.power_levels": + return EventTypes.RoomPowerLevels; + case "m.room.guest_access": + return EventTypes.GuestAccess; + case "m.room.history_visibility": + return EventTypes.HistoryVisibility; + case "m.room.message": + switch (content["msgtype"] ?? "m.text") { + case "m.text": + if (content.containsKey("m.relates_to")) { + return EventTypes.Reply; + } + return EventTypes.Text; + case "m.notice": + return EventTypes.Notice; + case "m.emote": + return EventTypes.Emote; + case "m.image": + return EventTypes.Image; + case "m.video": + return EventTypes.Video; + case "m.audio": + return EventTypes.Audio; + case "m.file": + return EventTypes.File; + case "m.location": + return EventTypes.Location; + } + } + return EventTypes.Unknown; + } +} + +enum EventTypes { + Text, + Emote, + Notice, + Image, + Video, + Audio, + File, + Location, + Reply, + RoomAliases, + RoomCanonicalAlias, + RoomCreate, + RoomJoinRules, + RoomMember, + RoomPowerLevels, + RoomName, + RoomTopic, + RoomAvatar, + GuestAccess, + HistoryVisibility, + Unknown, +} diff --git a/lib/src/Store.dart b/lib/src/Store.dart index 036efb95..15e021ab 100644 --- a/lib/src/Store.dart +++ b/lib/src/Store.dart @@ -25,6 +25,9 @@ import 'dart:async'; import 'dart:convert'; import 'dart:core'; +import 'package:famedlysdk/src/AccountData.dart'; +import 'package:famedlysdk/src/Presence.dart'; +import 'package:famedlysdk/src/RoomState.dart'; import 'package:path/path.dart' as p; import 'package:sqflite/sqflite.dart'; @@ -55,7 +58,7 @@ class Store { _init() async { var databasePath = await getDatabasesPath(); String path = p.join(databasePath, "FluffyMatrix.db"); - _db = await openDatabase(path, version: 12, + _db = await openDatabase(path, version: 14, onCreate: (Database db, int version) async { await createTables(db); }, onUpgrade: (Database db, int oldVersion, int newVersion) async { @@ -153,8 +156,8 @@ class Store { } Future storeRoomPrevBatch(Room room) async { - await _db.rawUpdate( - "UPDATE Rooms SET prev_batch=? WHERE id=?", [room.prev_batch, room.id]); + await _db.rawUpdate("UPDATE Rooms SET prev_batch=? WHERE room_id=?", + [room.prev_batch, room.id]); return null; } @@ -163,8 +166,7 @@ class Store { Future storeRoomUpdate(RoomUpdate roomUpdate) { // Insert the chat into the database if not exists txn.rawInsert( - "INSERT OR IGNORE INTO Rooms " + - "VALUES(?, ?, '', 0, 0, 0, 0, '', '', '', '', 0, '', '', '', '', '', '', '', '', 0, 50, 50, 0, 50, 50, 0, 50, 100, 50, 50, 50, 100) ", + "INSERT OR IGNORE INTO Rooms " + "VALUES(?, ?, 0, 0, '', 0, 0, '') ", [roomUpdate.id, roomUpdate.membership.toString().split('.').last]); // Update the notification counts and the limited timeline boolean and the summary @@ -187,15 +189,15 @@ class Store { updateQuery += ", heroes=?"; updateArgs.add(roomUpdate.summary.mHeroes.join(",")); } - updateQuery += " WHERE id=?"; + updateQuery += " WHERE room_id=?"; updateArgs.add(roomUpdate.id); txn.rawUpdate(updateQuery, updateArgs); // Is the timeline limited? Then all previous messages should be // removed from the database! if (roomUpdate.limitedTimeline) { - txn.rawDelete("DELETE FROM Events WHERE chat_id=?", [roomUpdate.id]); - txn.rawUpdate("UPDATE Rooms SET prev_batch=? WHERE id=?", + txn.rawDelete("DELETE FROM Events WHERE room_id=?", [roomUpdate.id]); + txn.rawUpdate("UPDATE Rooms SET prev_batch=? WHERE room_id=?", [roomUpdate.prev_batch, roomUpdate.id]); } return null; @@ -204,21 +206,17 @@ class Store { /// Stores an UserUpdate object in the database. Must be called inside of /// [transaction]. Future storeUserEventUpdate(UserUpdate userUpdate) { - switch (userUpdate.eventType) { - case "m.direct": - if (userUpdate.content["content"] is Map) { - final Map directMap = userUpdate.content["content"]; - directMap.forEach((String key, dynamic value) { - if (value is List && value.length > 0) - for (int i = 0; i < value.length; i++) { - txn.rawUpdate( - "UPDATE Rooms SET direct_chat_matrix_id=? WHERE id=?", - [key, value[i]]); - } - }); - } - break; - } + if (userUpdate.type == "account_data") + txn.rawInsert("INSERT OR REPLACE INTO AccountData VALUES(?, ?)", [ + userUpdate.eventType, + json.encode(userUpdate.content["content"]), + ]); + else if (userUpdate.type == "presence") + txn.rawInsert("INSERT OR REPLACE INTO Presences VALUES(?, ?, ?)", [ + userUpdate.eventType, + userUpdate.content["sender"], + json.encode(userUpdate.content["content"]), + ]); return null; } @@ -229,252 +227,96 @@ class Store { String type = eventUpdate.type; String chat_id = eventUpdate.roomID; + // Get the state_key for m.room.member events + String state_key = ""; + if (eventContent["state_key"] is String) { + state_key = eventContent["state_key"]; + } + if (type == "timeline" || type == "history") { // calculate the status num status = 2; if (eventContent["status"] is num) status = eventContent["status"]; - // Make unsigned part of the content - if (eventContent.containsKey("unsigned")) { - Map newContent = { - "unsigned": eventContent["unsigned"] - }; - eventContent["content"].forEach((key, val) => newContent[key] = val); - eventContent["content"] = newContent; - } - - // Get the state_key for m.room.member events - String state_key = ""; - if (eventContent["state_key"] is String) { - state_key = eventContent["state_key"]; - } - // Save the event in the database if ((status == 1 || status == -1) && eventContent["unsigned"] is Map && eventContent["unsigned"]["transaction_id"] is String) - txn.rawUpdate("UPDATE Events SET status=?, id=? WHERE id=?", [ + txn.rawUpdate( + "UPDATE Events SET status=?, event_id=? WHERE event_id=?", [ status, eventContent["event_id"], eventContent["unsigned"]["transaction_id"] ]); else txn.rawInsert( - "INSERT OR REPLACE INTO Events VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)", [ - eventContent["event_id"], - chat_id, - eventContent["origin_server_ts"], - eventContent["sender"], - state_key, - eventContent["content"]["body"], - eventContent["type"], - json.encode(eventContent["content"]), - status - ]); + "INSERT OR REPLACE INTO Events VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [ + eventContent["event_id"], + chat_id, + eventContent["origin_server_ts"], + eventContent["sender"], + eventContent["type"], + json.encode(eventContent["unsigned"] ?? ""), + json.encode(eventContent["content"]), + json.encode(eventContent["prevContent"]), + eventContent["state_key"], + status + ]); // Is there a transaction id? Then delete the event with this id. if (status != -1 && eventUpdate.content.containsKey("unsigned") && eventUpdate.content["unsigned"]["transaction_id"] is String) - txn.rawDelete("DELETE FROM Events WHERE id=?", + txn.rawDelete("DELETE FROM Events WHERE event_id=?", [eventUpdate.content["unsigned"]["transaction_id"]]); } if (type == "history") return null; - switch (eventUpdate.eventType) { - case "m.receipt": - if (eventContent["user"] == client.userID) { - txn.rawUpdate("UPDATE Rooms SET unread=? WHERE id=?", - [eventContent["ts"], chat_id]); - } else { - // Mark all previous received messages as seen - txn.rawUpdate( - "UPDATE Events SET status=3 WHERE origin_server_ts<=? AND chat_id=? AND status=2", - [eventContent["ts"], chat_id]); - } - break; - // This event means, that the name of a room has been changed, so - // it has to be changed in the database. - case "m.room.name": - txn.rawUpdate("UPDATE Rooms SET topic=? WHERE id=?", - [eventContent["content"]["name"], chat_id]); - break; - // This event means, that the topic of a room has been changed, so - // it has to be changed in the database - case "m.room.topic": - txn.rawUpdate("UPDATE Rooms SET description=? WHERE id=?", - [eventContent["content"]["topic"], chat_id]); - break; - // This event means, that the topic of a room has been changed, so - // it has to be changed in the database - case "m.room.history_visibility": - txn.rawUpdate("UPDATE Rooms SET history_visibility=? WHERE id=?", - [eventContent["content"]["history_visibility"], chat_id]); - break; - // This event means, that the topic of a room has been changed, so - // it has to be changed in the database - case "m.room.redaction": - txn.rawDelete( - "DELETE FROM Events WHERE id=?", [eventContent["redacts"]]); - break; - // This event means, that the topic of a room has been changed, so - // it has to be changed in the database - case "m.room.guest_access": - txn.rawUpdate("UPDATE Rooms SET guest_access=? WHERE id=?", - [eventContent["content"]["guest_access"], chat_id]); - break; - // This event means, that the canonical alias of a room has been changed, so - // it has to be changed in the database - case "m.room.canonical_alias": - txn.rawUpdate("UPDATE Rooms SET canonical_alias=? WHERE id=?", - [eventContent["content"]["alias"], chat_id]); - break; - // This event means, that the topic of a room has been changed, so - // it has to be changed in the database - case "m.room.join_rules": - txn.rawUpdate("UPDATE Rooms SET join_rules=? WHERE id=?", - [eventContent["content"]["join_rule"], chat_id]); - break; - // This event means, that the avatar of a room has been changed, so - // it has to be changed in the database - case "m.room.avatar": - txn.rawUpdate("UPDATE Rooms SET avatar_url=? WHERE id=?", - [eventContent["content"]["url"], chat_id]); - break; - // This event means, that the aliases of a room has been changed, so - // it has to be changed in the database - case "m.fully_read": - txn.rawUpdate("UPDATE Rooms SET fully_read=? WHERE id=?", - [eventContent["content"]["event_id"], chat_id]); - break; - // This event means, that someone joined the room, has left the room - // or has changed his nickname - case "m.room.member": - String membership = eventContent["content"]["membership"]; - String state_key = eventContent["state_key"]; - String insertDisplayname = ""; - String insertAvatarUrl = ""; - if (eventContent["content"]["displayname"] is String) { - insertDisplayname = eventContent["content"]["displayname"]; - } - if (eventContent["content"]["avatar_url"] is String) { - insertAvatarUrl = eventContent["content"]["avatar_url"]; - } + if (eventUpdate.content["event_id"] != null) { + txn.rawInsert( + "INSERT OR REPLACE INTO RoomStates VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)", + [ + eventContent["event_id"], + chat_id, + eventContent["origin_server_ts"], + eventContent["sender"], + state_key, + json.encode(eventContent["unsigned"] ?? ""), + json.encode(eventContent["prev_content"] ?? ""), + eventContent["type"], + json.encode(eventContent["content"]), + ]); + } else + txn.rawInsert("INSERT OR REPLACE INTO RoomAccountData VALUES(?, ?, ?)", [ + eventContent["type"], + chat_id, + json.encode(eventContent["content"]), + ]); - // Update membership table - txn.rawInsert("INSERT OR IGNORE INTO Users VALUES(?,?,?,?,?,0)", [ - chat_id, - state_key, - insertDisplayname, - insertAvatarUrl, - membership - ]); - String queryStr = "UPDATE Users SET membership=?"; - List queryArgs = [membership]; - - if (eventContent["content"]["displayname"] is String) { - queryStr += " , displayname=?"; - queryArgs.add(eventContent["content"]["displayname"]); - } - if (eventContent["content"]["avatar_url"] is String) { - queryStr += " , avatar_url=?"; - queryArgs.add(eventContent["content"]["avatar_url"]); - } - - queryStr += " WHERE matrix_id=? AND chat_id=?"; - queryArgs.add(state_key); - queryArgs.add(chat_id); - txn.rawUpdate(queryStr, queryArgs); - break; - // This event changes the permissions of the users and the power levels - case "m.room.power_levels": - String query = "UPDATE Rooms SET "; - if (eventContent["content"]["ban"] is num) - query += ", power_ban=" + eventContent["content"]["ban"].toString(); - if (eventContent["content"]["events_default"] is num) - query += ", power_events_default=" + - eventContent["content"]["events_default"].toString(); - if (eventContent["content"]["state_default"] is num) - query += ", power_state_default=" + - eventContent["content"]["state_default"].toString(); - if (eventContent["content"]["redact"] is num) - query += - ", power_redact=" + eventContent["content"]["redact"].toString(); - if (eventContent["content"]["invite"] is num) - query += - ", power_invite=" + eventContent["content"]["invite"].toString(); - if (eventContent["content"]["kick"] is num) - query += ", power_kick=" + eventContent["content"]["kick"].toString(); - if (eventContent["content"]["user_default"] is num) - query += ", power_user_default=" + - eventContent["content"]["user_default"].toString(); - if (eventContent["content"]["events"] is Map) { - if (eventContent["content"]["events"]["m.room.avatar"] is num) - query += ", power_event_avatar=" + - eventContent["content"]["events"]["m.room.avatar"].toString(); - if (eventContent["content"]["events"]["m.room.history_visibility"] - is num) - query += ", power_event_history_visibility=" + - eventContent["content"]["events"]["m.room.history_visibility"] - .toString(); - if (eventContent["content"]["events"]["m.room.canonical_alias"] - is num) - query += ", power_event_canonical_alias=" + - eventContent["content"]["events"]["m.room.canonical_alias"] - .toString(); - if (eventContent["content"]["events"]["m.room.aliases"] is num) - query += ", power_event_aliases=" + - eventContent["content"]["events"]["m.room.aliases"].toString(); - if (eventContent["content"]["events"]["m.room.name"] is num) - query += ", power_event_name=" + - eventContent["content"]["events"]["m.room.name"].toString(); - if (eventContent["content"]["events"]["m.room.power_levels"] is num) - query += ", power_event_power_levels=" + - eventContent["content"]["events"]["m.room.power_levels"] - .toString(); - } - if (query != "UPDATE Rooms SET ") { - query = query.replaceFirst(",", ""); - txn.rawUpdate(query + " WHERE id=?", [chat_id]); - } - - // Set the users power levels: - if (eventContent["content"]["users"] is Map) { - eventContent["content"]["users"] - .forEach((String user, dynamic value) async { - num power_level = eventContent["content"]["users"][user]; - txn.rawUpdate( - "UPDATE Users SET power_level=? WHERE matrix_id=? AND chat_id=?", - [power_level, user, chat_id]); - txn.rawInsert( - "INSERT OR IGNORE INTO Users VALUES(?, ?, '', '', ?, ?)", - [chat_id, user, "unknown", power_level]); - }); - } - break; - } return null; } /// Returns a User object by a given Matrix ID and a Room. Future getUser({String matrixID, Room room}) async { List> res = await db.rawQuery( - "SELECT * FROM Users WHERE matrix_id=? AND chat_id=?", + "SELECT * FROM RoomStates WHERE state_key=? AND room_id=?", [matrixID, room.id]); if (res.length != 1) return null; - return User.fromJson(res[0], room); + return RoomState.fromJson(res[0], room).asUser; } /// Loads all Users in the database to provide a contact list /// except users who are in the Room with the ID [exceptRoomID]. Future> loadContacts({String exceptRoomID = ""}) async { List> res = await db.rawQuery( - "SELECT * FROM Users WHERE matrix_id!=? AND chat_id!=? GROUP BY matrix_id ORDER BY displayname", + "SELECT * FROM RoomStates WHERE state_key LIKE '@%:%' AND state_key!=? AND room_id!=? GROUP BY state_key ORDER BY state_key", [client.userID, exceptRoomID]); List userList = []; for (int i = 0; i < res.length; i++) - userList.add(User.fromJson(res[i], Room(id: "", client: client))); + userList + .add(RoomState.fromJson(res[i], Room(id: "", client: client)).asUser); return userList; } @@ -482,15 +324,15 @@ class Store { Future> loadParticipants(Room room) async { List> res = await db.rawQuery( "SELECT * " + - " FROM Users " + - " WHERE chat_id=? " + - " AND membership='join'", + " FROM RoomStates " + + " WHERE room_id=? " + + " AND type='m.room.member'", [room.id]); List participants = []; for (num i = 0; i < res.length; i++) { - participants.add(User.fromJson(res[i], room)); + participants.add(RoomState.fromJson(res[i], room).asUser); } return participants; @@ -498,26 +340,18 @@ class Store { /// Returns a list of events for the given room and sets all participants. Future> getEventList(Room room) async { - List> memberRes = await db.rawQuery( - "SELECT * " + " FROM Users " + " WHERE Users.chat_id=?", [room.id]); - Map userMap = {}; - for (num i = 0; i < memberRes.length; i++) - userMap[memberRes[i]["matrix_id"]] = User.fromJson(memberRes[i], room); - List> eventRes = await db.rawQuery( "SELECT * " + - " FROM Events events " + - " WHERE events.chat_id=?" + - " GROUP BY events.id " + + " FROM Events " + + " WHERE room_id=?" + + " GROUP BY event_id " + " ORDER BY origin_server_ts DESC", [room.id]); List eventList = []; for (num i = 0; i < eventRes.length; i++) - eventList.add(Event.fromJson(eventRes[i], room, - senderUser: userMap[eventRes[i]["sender"]], - stateKeyUser: userMap[eventRes[i]["state_key"]])); + eventList.add(Event.fromJson(eventRes[i], room)); return eventList; } @@ -528,25 +362,17 @@ class Store { bool onlyDirect = false, bool onlyGroups = false}) async { if (onlyDirect && onlyGroups) return []; - List> res = await db.rawQuery( - "SELECT rooms.*, events.origin_server_ts, events.content_json, events.type, events.sender, events.status, events.state_key " + - " FROM Rooms rooms LEFT JOIN Events events " + - " ON rooms.id=events.chat_id " + - " WHERE rooms.membership" + - (onlyLeft ? "=" : "!=") + - "'leave' " + - (onlyDirect ? " AND rooms.direct_chat_matrix_id!= '' " : "") + - (onlyGroups ? " AND rooms.direct_chat_matrix_id= '' " : "") + - " GROUP BY rooms.id " + - " ORDER BY origin_server_ts DESC "); + List> res = await db.rawQuery("SELECT * " + + " FROM Rooms" + + " WHERE membership" + + (onlyLeft ? "=" : "!=") + + "'leave' " + + " GROUP BY room_id "); List roomList = []; for (num i = 0; i < res.length; i++) { - try { - Room room = await Room.getRoomFromTableRow(res[i], client); - roomList.add(room); - } catch (e) { - print(e.toString()); - } + Room room = await Room.getRoomFromTableRow(res[i], client, + states: getStatesFromRoomId(res[i]["room_id"])); + roomList.add(room); } return roomList; } @@ -554,114 +380,47 @@ class Store { /// Returns a room without events and participants. Future getRoomById(String id) async { List> res = - await db.rawQuery("SELECT * FROM Rooms WHERE id=?", [id]); + await db.rawQuery("SELECT * FROM Rooms WHERE room_id=?", [id]); if (res.length != 1) return null; - return Room.getRoomFromTableRow(res[0], client); + return Room.getRoomFromTableRow(res[0], client, + states: getStatesFromRoomId(id)); } - /// Returns a room without events and participants. - Future getRoomByAlias(String alias) async { - List> res = await db - .rawQuery("SELECT * FROM Rooms WHERE canonical_alias=?", [alias]); - if (res.length != 1) return null; - return Room.getRoomFromTableRow(res[0], client); - } - - /// Calculates and returns an avatar for a direct chat by a given [roomID]. - Future getAvatarFromSingleChat(String roomID) async { - String avatarStr = ""; - List> res = await db.rawQuery( - "SELECT avatar_url FROM Users " + - " WHERE Users.chat_id=? " + - " AND (Users.membership='join' OR Users.membership='invite') " + - " AND Users.matrix_id!=? ", - [roomID, client.userID]); - if (res.length == 1) avatarStr = res[0]["avatar_url"]; - return avatarStr; - } - - /// Calculates a chat name for a groupchat without a name. The chat name will - /// be the name of all users (excluding the user of this client) divided by - /// ','. - Future getChatNameFromMemberNames(String roomID) async { - String displayname = 'Empty chat'; - List> rs = await db.rawQuery( - "SELECT Users.displayname, Users.matrix_id, Users.membership FROM Users " + - " WHERE Users.chat_id=? " + - " AND (Users.membership='join' OR Users.membership='invite') " + - " AND Users.matrix_id!=? ", - [roomID, client.userID]); - if (rs.length > 0) { - displayname = ""; - for (var i = 0; i < rs.length; i++) { - String username = rs[i]["displayname"]; - if (username == "" || username == null) username = rs[i]["matrix_id"]; - if (rs[i]["state_key"] != client.userID) displayname += username + ", "; - } - if (displayname == "" || displayname == null) - displayname = 'Empty chat'; - else - displayname = displayname.substring(0, displayname.length - 2); - } - return displayname; - } - - /// Returns the (first) room ID from the store which is a private chat with - /// the user [userID]. Returns null if there is none. - Future getDirectChatRoomID(String userID) async { - List> res = await db.rawQuery( - "SELECT id FROM Rooms WHERE direct_chat_matrix_id=? AND membership!='leave' LIMIT 1", - [userID]); - if (res.length != 1) return null; - return res[0]["id"]; - } - - /// Returns the power level of the user for the given [roomID]. Returns null if - /// the room or the own user wasn't found. - Future getPowerLevel(String roomID) async { - List> res = await db.rawQuery( - "SELECT power_level FROM Users WHERE matrix_id=? AND chat_id=?", - [roomID, client.userID]); - if (res.length != 1) return null; - return res[0]["power_level"]; - } - - /// Returns the power levels from all users for the given [roomID]. - Future> getPowerLevels(String roomID) async { - List> res = await db.rawQuery( - "SELECT matrix_id, power_level FROM Users WHERE chat_id=?", - [roomID, client.userID]); - Map powerMap = {}; - for (int i = 0; i < res.length; i++) - powerMap[res[i]["matrix_id"]] = res[i]["power_level"]; - return powerMap; - } - - Future>> getAccountDataDirectChats() async { - Map> directChats = {}; - List> res = await db.rawQuery( - "SELECT id, direct_chat_matrix_id FROM Rooms WHERE direct_chat_matrix_id!=''"); - for (int i = 0; i < res.length; i++) { - if (directChats.containsKey(res[i]["direct_chat_matrix_id"])) - directChats[res[i]["direct_chat_matrix_id"]].add(res[i]["id"]); - else - directChats[res[i]["direct_chat_matrix_id"]] = [res[i]["id"]]; - } - return directChats; + Future>> getStatesFromRoomId(String id) async { + return db.rawQuery("SELECT * FROM RoomStates WHERE room_id=?", [id]); } Future forgetRoom(String roomID) async { - await db.rawDelete("DELETE FROM Rooms WHERE id=?", [roomID]); + await db.rawDelete("DELETE FROM Rooms WHERE room_id=?", [roomID]); return; } /// Searches for the event in the store. Future getEventById(String eventID, Room room) async { List> res = await db.rawQuery( - "SELECT * FROM Events WHERE id=? AND chat_id=?", [eventID, room.id]); + "SELECT * FROM Events WHERE id=? AND room_id=?", [eventID, room.id]); if (res.length == 0) return null; - return Event.fromJson(res[0], room, - senderUser: (await room.getUserByMXID(res[0]["sender"]))); + return Event.fromJson(res[0], room); + } + + Future> getAccountData() async { + Map newAccountData = {}; + List> rawAccountData = + await db.rawQuery("SELECT * FROM AccountData"); + for (int i = 0; i < rawAccountData.length; i++) + newAccountData[rawAccountData[i]["type"]] = + AccountData.fromJson(rawAccountData[i]); + return newAccountData; + } + + Future> getPresences() async { + Map newPresences = {}; + List> rawPresences = + await db.rawQuery("SELECT * FROM Presences"); + for (int i = 0; i < rawPresences.length; i++) + newPresences[rawPresences[i]["type"]] = + Presence.fromJson(rawPresences[i]); + return newPresences; } Future forgetNotification(String roomID) async { @@ -679,7 +438,8 @@ class Store { "INSERT INTO NotificationsCache(id, chat_id, event_id) VALUES (?, ?, ?)", [uniqueID, roomID, event_id]); // Make sure we got the same unique ID everywhere - await db.rawUpdate("UPDATE NotificationsCache SET id=? WHERE chat_id=?", [uniqueID, roomID]); + await db.rawUpdate("UPDATE NotificationsCache SET id=? WHERE chat_id=?", + [uniqueID, roomID]); return; } @@ -707,70 +467,63 @@ class Store { 'UNIQUE(client))', /// The database scheme for the Room class. - "Rooms": 'CREATE TABLE IF NOT EXISTS Rooms(' + - 'id TEXT PRIMARY KEY, ' + + 'Rooms': 'CREATE TABLE IF NOT EXISTS Rooms(' + + 'room_id TEXT PRIMARY KEY, ' + 'membership TEXT, ' + - 'topic TEXT, ' + 'highlight_count INTEGER, ' + 'notification_count INTEGER, ' + + 'prev_batch TEXT, ' + 'joined_member_count INTEGER, ' + 'invited_member_count INTEGER, ' + 'heroes TEXT, ' + - 'prev_batch TEXT, ' + - 'avatar_url TEXT, ' + - 'draft TEXT, ' + - 'unread INTEGER, ' + // Timestamp of when the user has last read the chat - 'fully_read TEXT, ' + // ID of the fully read marker event - 'description TEXT, ' + - 'canonical_alias TEXT, ' + // The address in the form: #roomname:homeserver.org - 'direct_chat_matrix_id TEXT, ' + //If this room is a direct chat, this is the matrix ID of the user - 'notification_settings TEXT, ' + // Must be one of [all, mention] + 'UNIQUE(room_id))', - // Security rules - 'guest_access TEXT, ' + - 'history_visibility TEXT, ' + - 'join_rules TEXT, ' + + /// The database scheme for the TimelineEvent class. + 'Events': 'CREATE TABLE IF NOT EXISTS Events(' + + 'event_id TEXT PRIMARY KEY, ' + + 'room_id TEXT, ' + + 'origin_server_ts INTEGER, ' + + 'sender TEXT, ' + + 'type TEXT, ' + + 'unsigned TEXT, ' + + 'content TEXT, ' + + 'prev_content TEXT, ' + + 'state_key TEXT, ' + + "status INTEGER, " + + 'UNIQUE(event_id))', - // Power levels - 'power_events_default INTEGER, ' + - 'power_state_default INTEGER, ' + - 'power_redact INTEGER, ' + - 'power_invite INTEGER, ' + - 'power_ban INTEGER, ' + - 'power_kick INTEGER, ' + - 'power_user_default INTEGER, ' + - - // Power levels for events - 'power_event_avatar INTEGER, ' + - 'power_event_history_visibility INTEGER, ' + - 'power_event_canonical_alias INTEGER, ' + - 'power_event_aliases INTEGER, ' + - 'power_event_name INTEGER, ' + - 'power_event_power_levels INTEGER, ' + - 'UNIQUE(id))', - - /// The database scheme for the Event class. - "Events": 'CREATE TABLE IF NOT EXISTS Events(' + - 'id TEXT PRIMARY KEY, ' + - 'chat_id TEXT, ' + + /// The database scheme for room states. + 'RoomStates': 'CREATE TABLE IF NOT EXISTS RoomStates(' + + 'event_id TEXT PRIMARY KEY, ' + + 'room_id TEXT, ' + 'origin_server_ts INTEGER, ' + 'sender TEXT, ' + 'state_key TEXT, ' + - 'content_body TEXT, ' + + 'unsigned TEXT, ' + + 'prev_content TEXT, ' + 'type TEXT, ' + - 'content_json TEXT, ' + - "status INTEGER, " + - 'UNIQUE(id))', + 'content TEXT, ' + + 'UNIQUE(room_id,state_key,type))', - /// The database scheme for the User class. - "Users": 'CREATE TABLE IF NOT EXISTS Users(' + - 'chat_id TEXT, ' + // The chat id of this membership - 'matrix_id TEXT, ' + // The matrix id of this user - 'displayname TEXT, ' + - 'avatar_url TEXT, ' + - 'membership TEXT, ' + // The status of the membership. Must be one of [join, invite, ban, leave] - 'power_level INTEGER, ' + // The power level of this user. Must be in [0,..,100] - 'UNIQUE(chat_id, matrix_id))', + /// The database scheme for room states. + 'AccountData': 'CREATE TABLE IF NOT EXISTS AccountData(' + + 'type TEXT PRIMARY KEY, ' + + 'content TEXT, ' + + 'UNIQUE(type))', + + /// The database scheme for room states. + 'RoomAccountData': 'CREATE TABLE IF NOT EXISTS RoomAccountData(' + + 'type TEXT PRIMARY KEY, ' + + 'room_id TEXT, ' + + 'content TEXT, ' + + 'UNIQUE(type,room_id))', + + /// The database scheme for room states. + 'Presences': 'CREATE TABLE IF NOT EXISTS Presences(' + + 'type TEXT PRIMARY KEY, ' + + 'sender TEXT, ' + + 'content TEXT, ' + + 'UNIQUE(sender))', /// The database scheme for the NotificationsCache class. "NotificationsCache": 'CREATE TABLE IF NOT EXISTS NotificationsCache(' + diff --git a/lib/src/Timeline.dart b/lib/src/Timeline.dart index 6fd2adb9..aeb89dfd 100644 --- a/lib/src/Timeline.dart +++ b/lib/src/Timeline.dart @@ -28,6 +28,9 @@ import 'Room.dart'; import 'User.dart'; import 'sync/EventUpdate.dart'; +typedef onTimelineUpdateCallback = void Function(); +typedef onTimelineInsertCallback = void Function(int insertID); + /// Represents the timeline of a room. The callbacks [onUpdate], [onDelete], /// [onInsert] and [onResort] will be triggered automatically. The initial /// event list will be retreived when created by the [room.getTimeline] method. @@ -47,8 +50,8 @@ class Timeline { int _findEvent({String event_id, String unsigned_txid}) { int i; for (i = 0; i < events.length; i++) { - if (events[i].id == event_id || - (unsigned_txid != null && events[i].id == unsigned_txid)) break; + if (events[i].eventId == event_id || + (unsigned_txid != null && events[i].eventId == unsigned_txid)) break; } return i; } @@ -82,33 +85,7 @@ class Timeline { eventUpdate.content["avatar_url"] = senderUser.avatarUrl.mxc; } - User stateKeyUser; - if (eventUpdate.content.containsKey("state_key")) { - stateKeyUser = await room.client.store?.getUser( - matrixID: eventUpdate.content["state_key"], room: room); - } - - if (senderUser != null && stateKeyUser != null) { - newEvent = Event.fromJson(eventUpdate.content, room, - senderUser: senderUser, stateKeyUser: stateKeyUser); - } else if (senderUser != null) { - newEvent = Event.fromJson(eventUpdate.content, room, - senderUser: senderUser); - } else if (stateKeyUser != null) { - newEvent = Event.fromJson(eventUpdate.content, room, - stateKeyUser: stateKeyUser); - } else { - newEvent = Event.fromJson(eventUpdate.content, room); - } - - // TODO update to type check when https://gitlab.com/famedly/famedlysdk/merge_requests/28/ is merged - if (newEvent.content.containsKey("m.relates_to")) { - Map relates_to = newEvent.content["m.relates_to"]; - if (relates_to.containsKey("m.in_reply_to")) { - newEvent.replyEvent = await room.getEventById(newEvent - .content["m.relates_to"]["m.in_reply_to"]["event_id"]); - } - } + newEvent = Event.fromJson(eventUpdate.content, room); events.insert(0, newEvent); if (onInsert != null) onInsert(0); @@ -128,6 +105,3 @@ class Timeline { if (onUpdate != null) onUpdate(); } } - -typedef onTimelineUpdateCallback = void Function(); -typedef onTimelineInsertCallback = void Function(int insertID); diff --git a/lib/src/User.dart b/lib/src/User.dart index a45d101f..311c5fe0 100644 --- a/lib/src/User.dart +++ b/lib/src/User.dart @@ -22,7 +22,9 @@ */ import 'package:famedlysdk/src/Room.dart'; +import 'package:famedlysdk/src/RoomState.dart'; import 'package:famedlysdk/src/responses/ErrorResponse.dart'; +import 'package:famedlysdk/src/utils/ChatTime.dart'; import 'package:famedlysdk/src/utils/MxContent.dart'; import 'Connection.dart'; @@ -30,85 +32,80 @@ import 'Connection.dart'; enum Membership { join, invite, leave, ban } /// Represents a Matrix User which may be a participant in a Matrix Room. -class User { +class User extends RoomState { + factory User( + String id, { + String membership, + String displayName, + String avatarUrl, + Room room, + }) { + Map content = {}; + if (membership != null) content["membership"] = membership; + if (displayName != null) content["displayname"] = displayName; + if (avatarUrl != null) content["avatar_url"] = avatarUrl; + return User.fromState( + stateKey: id, + content: content, + typeKey: "m.room.member", + roomId: room?.id, + room: room, + time: ChatTime.now(), + ); + } + + User.fromState( + {dynamic prevContent, + String stateKey, + dynamic content, + String typeKey, + String eventId, + String roomId, + String senderId, + ChatTime time, + dynamic unsigned, + Room room}) + : super( + stateKey: stateKey, + prevContent: prevContent, + content: content, + typeKey: typeKey, + eventId: eventId, + roomId: roomId, + senderId: senderId, + time: time, + unsigned: unsigned, + room: room); + /// The full qualified Matrix ID in the format @username:server.abc. - final String id; + String get id => stateKey; /// The displayname of the user if the user has set one. - final String displayName; + String get displayName => content != null ? content["displayname"] : null; /// The membership status of the user. One of: /// join /// invite /// leave /// ban - Membership membership; + Membership get membership => Membership.values.firstWhere((e) { + if (content["membership"] != null) { + return e.toString() == 'Membership.' + content['membership']; + } + return false; + }); /// The avatar if the user has one. - MxContent avatarUrl; - - /// The powerLevel of the user. Normally: - /// 0=Normal user - /// 50=Moderator - /// 100=Admin - int powerLevel = 0; - - /// All users normally belong to a room. - final Room room; - - @Deprecated("Use membership instead!") - String get status => membership.toString().split('.').last; - - @Deprecated("Use ID instead!") - String get mxid => id; - - @Deprecated("Use avatarUrl instead!") - MxContent get avatar_url => avatarUrl; - - User( - String id, { - this.membership, - this.displayName, - this.avatarUrl, - this.powerLevel, - this.room, - }) : this.id = id ?? ""; + MxContent get avatarUrl => content != null && content["avatar_url"] is String + ? MxContent(content["avatar_url"]) + : MxContent(""); /// Returns the displayname or the local part of the Matrix ID if the user /// has no displayname. String calcDisplayname() => (displayName == null || displayName.isEmpty) - ? id.replaceFirst("@", "").split(":")[0] + ? stateKey.replaceFirst("@", "").split(":")[0] : displayName; - /// Creates a new User object from a json string like a row from the database. - static User fromJson(Map json, Room room) { - return User(json['matrix_id'] ?? json['sender'], - displayName: json['displayname'], - avatarUrl: MxContent(json['avatar_url']), - membership: Membership.values.firstWhere((e) { - if (json["membership"] != null) { - return e.toString() == 'Membership.' + json['membership']; - } - return false; - }, orElse: () => null), - powerLevel: json['power_level'], - room: room); - } - - /// Checks if the client's user has the permission to kick this user. - Future get canKick async { - final int ownPowerLevel = await room.client.store.getPowerLevel(room.id); - return ownPowerLevel > powerLevel && - ownPowerLevel >= room.powerLevels["power_kick"]; - } - - /// Checks if the client's user has the permission to ban or unban this user. - Future get canBan async { - final int ownPowerLevel = await room.client.store.getPowerLevel(room.id); - return ownPowerLevel > powerLevel && - ownPowerLevel >= room.powerLevels["power_ban"]; - } - /// Call the Matrix API to kick this user from this room. Future kick() async { dynamic res = await room.kick(id); @@ -137,7 +134,7 @@ class User { /// Returns null on error. Future startDirectChat() async { // Try to find an existing direct chat - String roomID = await room.client?.store?.getDirectChatRoomID(id); + String roomID = await room.client?.getDirectChatFromUserId(id); if (roomID != null) return roomID; // Start a new direct chat diff --git a/lib/src/utils/ChatTime.dart b/lib/src/utils/ChatTime.dart index bd77d6d1..f53c0581 100644 --- a/lib/src/utils/ChatTime.dart +++ b/lib/src/utils/ChatTime.dart @@ -59,7 +59,6 @@ class ChatTime { return toTimeString(); } else if (sameWeek) { switch (dateTime.weekday) { - // TODO: Needs localization case 1: return "Montag"; case 2: diff --git a/test/Client_test.dart b/test/Client_test.dart index 40460393..8239e0b0 100644 --- a/test/Client_test.dart +++ b/test/Client_test.dart @@ -23,8 +23,10 @@ import 'dart:async'; +import 'package:famedlysdk/src/AccountData.dart'; import 'package:famedlysdk/src/Client.dart'; import 'package:famedlysdk/src/Connection.dart'; +import 'package:famedlysdk/src/Presence.dart'; import 'package:famedlysdk/src/User.dart'; import 'package:famedlysdk/src/requests/SetPushersRequest.dart'; import 'package:famedlysdk/src/responses/ErrorResponse.dart'; @@ -61,10 +63,19 @@ void main() { Future errorFuture = matrix.connection.onError.stream.first; + int presenceCounter = 0; + int accountDataCounter = 0; + matrix.onPresence = (Presence data) { + presenceCounter++; + }; + matrix.onAccountData = (AccountData data) { + accountDataCounter++; + }; + final bool checkResp1 = - await matrix.checkServer("https://fakeServer.wrongaddress"); + await matrix.checkServer("https://fakeserver.wrongaddress"); final bool checkResp2 = - await matrix.checkServer("https://fakeServer.notExisting"); + await matrix.checkServer("https://fakeserver.notexisting"); ErrorResponse checkError = await errorFuture; @@ -107,6 +118,23 @@ void main() { expect(loginState, LoginState.logged); expect(firstSync, true); expect(sync["next_batch"] == matrix.prevBatch, true); + + expect(matrix.accountData.length, 2); + expect(matrix.getDirectChatFromUserId("@bob:example.com"), + "!726s6s6q:example.com"); + expect(matrix.roomList.rooms[1].directChatMatrixID, "@bob:example.com"); + expect(matrix.directChats, matrix.accountData["m.direct"].content); + expect(matrix.presences.length, 1); + expect(matrix.roomList.rooms.length, 2); + expect(matrix.roomList.rooms[1].canonicalAlias, + "#famedlyContactDiscovery:${matrix.userID.split(":")[1]}"); + final List contacts = await matrix.loadFamedlyContacts(); + expect(contacts.length, 1); + expect(contacts[0].senderId, "@alice:example.org"); + expect( + matrix.presences["@alice:example.com"].content["presence"], "online"); + expect(presenceCounter, 1); + expect(accountDataCounter, 2); }); test('Try to get ErrorResponse', () async { @@ -172,36 +200,39 @@ void main() { List eventUpdateList = await eventUpdateListFuture; - expect(eventUpdateList.length, 7); + expect(eventUpdateList.length, 8); - expect(eventUpdateList[0].eventType == "m.room.member", true); - expect(eventUpdateList[0].roomID == "!726s6s6q:example.com", true); - expect(eventUpdateList[0].type == "state", true); + expect(eventUpdateList[0].eventType, "m.room.member"); + expect(eventUpdateList[0].roomID, "!726s6s6q:example.com"); + expect(eventUpdateList[0].type, "state"); - expect(eventUpdateList[1].eventType == "m.room.member", true); - expect(eventUpdateList[1].roomID == "!726s6s6q:example.com", true); - expect(eventUpdateList[1].type == "timeline", true); + expect(eventUpdateList[1].eventType, "m.room.canonical_alias"); + expect(eventUpdateList[1].roomID, "!726s6s6q:example.com"); + expect(eventUpdateList[1].type, "state"); - expect(eventUpdateList[2].eventType == "m.room.message", true); - expect(eventUpdateList[2].roomID == "!726s6s6q:example.com", true); - expect(eventUpdateList[2].type == "timeline", true); + expect(eventUpdateList[2].eventType, "m.room.member"); + expect(eventUpdateList[2].roomID, "!726s6s6q:example.com"); + expect(eventUpdateList[2].type, "timeline"); - expect(eventUpdateList[3].eventType == "m.tag", true); - expect(eventUpdateList[3].roomID == "!726s6s6q:example.com", true); - expect(eventUpdateList[3].type == "account_data", true); + expect(eventUpdateList[3].eventType, "m.room.message"); + expect(eventUpdateList[3].roomID, "!726s6s6q:example.com"); + expect(eventUpdateList[3].type, "timeline"); - expect(eventUpdateList[4].eventType == "org.example.custom.room.config", - true); - expect(eventUpdateList[4].roomID == "!726s6s6q:example.com", true); - expect(eventUpdateList[4].type == "account_data", true); + expect(eventUpdateList[4].eventType, "m.tag"); + expect(eventUpdateList[4].roomID, "!726s6s6q:example.com"); + expect(eventUpdateList[4].type, "account_data"); - expect(eventUpdateList[5].eventType == "m.room.name", true); - expect(eventUpdateList[5].roomID == "!696r7674:example.com", true); - expect(eventUpdateList[5].type == "invite_state", true); + expect(eventUpdateList[5].eventType, "org.example.custom.room.config"); + expect(eventUpdateList[5].roomID, "!726s6s6q:example.com"); + expect(eventUpdateList[5].type, "account_data"); - expect(eventUpdateList[6].eventType == "m.room.member", true); - expect(eventUpdateList[6].roomID == "!696r7674:example.com", true); - expect(eventUpdateList[6].type == "invite_state", true); + expect(eventUpdateList[6].eventType, "m.room.name"); + expect(eventUpdateList[6].roomID, "!696r7674:example.com"); + expect(eventUpdateList[6].type, "invite_state"); + + expect(eventUpdateList[7].eventType, "m.room.member"); + expect(eventUpdateList[7].roomID, "!696r7674:example.com"); + expect(eventUpdateList[7].type, "invite_state"); }); test('User Update Test', () async { @@ -209,16 +240,13 @@ void main() { List eventUpdateList = await userUpdateListFuture; - expect(eventUpdateList.length, 3); + expect(eventUpdateList.length, 4); expect(eventUpdateList[0].eventType == "m.presence", true); expect(eventUpdateList[0].type == "presence", true); expect(eventUpdateList[1].eventType == "org.example.custom.config", true); expect(eventUpdateList[1].type == "account_data", true); - - expect(eventUpdateList[2].eventType == "m.new_device", true); - expect(eventUpdateList[2].type == "to_device", true); }); testWidgets('should get created', create); diff --git a/test/Event_test.dart b/test/Event_test.dart index a6356591..3d32bf48 100644 --- a/test/Event_test.dart +++ b/test/Event_test.dart @@ -24,8 +24,7 @@ import 'dart:convert'; import 'package:famedlysdk/famedlysdk.dart'; -import 'package:famedlysdk/src/Event.dart'; -import 'package:famedlysdk/src/User.dart'; +import 'package:famedlysdk/src/RoomState.dart'; import 'package:flutter_test/flutter_test.dart'; import 'FakeMatrixApi.dart'; @@ -36,9 +35,6 @@ void main() { final int timestamp = DateTime.now().millisecondsSinceEpoch; final String id = "!4fsdfjisjf:server.abc"; final String senderID = "@alice:server.abc"; - final String senderDisplayname = "Alice"; - final String empty = ""; - final Membership membership = Membership.join; final String type = "m.room.message"; final String msgtype = "m.text"; final String body = "Hello World"; @@ -49,29 +45,29 @@ void main() { Map jsonObj = { "event_id": id, - "matrix_id": senderID, - "displayname": senderDisplayname, - "avatar_url": empty, - "membership": membership.toString().split('.').last, + "sender": senderID, "origin_server_ts": timestamp, - "state_key": empty, "type": type, - "content_json": contentJson, + "status": 2, + "content": contentJson, }; test("Create from json", () async { Event event = Event.fromJson(jsonObj, null); - expect(event.id, id); - expect(event.sender.id, senderID); - expect(event.sender.displayName, senderDisplayname); - expect(event.sender.avatarUrl.mxc, empty); - expect(event.sender.membership, membership); + expect(event.eventId, id); + expect(event.senderId, senderID); expect(event.status, 2); expect(event.text, body); expect(event.formattedText, formatted_body); expect(event.getBody(), body); expect(event.type, EventTypes.Text); + jsonObj["state_key"] = ""; + RoomState state = RoomState.fromJson(jsonObj, null); + expect(state.eventId, id); + expect(state.stateKey, ""); + expect(state.key, "m.room.message"); + expect(state.timelineEvent.status, 1); }); test("Test all EventTypes", () async { Event event; @@ -121,7 +117,7 @@ void main() { expect(event.type, EventTypes.HistoryVisibility); jsonObj["type"] = "m.room.message"; - jsonObj["content"] = json.decode(jsonObj["content_json"]); + jsonObj["content"] = json.decode(jsonObj["content"]); jsonObj["content"]["msgtype"] = "m.notice"; event = Event.fromJson(jsonObj, null); diff --git a/test/FakeMatrixApi.dart b/test/FakeMatrixApi.dart index 5f3e8a96..315645d2 100644 --- a/test/FakeMatrixApi.dart +++ b/test/FakeMatrixApi.dart @@ -39,7 +39,7 @@ class FakeMatrixApi extends MockClient { method == "GET" ? request.url.queryParameters : request.body; var res = {}; - print("$method request to $action with Data: $data"); + //print("$method request to $action with Data: $data"); // Sync requests with timeout if (data is Map && data["timeout"] is String) { @@ -64,6 +64,20 @@ class FakeMatrixApi extends MockClient { static final Map> api = { "GET": { + "/client/r0/rooms/!localpart:server.abc/state/m.room.member/@getme:example.com": + (var req) => { + "content": { + "membership": "join", + "displayname": "You got me", + }, + "type": "m.room.member", + "event_id": "143273582443PhrSn:example.org", + "room_id": "!localpart:server.abc", + "sender": "@getme:example.com", + "state_key": "@getme:example.com", + "origin_server_ts": 1432735824653, + "unsigned": {"age": 1234} + }, "/client/r0/rooms/!localpart:server.abc/event/1234": (var req) => { "content": { "body": "This is an example text message", @@ -78,7 +92,7 @@ class FakeMatrixApi extends MockClient { "origin_server_ts": 1432735824653, "unsigned": {"age": 1234} }, - "/client/r0/rooms/!1234:example.com/messages?from=1234&dir=b&limit=100": + "/client/r0/rooms/!1234:example.com/messages?from=1234&dir=b&limit=100&filter=%7B%22room%22:%7B%22state%22:%7B%22lazy_load_members%22:true%7D%7D%7D": (var req) => { "start": "t47429-4392820_219380_26003_2265", "end": "t47409-4357353_219380_26003_2265", @@ -153,6 +167,24 @@ class FakeMatrixApi extends MockClient { {"type": "m.login.password"} ] }, + "/client/r0/rooms/!726s6s6q:example.com/members": (var req) => { + "chunk": [ + { + "content": { + "membership": "join", + "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF", + "displayname": "Alice Margatroid" + }, + "type": "m.room.member", + "event_id": "ยง143273582443PhrSn:example.org", + "room_id": "!636q39766251:example.com", + "sender": "@alice:example.org", + "origin_server_ts": 1432735824653, + "unsigned": {"age": 1234}, + "state_key": "@alice:example.org" + } + ] + }, "/client/r0/rooms/!localpart:server.abc/members": (var req) => { "chunk": [ { @@ -333,7 +365,16 @@ class FakeMatrixApi extends MockClient { { "type": "org.example.custom.config", "content": {"custom_config_key": "custom_config_value"} - } + }, + { + "content": { + "@bob:example.com": [ + "!726s6s6q:example.com", + "!hgfedcba:example.com" + ] + }, + "type": "m.direct" + }, ] }, "to_device": { @@ -364,6 +405,17 @@ class FakeMatrixApi extends MockClient { "content": {"membership": "join"}, "origin_server_ts": 1417731086795, "event_id": "66697273743031:example.com" + }, + { + "sender": "@alice:example.com", + "type": "m.room.canonical_alias", + "content": { + "alias": + "#famedlyContactDiscovery:fakeServer.notExisting" + }, + "state_key": "", + "origin_server_ts": 1417731086796, + "event_id": "66697273743032:example.com" } ] }, @@ -465,12 +517,30 @@ class FakeMatrixApi extends MockClient { "room_id": "!1234:fakeServer.notExisting", }, "/client/r0/rooms/!localpart:server.abc/read_markers": (var reqI) => {}, + "/client/r0/rooms/!localpart:server.abc/kick": (var reqI) => {}, + "/client/r0/rooms/!localpart:server.abc/ban": (var reqI) => {}, + "/client/r0/rooms/!localpart:server.abc/unban": (var reqI) => {}, + "/client/r0/rooms/!localpart:server.abc/invite": (var reqI) => {}, }, "PUT": { "/client/r0/rooms/!1234:example.com/send/m.room.message/1234": (var reqI) => { "event_id": "42", }, + "/client/r0/rooms/!localpart:server.abc/state/m.room.name": (var reqI) => + { + "event_id": "42", + }, + "/client/r0/rooms/!localpart:server.abc/state/m.room.topic": (var reqI) => + { + "event_id": "42", + }, + "/client/r0/rooms/!localpart:server.abc/state/m.room.power_levels": + (var reqI) => { + "event_id": "42", + }, + "/client/r0/user/@test:fakeServer.notExisting/account_data/m.direct": + (var reqI) => {}, }, "DELETE": { "/unknown/token": (var req) => {"errcode": "M_UNKNOWN_TOKEN"}, diff --git a/test/Room_test.dart b/test/Room_test.dart index b70be7fe..e016f6be 100644 --- a/test/Room_test.dart +++ b/test/Room_test.dart @@ -24,7 +24,10 @@ import 'package:famedlysdk/src/Client.dart'; import 'package:famedlysdk/src/Event.dart'; import 'package:famedlysdk/src/Room.dart'; +import 'package:famedlysdk/src/RoomState.dart'; +import 'package:famedlysdk/src/Timeline.dart'; import 'package:famedlysdk/src/User.dart'; +import 'package:famedlysdk/src/utils/ChatTime.dart'; import 'package:flutter_test/flutter_test.dart'; import 'FakeMatrixApi.dart'; @@ -50,24 +53,9 @@ void main() { test("Create from json", () async { final String id = "!localpart:server.abc"; - final String name = "My Room"; final Membership membership = Membership.join; - final String topic = "This is my own room"; - final int unread = DateTime.now().millisecondsSinceEpoch; final int notificationCount = 2; final int highlightCount = 1; - final String fullyRead = "fjh82jdjifd:server.abc"; - final String notificationSettings = "all"; - final String guestAccess = "forbidden"; - final String canonicalAlias = "#testroom:example.com"; - final String historyVisibility = "invite"; - final String joinRules = "invite"; - final int now = DateTime.now().millisecondsSinceEpoch; - final String msgtype = "m.text"; - final String body = "Hello World"; - final String formatted_body = "Hello World"; - final String contentJson = - '{"msgtype":"$msgtype","body":"$body","formatted_body":"$formatted_body"}'; final List heroes = [ "@alice:matrix.org", "@bob:example.com", @@ -75,37 +63,12 @@ void main() { ]; Map jsonObj = { - "id": id, + "room_id": id, "membership": membership.toString().split('.').last, - "topic": name, - "description": topic, "avatar_url": "", "notification_count": notificationCount, "highlight_count": highlightCount, - "unread": unread, - "fully_read": fullyRead, - "notification_settings": notificationSettings, - "direct_chat_matrix_id": "", - "draft": "", "prev_batch": "", - "guest_access": guestAccess, - "history_visibility": historyVisibility, - "join_rules": joinRules, - "canonical_alias": canonicalAlias, - "power_events_default": 0, - "power_state_default": 0, - "power_redact": 0, - "power_invite": 0, - "power_ban": 0, - "power_kick": 0, - "power_user_default": 0, - "power_event_avatar": 0, - "power_event_history_visibility": 0, - "power_event_canonical_alias": 0, - "power_event_aliases": 0, - "power_event_name": 0, - "power_event_power_levels": 0, - "content_json": contentJson, "joined_member_count": notificationCount, "invited_member_count": notificationCount, "heroes": heroes.join(","), @@ -115,37 +78,69 @@ void main() { expect(room.id, id); expect(room.membership, membership); - expect(room.name, name); - expect(room.displayname, name); - expect(room.topic, topic); - expect(room.avatar.mxc, ""); expect(room.notificationCount, notificationCount); expect(room.highlightCount, highlightCount); - expect(room.unread.toTimeStamp(), unread); - expect(room.fullyRead, fullyRead); - expect(room.notificationSettings, notificationSettings); - expect(room.directChatMatrixID, ""); - expect(room.draft, ""); - expect(room.canonicalAlias, canonicalAlias); - expect(room.prev_batch, ""); - expect(room.guestAccess, guestAccess); - expect(room.historyVisibility, historyVisibility); - expect(room.joinRules, joinRules); - expect(room.lastMessage, body); - expect(room.timeCreated.toTimeStamp() >= now, true); - room.powerLevels.forEach((String key, int value) { - expect(value, 0); - }); expect(room.mJoinedMemberCount, notificationCount); expect(room.mInvitedMemberCount, notificationCount); expect(room.mHeroes, heroes); - - jsonObj["topic"] = ""; - room = await Room.getRoomFromTableRow(jsonObj, matrix); - expect(room.displayname, "testroom"); - jsonObj["canonical_alias"] = ""; - room = await Room.getRoomFromTableRow(jsonObj, matrix); expect(room.displayname, "alice, bob, charley"); + + room.states["m.room.canonical_alias"] = RoomState( + senderId: "@test:example.com", + typeKey: "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"] = RoomState( + senderId: "@test:example.com", + typeKey: "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"] = RoomState( + senderId: "@test:example.com", + typeKey: "m.room.topic", + roomId: room.id, + room: room, + eventId: "123", + content: {"topic": "testtopic"}, + stateKey: ""); + expect(room.topic, "testtopic"); + + expect(room.avatar.mxc, ""); + room.states["m.room.avatar"] = RoomState( + senderId: "@test:example.com", + typeKey: "m.room.avatar", + roomId: room.id, + room: room, + eventId: "123", + content: {"url": "mxc://testurl"}, + stateKey: ""); + expect(room.avatar.mxc, "mxc://testurl"); + + expect(room.lastEvent, null); + room.states["m.room.message"] = RoomState( + senderId: "@test:example.com", + typeKey: "m.room.message", + roomId: room.id, + room: room, + eventId: "12345", + time: ChatTime.now(), + content: {"msgtype": "m.text", "body": "test"}, + stateKey: ""); + expect(room.lastEvent.eventId, "12345"); + expect(room.lastMessage, "test"); + expect(room.timeCreated, room.lastEvent.time); }); test("sendReadReceipt", () async { @@ -167,7 +162,98 @@ void main() { test("getEventByID", () async { final Event event = await room.getEventById("1234"); - expect(event.id, "143273582443PhrSn:example.org"); + expect(event.eventId, "143273582443PhrSn:example.org"); + }); + + test("setName", () async { + final dynamic resp = await room.setName("Testname"); + expect(resp["event_id"], "42"); + }); + + test("setDescription", () async { + final dynamic resp = await room.setDescription("Testname"); + expect(resp["event_id"], "42"); + }); + + test("kick", () async { + final dynamic resp = await room.kick("Testname"); + expect(resp, {}); + }); + + test("ban", () async { + final dynamic resp = await room.ban("Testname"); + expect(resp, {}); + }); + + test("unban", () async { + final dynamic resp = await room.unban("Testname"); + expect(resp, {}); + }); + + test("PowerLevels", () async { + room.states["m.room.power_levels"] = RoomState( + senderId: "@test:example.com", + typeKey: "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); + expect(room.powerLevels, + room.states["m.room.power_levels"].content["users"]); + final dynamic resp = + await room.setPower("@test:fakeServer.notExisting", 90); + expect(resp["event_id"], "42"); + }); + + test("invite", () async { + final dynamic resp = await room.invite("Testname"); + expect(resp, {}); + }); + + test("getParticipants", () async { + room.states["@alice:test.abc"] = RoomState( + senderId: "@alice:test.abc", + typeKey: "m.room.member", + roomId: room.id, + room: room, + eventId: "12345", + time: ChatTime.now(), + content: {"displayname": "alice"}, + stateKey: "@alice:test.abc"); + final List userList = room.getParticipants(); + expect(userList.length, 1); + expect(userList[0].displayName, "alice"); + }); + + test("addToDirectChat", () async { + final dynamic resp = await room.addToDirectChat("Testname"); + expect(resp, {}); + }); + + test("getTimeline", () async { + final Timeline timeline = await room.getTimeline(); + expect(timeline.events, []); + }); + + test("getUserByMXID", () async { + final User user = await room.getUserByMXID("@getme:example.com"); + expect(user.stateKey, "@getme:example.com"); + expect(user.calcDisplayname(), "You got me"); }); }); } diff --git a/test/Timeline_test.dart b/test/Timeline_test.dart index b99c6494..956f1525 100644 --- a/test/Timeline_test.dart +++ b/test/Timeline_test.dart @@ -41,7 +41,12 @@ void main() { client.connection.httpClient = FakeMatrixApi(); client.homeserver = "https://fakeServer.notExisting"; - Room room = Room(id: roomID, client: client, prev_batch: "1234"); + Room room = Room( + id: roomID, + client: client, + prev_batch: "1234", + states: {}, + roomAccountData: {}); Timeline timeline = Timeline( room: room, events: [], @@ -87,10 +92,9 @@ void main() { expect(insertList, [0, 0]); expect(insertList.length, timeline.events.length); expect(timeline.events.length, 2); - expect(timeline.events[0].id, "1"); + expect(timeline.events[0].eventId, "1"); expect(timeline.events[0].sender.id, "@alice:example.com"); expect(timeline.events[0].time.toTimeStamp(), testTimeStamp); - expect(timeline.events[0].environment, "m.room.message"); expect(timeline.events[0].getBody(), "Testcase"); expect(timeline.events[0].time > timeline.events[1].time, true); }); @@ -103,7 +107,7 @@ void main() { expect(updateCount, 4); expect(insertList, [0, 0, 0]); expect(insertList.length, timeline.events.length); - expect(timeline.events[0].id, "42"); + expect(timeline.events[0].eventId, "42"); expect(timeline.events[0].status, 1); client.connection.onEvent.add(EventUpdate( @@ -125,7 +129,7 @@ void main() { expect(updateCount, 5); expect(insertList, [0, 0, 0]); expect(insertList.length, timeline.events.length); - expect(timeline.events[0].id, "42"); + expect(timeline.events[0].eventId, "42"); expect(timeline.events[0].status, 2); }); @@ -189,9 +193,9 @@ void main() { expect(updateCount, 19); expect(timeline.events.length, 9); - expect(timeline.events[6].id, "1143273582443PhrSn:example.org"); - expect(timeline.events[7].id, "2143273582443PhrSn:example.org"); - expect(timeline.events[8].id, "3143273582443PhrSn:example.org"); + expect(timeline.events[6].eventId, "1143273582443PhrSn:example.org"); + expect(timeline.events[7].eventId, "2143273582443PhrSn:example.org"); + expect(timeline.events[8].eventId, "3143273582443PhrSn:example.org"); expect(room.prev_batch, "t47409-4357353_219380_26003_2265"); }); }); diff --git a/test/User_test.dart b/test/User_test.dart index dcd8b594..dcb6c224 100644 --- a/test/User_test.dart +++ b/test/User_test.dart @@ -21,6 +21,7 @@ * along with famedlysdk. If not, see . */ +import 'package:famedlysdk/src/RoomState.dart'; import 'package:famedlysdk/src/User.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -32,30 +33,35 @@ void main() { final Membership membership = Membership.join; final String displayName = "Alice"; final String avatarUrl = ""; - final int powerLevel = 50; final Map jsonObj = { - "matrix_id": id, - "displayname": displayName, - "avatar_url": avatarUrl, - "membership": membership.toString().split('.').last, - "power_level": powerLevel, + "content": { + "membership": "join", + "avatar_url": avatarUrl, + "displayname": displayName + }, + "type": "m.room.member", + "event_id": "143273582443PhrSn:example.org", + "room_id": "!636q39766251:example.com", + "sender": id, + "origin_server_ts": 1432735824653, + "unsigned": {"age": 1234}, + "state_key": id }; - User user = User.fromJson(jsonObj, null); + User user = RoomState.fromJson(jsonObj, null).asUser; expect(user.id, id); expect(user.membership, membership); expect(user.displayName, displayName); expect(user.avatarUrl.mxc, avatarUrl); - expect(user.powerLevel, powerLevel); expect(user.calcDisplayname(), displayName); }); test("calcDisplayname", () async { final User user1 = User("@alice:example.com"); - final User user2 = User("@alice:example.com", displayName: "SuperAlice"); - final User user3 = User("@alice:example.com", displayName: ""); + final User user2 = User("@SuperAlice:example.com"); + final User user3 = User("@alice:example.com"); expect(user1.calcDisplayname(), "alice"); expect(user2.calcDisplayname(), "SuperAlice"); expect(user3.calcDisplayname(), "alice");