diff --git a/README.md b/README.md index c4409f0..c0ac0c2 100644 --- a/README.md +++ b/README.md @@ -39,15 +39,15 @@ Client matrix = Client("HappyChat", store: Store(this)); 3. Connect to a Matrix Homeserver and listen to the streams: ```dart -matrix.connection.onLoginStateChanged.stream.listen((bool loginState){ +matrix.onLoginStateChanged.stream.listen((bool loginState){ print("LoginState: ${loginState.toString()}"); }); -matrix.connection.onEvent.stream.listen((EventUpdate eventUpdate){ +matrix.onEvent.stream.listen((EventUpdate eventUpdate){ print("New event update!"); }); -matrix.connection.onRoomUpdate.stream.listen((RoomUpdate eventUpdate){ +matrix.onRoomUpdate.stream.listen((RoomUpdate eventUpdate){ print("New room update!"); }); @@ -59,7 +59,7 @@ final bool loginValid = await matrix.login("username", "password"); 4. Send a message to a Room: ```dart -final resp = await matrix.connection.jsonRequest( +final resp = await matrix.jsonRequest( type: "PUT", action: "/r0/rooms/!fjd823j:example.com/send/m.room.message/$txnId", data: { diff --git a/lib/famedlysdk.dart b/lib/famedlysdk.dart index cfdf3f6..223bb01 100644 --- a/lib/famedlysdk.dart +++ b/lib/famedlysdk.dart @@ -23,25 +23,21 @@ library famedlysdk; -export 'package:famedlysdk/src/requests/SetPushersRequest.dart'; -export 'package:famedlysdk/src/responses/PushrulesResponse.dart'; export 'package:famedlysdk/src/sync/RoomUpdate.dart'; export 'package:famedlysdk/src/sync/EventUpdate.dart'; export 'package:famedlysdk/src/sync/UserUpdate.dart'; -export 'package:famedlysdk/src/utils/ChatTime.dart'; export 'package:famedlysdk/src/utils/MatrixException.dart'; export 'package:famedlysdk/src/utils/MatrixFile.dart'; export 'package:famedlysdk/src/utils/MxContent.dart'; +export 'package:famedlysdk/src/utils/Profile.dart'; +export 'package:famedlysdk/src/utils/PushRules.dart'; export 'package:famedlysdk/src/utils/StatesMap.dart'; export 'package:famedlysdk/src/AccountData.dart'; export 'package:famedlysdk/src/Client.dart'; -export 'package:famedlysdk/src/Connection.dart'; export 'package:famedlysdk/src/Event.dart'; export 'package:famedlysdk/src/Presence.dart'; export 'package:famedlysdk/src/Room.dart'; export 'package:famedlysdk/src/RoomAccountData.dart'; -export 'package:famedlysdk/src/RoomList.dart'; -export 'package:famedlysdk/src/RoomState.dart'; export 'package:famedlysdk/src/StoreAPI.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 index b3a0a96..7d41ebc 100644 --- a/lib/src/AccountData.dart +++ b/lib/src/AccountData.dart @@ -21,8 +21,9 @@ * along with famedlysdk. If not, see . */ -import 'package:famedlysdk/src/RoomState.dart'; +import 'package:famedlysdk/famedlysdk.dart'; +/// The global private data created by this user. class AccountData { /// The json payload of the content. The content highly depends on the type. final Map content; @@ -35,7 +36,7 @@ class AccountData { /// 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']); + Event.getMapFromPayload(jsonPayload['content']); return AccountData(content: content, typeKey: jsonPayload['type']); } } diff --git a/lib/src/Client.dart b/lib/src/Client.dart index d6e8e79..da6f757 100644 --- a/lib/src/Client.dart +++ b/lib/src/Client.dart @@ -30,35 +30,40 @@ import 'package:famedlysdk/src/Presence.dart'; import 'package:famedlysdk/src/StoreAPI.dart'; import 'package:famedlysdk/src/sync/UserUpdate.dart'; import 'package:famedlysdk/src/utils/MatrixFile.dart'; - -import 'Connection.dart'; import 'Room.dart'; -import 'RoomList.dart'; -//import 'Store.dart'; -import 'RoomState.dart'; +import 'Event.dart'; import 'User.dart'; -import 'requests/SetPushersRequest.dart'; -import 'responses/PushrulesResponse.dart'; import 'utils/Profile.dart'; +import 'dart:convert'; +import 'package:famedlysdk/src/Room.dart'; +import 'package:http/http.dart' as http; +import 'package:mime_type/mime_type.dart'; +import 'sync/EventUpdate.dart'; +import 'sync/RoomUpdate.dart'; +import 'sync/UserUpdate.dart'; +import 'utils/MatrixException.dart'; typedef AccountDataEventCB = void Function(AccountData accountData); typedef PresenceCB = void Function(Presence presence); +enum HTTPType { GET, POST, PUT, DELETE } + +enum LoginState { logged, loggedOut } + /// Represents a Matrix client to communicate with a /// [Matrix](https://matrix.org) homeserver and is the entry point for this /// SDK. class Client { /// Handles the connection for this client. - Connection connection; + @deprecated + Client get connection => this; /// Optional persistent store for all data. StoreAPI store; Client(this.clientName, {this.debug = false, this.store}) { - connection = Connection(this); - if (this.clientName != "testclient") store = null; //Store(this); - connection.onLoginStateChanged.stream.listen((loginState) { + this.onLoginStateChanged.stream.listen((loginState) { print("LoginState: ${loginState.toString()}"); }); } @@ -70,35 +75,43 @@ class Client { final String clientName; /// The homeserver this client is communicating with. - String homeserver; + String get homeserver => _homeserver; + String _homeserver; /// The Matrix ID of the current logged user. - String userID; + String get userID => _userID; + String _userID; /// This is the access token for the matrix client. When it is undefined, then /// the user needs to sign in first. - String accessToken; + String get accessToken => _accessToken; + String _accessToken; /// This points to the position in the synchronization history. String prevBatch; /// The device ID is an unique identifier for this device. - String deviceID; + String get deviceID => _deviceID; + String _deviceID; /// The device name is a human readable identifier for this device. - String deviceName; + String get deviceName => _deviceName; + String _deviceName; /// Which version of the matrix specification does this server support? - List matrixVersions; + List get matrixVersions => _matrixVersions; + List _matrixVersions; /// Wheither the server supports lazy load members. - bool lazyLoadMembers = false; + bool get lazyLoadMembers => _lazyLoadMembers; + bool _lazyLoadMembers = false; /// Returns the current login state. bool isLogged() => accessToken != null; /// A list of all rooms the user is participating or invited. - RoomList roomList; + List get rooms => _rooms; + List _rooms = []; /// Key/Value store of account data. Map accountData = {}; @@ -112,6 +125,20 @@ class Client { /// Callback will be called on presences. PresenceCB onPresence; + 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 handleUserUpdate(UserUpdate userUpdate) { if (userUpdate.type == "account_data") { AccountData newAccountData = AccountData.fromJson(userUpdate.content); @@ -134,21 +161,21 @@ class Client { 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]; + if (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( + this.jsonRequest( type: HTTPType.PUT, action: "/client/r0/user/${userID}/account_data/m.direct", data: directChats); return getDirectChatFromUserId(userId); } - for (int i = 0; i < roomList.rooms.length; i++) - if (roomList.rooms[i].membership == Membership.invite && - roomList.rooms[i].states[userID]?.senderId == userId && - roomList.rooms[i].states[userID].content["is_direct"] == true) - return roomList.rooms[i].id; + for (int i = 0; i < this.rooms.length; i++) + if (this.rooms[i].membership == Membership.invite && + this.rooms[i].states[userID]?.senderId == userId && + this.rooms[i].states[userID].content["is_direct"] == true) + return this.rooms[i].id; return null; } @@ -158,9 +185,9 @@ class Client { /// Throws FormatException, TimeoutException and MatrixException on error. Future checkServer(serverUrl) async { try { - homeserver = serverUrl; - final versionResp = await connection.jsonRequest( - type: HTTPType.GET, action: "/client/versions"); + _homeserver = serverUrl; + final versionResp = await this + .jsonRequest(type: HTTPType.GET, action: "/client/versions"); final List versions = List.from(versionResp["versions"]); @@ -172,18 +199,18 @@ class Client { } } - matrixVersions = versions; + _matrixVersions = versions; if (versionResp.containsKey("unstable_features") && versionResp["unstable_features"].containsKey("m.lazy_load_members")) { - lazyLoadMembers = versionResp["unstable_features"] + _lazyLoadMembers = versionResp["unstable_features"] ["m.lazy_load_members"] ? true : false; } - final loginResp = await connection.jsonRequest( - type: HTTPType.GET, action: "/client/r0/login"); + final loginResp = await this + .jsonRequest(type: HTTPType.GET, action: "/client/r0/login"); final List flows = loginResp["flows"]; @@ -197,7 +224,7 @@ class Client { } return true; } catch (_) { - this.homeserver = this.matrixVersions = null; + this._homeserver = this._matrixVersions = null; rethrow; } } @@ -206,17 +233,19 @@ class Client { /// authentication. Returns false if the login was not successful. Throws /// MatrixException if login was not successful. Future login(String username, String password) async { - final loginResp = await connection - .jsonRequest(type: HTTPType.POST, action: "/client/r0/login", data: { - "type": "m.login.password", - "user": username, - "identifier": { - "type": "m.id.user", - "user": username, - }, - "password": password, - "initial_device_display_name": "Famedly Talk" - }); + final loginResp = await jsonRequest( + type: HTTPType.POST, + action: "/client/r0/login", + data: { + "type": "m.login.password", + "user": username, + "identifier": { + "type": "m.id.user", + "user": username, + }, + "password": password, + "initial_device_display_name": "Famedly Talk" + }); final userID = loginResp["user_id"]; final accessToken = loginResp["access_token"]; @@ -224,7 +253,7 @@ class Client { return false; } - await connection.connect( + await this.connect( newToken: accessToken, newUserID: userID, newHomeserver: homeserver, @@ -239,12 +268,11 @@ class Client { /// including all persistent data from the store. Future logout() async { try { - await connection.jsonRequest( - type: HTTPType.POST, action: "/client/r0/logout"); + await this.jsonRequest(type: HTTPType.POST, action: "/client/r0/logout"); } catch (exception) { rethrow; } finally { - await connection.clear(); + await this.clear(); } } @@ -252,33 +280,17 @@ class Client { /// fetch the user's own profile information or other users; either locally /// or on remote homeservers. Future getProfileFromUserId(String userId) async { - final dynamic resp = await connection.jsonRequest( + final dynamic resp = await this.jsonRequest( type: HTTPType.GET, action: "/client/r0/profile/${userId}"); return Profile.fromJson(resp); } - /// Creates a new [RoomList] object. - RoomList getRoomList( - {onRoomListUpdateCallback onUpdate, - onRoomListInsertCallback onInsert, - onRoomListRemoveCallback onRemove}) { - List rooms = roomList.rooms; - return RoomList( - client: this, - onlyLeft: false, - onUpdate: onUpdate, - onInsert: onInsert, - onRemove: onRemove, - rooms: rooms); - } - Future> get archive async { List archiveList = []; String syncFilters = '{"room":{"include_leave":true,"timeline":{"limit":10}}}'; String action = "/client/r0/sync?filter=$syncFilters&timeout=0"; - final sync = - await connection.jsonRequest(type: HTTPType.GET, action: action); + final sync = await this.jsonRequest(type: HTTPType.GET, action: action); if (sync["rooms"]["leave"] is Map) { for (var entry in sync["rooms"]["leave"].entries) { final String id = entry.key; @@ -301,13 +313,13 @@ class Client { if (room["timeline"] is Map && room["timeline"]["events"] is List) { for (dynamic event in room["timeline"]["events"]) { - leftRoom.setState(RoomState.fromJson(event, leftRoom)); + leftRoom.setState(Event.fromJson(event, leftRoom)); } } if (room["state"] is Map && room["state"]["events"] is List) { for (dynamic event in room["state"]["events"]) { - leftRoom.setState(RoomState.fromJson(event, leftRoom)); + leftRoom.setState(Event.fromJson(event, leftRoom)); } } archiveList.add(leftRoom); @@ -316,12 +328,9 @@ class Client { return archiveList; } - /// Searches in the roomList and in the archive for a room with the given [id]. - Room getRoomById(String id) => roomList.getRoomById(id); - Future joinRoomById(String id) async { - return await connection.jsonRequest( - type: HTTPType.POST, action: "/client/r0/join/$id"); + return await this + .jsonRequest(type: HTTPType.POST, action: "/client/r0/join/$id"); } /// Loads the contact list for this user excluding the user itself. @@ -330,14 +339,14 @@ class Client { /// defined by the autojoin room feature in Synapse. Future> loadFamedlyContacts() async { List contacts = []; - Room contactDiscoveryRoom = roomList - .getRoomByAlias("#famedlyContactDiscovery:${userID.split(":")[1]}"); + Room contactDiscoveryRoom = + this.getRoomByAlias("#famedlyContactDiscovery:${userID.split(":")[1]}"); if (contactDiscoveryRoom != null) contacts = await contactDiscoveryRoom.requestParticipants(); else { Map userMap = {}; - for (int i = 0; i < roomList.rooms.length; i++) { - List roomUsers = roomList.rooms[i].getParticipants(); + for (int i = 0; i < this.rooms.length; i++) { + List roomUsers = this.rooms[i].getParticipants(); for (int j = 0; j < roomUsers.length; j++) { if (userMap[roomUsers[j].id] != true) contacts.add(roomUsers[j]); userMap[roomUsers[j].id] = true; @@ -361,7 +370,7 @@ class Client { for (int i = 0; i < invite.length; i++) inviteIDs.add(invite[i].id); try { - final dynamic resp = await connection.jsonRequest( + final dynamic resp = await this.jsonRequest( type: HTTPType.POST, action: "/client/r0/createRoom", data: params == null @@ -377,8 +386,8 @@ class Client { /// Uploads a new user avatar for this user. Future setAvatar(MatrixFile file) async { - final uploadResp = await connection.upload(file); - await connection.jsonRequest( + final uploadResp = await this.upload(file); + await this.jsonRequest( type: HTTPType.PUT, action: "/client/r0/profile/$userID/avatar_url", data: {"avatar_url": uploadResp}); @@ -387,22 +396,584 @@ class Client { /// Fetches the pushrules for the logged in user. /// These are needed for notifications on Android - Future getPushrules() async { - final dynamic resp = await connection.jsonRequest( + Future getPushrules() async { + final dynamic resp = await this.jsonRequest( type: HTTPType.GET, action: "/client/r0/pushrules/", ); - return PushrulesResponse.fromJson(resp); + return PushRules.fromJson(resp); } /// This endpoint allows the creation, modification and deletion of pushers for this user ID. - Future setPushers(SetPushersRequest data) async { - await connection.jsonRequest( + Future setPushers(String pushKey, String kind, String appId, + String appDisplayName, String deviceDisplayName, String lang, String url, + {bool append, String profileTag, String format}) async { + Map data = { + "lang": lang, + "kind": kind, + "app_display_name": appDisplayName, + "device_display_name": deviceDisplayName, + "profile_tag": profileTag, + "app_id": appId, + "pushkey": pushKey, + "data": {"url": url} + }; + + if (format != null) data["data"]["format"] = format; + if (profileTag != null) data["profile_tag"] = profileTag; + if (append != null) data["append"] = append; + + await this.jsonRequest( type: HTTPType.POST, action: "/client/r0/pushers/set", - data: data.toJson(), + data: data, ); return; } + + static String syncFilters = '{"room":{"state":{"lazy_load_members":true}}}'; + + http.Client httpClient = http.Client(); + + /// The newEvent signal is the most important signal in this concept. Every time + /// the app receives a new synchronization, this event is called for every signal + /// to update the GUI. For example, for a new message, it is called: + /// onRoomEvent( "m.room.message", "!chat_id:server.com", "timeline", {sender: "@bob:server.com", body: "Hello world"} ) + final StreamController onEvent = + new StreamController.broadcast(); + + /// Outside of the events there are updates for the global chat states which + /// are handled by this signal: + final StreamController onRoomUpdate = + new StreamController.broadcast(); + + /// Outside of rooms there are account updates like account_data or presences. + final StreamController onUserEvent = + new StreamController.broadcast(); + + /// Called when the login state e.g. user gets logged out. + final StreamController onLoginStateChanged = + new StreamController.broadcast(); + + /// Synchronization erros are coming here. + final StreamController onError = + new StreamController.broadcast(); + + /// This is called once, when the first sync has received. + final StreamController onFirstSync = new StreamController.broadcast(); + + /// When a new sync response is coming in, this gives the complete payload. + final StreamController onSync = new StreamController.broadcast(); + + /// Matrix synchronisation is done with https long polling. This needs a + /// timeout which is usually 30 seconds. + int syncTimeoutSec = 30; + + /// How long should the app wait until it retrys the synchronisation after + /// an error? + int syncErrorTimeoutSec = 3; + + /// Sets the user credentials and starts the synchronisation. + /// + /// Before you can connect you need at least an [accessToken], a [homeserver], + /// a [userID], a [deviceID], and a [deviceName]. + /// + /// You get this informations + /// by logging in to your Matrix account, using the [login API](https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-login). + /// + /// To log in you can use [jsonRequest()] after you have set the [homeserver] + /// to a valid url. For example: + /// + /// ``` + /// final resp = await matrix + /// .jsonRequest(type: HTTPType.POST, action: "/client/r0/login", data: { + /// "type": "m.login.password", + /// "user": "test", + /// "password": "1234", + /// "initial_device_display_name": "Fluffy Matrix Client" + /// }); + /// ``` + /// + /// Returns: + /// + /// ``` + /// { + /// "user_id": "@cheeky_monkey:matrix.org", + /// "access_token": "abc123", + /// "device_id": "GHTYAJCE" + /// } + /// ``` + /// + /// Sends [LoginState.logged] to [onLoginStateChanged]. + void connect( + {String newToken, + String newHomeserver, + String newUserID, + String newDeviceName, + String newDeviceID, + List newMatrixVersions, + bool newLazyLoadMembers, + String newPrevBatch}) async { + this._accessToken = newToken; + this._homeserver = newHomeserver; + this._userID = newUserID; + this._deviceID = newDeviceID; + this._deviceName = newDeviceName; + this._matrixVersions = newMatrixVersions; + this._lazyLoadMembers = newLazyLoadMembers; + this.prevBatch = newPrevBatch; + + if (this.store != null) { + this.store.storeClient(); + this._rooms = await this.store.getRoomList(onlyLeft: false); + this.accountData = await this.store.getAccountData(); + this.presences = await this.store.getPresences(); + } + + _userEventSub ??= onUserEvent.stream.listen(this.handleUserUpdate); + + onLoginStateChanged.add(LoginState.logged); + + _sync(); + } + + StreamSubscription _userEventSub; + + /// Resets all settings and stops the synchronisation. + void clear() { + this.store?.clear(); + this._accessToken = this._homeserver = this._userID = this._deviceID = this + ._deviceName = + this._matrixVersions = this._lazyLoadMembers = this.prevBatch = null; + onLoginStateChanged.add(LoginState.loggedOut); + } + + /// Used for all Matrix json requests using the [c2s API](https://matrix.org/docs/spec/client_server/r0.4.0.html). + /// + /// Throws: TimeoutException, FormatException, MatrixException + /// + /// You must first call [this.connect()] or set [this.homeserver] before you can use + /// this! For example to send a message to a Matrix room with the id + /// '!fjd823j:example.com' you call: + /// + /// ``` + /// final resp = await jsonRequest( + /// type: HTTPType.PUT, + /// action: "/r0/rooms/!fjd823j:example.com/send/m.room.message/$txnId", + /// data: { + /// "msgtype": "m.text", + /// "body": "hello" + /// } + /// ); + /// ``` + /// + Future> jsonRequest( + {HTTPType type, + String action, + dynamic data = "", + int timeout, + String contentType = "application/json"}) async { + if (this.isLogged() == false && this.homeserver == null) + throw ("No homeserver specified."); + if (timeout == null) timeout = syncTimeoutSec + 5; + dynamic json; + if (data is Map) data.removeWhere((k, v) => v == null); + (!(data is String)) ? json = jsonEncode(data) : json = data; + if (data is List || action.startsWith("/media/r0/upload")) json = data; + + final url = "${this.homeserver}/_matrix${action}"; + + Map headers = {}; + if (type == HTTPType.PUT || type == HTTPType.POST) + headers["Content-Type"] = contentType; + if (this.isLogged()) + headers["Authorization"] = "Bearer ${this.accessToken}"; + + if (this.debug) + print( + "[REQUEST ${type.toString().split('.').last}] Action: $action, Data: $data"); + + http.Response resp; + Map jsonResp = {}; + try { + switch (type.toString().split('.').last) { + case "GET": + resp = await httpClient + .get(url, headers: headers) + .timeout(Duration(seconds: timeout)); + break; + case "POST": + resp = await httpClient + .post(url, body: json, headers: headers) + .timeout(Duration(seconds: timeout)); + break; + case "PUT": + resp = await httpClient + .put(url, body: json, headers: headers) + .timeout(Duration(seconds: timeout)); + break; + case "DELETE": + resp = await httpClient + .delete(url, headers: headers) + .timeout(Duration(seconds: timeout)); + break; + } + jsonResp = jsonDecode(resp.body) + as Map; // May throw FormatException + + if (jsonResp.containsKey("errcode") && jsonResp["errcode"] is String) { + // The server has responsed with an matrix related error. + MatrixException exception = MatrixException(resp); + if (exception.error == MatrixError.M_UNKNOWN_TOKEN) { + // The token is no longer valid. Need to sign off.... + onError.add(exception); + clear(); + } + + throw exception; + } + + if (this.debug) print("[RESPONSE] ${jsonResp.toString()}"); + } on ArgumentError catch (exception) { + print(exception); + // Ignore this error + } catch (_) { + print(_); + rethrow; + } + + return jsonResp; + } + + /// Uploads a file with the name [fileName] as base64 encoded to the server + /// and returns the mxc url as a string. + Future upload(MatrixFile file) async { + dynamic fileBytes; + if (this.homeserver != "https://fakeServer.notExisting") + fileBytes = file.bytes; + String fileName = file.path.split("/").last.toLowerCase(); + String mimeType = mime(file.path); + print("[UPLOADING] $fileName, type: $mimeType, size: ${fileBytes?.length}"); + final Map resp = await jsonRequest( + type: HTTPType.POST, + action: "/media/r0/upload?filename=$fileName", + data: fileBytes, + contentType: mimeType); + return resp["content_uri"]; + } + + Future _syncRequest; + + Future _sync() async { + if (this.isLogged() == false) return; + + String action = "/client/r0/sync?filter=$syncFilters"; + + if (this.prevBatch != null) { + action += "&timeout=30000"; + action += "&since=${this.prevBatch}"; + } + try { + _syncRequest = jsonRequest(type: HTTPType.GET, action: action); + final int hash = _syncRequest.hashCode; + final syncResp = await _syncRequest; + if (hash != _syncRequest.hashCode) return; + if (this.store != null) + await this.store.transaction(() { + handleSync(syncResp); + this.store.storePrevBatch(syncResp); + return; + }); + else + await handleSync(syncResp); + if (this.prevBatch == null) this.onFirstSync.add(true); + this.prevBatch = syncResp["next_batch"]; + if (hash == _syncRequest.hashCode) _sync(); + } on MatrixException catch (exception) { + onError.add(exception); + await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync); + } catch (exception) { + await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync); + } + } + + void handleSync(dynamic sync) { + if (sync["rooms"] is Map) { + if (sync["rooms"]["join"] is Map) + _handleRooms(sync["rooms"]["join"], Membership.join); + if (sync["rooms"]["invite"] is Map) + _handleRooms(sync["rooms"]["invite"], Membership.invite); + if (sync["rooms"]["leave"] is Map) + _handleRooms(sync["rooms"]["leave"], Membership.leave); + } + if (sync["presence"] is Map && + sync["presence"]["events"] is List) { + _handleGlobalEvents(sync["presence"]["events"], "presence"); + } + if (sync["account_data"] is Map && + sync["account_data"]["events"] is List) { + _handleGlobalEvents(sync["account_data"]["events"], "account_data"); + } + if (sync["to_device"] is Map && + sync["to_device"]["events"] is List) { + _handleGlobalEvents(sync["to_device"]["events"], "to_device"); + } + onSync.add(sync); + } + + void _handleRooms(Map rooms, Membership membership) { + rooms.forEach((String id, dynamic room) async { + // calculate the notification counts, the limitedTimeline and prevbatch + num highlight_count = 0; + num notification_count = 0; + String prev_batch = ""; + bool limitedTimeline = false; + + if (room["unread_notifications"] is Map) { + if (room["unread_notifications"]["highlight_count"] is num) + highlight_count = room["unread_notifications"]["highlight_count"]; + if (room["unread_notifications"]["notification_count"] is num) + notification_count = + room["unread_notifications"]["notification_count"]; + } + + if (room["timeline"] is Map) { + if (room["timeline"]["limited"] is bool) + limitedTimeline = room["timeline"]["limited"]; + if (room["timeline"]["prev_batch"] is String) + prev_batch = room["timeline"]["prev_batch"]; + } + + RoomSummary summary; + + if (room["summary"] is Map) { + summary = RoomSummary.fromJson(room["summary"]); + } + + RoomUpdate update = RoomUpdate( + id: id, + membership: membership, + notification_count: notification_count, + highlight_count: highlight_count, + limitedTimeline: limitedTimeline, + prev_batch: prev_batch, + summary: summary, + ); + _updateRoomsByRoomUpdate(update); + this.store?.storeRoomUpdate(update); + onRoomUpdate.add(update); + + /// Handle now all room events and save them in the database + if (room["state"] is Map && + room["state"]["events"] is List) + _handleRoomEvents(id, room["state"]["events"], "state"); + + if (room["invite_state"] is Map && + room["invite_state"]["events"] is List) + _handleRoomEvents(id, room["invite_state"]["events"], "invite_state"); + + if (room["timeline"] is Map && + room["timeline"]["events"] is List) + _handleRoomEvents(id, room["timeline"]["events"], "timeline"); + + if (room["ephemeral"] is Map && + room["ephemeral"]["events"] is List) + _handleEphemerals(id, room["ephemeral"]["events"]); + + if (room["account_data"] is Map && + room["account_data"]["events"] is List) + _handleRoomEvents(id, room["account_data"]["events"], "account_data"); + }); + } + + void _handleEphemerals(String id, List events) { + for (num i = 0; i < events.length; i++) { + _handleEvent(events[i], id, "ephemeral"); + + // Receipt events are deltas between two states. We will create a + // fake room account data event for this and store the difference + // there. + if (events[i]["type"] == "m.receipt") { + Room room = this.getRoomById(id); + if (room == null) room = Room(id: id); + + Map receiptStateContent = + room.roomAccountData["m.receipt"]?.content ?? {}; + for (var eventEntry in events[i]["content"].entries) { + final String eventID = eventEntry.key; + if (events[i]["content"][eventID]["m.read"] != null) { + final Map userTimestampMap = + events[i]["content"][eventID]["m.read"]; + for (var userTimestampMapEntry in userTimestampMap.entries) { + final String mxid = userTimestampMapEntry.key; + + // Remove previous receipt event from this user + for (var entry in receiptStateContent.entries) { + if (entry.value["m.read"] is Map && + entry.value["m.read"].containsKey(mxid)) { + entry.value["m.read"].remove(mxid); + break; + } + } + if (userTimestampMap[mxid] is Map && + userTimestampMap[mxid].containsKey("ts")) { + receiptStateContent[mxid] = { + "event_id": eventID, + "ts": userTimestampMap[mxid]["ts"], + }; + } + } + } + } + events[i]["content"] = receiptStateContent; + _handleEvent(events[i], id, "account_data"); + } + } + } + + void _handleRoomEvents(String chat_id, List events, String type) { + for (num i = 0; i < events.length; i++) { + _handleEvent(events[i], chat_id, type); + } + } + + void _handleGlobalEvents(List events, String type) { + for (int i = 0; i < events.length; i++) + if (events[i]["type"] is String && + events[i]["content"] is Map) { + UserUpdate update = UserUpdate( + eventType: events[i]["type"], + type: type, + content: events[i], + ); + this.store?.storeUserEventUpdate(update); + onUserEvent.add(update); + } + } + + void _handleEvent(Map event, String roomID, String type) { + if (event["type"] is String && event["content"] is Map) { + EventUpdate update = EventUpdate( + eventType: event["type"], + roomID: roomID, + type: type, + content: event, + ); + _updateRoomsByEventUpdate(update); + this.store?.storeEventUpdate(update); + onEvent.add(update); + } + } + + void _updateRoomsByRoomUpdate(RoomUpdate chatUpdate) { + // Update the chat list item. + // Search the room in the rooms + num j = 0; + for (j = 0; j < rooms.length; j++) { + if (rooms[j].id == chatUpdate.id) break; + } + final bool found = (j < rooms.length && rooms[j].id == chatUpdate.id); + final bool isLeftRoom = chatUpdate.membership == Membership.leave; + + // Does the chat already exist in the list rooms? + if (!found && !isLeftRoom) { + num position = chatUpdate.membership == Membership.invite ? 0 : j; + // Add the new chat to the list + Room newRoom = Room( + id: chatUpdate.id, + membership: chatUpdate.membership, + prev_batch: chatUpdate.prev_batch, + highlightCount: chatUpdate.highlight_count, + notificationCount: chatUpdate.notification_count, + mHeroes: chatUpdate.summary?.mHeroes, + mJoinedMemberCount: chatUpdate.summary?.mJoinedMemberCount, + mInvitedMemberCount: chatUpdate.summary?.mInvitedMemberCount, + roomAccountData: {}, + client: this, + ); + rooms.insert(position, newRoom); + } + // If the membership is "leave" then remove the item and stop here + else if (found && isLeftRoom) { + rooms.removeAt(j); + } + // Update notification, highlight count and/or additional informations + else if (found && + chatUpdate.membership != Membership.leave && + (rooms[j].membership != chatUpdate.membership || + rooms[j].notificationCount != chatUpdate.notification_count || + rooms[j].highlightCount != chatUpdate.highlight_count || + chatUpdate.summary != null)) { + rooms[j].membership = chatUpdate.membership; + rooms[j].notificationCount = chatUpdate.notification_count; + rooms[j].highlightCount = chatUpdate.highlight_count; + if (chatUpdate.prev_batch != null) + rooms[j].prev_batch = chatUpdate.prev_batch; + if (chatUpdate.summary != null) { + if (chatUpdate.summary.mHeroes != null) + rooms[j].mHeroes = chatUpdate.summary.mHeroes; + if (chatUpdate.summary.mJoinedMemberCount != null) + rooms[j].mJoinedMemberCount = chatUpdate.summary.mJoinedMemberCount; + if (chatUpdate.summary.mInvitedMemberCount != null) + rooms[j].mInvitedMemberCount = chatUpdate.summary.mInvitedMemberCount; + } + if (rooms[j].onUpdate != null) rooms[j].onUpdate(); + } + sortAndUpdate(); + } + + void _updateRoomsByEventUpdate(EventUpdate eventUpdate) { + if (eventUpdate.type == "history") return; + // Search the room in the rooms + num j = 0; + for (j = 0; j < rooms.length; j++) { + if (rooms[j].id == eventUpdate.roomID) break; + } + final bool found = (j < rooms.length && rooms[j].id == eventUpdate.roomID); + if (!found) return; + if (eventUpdate.type == "timeline" || + eventUpdate.type == "state" || + eventUpdate.type == "invite_state") { + Event stateEvent = Event.fromJson(eventUpdate.content, rooms[j]); + if (stateEvent.type == EventTypes.Redaction) { + final String redacts = eventUpdate.content["redacts"]; + rooms[j].states.states.forEach( + (String key, Map states) => states.forEach( + (String key, Event state) { + if (state.eventId == redacts) { + state.setRedactionEvent(stateEvent); + } + }, + ), + ); + } else { + Event prevState = + rooms[j].getState(stateEvent.typeKey, stateEvent.stateKey); + if (prevState != null && + prevState.time.millisecondsSinceEpoch > + stateEvent.time.millisecondsSinceEpoch) return; + rooms[j].setState(stateEvent); + } + } else if (eventUpdate.type == "account_data") { + rooms[j].roomAccountData[eventUpdate.eventType] = + RoomAccountData.fromJson(eventUpdate.content, rooms[j]); + } else if (eventUpdate.type == "ephemeral") { + rooms[j].ephemerals[eventUpdate.eventType] = + RoomAccountData.fromJson(eventUpdate.content, rooms[j]); + } + if (rooms[j].onUpdate != null) rooms[j].onUpdate(); + if (eventUpdate.type == "timeline") sortAndUpdate(); + } + + bool sortLock = false; + + sortAndUpdate() { + if (prevBatch == null) return; + if (sortLock || rooms.length < 2) return; + sortLock = true; + rooms?.sort((a, b) => b.timeCreated.millisecondsSinceEpoch + .compareTo(a.timeCreated.millisecondsSinceEpoch)); + sortLock = false; + } } diff --git a/lib/src/Connection.dart b/lib/src/Connection.dart deleted file mode 100644 index 52ff6ad..0000000 --- a/lib/src/Connection.dart +++ /dev/null @@ -1,494 +0,0 @@ -/* - * 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:async'; -import 'dart:convert'; -import 'dart:core'; - -import 'package:famedlysdk/src/Room.dart'; -import 'package:famedlysdk/src/RoomList.dart'; -import 'package:famedlysdk/src/utils/MatrixFile.dart'; -import 'package:http/http.dart' as http; -import 'package:mime_type/mime_type.dart'; - -import 'Client.dart'; -import 'User.dart'; -import 'sync/EventUpdate.dart'; -import 'sync/RoomUpdate.dart'; -import 'sync/UserUpdate.dart'; -import 'utils/MatrixException.dart'; - -enum HTTPType { GET, POST, PUT, DELETE } - -/// Represents a Matrix connection to communicate with a -/// [Matrix](https://matrix.org) homeserver. -class Connection { - final Client client; - - Connection(this.client); - - static String syncFilters = '{"room":{"state":{"lazy_load_members":true}}}'; - - /// Handles the connection to the Matrix Homeserver. You can change this to a - /// MockClient for testing. - http.Client httpClient = http.Client(); - - /// The newEvent signal is the most important signal in this concept. Every time - /// the app receives a new synchronization, this event is called for every signal - /// to update the GUI. For example, for a new message, it is called: - /// onRoomEvent( "m.room.message", "!chat_id:server.com", "timeline", {sender: "@bob:server.com", body: "Hello world"} ) - final StreamController onEvent = - new StreamController.broadcast(); - - /// Outside of the events there are updates for the global chat states which - /// are handled by this signal: - final StreamController onRoomUpdate = - new StreamController.broadcast(); - - /// Outside of rooms there are account updates like account_data or presences. - final StreamController onUserEvent = - new StreamController.broadcast(); - - /// Called when the login state e.g. user gets logged out. - final StreamController onLoginStateChanged = - new StreamController.broadcast(); - - /// Synchronization erros are coming here. - final StreamController onError = - new StreamController.broadcast(); - - /// This is called once, when the first sync has received. - final StreamController onFirstSync = new StreamController.broadcast(); - - /// When a new sync response is coming in, this gives the complete payload. - final StreamController onSync = new StreamController.broadcast(); - - /// Matrix synchronisation is done with https long polling. This needs a - /// timeout which is usually 30 seconds. - int syncTimeoutSec = 30; - - /// How long should the app wait until it retrys the synchronisation after - /// an error? - int syncErrorTimeoutSec = 3; - - /// Sets the user credentials and starts the synchronisation. - /// - /// Before you can connect you need at least an [accessToken], a [homeserver], - /// a [userID], a [deviceID], and a [deviceName]. - /// - /// You get this informations - /// by logging in to your Matrix account, using the [login API](https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-login). - /// - /// To log in you can use [jsonRequest()] after you have set the [homeserver] - /// to a valid url. For example: - /// - /// ``` - /// final resp = await matrix - /// .jsonRequest(type: HTTPType.POST, action: "/client/r0/login", data: { - /// "type": "m.login.password", - /// "user": "test", - /// "password": "1234", - /// "initial_device_display_name": "Fluffy Matrix Client" - /// }); - /// ``` - /// - /// Returns: - /// - /// ``` - /// { - /// "user_id": "@cheeky_monkey:matrix.org", - /// "access_token": "abc123", - /// "device_id": "GHTYAJCE" - /// } - /// ``` - /// - /// Sends [LoginState.logged] to [onLoginStateChanged]. - void connect( - {String newToken, - String newHomeserver, - String newUserID, - String newDeviceName, - String newDeviceID, - List newMatrixVersions, - bool newLazyLoadMembers, - String newPrevBatch}) async { - client.accessToken = newToken; - client.homeserver = newHomeserver; - client.userID = newUserID; - client.deviceID = newDeviceID; - client.deviceName = newDeviceName; - client.matrixVersions = newMatrixVersions; - client.lazyLoadMembers = newLazyLoadMembers; - client.prevBatch = newPrevBatch; - - List rooms = []; - if (client.store != null) { - client.store.storeClient(); - rooms = await client.store.getRoomList(onlyLeft: false); - client.accountData = await client.store.getAccountData(); - client.presences = await client.store.getPresences(); - } - - client.roomList = RoomList( - client: client, - onlyLeft: 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(); - client.accessToken = client.homeserver = client.userID = client.deviceID = - client.deviceName = client.matrixVersions = - client.lazyLoadMembers = client.prevBatch = null; - onLoginStateChanged.add(LoginState.loggedOut); - } - - /// Used for all Matrix json requests using the [c2s API](https://matrix.org/docs/spec/client_server/r0.4.0.html). - /// - /// Throws: TimeoutException, FormatException, MatrixException - /// - /// You must first call [this.connect()] or set [this.homeserver] before you can use - /// this! For example to send a message to a Matrix room with the id - /// '!fjd823j:example.com' you call: - /// - /// ``` - /// final resp = await jsonRequest( - /// type: HTTPType.PUT, - /// action: "/r0/rooms/!fjd823j:example.com/send/m.room.message/$txnId", - /// data: { - /// "msgtype": "m.text", - /// "body": "hello" - /// } - /// ); - /// ``` - /// - Future> jsonRequest( - {HTTPType type, - String action, - dynamic data = "", - int timeout, - String contentType = "application/json"}) async { - if (client.isLogged() == false && client.homeserver == null) - throw ("No homeserver specified."); - if (timeout == null) timeout = syncTimeoutSec + 5; - dynamic json; - if (data is Map) data.removeWhere((k, v) => v == null); - (!(data is String)) ? json = jsonEncode(data) : json = data; - if (data is List || action.startsWith("/media/r0/upload")) json = data; - - final url = "${client.homeserver}/_matrix${action}"; - - Map headers = {}; - if (type == HTTPType.PUT || type == HTTPType.POST) - headers["Content-Type"] = contentType; - if (client.isLogged()) - headers["Authorization"] = "Bearer ${client.accessToken}"; - - if (client.debug) - print( - "[REQUEST ${type.toString().split('.').last}] Action: $action, Data: $data"); - - http.Response resp; - Map jsonResp = {}; - try { - switch (type.toString().split('.').last) { - case "GET": - resp = await httpClient - .get(url, headers: headers) - .timeout(Duration(seconds: timeout)); - break; - case "POST": - resp = await httpClient - .post(url, body: json, headers: headers) - .timeout(Duration(seconds: timeout)); - break; - case "PUT": - resp = await httpClient - .put(url, body: json, headers: headers) - .timeout(Duration(seconds: timeout)); - break; - case "DELETE": - resp = await httpClient - .delete(url, headers: headers) - .timeout(Duration(seconds: timeout)); - break; - } - jsonResp = jsonDecode(resp.body) - as Map; // May throw FormatException - - if (jsonResp.containsKey("errcode") && jsonResp["errcode"] is String) { - // The server has responsed with an matrix related error. - MatrixException exception = MatrixException(resp); - if (exception.error == MatrixError.M_UNKNOWN_TOKEN) { - // The token is no longer valid. Need to sign off.... - onError.add(exception); - clear(); - } - - throw exception; - } - - if (client.debug) print("[RESPONSE] ${jsonResp.toString()}"); - } on ArgumentError catch (exception) { - print(exception); - // Ignore this error - } catch (_) { - print(_); - rethrow; - } - - return jsonResp; - } - - /// Uploads a file with the name [fileName] as base64 encoded to the server - /// and returns the mxc url as a string. - Future upload(MatrixFile file) async { - dynamic fileBytes; - if (client.homeserver != "https://fakeServer.notExisting") - fileBytes = file.bytes; - String fileName = file.path.split("/").last.toLowerCase(); - String mimeType = mime(file.path); - print("[UPLOADING] $fileName, type: $mimeType, size: ${fileBytes?.length}"); - final Map resp = await jsonRequest( - type: HTTPType.POST, - action: "/media/r0/upload?filename=$fileName", - data: fileBytes, - contentType: mimeType); - return resp["content_uri"]; - } - - Future _syncRequest; - - Future _sync() async { - if (client.isLogged() == false) return; - - String action = "/client/r0/sync?filter=$syncFilters"; - - if (client.prevBatch != null) { - action += "&timeout=30000"; - action += "&since=${client.prevBatch}"; - } - try { - _syncRequest = jsonRequest(type: HTTPType.GET, action: action); - final int hash = _syncRequest.hashCode; - final syncResp = await _syncRequest; - if (hash != _syncRequest.hashCode) return; - if (client.store != null) - await client.store.transaction(() { - handleSync(syncResp); - client.store.storePrevBatch(syncResp); - return; - }); - else - await handleSync(syncResp); - if (client.prevBatch == null) client.connection.onFirstSync.add(true); - client.prevBatch = syncResp["next_batch"]; - if (hash == _syncRequest.hashCode) _sync(); - } on MatrixException catch (exception) { - onError.add(exception); - await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync); - } catch (exception) { - await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync); - } - } - - void handleSync(dynamic sync) { - if (sync["rooms"] is Map) { - if (sync["rooms"]["join"] is Map) - _handleRooms(sync["rooms"]["join"], Membership.join); - if (sync["rooms"]["invite"] is Map) - _handleRooms(sync["rooms"]["invite"], Membership.invite); - if (sync["rooms"]["leave"] is Map) - _handleRooms(sync["rooms"]["leave"], Membership.leave); - } - if (sync["presence"] is Map && - sync["presence"]["events"] is List) { - _handleGlobalEvents(sync["presence"]["events"], "presence"); - } - if (sync["account_data"] is Map && - sync["account_data"]["events"] is List) { - _handleGlobalEvents(sync["account_data"]["events"], "account_data"); - } - if (sync["to_device"] is Map && - sync["to_device"]["events"] is List) { - _handleGlobalEvents(sync["to_device"]["events"], "to_device"); - } - onSync.add(sync); - } - - void _handleRooms(Map rooms, Membership membership) { - rooms.forEach((String id, dynamic room) async { - // calculate the notification counts, the limitedTimeline and prevbatch - num highlight_count = 0; - num notification_count = 0; - String prev_batch = ""; - bool limitedTimeline = false; - - if (room["unread_notifications"] is Map) { - if (room["unread_notifications"]["highlight_count"] is num) - highlight_count = room["unread_notifications"]["highlight_count"]; - if (room["unread_notifications"]["notification_count"] is num) - notification_count = - room["unread_notifications"]["notification_count"]; - } - - if (room["timeline"] is Map) { - if (room["timeline"]["limited"] is bool) - limitedTimeline = room["timeline"]["limited"]; - if (room["timeline"]["prev_batch"] is String) - prev_batch = room["timeline"]["prev_batch"]; - } - - RoomSummary summary; - - if (room["summary"] is Map) { - summary = RoomSummary.fromJson(room["summary"]); - } - - RoomUpdate update = RoomUpdate( - id: id, - membership: membership, - notification_count: notification_count, - highlight_count: highlight_count, - limitedTimeline: limitedTimeline, - prev_batch: prev_batch, - summary: summary, - ); - client.store?.storeRoomUpdate(update); - onRoomUpdate.add(update); - - /// Handle now all room events and save them in the database - if (room["state"] is Map && - room["state"]["events"] is List) - _handleRoomEvents(id, room["state"]["events"], "state"); - - if (room["invite_state"] is Map && - room["invite_state"]["events"] is List) - _handleRoomEvents(id, room["invite_state"]["events"], "invite_state"); - - if (room["timeline"] is Map && - room["timeline"]["events"] is List) - _handleRoomEvents(id, room["timeline"]["events"], "timeline"); - - if (room["ephemeral"] is Map && - room["ephemeral"]["events"] is List) - _handleEphemerals(id, room["ephemeral"]["events"]); - - if (room["account_data"] is Map && - room["account_data"]["events"] is List) - _handleRoomEvents(id, room["account_data"]["events"], "account_data"); - }); - } - - void _handleEphemerals(String id, List events) { - for (num i = 0; i < events.length; i++) { - _handleEvent(events[i], id, "ephemeral"); - - // Receipt events are deltas between two states. We will create a - // fake room account data event for this and store the difference - // there. - if (events[i]["type"] == "m.receipt") { - Room room = client.roomList.getRoomById(id); - if (room == null) room = Room(id: id); - - Map receiptStateContent = - room.roomAccountData["m.receipt"]?.content ?? {}; - for (var eventEntry in events[i]["content"].entries) { - final String eventID = eventEntry.key; - if (events[i]["content"][eventID]["m.read"] != null) { - final Map userTimestampMap = - events[i]["content"][eventID]["m.read"]; - for (var userTimestampMapEntry in userTimestampMap.entries) { - final String mxid = userTimestampMapEntry.key; - - // Remove previous receipt event from this user - for (var entry in receiptStateContent.entries) { - if (entry.value["m.read"] is Map && - entry.value["m.read"].containsKey(mxid)) { - entry.value["m.read"].remove(mxid); - break; - } - } - if (userTimestampMap[mxid] is Map && - userTimestampMap[mxid].containsKey("ts")) { - receiptStateContent[mxid] = { - "event_id": eventID, - "ts": userTimestampMap[mxid]["ts"], - }; - } - } - } - } - events[i]["content"] = receiptStateContent; - _handleEvent(events[i], id, "account_data"); - } - } - } - - void _handleRoomEvents(String chat_id, List events, String type) { - for (num i = 0; i < events.length; i++) { - _handleEvent(events[i], chat_id, type); - } - } - - void _handleGlobalEvents(List events, String type) { - for (int i = 0; i < events.length; i++) - if (events[i]["type"] is String && - events[i]["content"] is Map) { - UserUpdate update = UserUpdate( - eventType: events[i]["type"], - type: type, - content: events[i], - ); - client.store?.storeUserEventUpdate(update); - onUserEvent.add(update); - } - } - - void _handleEvent(Map event, String roomID, String type) { - if (event["type"] is String && event["content"] is Map) { - EventUpdate update = EventUpdate( - eventType: event["type"], - roomID: roomID, - type: type, - content: event, - ); - client.store?.storeEventUpdate(update); - onEvent.add(update); - } - } -} - -enum LoginState { logged, loggedOut } diff --git a/lib/src/Event.dart b/lib/src/Event.dart index ea12301..cb09ec6 100644 --- a/lib/src/Event.dart +++ b/lib/src/Event.dart @@ -21,73 +21,268 @@ * along with famedlysdk. If not, see . */ -import 'package:famedlysdk/src/RoomState.dart'; -import 'package:famedlysdk/src/sync/EventUpdate.dart'; -import 'package:famedlysdk/src/utils/ChatTime.dart'; +import 'dart:convert'; +import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/src/utils/Receipt.dart'; - import './Room.dart'; -/// Defines a timeline event for a room. -class Event extends RoomState { +class Event { + /// 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. + 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.getUserByMXIDSync(senderId); + + /// The time this event has received at the server. May be null for events like + /// account data. + final DateTime time; + + /// Optional additional content for this event. + 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. + 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; + /// The status of this event. /// -1=ERROR /// 0=SENDING /// 1=SENT - /// 2=RECEIVED + /// 2=TIMELINE + /// 3=ROOM_STATE int status; static const int defaultStatus = 2; + static const Map STATUS_TYPE = { + "ERROR": -1, + "SENDING": 0, + "SENT": 1, + "TIMELINE": 2, + "ROOM_STATE": 3, + }; + + /// Optional. The event that redacted this event, if any. Otherwise null. + Event get redactedBecause => + unsigned != null && unsigned.containsKey("redacted_because") + ? Event.fromJson(unsigned["redacted_because"], room) + : null; + + bool get redacted => redactedBecause != null; + + User get stateKeyUser => room.getUserByMXIDSync(stateKey); Event( {this.status = defaultStatus, - dynamic content, - String typeKey, - String eventId, - String roomId, - String senderId, - ChatTime time, - dynamic unsigned, - dynamic prevContent, - String stateKey, - Room room, - Event redactedBecause}) - : super( - content: content, - typeKey: typeKey, - eventId: eventId, - roomId: roomId, - senderId: senderId, - time: time, - unsigned: unsigned, - prevContent: prevContent, - stateKey: stateKey, - room: room); + 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 Event.fromJson(Map jsonPayload, Room room) { final Map content = - RoomState.getMapFromPayload(jsonPayload['content']); + Event.getMapFromPayload(jsonPayload['content']); final Map unsigned = - RoomState.getMapFromPayload(jsonPayload['unsigned']); + Event.getMapFromPayload(jsonPayload['unsigned']); final Map prevContent = - RoomState.getMapFromPayload(jsonPayload['prev_content']); - Event redactedBecause = null; - if (unsigned.containsKey("redacted_because")) - redactedBecause = Event.fromJson(unsigned["redacted_because"], room); + Event.getMapFromPayload(jsonPayload['prev_content']); return Event( - status: jsonPayload['status'] ?? defaultStatus, + status: jsonPayload['status'] ?? defaultStatus, + stateKey: jsonPayload['state_key'], + prevContent: prevContent, + content: content, + typeKey: jsonPayload['type'], + eventId: jsonPayload['event_id'], + roomId: jsonPayload['room_id'], + senderId: jsonPayload['sender'], + time: jsonPayload.containsKey('origin_server_ts') + ? DateTime.fromMillisecondsSinceEpoch(jsonPayload['origin_server_ts']) + : DateTime.now(), + unsigned: unsigned, + room: room, + ); + } + + Map toJson() { + final Map data = new Map(); + if (this.stateKey != null) data['state_key'] = this.stateKey; + if (this.prevContent != null && this.prevContent.isNotEmpty) + data['prev_content'] = this.prevContent; + data['content'] = this.content; + data['type'] = this.typeKey; + data['event_id'] = this.eventId; + data['room_id'] = this.roomId; + data['sender'] = this.senderId; + data['origin_server_ts'] = this.time.millisecondsSinceEpoch; + if (this.unsigned != null && this.unsigned.isNotEmpty) + data['unsigned'] = this.unsigned; + return data; + } + + Event get timelineEvent => Event( 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'], + typeKey: typeKey, + eventId: eventId, room: room, - redactedBecause: redactedBecause); + 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. + @deprecated + 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.redaction": + return EventTypes.Redaction; + 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; + } + + void setRedactionEvent(Event redactedBecause) { + unsigned = { + "redacted_because": redactedBecause.toJson(), + }; + prevContent = null; + List contentKeyWhiteList = []; + switch (type) { + case EventTypes.RoomMember: + contentKeyWhiteList.add("membership"); + break; + case EventTypes.RoomMember: + contentKeyWhiteList.add("membership"); + break; + case EventTypes.RoomCreate: + contentKeyWhiteList.add("creator"); + break; + case EventTypes.RoomJoinRules: + contentKeyWhiteList.add("join_rule"); + break; + case EventTypes.RoomPowerLevels: + contentKeyWhiteList.add("ban"); + contentKeyWhiteList.add("events"); + contentKeyWhiteList.add("events_default"); + contentKeyWhiteList.add("kick"); + contentKeyWhiteList.add("redact"); + contentKeyWhiteList.add("state_default"); + contentKeyWhiteList.add("users"); + contentKeyWhiteList.add("users_default"); + break; + case EventTypes.RoomAliases: + contentKeyWhiteList.add("aliases"); + break; + case EventTypes.HistoryVisibility: + contentKeyWhiteList.add("history_visibility"); + break; + default: + break; + } + List toRemoveList = []; + for (var entry in content.entries) { + if (contentKeyWhiteList.indexOf(entry.key) == -1) { + toRemoveList.add(entry.key); + } + } + toRemoveList.forEach((s) => content.remove(s)); } /// Returns the body of this event if it has a body. @@ -110,8 +305,8 @@ class Event extends RoomState { List receiptsList = []; for (var entry in room.roomAccountData["m.receipt"].content.entries) { if (entry.value["event_id"] == eventId) - receiptsList.add(Receipt( - room.getUserByMXIDSync(entry.key), ChatTime(entry.value["ts"]))); + receiptsList.add(Receipt(room.getUserByMXIDSync(entry.key), + DateTime.fromMillisecondsSinceEpoch(entry.value["ts"]))); } return receiptsList; } @@ -123,7 +318,7 @@ class Event extends RoomState { if (room.client.store != null) await room.client.store.removeEvent(eventId); - room.client.connection.onEvent.add(EventUpdate( + room.client.onEvent.add(EventUpdate( roomID: room.id, type: "timeline", eventType: typeKey, @@ -152,3 +347,28 @@ class Event extends RoomState { Future redact({String reason, String txid}) => room.redactEvent(eventId, reason: reason, txid: txid); } + +enum EventTypes { + Text, + Emote, + Notice, + Image, + Video, + Audio, + Redaction, + File, + Location, + Reply, + RoomAliases, + RoomCanonicalAlias, + RoomCreate, + RoomJoinRules, + RoomMember, + RoomPowerLevels, + RoomName, + RoomTopic, + RoomAvatar, + GuestAccess, + HistoryVisibility, + Unknown, +} diff --git a/lib/src/Presence.dart b/lib/src/Presence.dart index 98b925b..7ff871a 100644 --- a/lib/src/Presence.dart +++ b/lib/src/Presence.dart @@ -42,8 +42,8 @@ class Presence { Presence.fromJson(Map json) : sender = json['sender'], - displayname = json['content']['avatar_url'], - avatarUrl = MxContent(json['content']['avatar_url']), + displayname = json['content']['displayname'], + avatarUrl = MxContent(json['content']['avatar_url'] ?? ""), currentlyActive = json['content']['currently_active'], lastActiveAgo = json['content']['last_active_ago'], presence = PresenceType.values.firstWhere( diff --git a/lib/src/Room.dart b/lib/src/Room.dart index 55b71c1..e272261 100644 --- a/lib/src/Room.dart +++ b/lib/src/Room.dart @@ -24,10 +24,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/sync/EventUpdate.dart'; import 'package:famedlysdk/src/sync/RoomUpdate.dart'; -import 'package:famedlysdk/src/utils/ChatTime.dart'; import 'package:famedlysdk/src/utils/MatrixException.dart'; import 'package:famedlysdk/src/utils/MatrixFile.dart'; import 'package:famedlysdk/src/utils/MxContent.dart'; @@ -35,7 +33,6 @@ import 'package:famedlysdk/src/utils/MxContent.dart'; import 'package:mime_type/mime_type.dart'; import './User.dart'; -import 'Connection.dart'; import 'Timeline.dart'; import 'utils/StatesMap.dart'; @@ -76,14 +73,14 @@ class Room { /// Key-Value store for private account data only visible for this user. Map roomAccountData = {}; - /// Returns the [RoomState] for the given [typeKey] and optional [stateKey]. + /// Returns the [Event] for the given [typeKey] and optional [stateKey]. /// If no [stateKey] is provided, it defaults to an empty string. - RoomState getState(String typeKey, [String stateKey = ""]) => + Event getState(String typeKey, [String stateKey = ""]) => states.states[typeKey] != null ? states.states[typeKey][stateKey] : null; /// Adds the [state] to this room and overwrites a state with the same /// typeKey/stateKey key pair if there is one. - void setState(RoomState state) { + void setState(Event state) { if (!states.states.containsKey(state.typeKey)) states.states[state.typeKey] = {}; states.states[state.typeKey][state.stateKey ?? ""] = state; @@ -150,13 +147,15 @@ class Room { String notificationSettings; Event get lastEvent { - ChatTime lastTime = ChatTime(0); + DateTime lastTime = DateTime.fromMillisecondsSinceEpoch(0); Event lastEvent = getState("m.room.message")?.timelineEvent; if (lastEvent == null) states.forEach((final String key, final entry) { if (!entry.containsKey("")) return; - final RoomState state = entry[""]; - if (state.time != null && state.time > lastTime) { + final Event state = entry[""]; + if (state.time != null && + state.time.millisecondsSinceEpoch > + lastTime.millisecondsSinceEpoch) { lastTime = state.time; lastEvent = state.timelineEvent; } @@ -211,7 +210,7 @@ class Room { } else { if (states["m.room.member"] is Map) { for (var entry in states["m.room.member"].entries) { - RoomState state = entry.value; + Event state = entry.value; if (state.type == EventTypes.RoomMember && state.stateKey != client?.userID) heroes.add(state.stateKey); } @@ -241,17 +240,17 @@ class Room { } /// When the last message received. - ChatTime get timeCreated { + DateTime get timeCreated { if (lastEvent != null) return lastEvent.time; else - return ChatTime.now(); + return DateTime.now(); } /// Call the Matrix API to change the name of this room. Returns the event ID of the /// new m.room.name event. Future setName(String newName) async { - final Map resp = await client.connection.jsonRequest( + final Map resp = await client.jsonRequest( type: HTTPType.PUT, action: "/client/r0/rooms/${id}/state/m.room.name", data: {"name": newName}); @@ -260,7 +259,7 @@ class Room { /// Call the Matrix API to change the topic of this room. Future setDescription(String newName) async { - final Map resp = await client.connection.jsonRequest( + final Map resp = await client.jsonRequest( type: HTTPType.PUT, action: "/client/r0/rooms/${id}/state/m.room.topic", data: {"topic": newName}); @@ -270,7 +269,7 @@ class Room { Future _sendRawEventNow(Map content, {String txid = null}) async { if (txid == null) txid = "txid${DateTime.now().millisecondsSinceEpoch}"; - final Map res = await client.connection.jsonRequest( + final Map res = await client.jsonRequest( type: HTTPType.PUT, action: "/client/r0/rooms/${id}/send/m.room.message/$txid", data: content); @@ -289,7 +288,7 @@ class Room { if (msgType == "m.video") return sendAudioEvent(file); String fileName = file.path.split("/").last; - final String uploadResp = await client.connection.upload(file); + final String uploadResp = await client.upload(file); // Send event Map content = { @@ -308,7 +307,7 @@ class Room { Future sendAudioEvent(MatrixFile file, {String txid = null, int width, int height}) async { String fileName = file.path.split("/").last; - final String uploadResp = await client.connection.upload(file); + final String uploadResp = await client.upload(file); Map content = { "msgtype": "m.audio", "body": fileName, @@ -325,7 +324,7 @@ class Room { Future sendImageEvent(MatrixFile file, {String txid = null, int width, int height}) async { String fileName = file.path.split("/").last; - final String uploadResp = await client.connection.upload(file); + final String uploadResp = await client.upload(file); Map content = { "msgtype": "m.image", "body": fileName, @@ -349,7 +348,7 @@ class Room { int thumbnailWidth, int thumbnailHeight}) async { String fileName = file.path.split("/").last; - final String uploadResp = await client.connection.upload(file); + final String uploadResp = await client.upload(file); Map content = { "msgtype": "m.video", "body": fileName, @@ -370,7 +369,7 @@ class Room { } if (thumbnail != null) { String thumbnailName = file.path.split("/").last; - final String thumbnailUploadResp = await client.connection.upload(file); + final String thumbnailUploadResp = await client.upload(file); content["info"]["thumbnail_url"] = thumbnailUploadResp; content["info"]["thumbnail_info"] = { "size": thumbnail.size, @@ -408,7 +407,7 @@ class Room { "origin_server_ts": now, "content": content }); - client.connection.onEvent.add(eventUpdate); + client.onEvent.add(eventUpdate); await client.store?.transaction(() { client.store.storeEventUpdate(eventUpdate); return; @@ -420,7 +419,7 @@ class Room { eventUpdate.content["status"] = 1; eventUpdate.content["unsigned"] = {"transaction_id": messageID}; eventUpdate.content["event_id"] = res; - client.connection.onEvent.add(eventUpdate); + client.onEvent.add(eventUpdate); await client.store?.transaction(() { client.store.storeEventUpdate(eventUpdate); return; @@ -430,7 +429,7 @@ class Room { // On error, set status to -1 eventUpdate.content["status"] = -1; eventUpdate.content["unsigned"] = {"transaction_id": messageID}; - client.connection.onEvent.add(eventUpdate); + client.onEvent.add(eventUpdate); await client.store?.transaction(() { client.store.storeEventUpdate(eventUpdate); return; @@ -444,7 +443,7 @@ class Room { /// automatically be set. Future join() async { try { - await client.connection.jsonRequest( + await client.jsonRequest( type: HTTPType.POST, action: "/client/r0/rooms/${id}/join"); if (states.containsKey(client.userID) && states[client.userID].content["is_direct"] is bool && @@ -453,7 +452,7 @@ class Room { } on MatrixException catch (exception) { if (exception.errorMessage == "No known servers") { client.store?.forgetRoom(id); - client.connection.onRoomUpdate.add( + client.onRoomUpdate.add( RoomUpdate( id: id, membership: Membership.leave, @@ -469,7 +468,7 @@ class Room { /// chat, this will be removed too. Future leave() async { if (directChatMatrixID != "") await removeFromDirectChat(); - await client.connection.jsonRequest( + await client.jsonRequest( type: HTTPType.POST, action: "/client/r0/rooms/${id}/leave"); return; } @@ -477,14 +476,14 @@ class Room { /// Call the Matrix API to forget this room if you already left it. Future forget() async { client.store.forgetRoom(id); - await client.connection.jsonRequest( + await client.jsonRequest( type: HTTPType.POST, action: "/client/r0/rooms/${id}/forget"); return; } /// Call the Matrix API to kick a user from this room. Future kick(String userID) async { - await client.connection.jsonRequest( + await client.jsonRequest( type: HTTPType.POST, action: "/client/r0/rooms/${id}/kick", data: {"user_id": userID}); @@ -493,7 +492,7 @@ class Room { /// Call the Matrix API to ban a user from this room. Future ban(String userID) async { - await client.connection.jsonRequest( + await client.jsonRequest( type: HTTPType.POST, action: "/client/r0/rooms/${id}/ban", data: {"user_id": userID}); @@ -502,7 +501,7 @@ class Room { /// Call the Matrix API to unban a banned user from this room. Future unban(String userID) async { - await client.connection.jsonRequest( + await client.jsonRequest( type: HTTPType.POST, action: "/client/r0/rooms/${id}/unban", data: {"user_id": userID}); @@ -519,7 +518,7 @@ class Room { if (powerMap["users"] == null) powerMap["users"] = {}; powerMap["users"][userID] = power; - final Map resp = await client.connection.jsonRequest( + final Map resp = await client.jsonRequest( type: HTTPType.PUT, action: "/client/r0/rooms/$id/state/m.room.power_levels", data: powerMap); @@ -528,7 +527,7 @@ class Room { /// Call the Matrix API to invite a user to this room. Future invite(String userID) async { - await client.connection.jsonRequest( + await client.jsonRequest( type: HTTPType.POST, action: "/client/r0/rooms/${id}/invite", data: {"user_id": userID}); @@ -540,10 +539,10 @@ class Room { /// the historical events will be published in the onEvent stream. Future requestHistory( {int historyCount = DefaultHistoryCount, onHistoryReceived}) async { - final dynamic resp = await client.connection.jsonRequest( + final dynamic resp = await client.jsonRequest( type: HTTPType.GET, action: - "/client/r0/rooms/$id/messages?from=${prev_batch}&dir=b&limit=$historyCount&filter=${Connection.syncFilters}"); + "/client/r0/rooms/$id/messages?from=${prev_batch}&dir=b&limit=$historyCount&filter=${Client.syncFilters}"); if (onHistoryReceived != null) onHistoryReceived(); prev_batch = resp["end"]; @@ -562,7 +561,7 @@ class Room { eventType: resp["state"][i]["type"], content: resp["state"][i], ); - client.connection.onEvent.add(eventUpdate); + client.onEvent.add(eventUpdate); client.store.storeEventUpdate(eventUpdate); } return; @@ -575,7 +574,7 @@ class Room { eventType: resp["state"][i]["type"], content: resp["state"][i], ); - client.connection.onEvent.add(eventUpdate); + client.onEvent.add(eventUpdate); } } } @@ -589,7 +588,7 @@ class Room { eventType: history[i]["type"], content: history[i], ); - client.connection.onEvent.add(eventUpdate); + client.onEvent.add(eventUpdate); client.store.storeEventUpdate(eventUpdate); client.store.txn.rawUpdate( "UPDATE Rooms SET prev_batch=? WHERE room_id=?", [resp["end"], id]); @@ -604,10 +603,10 @@ class Room { eventType: history[i]["type"], content: history[i], ); - client.connection.onEvent.add(eventUpdate); + client.onEvent.add(eventUpdate); } } - client.connection.onRoomUpdate.add( + client.onRoomUpdate.add( RoomUpdate( id: id, membership: membership, @@ -628,7 +627,7 @@ class Room { else directChats[userID] = [id]; - await client.connection.jsonRequest( + await client.jsonRequest( type: HTTPType.PUT, action: "/client/r0/user/${client.userID}/account_data/m.direct", data: directChats); @@ -644,7 +643,7 @@ class Room { else return; // Nothing to do here - await client.connection.jsonRequest( + await client.jsonRequest( type: HTTPType.PUT, action: "/client/r0/user/${client.userID}/account_data/m.direct", data: directChats); @@ -655,7 +654,7 @@ class Room { Future sendReadReceipt(String eventID) async { this.notificationCount = 0; client?.store?.resetNotificationCount(this.id); - client.connection.jsonRequest( + client.jsonRequest( type: HTTPType.POST, action: "/client/r0/rooms/$id/read_markers", data: { @@ -689,7 +688,7 @@ class Room { if (states != null) { List> rawStates = await states; for (int i = 0; i < rawStates.length; i++) { - RoomState newState = RoomState.fromJson(rawStates[i], newRoom); + Event newState = Event.fromJson(rawStates[i], newRoom); newRoom.setState(newState); } } @@ -740,7 +739,7 @@ class Room { List userList = []; if (states["m.room.member"] is Map) { for (var entry in states["m.room.member"].entries) { - RoomState state = entry.value; + Event state = entry.value; if (state.type == EventTypes.RoomMember) userList.add(state.asUser); } } @@ -752,11 +751,11 @@ class Room { Future> requestParticipants() async { List participants = []; - dynamic res = await client.connection.jsonRequest( + dynamic res = await client.jsonRequest( type: HTTPType.GET, action: "/client/r0/rooms/${id}/members"); for (num i = 0; i < res["chunk"].length; i++) { - User newUser = RoomState.fromJson(res["chunk"][i], this).asUser; + User newUser = Event.fromJson(res["chunk"][i], this).asUser; if (newUser.membership != Membership.leave) participants.add(newUser); } @@ -791,7 +790,7 @@ class Room { if (mxID == null || !_requestingMatrixIds.add(mxID)) return null; Map resp; try { - resp = await client.connection.jsonRequest( + resp = await client.jsonRequest( type: HTTPType.GET, action: "/client/r0/rooms/$id/state/m.room.member/$mxID"); } catch (exception) { @@ -821,7 +820,7 @@ class Room { /// Searches for the event on the server. Returns null if not found. Future getEventById(String eventID) async { - final dynamic resp = await client.connection.jsonRequest( + final dynamic resp = await client.jsonRequest( type: HTTPType.GET, action: "/client/r0/rooms/$id/event/$eventID"); return Event.fromJson(resp, this); } @@ -829,7 +828,7 @@ class Room { /// Returns the power level of the given user ID. int getPowerLevelByUserId(String userId) { int powerLevel = 0; - RoomState powerLevelState = states["m.room.power_levels"]; + Event powerLevelState = states["m.room.power_levels"]; if (powerLevelState == null) return powerLevel; if (powerLevelState.content["users_default"] is int) powerLevel = powerLevelState.content["users_default"]; @@ -844,7 +843,7 @@ class Room { /// 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"]; + Event powerLevelState = states["m.room.power_levels"]; if (powerLevelState.content["users"] is Map) return powerLevelState.content["users"]; return null; @@ -853,12 +852,11 @@ class Room { /// Uploads a new user avatar for this room. Returns the event ID of the new /// m.room.avatar event. Future setAvatar(MatrixFile file) async { - final String uploadResp = await client.connection.upload(file); - final Map setAvatarResp = await client.connection - .jsonRequest( - type: HTTPType.PUT, - action: "/client/r0/rooms/$id/state/m.room.avatar/", - data: {"url": uploadResp}); + final String uploadResp = await client.upload(file); + final Map setAvatarResp = await client.jsonRequest( + type: HTTPType.PUT, + action: "/client/r0/rooms/$id/state/m.room.avatar/", + data: {"url": uploadResp}); return setAvatarResp["event_id"]; } @@ -946,12 +944,12 @@ class Room { // All push notifications should be sent to the user case PushRuleState.notify: if (pushRuleState == PushRuleState.dont_notify) - resp = await client.connection.jsonRequest( + resp = await client.jsonRequest( type: HTTPType.DELETE, action: "/client/r0/pushrules/global/override/$id", data: {}); else if (pushRuleState == PushRuleState.mentions_only) - resp = await client.connection.jsonRequest( + resp = await client.jsonRequest( type: HTTPType.DELETE, action: "/client/r0/pushrules/global/room/$id", data: {}); @@ -959,18 +957,18 @@ class Room { // Only when someone mentions the user, a push notification should be sent case PushRuleState.mentions_only: if (pushRuleState == PushRuleState.dont_notify) { - resp = await client.connection.jsonRequest( + resp = await client.jsonRequest( type: HTTPType.DELETE, action: "/client/r0/pushrules/global/override/$id", data: {}); - resp = await client.connection.jsonRequest( + resp = await client.jsonRequest( type: HTTPType.PUT, action: "/client/r0/pushrules/global/room/$id", data: { "actions": ["dont_notify"] }); } else if (pushRuleState == PushRuleState.notify) - resp = await client.connection.jsonRequest( + resp = await client.jsonRequest( type: HTTPType.PUT, action: "/client/r0/pushrules/global/room/$id", data: { @@ -980,12 +978,12 @@ class Room { // No push notification should be ever sent for this room. case PushRuleState.dont_notify: if (pushRuleState == PushRuleState.mentions_only) { - resp = await client.connection.jsonRequest( + resp = await client.jsonRequest( type: HTTPType.DELETE, action: "/client/r0/pushrules/global/room/$id", data: {}); } - resp = await client.connection.jsonRequest( + resp = await client.jsonRequest( type: HTTPType.PUT, action: "/client/r0/pushrules/global/override/$id", data: { @@ -1010,7 +1008,7 @@ class Room { messageID = txid; Map data = {}; if (reason != null) data["reason"] = reason; - final dynamic resp = await client.connection.jsonRequest( + final dynamic resp = await client.jsonRequest( type: HTTPType.PUT, action: "/client/r0/rooms/$id/redact/$eventId/$messageID", data: data); @@ -1022,7 +1020,7 @@ class Room { "typing": isTyping, }; if (timeout != null) data["timeout"] = timeout; - return client.connection.jsonRequest( + return client.jsonRequest( type: HTTPType.PUT, action: "/client/r0/rooms/${this.id}/typing/${client.userID}", data: data, diff --git a/lib/src/RoomAccountData.dart b/lib/src/RoomAccountData.dart index e139a6a..932ae67 100644 --- a/lib/src/RoomAccountData.dart +++ b/lib/src/RoomAccountData.dart @@ -23,7 +23,7 @@ import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/src/AccountData.dart'; -import 'package:famedlysdk/src/RoomState.dart'; +import 'package:famedlysdk/src/Event.dart'; /// Stripped down events for account data and ephemrals of a room. class RoomAccountData extends AccountData { @@ -40,7 +40,7 @@ class RoomAccountData extends AccountData { factory RoomAccountData.fromJson( Map jsonPayload, Room room) { final Map content = - RoomState.getMapFromPayload(jsonPayload['content']); + Event.getMapFromPayload(jsonPayload['content']); return RoomAccountData( content: content, typeKey: jsonPayload['type'], diff --git a/lib/src/RoomList.dart b/lib/src/RoomList.dart deleted file mode 100644 index d3090c2..0000000 --- a/lib/src/RoomList.dart +++ /dev/null @@ -1,226 +0,0 @@ -/* - * 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:async'; -import 'dart:core'; - -import 'package:famedlysdk/famedlysdk.dart'; -import 'package:famedlysdk/src/RoomState.dart'; - -import 'Client.dart'; -import 'Room.dart'; -import 'User.dart'; -import 'sync/EventUpdate.dart'; -import 'sync/RoomUpdate.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 -/// the initial room list, use the store or create a RoomList instance by using -/// [client.getRoomList]. -class RoomList { - final Client client; - List rooms = []; - - final bool onlyLeft; - - /// Will be called, when the room list has changed. Can be used e.g. to update - /// the state of a StatefulWidget. - final onRoomListUpdateCallback onUpdate; - - /// Will be called, when a new room is added to the list. - final onRoomListInsertCallback onInsert; - - /// Will be called, when a room has been removed from the list. - final onRoomListRemoveCallback onRemove; - - StreamSubscription eventSub; - StreamSubscription roomSub; - StreamSubscription firstSyncSub; - - RoomList( - {this.client, - this.rooms, - this.onUpdate, - this.onInsert, - this.onRemove, - this.onlyLeft = false}) { - eventSub ??= client.connection.onEvent.stream.listen(_handleEventUpdate); - roomSub ??= client.connection.onRoomUpdate.stream.listen(_handleRoomUpdate); - firstSyncSub ??= - client.connection.onFirstSync.stream.listen((b) => sortAndUpdate()); - sort(); - } - - RoomList copyWith({ - Client client, - List rooms, - onRoomListUpdateCallback onUpdate, - onRoomListInsertCallback onInsert, - onRoomListRemoveCallback onRemove, - bool onlyLeft, - }) { - return RoomList( - client: client ?? this.client, - rooms: rooms ?? this.rooms, - onUpdate: onUpdate ?? this.onUpdate, - onInsert: onInsert ?? this.onInsert, - onRemove: onRemove ?? this.onRemove, - onlyLeft: onlyLeft ?? this.onlyLeft, - ); - } - - 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) { - // Update the chat list item. - // Search the room in the rooms - num j = 0; - for (j = 0; j < rooms.length; j++) { - if (rooms[j].id == chatUpdate.id) break; - } - final bool found = (j < rooms.length && rooms[j].id == chatUpdate.id); - final bool isLeftRoom = chatUpdate.membership == Membership.leave; - - // Does the chat already exist in the list rooms? - if (!found && ((!onlyLeft && !isLeftRoom) || (onlyLeft && isLeftRoom))) { - num position = chatUpdate.membership == Membership.invite ? 0 : j; - // Add the new chat to the list - Room newRoom = Room( - id: chatUpdate.id, - membership: chatUpdate.membership, - prev_batch: chatUpdate.prev_batch, - highlightCount: chatUpdate.highlight_count, - notificationCount: chatUpdate.notification_count, - mHeroes: chatUpdate.summary?.mHeroes, - mJoinedMemberCount: chatUpdate.summary?.mJoinedMemberCount, - mInvitedMemberCount: chatUpdate.summary?.mInvitedMemberCount, - roomAccountData: {}, - client: client, - ); - rooms.insert(position, newRoom); - if (onInsert != null) onInsert(position); - } - // If the membership is "leave" or not "leave" but onlyLeft=true then remove the item and stop here - else if (found && - ((!onlyLeft && isLeftRoom) || (onlyLeft && !isLeftRoom))) { - rooms.removeAt(j); - if (onRemove != null) onRemove(j); - } - // Update notification, highlight count and/or additional informations - else if (found && - chatUpdate.membership != Membership.leave && - (rooms[j].membership != chatUpdate.membership || - rooms[j].notificationCount != chatUpdate.notification_count || - rooms[j].highlightCount != chatUpdate.highlight_count || - chatUpdate.summary != null)) { - rooms[j].membership = chatUpdate.membership; - rooms[j].notificationCount = chatUpdate.notification_count; - rooms[j].highlightCount = chatUpdate.highlight_count; - if (chatUpdate.prev_batch != null) - rooms[j].prev_batch = chatUpdate.prev_batch; - if (chatUpdate.summary != null) { - if (chatUpdate.summary.mHeroes != null) - rooms[j].mHeroes = chatUpdate.summary.mHeroes; - if (chatUpdate.summary.mJoinedMemberCount != null) - rooms[j].mJoinedMemberCount = chatUpdate.summary.mJoinedMemberCount; - if (chatUpdate.summary.mInvitedMemberCount != null) - rooms[j].mInvitedMemberCount = chatUpdate.summary.mInvitedMemberCount; - } - if (rooms[j].onUpdate != null) rooms[j].onUpdate(); - } - sortAndUpdate(); - } - - void _handleEventUpdate(EventUpdate eventUpdate) { - if (eventUpdate.type == "history") return; - // Search the room in the rooms - num j = 0; - for (j = 0; j < rooms.length; j++) { - if (rooms[j].id == eventUpdate.roomID) break; - } - final bool found = (j < rooms.length && rooms[j].id == eventUpdate.roomID); - if (!found) return; - if (eventUpdate.type == "timeline" || - eventUpdate.type == "state" || - eventUpdate.type == "invite_state") { - RoomState stateEvent = RoomState.fromJson(eventUpdate.content, rooms[j]); - if (stateEvent.type == EventTypes.Redaction) { - final String redacts = eventUpdate.content["redacts"]; - rooms[j].states.states.forEach( - (String key, Map states) => states.forEach( - (String key, RoomState state) { - if (state.eventId == redacts) { - state.setRedactionEvent(stateEvent); - } - }, - ), - ); - } else { - RoomState prevState = - rooms[j].getState(stateEvent.typeKey, stateEvent.stateKey); - if (prevState != null && prevState.time > stateEvent.time) return; - rooms[j].setState(stateEvent); - } - } else if (eventUpdate.type == "account_data") { - rooms[j].roomAccountData[eventUpdate.eventType] = - RoomAccountData.fromJson(eventUpdate.content, rooms[j]); - } else if (eventUpdate.type == "ephemeral") { - rooms[j].ephemerals[eventUpdate.eventType] = - RoomAccountData.fromJson(eventUpdate.content, rooms[j]); - } - if (rooms[j].onUpdate != null) rooms[j].onUpdate(); - if (eventUpdate.type == "timeline") sortAndUpdate(); - } - - bool sortLock = false; - - sort() { - if (sortLock || rooms.length < 2) return; - sortLock = true; - rooms?.sort((a, b) => - b.timeCreated.toTimeStamp().compareTo(a.timeCreated.toTimeStamp())); - sortLock = false; - } - - sortAndUpdate() { - if (client.prevBatch == null) return; - sort(); - if (onUpdate != null) onUpdate(); - } -} diff --git a/lib/src/RoomState.dart b/lib/src/RoomState.dart deleted file mode 100644 index 2b58422..0000000 --- a/lib/src/RoomState.dart +++ /dev/null @@ -1,291 +0,0 @@ -/* - * 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. - 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.getUserByMXIDSync(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. - 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. - 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; - - /// Optional. The event that redacted this event, if any. Otherwise null. - RoomState get redactedBecause => - unsigned != null && unsigned.containsKey("redacted_because") - ? RoomState.fromJson(unsigned["redacted_because"], room) - : null; - - bool get redacted => redactedBecause != null; - - User get stateKeyUser => room.getUserByMXIDSync(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, - ); - } - - Map toJson() { - final Map data = new Map(); - if (this.stateKey != null) data['state_key'] = this.stateKey; - if (this.prevContent != null && this.prevContent.isNotEmpty) - data['prev_content'] = this.prevContent; - data['content'] = this.content; - data['type'] = this.typeKey; - data['event_id'] = this.eventId; - data['room_id'] = this.roomId; - data['sender'] = this.senderId; - data['origin_server_ts'] = this.time.toTimeStamp(); - if (this.unsigned != null && this.unsigned.isNotEmpty) - data['unsigned'] = this.unsigned; - return data; - } - - 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. - @deprecated - 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.redaction": - return EventTypes.Redaction; - 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; - } - - void setRedactionEvent(RoomState redactedBecause) { - unsigned = { - "redacted_because": redactedBecause.toJson(), - }; - prevContent = null; - List contentKeyWhiteList = []; - switch (type) { - case EventTypes.RoomMember: - contentKeyWhiteList.add("membership"); - break; - case EventTypes.RoomMember: - contentKeyWhiteList.add("membership"); - break; - case EventTypes.RoomCreate: - contentKeyWhiteList.add("creator"); - break; - case EventTypes.RoomJoinRules: - contentKeyWhiteList.add("join_rule"); - break; - case EventTypes.RoomPowerLevels: - contentKeyWhiteList.add("ban"); - contentKeyWhiteList.add("events"); - contentKeyWhiteList.add("events_default"); - contentKeyWhiteList.add("kick"); - contentKeyWhiteList.add("redact"); - contentKeyWhiteList.add("state_default"); - contentKeyWhiteList.add("users"); - contentKeyWhiteList.add("users_default"); - break; - case EventTypes.RoomAliases: - contentKeyWhiteList.add("aliases"); - break; - case EventTypes.HistoryVisibility: - contentKeyWhiteList.add("history_visibility"); - break; - default: - break; - } - List toRemoveList = []; - for (var entry in content.entries) { - if (contentKeyWhiteList.indexOf(entry.key) == -1) { - toRemoveList.add(entry.key); - } - } - toRemoveList.forEach((s) => content.remove(s)); - } -} - -enum EventTypes { - Text, - Emote, - Notice, - Image, - Video, - Audio, - Redaction, - File, - Location, - Reply, - RoomAliases, - RoomCanonicalAlias, - RoomCreate, - RoomJoinRules, - RoomMember, - RoomPowerLevels, - RoomName, - RoomTopic, - RoomAvatar, - GuestAccess, - HistoryVisibility, - Unknown, -} diff --git a/lib/src/Timeline.dart b/lib/src/Timeline.dart index 2a9f92f..0f1be89 100644 --- a/lib/src/Timeline.dart +++ b/lib/src/Timeline.dart @@ -75,7 +75,7 @@ class Timeline { } Timeline({this.room, this.events, this.onUpdate, this.onInsert}) { - sub ??= room.client.connection.onEvent.stream.listen(_handleEventUpdate); + sub ??= room.client.onEvent.stream.listen(_handleEventUpdate); } int _findEvent({String event_id, String unsigned_txid}) { @@ -148,8 +148,8 @@ class Timeline { sort() { if (sortLock || events.length < 2) return; sortLock = true; - events - ?.sort((a, b) => b.time.toTimeStamp().compareTo(a.time.toTimeStamp())); + events?.sort((a, b) => + b.time.millisecondsSinceEpoch.compareTo(a.time.millisecondsSinceEpoch)); sortLock = false; } diff --git a/lib/src/User.dart b/lib/src/User.dart index 5108a2e..e6f740f 100644 --- a/lib/src/User.dart +++ b/lib/src/User.dart @@ -23,16 +23,13 @@ import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/src/Room.dart'; -import 'package:famedlysdk/src/RoomState.dart'; -import 'package:famedlysdk/src/utils/ChatTime.dart'; +import 'package:famedlysdk/src/Event.dart'; import 'package:famedlysdk/src/utils/MxContent.dart'; -import 'Connection.dart'; - enum Membership { join, invite, leave, ban } /// Represents a Matrix User which may be a participant in a Matrix Room. -class User extends RoomState { +class User extends Event { factory User( String id, { String membership, @@ -50,7 +47,7 @@ class User extends RoomState { typeKey: "m.room.member", roomId: room?.id, room: room, - time: ChatTime.now(), + time: DateTime.now(), ); } @@ -62,7 +59,7 @@ class User extends RoomState { String eventId, String roomId, String senderId, - ChatTime time, + DateTime time, dynamic unsigned, Room room}) : super( @@ -131,7 +128,7 @@ class User extends RoomState { if (roomID != null) return roomID; // Start a new direct chat - final dynamic resp = await room.client.connection.jsonRequest( + final dynamic resp = await room.client.jsonRequest( type: HTTPType.POST, action: "/client/r0/createRoom", data: { diff --git a/lib/src/requests/SetPushersRequest.dart b/lib/src/requests/SetPushersRequest.dart deleted file mode 100644 index 240ea21..0000000 --- a/lib/src/requests/SetPushersRequest.dart +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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 famedly. If not, see . - */ -import 'package:json_annotation/json_annotation.dart'; - -part 'SetPushersRequest.g.dart'; - -@JsonSerializable(explicitToJson: true, nullable: false, includeIfNull: false) -class SetPushersRequest { - // Required Keys - @JsonKey(nullable: false) - String lang; - @JsonKey(nullable: false) - String device_display_name; - @JsonKey(nullable: false) - String app_display_name; - @JsonKey(nullable: false) - String app_id; - @JsonKey(nullable: false) - String kind; - @JsonKey(nullable: false) - String pushkey; - @JsonKey(nullable: false) - PusherData data; - - // Optional keys - String profile_tag; - bool append; - - SetPushersRequest({ - this.lang, - this.device_display_name, - this.app_display_name, - this.app_id, - this.kind, - this.pushkey, - this.data, - this.profile_tag, - this.append, - }); - - factory SetPushersRequest.fromJson(Map json) => - _$SetPushersRequestFromJson(json); - - Map toJson() => _$SetPushersRequestToJson(this); -} - -@JsonSerializable(explicitToJson: true, nullable: false, includeIfNull: false) -class PusherData { - String url; - String format; - - PusherData({ - this.url, - this.format, - }); - - factory PusherData.fromJson(Map json) => - _$PusherDataFromJson(json); - - Map toJson() => _$PusherDataToJson(this); -} diff --git a/lib/src/requests/SetPushersRequest.g.dart b/lib/src/requests/SetPushersRequest.g.dart deleted file mode 100644 index 0b4a5e1..0000000 --- a/lib/src/requests/SetPushersRequest.g.dart +++ /dev/null @@ -1,41 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'SetPushersRequest.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SetPushersRequest _$SetPushersRequestFromJson(Map json) { - return SetPushersRequest( - lang: json['lang'] as String, - device_display_name: json['device_display_name'] as String, - app_display_name: json['app_display_name'] as String, - app_id: json['app_id'] as String, - kind: json['kind'] as String, - pushkey: json['pushkey'] as String, - data: PusherData.fromJson(json['data'] as Map), - profile_tag: json['profile_tag'] as String, - append: json['append'] as bool); -} - -Map _$SetPushersRequestToJson(SetPushersRequest instance) => - { - 'lang': instance.lang, - 'device_display_name': instance.device_display_name, - 'app_display_name': instance.app_display_name, - 'app_id': instance.app_id, - 'kind': instance.kind, - 'pushkey': instance.pushkey, - 'data': instance.data.toJson(), - 'profile_tag': instance.profile_tag, - 'append': instance.append - }; - -PusherData _$PusherDataFromJson(Map json) { - return PusherData( - url: json['url'] as String, format: json['format'] as String); -} - -Map _$PusherDataToJson(PusherData instance) => - {'url': instance.url, 'format': instance.format}; diff --git a/lib/src/responses/PushrulesResponse.dart b/lib/src/responses/PushrulesResponse.dart deleted file mode 100644 index 0e039ac..0000000 --- a/lib/src/responses/PushrulesResponse.dart +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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 famedly. If not, see . - */ -import 'package:json_annotation/json_annotation.dart'; - -part 'PushrulesResponse.g.dart'; - -@JsonSerializable(explicitToJson: true, nullable: false) -class PushrulesResponse { - @JsonKey(nullable: false) - Global global; - - PushrulesResponse( - this.global, - ); - - factory PushrulesResponse.fromJson(Map json) => - _$PushrulesResponseFromJson(json); - - Map toJson() => _$PushrulesResponseToJson(this); -} - -@JsonSerializable(explicitToJson: true) -class Global { - List content; - List room; - List sender; - List override; - List underride; - - Global( - this.content, - this.room, - this.sender, - this.override, - this.underride, - ); - - factory Global.fromJson(Map json) => _$GlobalFromJson(json); - - Map toJson() => _$GlobalToJson(this); -} - -@JsonSerializable(explicitToJson: true) -class PushRule { - @JsonKey(nullable: false) - List actions; - List conditions; - @JsonKey(nullable: false, name: "default") - bool contentDefault; - @JsonKey(nullable: false) - bool enabled; - @JsonKey(nullable: false) - String ruleId; - String pattern; - - PushRule( - this.actions, - this.conditions, - this.contentDefault, - this.enabled, - this.ruleId, - this.pattern, - ); - - factory PushRule.fromJson(Map json) => - _$PushRuleFromJson(json); - - Map toJson() => _$PushRuleToJson(this); -} - -@JsonSerializable(explicitToJson: true) -class Condition { - String key; - @JsonKey(name: "is") - String conditionIs; - @JsonKey(nullable: false) - String kind; - String pattern; - - Condition( - this.key, - this.conditionIs, - this.kind, - this.pattern, - ); - - factory Condition.fromJson(Map json) => - _$ConditionFromJson(json); - - Map toJson() => _$ConditionToJson(this); -} diff --git a/lib/src/responses/PushrulesResponse.g.dart b/lib/src/responses/PushrulesResponse.g.dart deleted file mode 100644 index ea0977c..0000000 --- a/lib/src/responses/PushrulesResponse.g.dart +++ /dev/null @@ -1,81 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'PushrulesResponse.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -PushrulesResponse _$PushrulesResponseFromJson(Map json) { - return PushrulesResponse( - Global.fromJson(json['global'] as Map)); -} - -Map _$PushrulesResponseToJson(PushrulesResponse instance) => - {'global': instance.global.toJson()}; - -Global _$GlobalFromJson(Map json) { - return Global( - (json['content'] as List) - ?.map((e) => - e == null ? null : PushRule.fromJson(e as Map)) - ?.toList(), - (json['room'] as List) - ?.map((e) => - e == null ? null : PushRule.fromJson(e as Map)) - ?.toList(), - (json['sender'] as List) - ?.map((e) => - e == null ? null : PushRule.fromJson(e as Map)) - ?.toList(), - (json['override'] as List) - ?.map((e) => - e == null ? null : PushRule.fromJson(e as Map)) - ?.toList(), - (json['underride'] as List) - ?.map((e) => - e == null ? null : PushRule.fromJson(e as Map)) - ?.toList()); -} - -Map _$GlobalToJson(Global instance) => { - 'content': instance.content?.map((e) => e?.toJson())?.toList(), - 'room': instance.room?.map((e) => e?.toJson())?.toList(), - 'sender': instance.sender?.map((e) => e?.toJson())?.toList(), - 'override': instance.override?.map((e) => e?.toJson())?.toList(), - 'underride': instance.underride?.map((e) => e?.toJson())?.toList() - }; - -PushRule _$PushRuleFromJson(Map json) { - return PushRule( - json['actions'] as List, - (json['conditions'] as List) - ?.map((e) => - e == null ? null : Condition.fromJson(e as Map)) - ?.toList(), - json['default'] as bool, - json['enabled'] as bool, - json['ruleId'] as String, - json['pattern'] as String); -} - -Map _$PushRuleToJson(PushRule instance) => { - 'actions': instance.actions, - 'conditions': instance.conditions?.map((e) => e?.toJson())?.toList(), - 'default': instance.contentDefault, - 'enabled': instance.enabled, - 'ruleId': instance.ruleId, - 'pattern': instance.pattern - }; - -Condition _$ConditionFromJson(Map json) { - return Condition(json['key'] as String, json['is'] as String, - json['kind'] as String, json['pattern'] as String); -} - -Map _$ConditionToJson(Condition instance) => { - 'key': instance.key, - 'is': instance.conditionIs, - 'kind': instance.kind, - 'pattern': instance.pattern - }; diff --git a/lib/src/utils/ChatTime.dart b/lib/src/utils/ChatTime.dart deleted file mode 100644 index 98e8ca5..0000000 --- a/lib/src/utils/ChatTime.dart +++ /dev/null @@ -1,139 +0,0 @@ -/* - * 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 . - */ - -/// Used to localize and present time in a chat application manner. -class ChatTime { - DateTime dateTime = DateTime.now(); - - /// Insert with a timestamp [ts] which represents the milliseconds since - /// the Unix epoch. - ChatTime(num ts) { - if (ts != null) dateTime = DateTime.fromMillisecondsSinceEpoch(ts); - } - - /// Returns a ChatTime object which represents the current time. - ChatTime.now() { - dateTime = DateTime.now(); - } - - /// Returns [toTimeString()] if the ChatTime is today, the name of the week - /// day if the ChatTime is this week and a date string else. - String toString() { - DateTime now = DateTime.now(); - - bool sameYear = now.year == dateTime.year; - - bool sameDay = - sameYear && now.month == dateTime.month && now.day == dateTime.day; - - bool sameWeek = sameYear && - !sameDay && - now.millisecondsSinceEpoch - dateTime.millisecondsSinceEpoch < - 1000 * 60 * 60 * 24 * 7; - - if (sameDay) { - return toTimeString(); - } else if (sameWeek) { - switch (dateTime.weekday) { - case 1: - return "Montag"; - case 2: - return "Dienstag"; - case 3: - return "Mittwoch"; - case 4: - return "Donnerstag"; - case 5: - return "Freitag"; - case 6: - return "Samstag"; - case 7: - return "Sonntag"; - } - } else if (sameYear) { - return "${_z(dateTime.day)}.${_z(dateTime.month)}"; - } - return "${_z(dateTime.day)}.${_z(dateTime.month)}.${_z(dateTime.year)}"; - } - - /// Returns the milliseconds since the Unix epoch. - num toTimeStamp() { - return dateTime.millisecondsSinceEpoch; - } - - operator <(ChatTime other) { - return this.toTimeStamp() < other.toTimeStamp(); - } - - operator >(ChatTime other) { - return this.toTimeStamp() > other.toTimeStamp(); - } - - operator >=(ChatTime other) { - return this.toTimeStamp() >= other.toTimeStamp(); - } - - operator <=(ChatTime other) { - return this.toTimeStamp() <= other.toTimeStamp(); - } - - operator ==(dynamic other) { - if (other is ChatTime) - return this.toTimeStamp() == other.toTimeStamp(); - else - return false; - } - - /// Two message events can belong to the same environment. That means that they - /// don't need to display the time they were sent because they are close - /// enaugh. - static final minutesBetweenEnvironments = 5; - - /// Checks if two ChatTimes are close enough to belong to the same - /// environment. - bool sameEnvironment(ChatTime prevTime) { - return toTimeStamp() - prevTime.toTimeStamp() < - 1000 * 60 * minutesBetweenEnvironments; - } - - /// Returns a simple time String. - String toTimeString() { - return "${_z(dateTime.hour)}:${_z(dateTime.minute)}"; - } - - /// If the ChatTime is today, this returns [toTimeString()], if not it also - /// shows the date. - String toEventTimeString() { - DateTime now = DateTime.now(); - - bool sameYear = now.year == dateTime.year; - - bool sameDay = - sameYear && now.month == dateTime.month && now.day == dateTime.day; - - if (sameDay) return toTimeString(); - return "${toString()}, ${toTimeString()}"; - } - - static String _z(int i) => i < 10 ? "0${i.toString()}" : i.toString(); -} diff --git a/lib/src/utils/MatrixFile.dart b/lib/src/utils/MatrixFile.dart index 87bb560..3d024d8 100644 --- a/lib/src/utils/MatrixFile.dart +++ b/lib/src/utils/MatrixFile.dart @@ -1,3 +1,4 @@ +/// Workaround until [File] in dart:io and dart:html is unified class MatrixFile { List bytes; String path; diff --git a/lib/src/utils/PushRule.dart b/lib/src/utils/PushRule.dart deleted file mode 100644 index 7a404ca..0000000 --- a/lib/src/utils/PushRule.dart +++ /dev/null @@ -1,64 +0,0 @@ -class PushRule { - final String ruleId; - final bool isDefault; - final bool enabled; - final List conditions; - final List actions; - - PushRule( - {this.ruleId, - this.isDefault, - this.enabled, - this.conditions, - this.actions}); - - PushRule.fromJson(Map json) - : ruleId = json['rule_id'], - isDefault = json['is_default'], - enabled = json['enabled'], - conditions = _getConditionsFromJson(json['conditions']), - actions = json['actions']; - - Map toJson() { - final Map data = new Map(); - data['rule_id'] = this.ruleId; - data['is_default'] = this.isDefault; - data['enabled'] = this.enabled; - if (this.conditions != null) { - data['conditions'] = this.conditions.map((v) => v.toJson()).toList(); - } - data['actions'] = this.actions; - return data; - } - - static List _getConditionsFromJson(List json) { - List conditions = []; - if (json == null) return conditions; - for (int i = 0; i < json.length; i++) { - conditions.add(Conditions.fromJson(json[i])); - } - return conditions; - } -} - -class Conditions { - String key; - String kind; - String pattern; - - Conditions({this.key, this.kind, this.pattern}); - - Conditions.fromJson(Map json) { - key = json['key']; - kind = json['kind']; - pattern = json['pattern']; - } - - Map toJson() { - final Map data = new Map(); - data['key'] = this.key; - data['kind'] = this.kind; - data['pattern'] = this.pattern; - return data; - } -} diff --git a/lib/src/utils/PushRules.dart b/lib/src/utils/PushRules.dart new file mode 100644 index 0000000..dff91d7 --- /dev/null +++ b/lib/src/utils/PushRules.dart @@ -0,0 +1,83 @@ +/// The global ruleset. +class PushRules { + final GlobalPushRules global; + + PushRules.fromJson(Map json) + : this.global = GlobalPushRules.fromJson(json["global"]); +} + +/// The global ruleset. +class GlobalPushRules { + final List content; + final List override; + final List room; + final List sender; + final List underride; + + GlobalPushRules.fromJson(Map json) + : this.content = json.containsKey("content") + ? PushRule.fromJsonList(json["content"]) + : null, + this.override = json.containsKey("override") + ? PushRule.fromJsonList(json["content"]) + : null, + this.room = json.containsKey("room") + ? PushRule.fromJsonList(json["room"]) + : null, + this.sender = json.containsKey("sender") + ? PushRule.fromJsonList(json["sender"]) + : null, + this.underride = json.containsKey("underride") + ? PushRule.fromJsonList(json["underride"]) + : null; +} + +/// A single pushrule. +class PushRule { + final List actions; + final bool isDefault; + final bool enabled; + final String ruleId; + final List conditions; + final String pattern; + + static List fromJsonList(List list) { + List objList = []; + list.forEach((json) { + objList.add(PushRule.fromJson(json)); + }); + return objList; + } + + PushRule.fromJson(Map json) + : this.actions = json["actions"], + this.isDefault = json["default"], + this.enabled = json["enabled"], + this.ruleId = json["rule_id"], + this.conditions = json.containsKey("conditions") + ? PushRuleConditions.fromJsonList(json["conditions"]) + : null, + this.pattern = json["pattern"]; +} + +/// Conditions when this pushrule should be active. +class PushRuleConditions { + final String kind; + final String key; + final String pattern; + final String is_; + + static List fromJsonList(List list) { + List objList = []; + list.forEach((json) { + objList.add(PushRuleConditions.fromJson(json)); + }); + return objList; + } + + PushRuleConditions.fromJson(Map json) + : this.kind = json["kind"], + this.key = json["key"], + this.pattern = json["pattern"], + this.is_ = json["is"]; +} diff --git a/lib/src/utils/Receipt.dart b/lib/src/utils/Receipt.dart index 6115530..56f2748 100644 --- a/lib/src/utils/Receipt.dart +++ b/lib/src/utils/Receipt.dart @@ -1,11 +1,10 @@ -import 'package:famedlysdk/src/utils/ChatTime.dart'; import '../User.dart'; /// Represents a receipt. /// This [user] has read an event at the given [time]. class Receipt { final User user; - final ChatTime time; + final DateTime time; const Receipt(this.user, this.time); } diff --git a/lib/src/utils/StatesMap.dart b/lib/src/utils/StatesMap.dart index 5a0bdf9..a92f933 100644 --- a/lib/src/utils/StatesMap.dart +++ b/lib/src/utils/StatesMap.dart @@ -3,9 +3,9 @@ import 'package:famedlysdk/famedlysdk.dart'; /// Matrix room states are addressed by a tuple of the [type] and an /// optional [stateKey]. class StatesMap { - Map> states = {}; + Map> states = {}; - /// Returns either the [RoomState] or a map of state_keys to [RoomState] objects. + /// Returns either the [Event] or a map of state_keys to [Event] objects. /// If you just enter a MatrixID, it will try to return the corresponding m.room.member event. dynamic operator [](String key) { //print("[Warning] This method will be depracated in the future!"); @@ -14,7 +14,7 @@ class StatesMap { return states["m.room.member"][key]; } if (!states.containsKey(key)) states[key] = {}; - if (states[key][""] is RoomState) + if (states[key][""] is Event) return states[key][""]; else if (states[key].length == 0) return null; @@ -22,7 +22,7 @@ class StatesMap { return states[key]; } - void operator []=(String key, RoomState val) { + void operator []=(String key, Event val) { //print("[Warning] This method will be depracated in the future!"); if (key.startsWith("@") && key.contains(":")) { if (!states.containsKey("m.room.member")) states["m.room.member"] = {}; diff --git a/test/ChatTime_test.dart b/test/ChatTime_test.dart deleted file mode 100644 index e6e9ff4..0000000 --- a/test/ChatTime_test.dart +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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:test/test.dart'; -import 'package:famedlysdk/src/utils/ChatTime.dart'; - -void main() { - /// All Tests related to the ChatTime - group("ChatTime", () { - test("Comparing", () async { - final int originServerTs = DateTime.now().millisecondsSinceEpoch - - (ChatTime.minutesBetweenEnvironments - 1) * 1000 * 60; - final int oldOriginServerTs = DateTime.now().millisecondsSinceEpoch - - (ChatTime.minutesBetweenEnvironments + 1) * 1000 * 60; - - final ChatTime chatTime = ChatTime(originServerTs); - final ChatTime oldChatTime = ChatTime(oldOriginServerTs); - final ChatTime nowTime = ChatTime.now(); - - expect(chatTime.toTimeStamp(), originServerTs); - expect(nowTime.toTimeStamp() > chatTime.toTimeStamp(), true); - expect(nowTime.sameEnvironment(chatTime), true); - expect(nowTime.sameEnvironment(oldChatTime), false); - - expect(chatTime > oldChatTime, true); - expect(chatTime < oldChatTime, false); - expect(chatTime >= oldChatTime, true); - expect(chatTime <= oldChatTime, false); - expect(chatTime == chatTime, true); - expect(chatTime == oldChatTime, false); - }); - - test("Formatting", () async { - final int timestamp = DateTime.now().millisecondsSinceEpoch; - final ChatTime chatTime = ChatTime(timestamp); - //expect(chatTime.toTimeString(),"05:36"); // This depends on the time and your timezone ;) - expect(chatTime.toTimeString(), chatTime.toEventTimeString()); - - final ChatTime oldChatTime = ChatTime(156014498475); - expect(oldChatTime.toString(), "11.12.1974"); - }); - }); -} diff --git a/test/Client_test.dart b/test/Client_test.dart index 55b43ab..1551c33 100644 --- a/test/Client_test.dart +++ b/test/Client_test.dart @@ -25,12 +25,9 @@ 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/Room.dart'; import 'package:famedlysdk/src/User.dart'; -import 'package:famedlysdk/src/requests/SetPushersRequest.dart'; -import 'package:famedlysdk/src/responses/PushrulesResponse.dart'; import 'package:famedlysdk/src/sync/EventUpdate.dart'; import 'package:famedlysdk/src/sync/RoomUpdate.dart'; import 'package:famedlysdk/src/sync/UserUpdate.dart'; @@ -53,11 +50,11 @@ void main() { /// Check if all Elements get created matrix = Client("testclient", debug: true); - matrix.connection.httpClient = FakeMatrixApi(); + matrix.httpClient = FakeMatrixApi(); - roomUpdateListFuture = matrix.connection.onRoomUpdate.stream.toList(); - eventUpdateListFuture = matrix.connection.onEvent.stream.toList(); - userUpdateListFuture = matrix.connection.onUserEvent.stream.toList(); + roomUpdateListFuture = matrix.onRoomUpdate.stream.toList(); + eventUpdateListFuture = matrix.onEvent.stream.toList(); + userUpdateListFuture = matrix.onUserEvent.stream.toList(); test('Login', () async { int presenceCounter = 0; @@ -82,7 +79,7 @@ void main() { expect(matrix.matrixVersions, ["r0.0.1", "r0.1.0", "r0.2.0", "r0.3.0", "r0.4.0", "r0.5.0"]); - final Map resp = await matrix.connection + final Map resp = await matrix .jsonRequest(type: HTTPType.POST, action: "/client/r0/login", data: { "type": "m.login.password", "user": "test", @@ -91,11 +88,11 @@ void main() { }); Future loginStateFuture = - matrix.connection.onLoginStateChanged.stream.first; - Future firstSyncFuture = matrix.connection.onFirstSync.stream.first; - Future syncFuture = matrix.connection.onSync.stream.first; + matrix.onLoginStateChanged.stream.first; + Future firstSyncFuture = matrix.onFirstSync.stream.first; + Future syncFuture = matrix.onSync.stream.first; - matrix.connection.connect( + matrix.connect( newToken: resp["access_token"], newUserID: resp["user_id"], newHomeserver: matrix.homeserver, @@ -121,23 +118,23 @@ void main() { expect(matrix.accountData.length, 3); expect(matrix.getDirectChatFromUserId("@bob:example.com"), "!726s6s6q:example.com"); - expect(matrix.roomList.rooms[1].directChatMatrixID, "@bob:example.com"); + expect(matrix.rooms[1].directChatMatrixID, "@bob:example.com"); expect(matrix.directChats, matrix.accountData["m.direct"].content); expect(matrix.presences.length, 1); - expect(matrix.roomList.rooms[1].ephemerals.length, 2); - expect(matrix.roomList.rooms[1].typingUsers.length, 1); - expect(matrix.roomList.rooms[1].typingUsers[0].id, "@alice:example.com"); - expect(matrix.roomList.rooms[1].roomAccountData.length, 3); + expect(matrix.rooms[1].ephemerals.length, 2); + expect(matrix.rooms[1].typingUsers.length, 1); + expect(matrix.rooms[1].typingUsers[0].id, "@alice:example.com"); + expect(matrix.rooms[1].roomAccountData.length, 3); expect( - matrix.roomList.rooms[1].roomAccountData["m.receipt"] + matrix.rooms[1].roomAccountData["m.receipt"] .content["@alice:example.com"]["ts"], 1436451550453); expect( - matrix.roomList.rooms[1].roomAccountData["m.receipt"] + matrix.rooms[1].roomAccountData["m.receipt"] .content["@alice:example.com"]["event_id"], "7365636s6r6432:example.com"); - expect(matrix.roomList.rooms.length, 2); - expect(matrix.roomList.rooms[1].canonicalAlias, + expect(matrix.rooms.length, 2); + expect(matrix.rooms[1].canonicalAlias, "#famedlyContactDiscovery:${matrix.userID.split(":")[1]}"); final List contacts = await matrix.loadFamedlyContacts(); expect(contacts.length, 1); @@ -147,25 +144,30 @@ void main() { expect(presenceCounter, 1); expect(accountDataCounter, 3); - matrix.connection.onEvent.add( - EventUpdate( - roomID: "!726s6s6q:example.com", - type: "state", - eventType: "m.room.canonical_alias", - content: { - "sender": "@alice:example.com", - "type": "m.room.canonical_alias", - "content": {"alias": ""}, - "state_key": "", - "origin_server_ts": 1417731086799, - "event_id": "66697273743033:example.com" - }, - ), - ); + matrix.handleSync({ + "rooms": { + "join": { + "!726s6s6q:example.com": { + "state": { + "events": [ + { + "sender": "@alice:example.com", + "type": "m.room.canonical_alias", + "content": {"alias": ""}, + "state_key": "", + "origin_server_ts": 1417731086799, + "event_id": "66697273743033:example.com" + } + ] + } + } + } + } + }); await new Future.delayed(new Duration(milliseconds: 50)); expect( - matrix.roomList.getRoomByAlias( + matrix.getRoomByAlias( "#famedlyContactDiscovery:${matrix.userID.split(":")[1]}"), null); final List altContacts = await matrix.loadFamedlyContacts(); @@ -176,8 +178,8 @@ void main() { test('Try to get ErrorResponse', () async { MatrixException expectedException; try { - await matrix.connection - .jsonRequest(type: HTTPType.PUT, action: "/non/existing/path"); + await matrix.jsonRequest( + type: HTTPType.PUT, action: "/non/existing/path"); } on MatrixException catch (exception) { expectedException = exception; } @@ -185,13 +187,13 @@ void main() { }); test('Logout', () async { - await matrix.connection - .jsonRequest(type: HTTPType.POST, action: "/client/r0/logout"); + await matrix.jsonRequest( + type: HTTPType.POST, action: "/client/r0/logout"); Future loginStateFuture = - matrix.connection.onLoginStateChanged.stream.first; + matrix.onLoginStateChanged.stream.first; - matrix.connection.clear(); + matrix.clear(); expect(matrix.accessToken == null, true); expect(matrix.homeserver == null, true); @@ -207,11 +209,11 @@ void main() { }); test('Room Update Test', () async { - matrix.connection.onRoomUpdate.close(); + matrix.onRoomUpdate.close(); List roomUpdateList = await roomUpdateListFuture; - expect(roomUpdateList.length, 2); + expect(roomUpdateList.length, 3); expect(roomUpdateList[0].id == "!726s6s6q:example.com", true); expect(roomUpdateList[0].membership == Membership.join, true); @@ -229,7 +231,7 @@ void main() { }); test('Event Update Test', () async { - matrix.connection.onEvent.close(); + matrix.onEvent.close(); List eventUpdateList = await eventUpdateListFuture; @@ -281,7 +283,7 @@ void main() { }); test('User Update Test', () async { - matrix.connection.onUserEvent.close(); + matrix.onUserEvent.close(); List eventUpdateList = await userUpdateListFuture; @@ -299,11 +301,11 @@ void main() { test('Login', () async { matrix = Client("testclient", debug: true); - matrix.connection.httpClient = FakeMatrixApi(); + matrix.httpClient = FakeMatrixApi(); - roomUpdateListFuture = matrix.connection.onRoomUpdate.stream.toList(); - eventUpdateListFuture = matrix.connection.onEvent.stream.toList(); - userUpdateListFuture = matrix.connection.onUserEvent.stream.toList(); + roomUpdateListFuture = matrix.onRoomUpdate.stream.toList(); + eventUpdateListFuture = matrix.onEvent.stream.toList(); + userUpdateListFuture = matrix.onUserEvent.stream.toList(); final bool checkResp = await matrix.checkServer("https://fakeServer.notExisting"); @@ -326,7 +328,7 @@ void main() { final MatrixFile testFile = MatrixFile(bytes: [], path: "fake/path/file.jpeg"); - final dynamic resp = await matrix.connection.upload(testFile); + final dynamic resp = await matrix.upload(testFile); expect(resp, "mxc://example.com/AQwafuaFswefuhsfAFAgsw"); }); @@ -337,23 +339,14 @@ void main() { }); test('getPushrules', () async { - final PushrulesResponse pushrules = await matrix.getPushrules(); - final PushrulesResponse awaited_resp = PushrulesResponse.fromJson( - FakeMatrixApi.api["GET"]["/client/r0/pushrules/"]("")); - expect(pushrules.toJson(), awaited_resp.toJson()); + final pushrules = await matrix.getPushrules(); + expect(pushrules != null, true); }); test('setPushers', () async { - final SetPushersRequest data = SetPushersRequest( - app_id: "com.famedly.famedlysdk", - device_display_name: "GitLabCi", - app_display_name: "famedlySDK", - pushkey: "abcdefg", - kind: "http", - lang: "en", - data: PusherData( - format: "event_id_only", url: "https://examplepushserver.com")); - await matrix.setPushers(data); + await matrix.setPushers("abcdefg", "http", "com.famedly.famedlysdk", + "famedlySDK", "GitLabCi", "en", "https://examplepushserver.com", + format: "event_id_only"); }); test('joinRoomById', () async { @@ -388,11 +381,11 @@ void main() { test('Logout when token is unknown', () async { Future loginStateFuture = - matrix.connection.onLoginStateChanged.stream.first; + matrix.onLoginStateChanged.stream.first; try { - await matrix.connection - .jsonRequest(type: HTTPType.DELETE, action: "/unknown/token"); + await matrix.jsonRequest( + type: HTTPType.DELETE, action: "/unknown/token"); } on MatrixException catch (exception) { expect(exception.error, MatrixError.M_UNKNOWN_TOKEN); } diff --git a/test/Event_test.dart b/test/Event_test.dart index 139132f..8bfb675 100644 --- a/test/Event_test.dart +++ b/test/Event_test.dart @@ -24,7 +24,7 @@ import 'dart:convert'; import 'package:famedlysdk/famedlysdk.dart'; -import 'package:famedlysdk/src/RoomState.dart'; +import 'package:famedlysdk/src/Event.dart'; import 'package:test/test.dart'; import 'FakeMatrixApi.dart'; @@ -68,7 +68,7 @@ void main() { expect(event.getBody(), body); expect(event.type, EventTypes.Text); jsonObj["state_key"] = ""; - RoomState state = RoomState.fromJson(jsonObj, null); + Event state = Event.fromJson(jsonObj, null); expect(state.eventId, id); expect(state.stateKey, ""); expect(state.timelineEvent.status, 1); @@ -174,7 +174,7 @@ void main() { "type": "m.room.redaction", "unsigned": {"age": 1234} }; - RoomState redactedBecause = RoomState.fromJson(redactionEventJson, room); + Event redactedBecause = Event.fromJson(redactionEventJson, room); Event event = Event.fromJson(jsonObj, room); event.setRedactionEvent(redactedBecause); expect(event.redacted, true); @@ -196,7 +196,7 @@ void main() { test("sendAgain", () async { Client matrix = Client("testclient", debug: true); - matrix.connection.httpClient = FakeMatrixApi(); + matrix.httpClient = FakeMatrixApi(); await matrix.checkServer("https://fakeServer.notExisting"); await matrix.login("test", "1234"); diff --git a/test/MxContent_test.dart b/test/MxContent_test.dart index 9b2d5af..6f6ea4c 100644 --- a/test/MxContent_test.dart +++ b/test/MxContent_test.dart @@ -25,12 +25,15 @@ import 'package:test/test.dart'; import 'package:famedlysdk/src/Client.dart'; import 'package:famedlysdk/src/utils/MxContent.dart'; +import 'FakeMatrixApi.dart'; + void main() { /// All Tests related to the MxContent group("MxContent", () { test("Formatting", () async { Client client = Client("testclient"); - client.homeserver = "https://testserver.abc"; + client.httpClient = FakeMatrixApi(); + await client.checkServer("https://fakeserver.notexisting"); final String mxc = "mxc://exampleserver.abc/abcdefghijklmn"; final MxContent content = MxContent(mxc); @@ -45,7 +48,8 @@ void main() { }); test("Not crashing if null", () async { Client client = Client("testclient"); - client.homeserver = "https://testserver.abc"; + client.httpClient = FakeMatrixApi(); + await client.checkServer("https://fakeserver.notexisting"); final MxContent content = MxContent(null); expect(content.getDownloadLink(client), "${client.homeserver}/_matrix/media/r0/download/"); diff --git a/test/PushRules_test.dart b/test/PushRules_test.dart new file mode 100644 index 0000000..612e22a --- /dev/null +++ b/test/PushRules_test.dart @@ -0,0 +1,185 @@ +/* + * 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/utils/PushRules.dart'; +import 'package:test/test.dart'; + +void main() { + /// All Tests related to the MxContent + group("PushRules", () { + test("Create", () async { + final Map json = { + "global": { + "content": [ + { + "actions": [ + "notify", + {"set_tweak": "sound", "value": "default"}, + {"set_tweak": "highlight"} + ], + "default": true, + "enabled": true, + "pattern": "alice", + "rule_id": ".m.rule.contains_user_name" + } + ], + "override": [ + { + "actions": ["dont_notify"], + "conditions": [], + "default": true, + "enabled": false, + "rule_id": ".m.rule.master" + }, + { + "actions": ["dont_notify"], + "conditions": [ + { + "key": "content.msgtype", + "kind": "event_match", + "pattern": "m.notice" + } + ], + "default": true, + "enabled": true, + "rule_id": ".m.rule.suppress_notices" + } + ], + "room": [], + "sender": [], + "underride": [ + { + "actions": [ + "notify", + {"set_tweak": "sound", "value": "ring"}, + {"set_tweak": "highlight", "value": false} + ], + "conditions": [ + { + "key": "type", + "kind": "event_match", + "pattern": "m.call.invite" + } + ], + "default": true, + "enabled": true, + "rule_id": ".m.rule.call" + }, + { + "actions": [ + "notify", + {"set_tweak": "sound", "value": "default"}, + {"set_tweak": "highlight"} + ], + "conditions": [ + {"kind": "contains_display_name"} + ], + "default": true, + "enabled": true, + "rule_id": ".m.rule.contains_display_name" + }, + { + "actions": [ + "notify", + {"set_tweak": "sound", "value": "default"}, + {"set_tweak": "highlight", "value": false} + ], + "conditions": [ + {"kind": "room_member_count", "is": "2"}, + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.message" + } + ], + "default": true, + "enabled": true, + "rule_id": ".m.rule.room_one_to_one" + }, + { + "actions": [ + "notify", + {"set_tweak": "sound", "value": "default"}, + {"set_tweak": "highlight", "value": false} + ], + "conditions": [ + { + "key": "type", + "kind": "event_match", + "pattern": "m.room.member" + }, + { + "key": "content.membership", + "kind": "event_match", + "pattern": "invite" + }, + { + "key": "state_key", + "kind": "event_match", + "pattern": "@alice:example.com" + } + ], + "default": true, + "enabled": true, + "rule_id": ".m.rule.invite_for_me" + }, + { + "actions": [ + "notify", + {"set_tweak": "highlight", "value": false} + ], + "conditions": [ + { + "key": "type", + "kind": "event_match", + "pattern": "m.room.member" + } + ], + "default": true, + "enabled": true, + "rule_id": ".m.rule.member_event" + }, + { + "actions": [ + "notify", + {"set_tweak": "highlight", "value": false} + ], + "conditions": [ + { + "key": "type", + "kind": "event_match", + "pattern": "m.room.message" + } + ], + "default": true, + "enabled": true, + "rule_id": ".m.rule.message" + } + ] + } + }; + + expect(PushRules.fromJson(json) != null, true); + }); + }); +} diff --git a/test/RoomList_test.dart b/test/RoomList_test.dart deleted file mode 100644 index dd30a4b..0000000 --- a/test/RoomList_test.dart +++ /dev/null @@ -1,281 +0,0 @@ -/* - * 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/Client.dart'; -import 'package:famedlysdk/src/RoomList.dart'; -import 'package:famedlysdk/src/User.dart'; -import 'package:famedlysdk/src/sync/EventUpdate.dart'; -import 'package:famedlysdk/src/sync/RoomUpdate.dart'; -import 'package:famedlysdk/src/utils/ChatTime.dart'; -import 'package:test/test.dart'; - -import 'FakeMatrixApi.dart'; - -void main() { - /// All Tests related to the MxContent - group("RoomList", () { - final roomID = "!1:example.com"; - - test("Create and insert one room", () async { - final Client client = Client("testclient", debug: true); - client.connection.httpClient = FakeMatrixApi(); - await client.checkServer("https://fakeserver.notexisting"); - client.prevBatch = "1234"; - - int updateCount = 0; - List insertList = []; - List removeList = []; - - RoomList roomList = RoomList( - client: client, - rooms: [], - onUpdate: () { - updateCount++; - }, - onInsert: (int insertID) { - insertList.add(insertID); - }, - onRemove: (int removeID) { - insertList.add(removeID); - }); - - expect(roomList.eventSub != null, true); - expect(roomList.roomSub != null, true); - - client.connection.onRoomUpdate.add(RoomUpdate( - id: roomID, - membership: Membership.join, - notification_count: 2, - highlight_count: 1, - limitedTimeline: false, - prev_batch: "1234", - )); - - await new Future.delayed(new Duration(milliseconds: 50)); - - expect(updateCount, 1); - expect(insertList, [0]); - expect(removeList, []); - - expect(roomList.rooms.length, 1); - expect(roomList.rooms[0].id, roomID); - expect(roomList.rooms[0].membership, Membership.join); - expect(roomList.rooms[0].notificationCount, 2); - expect(roomList.rooms[0].highlightCount, 1); - expect(roomList.rooms[0].prev_batch, "1234"); - expect(roomList.rooms[0].timeCreated, ChatTime.now()); - }); - - test("Restort", () async { - final Client client = Client("testclient", debug: true); - client.connection.httpClient = FakeMatrixApi(); - await client.checkServer("https://fakeserver.notexisting"); - client.prevBatch = "1234"; - - int updateCount = 0; - List insertList = []; - List removeList = []; - - RoomList roomList = RoomList( - client: client, - rooms: [], - onUpdate: () { - updateCount++; - }, - onInsert: (int insertID) { - insertList.add(insertID); - }, - onRemove: (int removeID) { - insertList.add(removeID); - }); - - client.connection.onRoomUpdate.add(RoomUpdate( - id: "1", - membership: Membership.join, - notification_count: 2, - highlight_count: 1, - limitedTimeline: false, - prev_batch: "1234", - )); - client.connection.onRoomUpdate.add(RoomUpdate( - id: "2", - membership: Membership.join, - notification_count: 2, - highlight_count: 1, - limitedTimeline: false, - prev_batch: "1234", - )); - client.connection.onRoomUpdate.add(RoomUpdate( - id: "1", - membership: Membership.join, - notification_count: 2, - highlight_count: 1, - limitedTimeline: false, - prev_batch: "12345", - summary: RoomSummary( - mHeroes: ["@alice:example.com"], - mJoinedMemberCount: 1, - mInvitedMemberCount: 1))); - - await new Future.delayed(new Duration(milliseconds: 50)); - - expect(roomList.eventSub != null, true); - expect(roomList.roomSub != null, true); - expect(roomList.rooms[0].id, "1"); - expect(roomList.rooms[1].id, "2"); - expect(roomList.rooms[0].prev_batch, "12345"); - expect(roomList.rooms[0].displayname, "alice"); - expect(roomList.rooms[0].mJoinedMemberCount, 1); - expect(roomList.rooms[0].mInvitedMemberCount, 1); - - ChatTime now = ChatTime.now(); - - int roomUpdates = 0; - - roomList.rooms[0].onUpdate = () { - roomUpdates++; - }; - roomList.rooms[1].onUpdate = () { - roomUpdates++; - }; - - client.connection.onEvent.add(EventUpdate( - type: "timeline", - roomID: "1", - eventType: "m.room.message", - content: { - "type": "m.room.message", - "content": {"msgtype": "m.text", "body": "Testcase"}, - "sender": "@alice:example.com", - "room_id": "1", - "status": 2, - "event_id": "1", - "origin_server_ts": now.toTimeStamp() - 1000 - })); - - client.connection.onEvent.add(EventUpdate( - type: "timeline", - roomID: "2", - eventType: "m.room.message", - content: { - "type": "m.room.message", - "content": {"msgtype": "m.text", "body": "Testcase 2"}, - "sender": "@alice:example.com", - "room_id": "1", - "status": 2, - "event_id": "2", - "origin_server_ts": now.toTimeStamp() - })); - - await new Future.delayed(new Duration(milliseconds: 50)); - - expect(updateCount, 5); - expect(roomUpdates, 3); - expect(insertList, [0, 1]); - expect(removeList, []); - - expect(roomList.rooms.length, 2); - expect( - roomList.rooms[0].timeCreated > roomList.rooms[1].timeCreated, true); - expect(roomList.rooms[0].id, "2"); - expect(roomList.rooms[1].id, "1"); - expect(roomList.rooms[0].lastMessage, "Testcase 2"); - expect(roomList.rooms[0].timeCreated, now); - - client.connection.onEvent.add(EventUpdate( - type: "timeline", - roomID: "1", - eventType: "m.room.redaction", - content: { - "content": {"reason": "Spamming"}, - "event_id": "143273582443PhrSn:example.org", - "origin_server_ts": 1432735824653, - "redacts": "1", - "room_id": "1", - "sender": "@example:example.org", - "type": "m.room.redaction", - "unsigned": {"age": 1234} - })); - - await new Future.delayed(new Duration(milliseconds: 50)); - - expect(updateCount, 6); - expect(insertList, [0, 1]); - expect(removeList, []); - expect(roomList.rooms.length, 2); - expect(roomList.rooms[1].getState("m.room.message").eventId, "1"); - expect(roomList.rooms[1].getState("m.room.message").redacted, true); - }); - - test("onlyLeft", () async { - final Client client = Client("testclient", debug: true); - client.connection.httpClient = FakeMatrixApi(); - await client.checkServer("https://fakeserver.notexisting"); - client.prevBatch = "1234"; - - int updateCount = 0; - List insertList = []; - List removeList = []; - - RoomList roomList = RoomList( - client: client, - onlyLeft: true, - rooms: [], - onUpdate: () { - updateCount++; - }, - onInsert: (int insertID) { - insertList.add(insertID); - }, - onRemove: (int removeID) { - insertList.add(removeID); - }); - - client.connection.onRoomUpdate.add(RoomUpdate( - id: "1", - membership: Membership.join, - notification_count: 2, - highlight_count: 1, - limitedTimeline: false, - prev_batch: "1234", - )); - client.connection.onRoomUpdate.add(RoomUpdate( - id: "2", - membership: Membership.leave, - notification_count: 2, - highlight_count: 1, - limitedTimeline: false, - prev_batch: "1234", - )); - - await new Future.delayed(new Duration(milliseconds: 50)); - - expect(roomList.eventSub != null, true); - expect(roomList.roomSub != null, true); - expect(roomList.rooms[0].id, "2"); - expect(insertList, [0]); - expect(removeList, []); - expect(updateCount, 2); - }); - }); -} diff --git a/test/Room_test.dart b/test/Room_test.dart index 3ceaca8..e96feda 100644 --- a/test/Room_test.dart +++ b/test/Room_test.dart @@ -24,10 +24,8 @@ 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:famedlysdk/src/utils/MatrixFile.dart'; import 'package:test/test.dart'; @@ -41,7 +39,7 @@ void main() { group("Room", () { test('Login', () async { matrix = Client("testclient", debug: true); - matrix.connection.httpClient = FakeMatrixApi(); + matrix.httpClient = FakeMatrixApi(); final bool checkResp = await matrix.checkServer("https://fakeServer.notExisting"); @@ -86,7 +84,7 @@ void main() { expect(room.mHeroes, heroes); expect(room.displayname, "alice, bob, charley"); - room.states["m.room.canonical_alias"] = RoomState( + room.states["m.room.canonical_alias"] = Event( senderId: "@test:example.com", typeKey: "m.room.canonical_alias", roomId: room.id, @@ -97,7 +95,7 @@ void main() { expect(room.displayname, "testalias"); expect(room.canonicalAlias, "#testalias:example.com"); - room.states["m.room.name"] = RoomState( + room.states["m.room.name"] = Event( senderId: "@test:example.com", typeKey: "m.room.name", roomId: room.id, @@ -108,7 +106,7 @@ void main() { expect(room.displayname, "testname"); expect(room.topic, ""); - room.states["m.room.topic"] = RoomState( + room.states["m.room.topic"] = Event( senderId: "@test:example.com", typeKey: "m.room.topic", roomId: room.id, @@ -119,7 +117,7 @@ void main() { expect(room.topic, "testtopic"); expect(room.avatar.mxc, ""); - room.states["m.room.avatar"] = RoomState( + room.states["m.room.avatar"] = Event( senderId: "@test:example.com", typeKey: "m.room.avatar", roomId: room.id, @@ -130,13 +128,13 @@ void main() { expect(room.avatar.mxc, "mxc://testurl"); expect(room.lastEvent, null); - room.states["m.room.message"] = RoomState( + room.states["m.room.message"] = Event( senderId: "@test:example.com", typeKey: "m.room.message", roomId: room.id, room: room, eventId: "12345", - time: ChatTime.now(), + time: DateTime.now(), content: {"msgtype": "m.text", "body": "test"}, stateKey: ""); expect(room.lastEvent.eventId, "12345"); @@ -187,7 +185,7 @@ void main() { }); test("PowerLevels", () async { - room.states["m.room.power_levels"] = RoomState( + room.states["m.room.power_levels"] = Event( senderId: "@test:example.com", typeKey: "m.room.power_levels", roomId: room.id, @@ -223,7 +221,7 @@ void main() { expect(room.powerLevels, room.states["m.room.power_levels"].content["users"]); - room.states["m.room.power_levels"] = RoomState( + room.states["m.room.power_levels"] = Event( senderId: "@test:example.com", typeKey: "m.room.power_levels", roomId: room.id, @@ -264,13 +262,13 @@ void main() { }); test("getParticipants", () async { - room.setState(RoomState( + room.setState(Event( senderId: "@alice:test.abc", typeKey: "m.room.member", roomId: room.id, room: room, eventId: "12345", - time: ChatTime.now(), + time: DateTime.now(), content: {"displayname": "alice"}, stateKey: "@alice:test.abc")); final List userList = room.getParticipants(); diff --git a/test/StatesMap_test.dart b/test/StatesMap_test.dart index 77e8cdb..e8c6f41 100644 --- a/test/StatesMap_test.dart +++ b/test/StatesMap_test.dart @@ -30,7 +30,7 @@ void main() { group("StateKeys", () { test("Operator overload", () async { StatesMap states = StatesMap(); - states["m.room.name"] = RoomState( + states["m.room.name"] = Event( eventId: "1", content: {"name": "test"}, typeKey: "m.room.name", @@ -38,7 +38,7 @@ void main() { roomId: "!test:test.test", senderId: "@alice:test.test"); - states["@alice:test.test"] = RoomState( + states["@alice:test.test"] = Event( eventId: "2", content: {"membership": "join"}, typeKey: "m.room.name", @@ -46,7 +46,7 @@ void main() { roomId: "!test:test.test", senderId: "@alice:test.test"); - states["m.room.member"]["@bob:test.test"] = RoomState( + states["m.room.member"]["@bob:test.test"] = Event( eventId: "3", content: {"membership": "join"}, typeKey: "m.room.name", @@ -54,7 +54,7 @@ void main() { roomId: "!test:test.test", senderId: "@bob:test.test"); - states["com.test.custom"] = RoomState( + states["com.test.custom"] = Event( eventId: "4", content: {"custom": "stuff"}, typeKey: "com.test.custom", diff --git a/test/Timeline_test.dart b/test/Timeline_test.dart index bb17e28..abaf4e1 100644 --- a/test/Timeline_test.dart +++ b/test/Timeline_test.dart @@ -27,20 +27,18 @@ import 'package:famedlysdk/src/Client.dart'; import 'package:famedlysdk/src/Room.dart'; import 'package:famedlysdk/src/Timeline.dart'; import 'package:famedlysdk/src/sync/EventUpdate.dart'; -import 'package:famedlysdk/src/utils/ChatTime.dart'; import 'FakeMatrixApi.dart'; void main() { /// All Tests related to the MxContent group("Timeline", () { final String roomID = "!1234:example.com"; - final testTimeStamp = ChatTime.now().toTimeStamp(); + final testTimeStamp = DateTime.now().millisecondsSinceEpoch; int updateCount = 0; List insertList = []; Client client = Client("testclient", debug: true); - client.connection.httpClient = FakeMatrixApi(); - client.homeserver = "https://fakeServer.notExisting"; + client.httpClient = FakeMatrixApi(); Room room = Room( id: roomID, client: client, prev_batch: "1234", roomAccountData: {}); @@ -55,7 +53,8 @@ void main() { }); test("Create", () async { - client.connection.onEvent.add(EventUpdate( + await client.checkServer("https://fakeServer.notExisting"); + client.onEvent.add(EventUpdate( type: "timeline", roomID: roomID, eventType: "m.room.message", @@ -68,7 +67,7 @@ void main() { "origin_server_ts": testTimeStamp })); - client.connection.onEvent.add(EventUpdate( + client.onEvent.add(EventUpdate( type: "timeline", roomID: roomID, eventType: "m.room.message", @@ -91,9 +90,12 @@ void main() { expect(timeline.events.length, 2); 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].time.millisecondsSinceEpoch, testTimeStamp); expect(timeline.events[0].getBody(), "Testcase"); - expect(timeline.events[0].time > timeline.events[1].time, true); + expect( + timeline.events[0].time.millisecondsSinceEpoch > + timeline.events[1].time.millisecondsSinceEpoch, + true); expect(timeline.events[0].receipts, []); room.roomAccountData["m.receipt"] = RoomAccountData.fromJson({ @@ -112,7 +114,7 @@ void main() { expect(timeline.events[0].receipts.length, 1); expect(timeline.events[0].receipts[0].user.id, "@alice:example.com"); - client.connection.onEvent.add(EventUpdate( + client.onEvent.add(EventUpdate( type: "timeline", roomID: roomID, eventType: "m.room.redaction", @@ -145,7 +147,7 @@ void main() { expect(timeline.events[0].eventId, "42"); expect(timeline.events[0].status, 1); - client.connection.onEvent.add(EventUpdate( + client.onEvent.add(EventUpdate( type: "timeline", roomID: roomID, eventType: "m.room.message", @@ -169,7 +171,7 @@ void main() { }); test("Send message with error", () async { - client.connection.onEvent.add(EventUpdate( + client.onEvent.add(EventUpdate( type: "timeline", roomID: roomID, eventType: "m.room.message", diff --git a/test/User_test.dart b/test/User_test.dart index 4075e30..1624d1d 100644 --- a/test/User_test.dart +++ b/test/User_test.dart @@ -21,7 +21,7 @@ * along with famedlysdk. If not, see . */ -import 'package:famedlysdk/src/RoomState.dart'; +import 'package:famedlysdk/src/Event.dart'; import 'package:famedlysdk/src/User.dart'; import 'package:test/test.dart'; @@ -49,7 +49,7 @@ void main() { "state_key": id }; - User user = RoomState.fromJson(jsonObj, null).asUser; + User user = Event.fromJson(jsonObj, null).asUser; expect(user.id, id); expect(user.membership, membership);