diff --git a/lib/famedlysdk.dart b/lib/famedlysdk.dart index 8288e513..cfdf3f6c 100644 --- a/lib/famedlysdk.dart +++ b/lib/famedlysdk.dart @@ -24,12 +24,12 @@ library famedlysdk; export 'package:famedlysdk/src/requests/SetPushersRequest.dart'; -export 'package:famedlysdk/src/responses/ErrorResponse.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/StatesMap.dart'; diff --git a/lib/src/Client.dart b/lib/src/Client.dart index b61884b0..d6e8e799 100644 --- a/lib/src/Client.dart +++ b/lib/src/Client.dart @@ -38,7 +38,6 @@ import 'RoomList.dart'; import 'RoomState.dart'; import 'User.dart'; import 'requests/SetPushersRequest.dart'; -import 'responses/ErrorResponse.dart'; import 'responses/PushrulesResponse.dart'; import 'utils/Profile.dart'; @@ -156,65 +155,56 @@ class Client { /// Checks the supported versions of the Matrix protocol and the supported /// login types. Returns false if the server is not compatible with the /// client. Automatically sets [matrixVersions] and [lazyLoadMembers]. + /// Throws FormatException, TimeoutException and MatrixException on error. Future checkServer(serverUrl) async { - homeserver = serverUrl; + try { + homeserver = serverUrl; + final versionResp = await connection.jsonRequest( + type: HTTPType.GET, action: "/client/versions"); - final versionResp = await connection.jsonRequest( - type: HTTPType.GET, action: "/client/versions"); - if (versionResp is ErrorResponse) { - connection.onError.add(ErrorResponse(errcode: "NO_RESPONSE", error: "")); - return false; - } + final List versions = List.from(versionResp["versions"]); - final List versions = List.from(versionResp["versions"]); - - if (versions == null) { - connection.onError.add(ErrorResponse(errcode: "NO_RESPONSE", error: "")); - return false; - } - - for (int i = 0; i < versions.length; i++) { - if (versions[i] == "r0.5.0") - break; - else if (i == versions.length - 1) { - connection.onError.add(ErrorResponse(errcode: "NO_SUPPORT", error: "")); - return false; + for (int i = 0; i < versions.length; i++) { + if (versions[i] == "r0.5.0") + break; + else if (i == versions.length - 1) { + return false; + } } - } - matrixVersions = versions; + matrixVersions = versions; - if (versionResp.containsKey("unstable_features") && - versionResp["unstable_features"].containsKey("m.lazy_load_members")) { - lazyLoadMembers = versionResp["unstable_features"]["m.lazy_load_members"] - ? true - : false; - } - - final loginResp = await connection.jsonRequest( - type: HTTPType.GET, action: "/client/r0/login"); - if (loginResp is ErrorResponse) { - connection.onError.add(loginResp); - return false; - } - - final List flows = loginResp["flows"]; - - for (int i = 0; i < flows.length; i++) { - if (flows[i].containsKey("type") && - flows[i]["type"] == "m.login.password") - break; - else if (i == flows.length - 1) { - connection.onError.add(ErrorResponse(errcode: "NO_SUPPORT", error: "")); - return false; + if (versionResp.containsKey("unstable_features") && + versionResp["unstable_features"].containsKey("m.lazy_load_members")) { + lazyLoadMembers = versionResp["unstable_features"] + ["m.lazy_load_members"] + ? true + : false; } - } - return true; + final loginResp = await connection.jsonRequest( + type: HTTPType.GET, action: "/client/r0/login"); + + final List flows = loginResp["flows"]; + + for (int i = 0; i < flows.length; i++) { + if (flows[i].containsKey("type") && + flows[i]["type"] == "m.login.password") + break; + else if (i == flows.length - 1) { + return false; + } + } + return true; + } catch (_) { + this.homeserver = this.matrixVersions = null; + rethrow; + } } /// Handles the login and allows the client to call all APIs which require - /// authentication. Returns false if the login was not successful. + /// 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: { @@ -228,15 +218,10 @@ class Client { "initial_device_display_name": "Famedly Talk" }); - if (loginResp is ErrorResponse) { - connection.onError.add(loginResp); - return false; - } - final userID = loginResp["user_id"]; final accessToken = loginResp["access_token"]; if (userID == null || accessToken == null) { - connection.onError.add(ErrorResponse(errcode: "NO_SUPPORT", error: "")); + return false; } await connection.connect( @@ -253,11 +238,14 @@ class Client { /// Sends a logout command to the homeserver and clears all local data, /// including all persistent data from the store. Future logout() async { - final dynamic resp = await connection.jsonRequest( - type: HTTPType.POST, action: "/client/r0/logout"); - if (resp is ErrorResponse) connection.onError.add(resp); - - await connection.clear(); + try { + await connection.jsonRequest( + type: HTTPType.POST, action: "/client/r0/logout"); + } catch (exception) { + rethrow; + } finally { + await connection.clear(); + } } /// Get the combined profile information for this user. This API may be used to @@ -266,10 +254,6 @@ class Client { Future getProfileFromUserId(String userId) async { final dynamic resp = await connection.jsonRequest( type: HTTPType.GET, action: "/client/r0/profile/${userId}"); - if (resp is ErrorResponse) { - connection.onError.add(resp); - return null; - } return Profile.fromJson(resp); } @@ -295,8 +279,7 @@ class Client { String action = "/client/r0/sync?filter=$syncFilters&timeout=0"; final sync = await connection.jsonRequest(type: HTTPType.GET, action: action); - if (!(sync is ErrorResponse) && - sync["rooms"]["leave"] is Map) { + if (sync["rooms"]["leave"] is Map) { for (var entry in sync["rooms"]["leave"].entries) { final String id = entry.key; final dynamic room = entry.value; @@ -377,33 +360,29 @@ class Client { if (params == null && invite != null) for (int i = 0; i < invite.length; i++) inviteIDs.add(invite[i].id); - final dynamic resp = await connection.jsonRequest( - type: HTTPType.POST, - action: "/client/r0/createRoom", - data: params == null - ? { - "invite": inviteIDs, - } - : params); - - if (resp is ErrorResponse) { - connection.onError.add(resp); - return null; + try { + final dynamic resp = await connection.jsonRequest( + type: HTTPType.POST, + action: "/client/r0/createRoom", + data: params == null + ? { + "invite": inviteIDs, + } + : params); + return resp["room_id"]; + } catch (e) { + rethrow; } - - return resp["room_id"]; } - /// Uploads a new user avatar for this user. Returns ErrorResponse if something went wrong. - Future setAvatar(MatrixFile file) async { + /// Uploads a new user avatar for this user. + Future setAvatar(MatrixFile file) async { final uploadResp = await connection.upload(file); - if (uploadResp is ErrorResponse) return uploadResp; - final setAvatarResp = await connection.jsonRequest( + await connection.jsonRequest( type: HTTPType.PUT, action: "/client/r0/profile/$userID/avatar_url", data: {"avatar_url": uploadResp}); - if (setAvatarResp is ErrorResponse) return setAvatarResp; - return null; + return; } /// Fetches the pushrules for the logged in user. @@ -414,26 +393,16 @@ class Client { action: "/client/r0/pushrules/", ); - if (resp is ErrorResponse) { - connection.onError.add(resp); - return null; - } - return PushrulesResponse.fromJson(resp); } /// This endpoint allows the creation, modification and deletion of pushers for this user ID. - Future setPushers(SetPushersRequest data) async { - final dynamic resp = await connection.jsonRequest( + Future setPushers(SetPushersRequest data) async { + await connection.jsonRequest( type: HTTPType.POST, action: "/client/r0/pushers/set", data: data.toJson(), ); - - if (resp is ErrorResponse) { - connection.onError.add(resp); - } - - return resp; + return; } } diff --git a/lib/src/Connection.dart b/lib/src/Connection.dart index d201b796..52ff6adc 100644 --- a/lib/src/Connection.dart +++ b/lib/src/Connection.dart @@ -33,10 +33,10 @@ import 'package:mime_type/mime_type.dart'; import 'Client.dart'; import 'User.dart'; -import 'responses/ErrorResponse.dart'; import 'sync/EventUpdate.dart'; import 'sync/RoomUpdate.dart'; import 'sync/UserUpdate.dart'; +import 'utils/MatrixException.dart'; enum HTTPType { GET, POST, PUT, DELETE } @@ -74,7 +74,7 @@ class Connection { new StreamController.broadcast(); /// Synchronization erros are coming here. - final StreamController onError = + final StreamController onError = new StreamController.broadcast(); /// This is called once, when the first sync has received. @@ -177,6 +177,8 @@ class Connection { /// 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: @@ -192,7 +194,7 @@ class Connection { /// ); /// ``` /// - Future jsonRequest( + Future> jsonRequest( {HTTPType type, String action, dynamic data = "", @@ -219,6 +221,7 @@ class Connection { "[REQUEST ${type.toString().split('.').last}] Action: $action, Data: $data"); http.Response resp; + Map jsonResp = {}; try { switch (type.toString().split('.').last) { case "GET": @@ -242,52 +245,47 @@ class Connection { .timeout(Duration(seconds: timeout)); break; } - } on TimeoutException catch (_) { - return ErrorResponse( - error: "No connection possible...", - errcode: "TIMEOUT", - request: resp?.request); - } catch (e) { - return ErrorResponse( - error: "No connection possible...", - errcode: "NO_CONNECTION", - request: resp?.request); - } + jsonResp = jsonDecode(resp.body) + as Map; // May throw FormatException - Map jsonResp; - try { - jsonResp = jsonDecode(resp.body) as Map; - } catch (e) { - return ErrorResponse( - error: "No connection possible...", - errcode: "MALFORMED", - request: resp?.request); - } - if (jsonResp.containsKey("errcode") && jsonResp["errcode"] is String) { - if (jsonResp["errcode"] == "M_UNKNOWN_TOKEN") clear(); - return ErrorResponse.fromJson(jsonResp, resp?.request); - } + 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(); + } - if (client.debug) print("[RESPONSE] ${jsonResp.toString()}"); + 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 or an [ErrorResponse]. - Future upload(MatrixFile file) async { + /// 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 dynamic resp = await jsonRequest( + final Map resp = await jsonRequest( type: HTTPType.POST, action: "/media/r0/upload?filename=$fileName", data: fileBytes, contentType: mimeType); - if (resp is ErrorResponse) return resp; return resp["content_uri"]; } @@ -302,32 +300,28 @@ class Connection { action += "&timeout=30000"; action += "&since=${client.prevBatch}"; } - _syncRequest = jsonRequest(type: HTTPType.GET, action: action); - final int hash = _syncRequest.hashCode; - final syncResp = await _syncRequest; - if (hash != _syncRequest.hashCode) return; - if (syncResp is ErrorResponse) { - //onError.add(syncResp); - await Future.delayed(Duration(seconds: syncErrorTimeoutSec), () {}); - } else { - try { - 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"]; - } catch (e) { - onError - .add(ErrorResponse(errcode: "CRITICAL_ERROR", error: e.toString())); - await Future.delayed(Duration(seconds: syncErrorTimeoutSec), () {}); - } + 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); } - if (hash == _syncRequest.hashCode) _sync(); } void handleSync(dynamic sync) { diff --git a/lib/src/Room.dart b/lib/src/Room.dart index 14950817..55b71c1e 100644 --- a/lib/src/Room.dart +++ b/lib/src/Room.dart @@ -25,10 +25,10 @@ import 'package:famedlysdk/src/Client.dart'; import 'package:famedlysdk/src/Event.dart'; import 'package:famedlysdk/src/RoomAccountData.dart'; import 'package:famedlysdk/src/RoomState.dart'; -import 'package:famedlysdk/src/responses/ErrorResponse.dart'; import 'package:famedlysdk/src/sync/EventUpdate.dart'; import 'package:famedlysdk/src/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'; //import 'package:image/image.dart'; @@ -248,35 +248,33 @@ class Room { return ChatTime.now(); } - /// Call the Matrix API to change the name of this room. - Future setName(String newName) async { - dynamic res = await client.connection.jsonRequest( + /// 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( type: HTTPType.PUT, action: "/client/r0/rooms/${id}/state/m.room.name", data: {"name": newName}); - if (res is ErrorResponse) client.connection.onError.add(res); - return res; + return resp["event_id"]; } /// Call the Matrix API to change the topic of this room. - Future setDescription(String newName) async { - dynamic res = await client.connection.jsonRequest( + Future setDescription(String newName) async { + final Map resp = await client.connection.jsonRequest( type: HTTPType.PUT, action: "/client/r0/rooms/${id}/state/m.room.topic", data: {"topic": newName}); - if (res is ErrorResponse) client.connection.onError.add(res); - return res; + return resp["event_id"]; } - Future _sendRawEventNow(Map content, + Future _sendRawEventNow(Map content, {String txid = null}) async { if (txid == null) txid = "txid${DateTime.now().millisecondsSinceEpoch}"; - final dynamic res = await client.connection.jsonRequest( + final Map res = await client.connection.jsonRequest( type: HTTPType.PUT, action: "/client/r0/rooms/${id}/send/m.room.message/$txid", data: content); - if (res is ErrorResponse) client.connection.onError.add(res); - return res; + return res["event_id"]; } Future sendTextEvent(String message, {String txid = null}) => @@ -291,8 +289,7 @@ class Room { if (msgType == "m.video") return sendAudioEvent(file); String fileName = file.path.split("/").last; - final dynamic uploadResp = await client.connection.upload(file); - if (uploadResp is ErrorResponse) return null; + final String uploadResp = await client.connection.upload(file); // Send event Map content = { @@ -311,8 +308,7 @@ class Room { Future sendAudioEvent(MatrixFile file, {String txid = null, int width, int height}) async { String fileName = file.path.split("/").last; - final dynamic uploadResp = await client.connection.upload(file); - if (uploadResp is ErrorResponse) return null; + final String uploadResp = await client.connection.upload(file); Map content = { "msgtype": "m.audio", "body": fileName, @@ -329,8 +325,7 @@ class Room { Future sendImageEvent(MatrixFile file, {String txid = null, int width, int height}) async { String fileName = file.path.split("/").last; - final dynamic uploadResp = await client.connection.upload(file); - if (uploadResp is ErrorResponse) return null; + final String uploadResp = await client.connection.upload(file); Map content = { "msgtype": "m.image", "body": fileName, @@ -354,8 +349,7 @@ class Room { int thumbnailWidth, int thumbnailHeight}) async { String fileName = file.path.split("/").last; - final dynamic uploadResp = await client.connection.upload(file); - if (uploadResp is ErrorResponse) return null; + final String uploadResp = await client.connection.upload(file); Map content = { "msgtype": "m.video", "body": fileName, @@ -376,8 +370,7 @@ class Room { } if (thumbnail != null) { String thumbnailName = file.path.split("/").last; - final dynamic thumbnailUploadResp = await client.connection.upload(file); - if (thumbnailUploadResp is ErrorResponse) return null; + final String thumbnailUploadResp = await client.connection.upload(file); content["info"]["thumbnail_url"] = thumbnailUploadResp; content["info"]["thumbnail_info"] = { "size": thumbnail.size, @@ -422,9 +415,18 @@ class Room { }); // Send the text and on success, store and display a *sent* event. - final dynamic res = await _sendRawEventNow(content, txid: messageID); - - if (res is ErrorResponse || !(res["event_id"] is String)) { + try { + final String res = await _sendRawEventNow(content, txid: messageID); + eventUpdate.content["status"] = 1; + eventUpdate.content["unsigned"] = {"transaction_id": messageID}; + eventUpdate.content["event_id"] = res; + client.connection.onEvent.add(eventUpdate); + await client.store?.transaction(() { + client.store.storeEventUpdate(eventUpdate); + return; + }); + return res; + } catch (exception) { // On error, set status to -1 eventUpdate.content["status"] = -1; eventUpdate.content["unsigned"] = {"transaction_id": messageID}; @@ -433,16 +435,6 @@ class Room { client.store.storeEventUpdate(eventUpdate); return; }); - } else { - eventUpdate.content["status"] = 1; - eventUpdate.content["unsigned"] = {"transaction_id": messageID}; - eventUpdate.content["event_id"] = res["event_id"]; - client.connection.onEvent.add(eventUpdate); - await client.store?.transaction(() { - client.store.storeEventUpdate(eventUpdate); - return; - }); - return res["event_id"]; } return null; } @@ -450,12 +442,16 @@ class Room { /// Call the Matrix API to join this room if the user is not already a member. /// If this room is intended to be a direct chat, the direct chat flag will /// automatically be set. - Future join() async { - dynamic res = await client.connection.jsonRequest( - type: HTTPType.POST, action: "/client/r0/rooms/${id}/join"); - if (res is ErrorResponse) { - client.connection.onError.add(res); - if (res.error == "No known servers") { + Future join() async { + try { + await client.connection.jsonRequest( + type: HTTPType.POST, action: "/client/r0/rooms/${id}/join"); + if (states.containsKey(client.userID) && + states[client.userID].content["is_direct"] is bool && + states[client.userID].content["is_direct"]) + addToDirectChat(states[client.userID].sender.id); + } on MatrixException catch (exception) { + if (exception.errorMessage == "No known servers") { client.store?.forgetRoom(id); client.connection.onRoomUpdate.add( RoomUpdate( @@ -465,88 +461,78 @@ class Room { highlight_count: 0), ); } - return res; + rethrow; } - if (states.containsKey(client.userID) && - states[client.userID].content["is_direct"] is bool && - states[client.userID].content["is_direct"]) - addToDirectChat(states[client.userID].sender.id); - return res; } /// Call the Matrix API to leave this room. If this room is set as a direct /// chat, this will be removed too. - Future leave() async { + Future leave() async { if (directChatMatrixID != "") await removeFromDirectChat(); - dynamic res = await client.connection.jsonRequest( + await client.connection.jsonRequest( type: HTTPType.POST, action: "/client/r0/rooms/${id}/leave"); - if (res is ErrorResponse) client.connection.onError.add(res); - return res; + return; } /// Call the Matrix API to forget this room if you already left it. - Future forget() async { + Future forget() async { client.store.forgetRoom(id); - dynamic res = await client.connection.jsonRequest( + await client.connection.jsonRequest( type: HTTPType.POST, action: "/client/r0/rooms/${id}/forget"); - if (res is ErrorResponse) client.connection.onError.add(res); - return res; + return; } /// Call the Matrix API to kick a user from this room. - Future kick(String userID) async { - dynamic res = await client.connection.jsonRequest( + Future kick(String userID) async { + await client.connection.jsonRequest( type: HTTPType.POST, action: "/client/r0/rooms/${id}/kick", data: {"user_id": userID}); - if (res is ErrorResponse) client.connection.onError.add(res); - return res; + return; } /// Call the Matrix API to ban a user from this room. - Future ban(String userID) async { - dynamic res = await client.connection.jsonRequest( + Future ban(String userID) async { + await client.connection.jsonRequest( type: HTTPType.POST, action: "/client/r0/rooms/${id}/ban", data: {"user_id": userID}); - if (res is ErrorResponse) client.connection.onError.add(res); - return res; + return; } /// Call the Matrix API to unban a banned user from this room. - Future unban(String userID) async { - dynamic res = await client.connection.jsonRequest( + Future unban(String userID) async { + await client.connection.jsonRequest( type: HTTPType.POST, action: "/client/r0/rooms/${id}/unban", data: {"user_id": userID}); - if (res is ErrorResponse) client.connection.onError.add(res); - return res; + return; } /// Set the power level of the user with the [userID] to the value [power]. - Future setPower(String userID, int power) async { + /// Returns the event ID of the new state event. If there is no known + /// power level event, there might something broken and this returns null. + Future setPower(String userID, int power) async { if (states["m.room.power_levels"] == null) return null; Map powerMap = {} ..addAll(states["m.room.power_levels"].content); if (powerMap["users"] == null) powerMap["users"] = {}; powerMap["users"][userID] = power; - dynamic res = await client.connection.jsonRequest( + final Map resp = await client.connection.jsonRequest( type: HTTPType.PUT, action: "/client/r0/rooms/$id/state/m.room.power_levels", data: powerMap); - if (res is ErrorResponse) client.connection.onError.add(res); - return res; + return resp["event_id"]; } /// Call the Matrix API to invite a user to this room. - Future invite(String userID) async { - dynamic res = await client.connection.jsonRequest( + Future invite(String userID) async { + await client.connection.jsonRequest( type: HTTPType.POST, action: "/client/r0/rooms/${id}/invite", data: {"user_id": userID}); - if (res is ErrorResponse) client.connection.onError.add(res); - return res; + return; } /// Request more previous events from the server. [historyCount] defines how much events should @@ -559,8 +545,6 @@ class Room { action: "/client/r0/rooms/$id/messages?from=${prev_batch}&dir=b&limit=$historyCount&filter=${Connection.syncFilters}"); - if (resp is ErrorResponse) return; - if (onHistoryReceived != null) onHistoryReceived(); prev_batch = resp["end"]; client.store?.storeRoomPrevBatch(this); @@ -634,51 +618,51 @@ class Room { ); } - /// Sets this room as a direct chat for this user. - Future addToDirectChat(String userID) async { + /// Sets this room as a direct chat for this user if not already. + Future addToDirectChat(String userID) async { Map directChats = client.directChats; if (directChats.containsKey(userID)) if (!directChats[userID].contains(id)) directChats[userID].add(id); else - return null; // Is already in direct chats + return; // Is already in direct chats else directChats[userID] = [id]; - final resp = await client.connection.jsonRequest( + await client.connection.jsonRequest( type: HTTPType.PUT, action: "/client/r0/user/${client.userID}/account_data/m.direct", data: directChats); - return resp; + return; } - /// Sets this room as a direct chat for this user. - Future removeFromDirectChat() async { + /// Removes this room from all direct chat tags. + Future removeFromDirectChat() async { Map directChats = client.directChats; if (directChats.containsKey(directChatMatrixID) && directChats[directChatMatrixID].contains(id)) directChats[directChatMatrixID].remove(id); else - return null; // Nothing to do here + return; // Nothing to do here - final resp = await client.connection.jsonRequest( + await client.connection.jsonRequest( type: HTTPType.PUT, action: "/client/r0/user/${client.userID}/account_data/m.direct", data: directChats); - return resp; + return; } /// Sends *m.fully_read* and *m.read* for the given event ID. - Future sendReadReceipt(String eventID) async { + Future sendReadReceipt(String eventID) async { this.notificationCount = 0; client?.store?.resetNotificationCount(this.id); - final dynamic resp = client.connection.jsonRequest( + client.connection.jsonRequest( type: HTTPType.POST, action: "/client/r0/rooms/$id/read_markers", data: { "m.fully_read": eventID, "m.read": eventID, }); - return resp; + return; } /// Returns a Room from a json String which comes normally from the store. If the @@ -770,8 +754,6 @@ class Room { dynamic res = await client.connection.jsonRequest( type: HTTPType.GET, action: "/client/r0/rooms/${id}/members"); - if (res is ErrorResponse || !(res["chunk"] is List)) - return participants; for (num i = 0; i < res["chunk"].length; i++) { User newUser = RoomState.fromJson(res["chunk"][i], this).asUser; @@ -794,7 +776,9 @@ class Room { if (states[mxID] != null) return states[mxID].asUser; else { - requestUser(mxID); + try { + requestUser(mxID); + } catch (_) {} return User(mxID, room: this); } } @@ -805,12 +789,14 @@ class Room { /// lazy loading. Future requestUser(String mxID) async { if (mxID == null || !_requestingMatrixIds.add(mxID)) return null; - final dynamic resp = await client.connection.jsonRequest( - type: HTTPType.GET, - action: "/client/r0/rooms/$id/state/m.room.member/$mxID"); - if (resp is ErrorResponse) { + Map resp; + try { + resp = await client.connection.jsonRequest( + type: HTTPType.GET, + action: "/client/r0/rooms/$id/state/m.room.member/$mxID"); + } catch (exception) { _requestingMatrixIds.remove(mxID); - return null; + rethrow; } final User user = User(mxID, displayName: resp["displayname"], @@ -837,7 +823,6 @@ class Room { Future getEventById(String eventID) async { final dynamic resp = await client.connection.jsonRequest( type: HTTPType.GET, action: "/client/r0/rooms/$id/event/$eventID"); - if (resp is ErrorResponse) return null; return Event.fromJson(resp, this); } @@ -865,16 +850,15 @@ class Room { return null; } - /// Uploads a new user avatar for this room. Returns ErrorResponse if something went wrong - /// and the event ID otherwise. - Future setAvatar(MatrixFile file) async { - final uploadResp = await client.connection.upload(file); - if (uploadResp is ErrorResponse) return uploadResp; - final setAvatarResp = await client.connection.jsonRequest( - type: HTTPType.PUT, - action: "/client/r0/rooms/$id/state/m.room.avatar/", - data: {"url": uploadResp}); - if (setAvatarResp is ErrorResponse) return setAvatarResp; + /// 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}); return setAvatarResp["event_id"]; } @@ -979,7 +963,6 @@ class Room { type: HTTPType.DELETE, action: "/client/r0/pushrules/global/override/$id", data: {}); - if (resp == ErrorResponse) return resp; resp = await client.connection.jsonRequest( type: HTTPType.PUT, action: "/client/r0/pushrules/global/room/$id", @@ -1001,7 +984,6 @@ class Room { type: HTTPType.DELETE, action: "/client/r0/pushrules/global/room/$id", data: {}); - if (resp == ErrorResponse) return resp; } resp = await client.connection.jsonRequest( type: HTTPType.PUT, diff --git a/lib/src/User.dart b/lib/src/User.dart index da373f09..5108a2eb 100644 --- a/lib/src/User.dart +++ b/lib/src/User.dart @@ -24,7 +24,6 @@ import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/src/Room.dart'; import 'package:famedlysdk/src/RoomState.dart'; -import 'package:famedlysdk/src/responses/ErrorResponse.dart'; import 'package:famedlysdk/src/utils/ChatTime.dart'; import 'package:famedlysdk/src/utils/MxContent.dart'; @@ -113,28 +112,16 @@ class User extends RoomState { : displayName; /// Call the Matrix API to kick this user from this room. - Future kick() async { - dynamic res = await room.kick(id); - return res; - } + Future kick() => room.kick(id); /// Call the Matrix API to ban this user from this room. - Future ban() async { - dynamic res = await room.ban(id); - return res; - } + Future ban() => room.ban(id); /// Call the Matrix API to unban this banned user from this room. - Future unban() async { - dynamic res = await room.unban(id); - return res; - } + Future unban() => room.unban(id); /// Call the Matrix API to change the power level of this user. - Future setPower(int power) async { - dynamic res = await room.setPower(id, power); - return res; - } + Future setPower(int power) => room.setPower(id, power); /// Returns an existing direct chat ID with this user or creates a new one. /// Returns null on error. @@ -153,11 +140,6 @@ class User extends RoomState { "preset": "trusted_private_chat" }); - if (resp is ErrorResponse) { - room.client.connection.onError.add(resp); - return null; - } - final String newRoomID = resp["room_id"]; if (newRoomID == null) return newRoomID; diff --git a/lib/src/responses/ErrorResponse.dart b/lib/src/responses/ErrorResponse.dart deleted file mode 100644 index ff674be6..00000000 --- a/lib/src/responses/ErrorResponse.dart +++ /dev/null @@ -1,51 +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:http/http.dart' as http; - -/// Represents a special response from the Homeserver for errors. -class ErrorResponse { - /// The unique identifier for this error. - String errcode; - - /// A human readable error description. - String error; - - /// The frozen request which triggered this Error - http.Request request; - - ErrorResponse({this.errcode, this.error, this.request}); - - ErrorResponse.fromJson(Map json, http.Request newRequest) { - errcode = json['errcode']; - error = json['error'] ?? ""; - request = newRequest; - } - - Map toJson() { - final Map data = new Map(); - data['errcode'] = this.errcode; - data['error'] = this.error; - return data; - } -} diff --git a/lib/src/utils/MatrixException.dart b/lib/src/utils/MatrixException.dart new file mode 100644 index 00000000..f542148e --- /dev/null +++ b/lib/src/utils/MatrixException.dart @@ -0,0 +1,105 @@ +/* + * 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:http/http.dart' as http; + +enum MatrixError { + M_UNKNOWN, + M_UNKNOWN_TOKEN, + M_NOT_FOUND, + M_FORBIDDEN, + M_LIMIT_EXCEEDED, + M_USER_IN_USE, + M_THREEPID_IN_USE, + M_THREEPID_DENIED, + M_THREEPID_NOT_FOUND, + M_THREEPID_AUTH_FAILED, + M_TOO_LARGE, + M_MISSING_PARAM, + M_UNSUPPORTED_ROOM_VERSION, + M_UNRECOGNIZED, +} + +/// Represents a special response from the Homeserver for errors. +class MatrixException implements Exception { + final Map raw; + + /// The unique identifier for this error. + String get errcode => raw["errcode"]; + + /// A human readable error description. + String get errorMessage => raw["error"]; + + /// The frozen request which triggered this Error + http.Response response; + + MatrixException(this.response) : this.raw = json.decode(response.body); + + @override + String toString() => "$errcode: $errorMessage"; + + /// Returns the [ResponseError]. Is ResponseError.NONE if there wasn't an error. + MatrixError get error => MatrixError.values.firstWhere( + (e) => e.toString() == 'MatrixError.${(raw["errcode"] ?? "")}', + orElse: () => MatrixError.M_UNKNOWN); + + int get retryAfterMs => raw["retry_after_ms"]; + + /// This is a session identifier that the client must pass back to the homeserver, if one is provided, + /// in subsequent attempts to authenticate in the same API call. + String get session => raw["session"]; + + /// Returns true if the server requires additional authentication. + bool get requireAdditionalAuthentication => response.statusCode == 401; + + /// For each endpoint, a server offers one or more 'flows' that the client can use + /// to authenticate itself. Each flow comprises a series of stages. If this request + /// doesn't need additional authentication, then this is null. + List get authenticationFlows { + if (!raw.containsKey("flows") || !(raw["flows"] is List)) return null; + List flows = []; + for (Map flow in raw["flows"]) { + if (flow["stages"] is List) { + flows.add(AuthenticationFlow(flow["stages"])); + } + } + return flows; + } + + /// This section contains any information that the client will need to know in order to use a given type + /// of authentication. For each authentication type presented, that type may be present as a key in this + /// dictionary. For example, the public part of an OAuth client ID could be given here. + Map get authenticationParams => raw["params"]; + + /// Returns the list of already completed authentication flows from previous requests. + List get completedAuthenticationFlows => raw["completed"]; +} + +/// For each endpoint, a server offers one or more 'flows' that the client can use +/// to authenticate itself. Each flow comprises a series of stages +class AuthenticationFlow { + final List stages; + const AuthenticationFlow(this.stages); +} diff --git a/test/Client_test.dart b/test/Client_test.dart index d646ff5c..55b43ab4 100644 --- a/test/Client_test.dart +++ b/test/Client_test.dart @@ -30,11 +30,11 @@ 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/ErrorResponse.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'; +import 'package:famedlysdk/src/utils/MatrixException.dart'; import 'package:famedlysdk/src/utils/MatrixFile.dart'; import 'package:famedlysdk/src/utils/Profile.dart'; import 'package:test/test.dart'; @@ -60,9 +60,6 @@ void main() { userUpdateListFuture = matrix.connection.onUserEvent.stream.toList(); test('Login', () async { - Future errorFuture = - matrix.connection.onError.stream.first; - int presenceCounter = 0; int accountDataCounter = 0; matrix.onPresence = (Presence data) { @@ -72,25 +69,26 @@ void main() { accountDataCounter++; }; - final bool checkResp1 = - await matrix.checkServer("https://fakeserver.wrongaddress"); - final bool checkResp2 = - await matrix.checkServer("https://fakeserver.notexisting"); + expect(matrix.homeserver, null); + expect(matrix.matrixVersions, null); - ErrorResponse checkError = await errorFuture; + try { + await matrix.checkServer("https://fakeserver.wrongaddress"); + } on FormatException catch (exception) { + expect(exception != null, true); + } + await matrix.checkServer("https://fakeserver.notexisting"); + expect(matrix.homeserver, "https://fakeserver.notexisting"); + expect(matrix.matrixVersions, + ["r0.0.1", "r0.1.0", "r0.2.0", "r0.3.0", "r0.4.0", "r0.5.0"]); - expect(checkResp1, false); - expect(checkResp2, true); - expect(checkError.errcode, "NO_RESPONSE"); - - final resp = await matrix.connection + final Map resp = await matrix.connection .jsonRequest(type: HTTPType.POST, action: "/client/r0/login", data: { "type": "m.login.password", "user": "test", "password": "1234", "initial_device_display_name": "Fluffy Matrix Client" }); - expect(resp is ErrorResponse, false); Future loginStateFuture = matrix.connection.onLoginStateChanged.stream.first; @@ -176,15 +174,19 @@ void main() { }); test('Try to get ErrorResponse', () async { - final resp = await matrix.connection - .jsonRequest(type: HTTPType.PUT, action: "/non/existing/path"); - expect(resp is ErrorResponse, true); + MatrixException expectedException; + try { + await matrix.connection + .jsonRequest(type: HTTPType.PUT, action: "/non/existing/path"); + } on MatrixException catch (exception) { + expectedException = exception; + } + expect(expectedException.error, MatrixError.M_UNRECOGNIZED); }); test('Logout', () async { - final dynamic resp = await matrix.connection + await matrix.connection .jsonRequest(type: HTTPType.POST, action: "/client/r0/logout"); - expect(resp is ErrorResponse, false); Future loginStateFuture = matrix.connection.onLoginStateChanged.stream.first; @@ -331,8 +333,7 @@ void main() { test('setAvatar', () async { final MatrixFile testFile = MatrixFile(bytes: [], path: "fake/path/file.jpeg"); - final dynamic resp = await matrix.setAvatar(testFile); - expect(resp, null); + await matrix.setAvatar(testFile); }); test('getPushrules', () async { @@ -352,8 +353,7 @@ void main() { lang: "en", data: PusherData( format: "event_id_only", url: "https://examplepushserver.com")); - final dynamic resp = await matrix.setPushers(data); - expect(resp is ErrorResponse, false); + await matrix.setPushers(data); }); test('joinRoomById', () async { @@ -389,8 +389,13 @@ void main() { test('Logout when token is unknown', () async { Future loginStateFuture = matrix.connection.onLoginStateChanged.stream.first; - await matrix.connection - .jsonRequest(type: HTTPType.DELETE, action: "/unknown/token"); + + try { + await matrix.connection + .jsonRequest(type: HTTPType.DELETE, action: "/unknown/token"); + } on MatrixException catch (exception) { + expect(exception.error, MatrixError.M_UNKNOWN_TOKEN); + } LoginState state = await loginStateFuture; expect(state, LoginState.loggedOut); diff --git a/test/FakeMatrixApi.dart b/test/FakeMatrixApi.dart index 745c0e6b..5c4f471e 100644 --- a/test/FakeMatrixApi.dart +++ b/test/FakeMatrixApi.dart @@ -48,12 +48,17 @@ class FakeMatrixApi extends MockClient { if (request.url.origin != "https://fakeserver.notexisting") return Response( - "Not found...", 50); + "Not found...", 404); // Call API if (api.containsKey(method) && api[method].containsKey(action)) res = api[method][action](data); - else + else if (method == "GET" && + action.contains("/client/r0/rooms/") && + action.contains("/state/m.room.member/")) { + res = {"displayname": ""}; + return Response(json.encode(res), 200); + } else res = { "errcode": "M_UNRECOGNIZED", "error": "Unrecognized request" @@ -62,6 +67,64 @@ class FakeMatrixApi extends MockClient { return Response(json.encode(res), 100); }); + static Map messagesResponse = { + "start": "t47429-4392820_219380_26003_2265", + "end": "t47409-4357353_219380_26003_2265", + "chunk": [ + { + "content": { + "body": "This is an example text message", + "msgtype": "m.text", + "format": "org.matrix.custom.html", + "formatted_body": "This is an example text message" + }, + "type": "m.room.message", + "event_id": "3143273582443PhrSn:example.org", + "room_id": "!1234:example.com", + "sender": "@example:example.org", + "origin_server_ts": 1432735824653, + "unsigned": {"age": 1234} + }, + { + "content": {"name": "The room name"}, + "type": "m.room.name", + "event_id": "2143273582443PhrSn:example.org", + "room_id": "!1234:example.com", + "sender": "@example:example.org", + "origin_server_ts": 1432735824653, + "unsigned": {"age": 1234}, + "state_key": "" + }, + { + "content": { + "body": "Gangnam Style", + "url": "mxc://example.org/a526eYUSFFxlgbQYZmo442", + "info": { + "thumbnail_url": "mxc://example.org/FHyPlCeYUSFFxlgbQYZmoEoe", + "thumbnail_info": { + "mimetype": "image/jpeg", + "size": 46144, + "w": 300, + "h": 300 + }, + "w": 480, + "h": 320, + "duration": 2140786, + "size": 1563685, + "mimetype": "video/mp4" + }, + "msgtype": "m.video" + }, + "type": "m.room.message", + "event_id": "1143273582443PhrSn:example.org", + "room_id": "!1234:example.com", + "sender": "@example:example.org", + "origin_server_ts": 1432735824653, + "unsigned": {"age": 1234} + } + ] + }; + static Map syncResponse = { "next_batch": Random().nextDouble().toString(), "presence": { @@ -457,6 +520,8 @@ class FakeMatrixApi extends MockClient { static final Map> api = { "GET": { + "/client/r0/rooms/1/state/m.room.member/@alice:example.com": (var req) => + {"displayname": "Alice"}, "/client/r0/profile/@getme:example.com": (var req) => { "avatar_url": "mxc://test", "displayname": "You got me", @@ -480,65 +545,10 @@ class FakeMatrixApi extends MockClient { "origin_server_ts": 1432735824653, "unsigned": {"age": 1234} }, + "/client/r0/rooms/!localpart:server.abc/messages?from=&dir=b&limit=100&filter=%7B%22room%22:%7B%22state%22:%7B%22lazy_load_members%22:true%7D%7D%7D": + (var req) => messagesResponse, "/client/r0/rooms/!1234:example.com/messages?from=1234&dir=b&limit=100&filter=%7B%22room%22:%7B%22state%22:%7B%22lazy_load_members%22:true%7D%7D%7D": - (var req) => { - "start": "t47429-4392820_219380_26003_2265", - "end": "t47409-4357353_219380_26003_2265", - "chunk": [ - { - "content": { - "body": "This is an example text message", - "msgtype": "m.text", - "format": "org.matrix.custom.html", - "formatted_body": "This is an example text message" - }, - "type": "m.room.message", - "event_id": "3143273582443PhrSn:example.org", - "room_id": "!1234:example.com", - "sender": "@example:example.org", - "origin_server_ts": 1432735824653, - "unsigned": {"age": 1234} - }, - { - "content": {"name": "The room name"}, - "type": "m.room.name", - "event_id": "2143273582443PhrSn:example.org", - "room_id": "!1234:example.com", - "sender": "@example:example.org", - "origin_server_ts": 1432735824653, - "unsigned": {"age": 1234}, - "state_key": "" - }, - { - "content": { - "body": "Gangnam Style", - "url": "mxc://example.org/a526eYUSFFxlgbQYZmo442", - "info": { - "thumbnail_url": - "mxc://example.org/FHyPlCeYUSFFxlgbQYZmoEoe", - "thumbnail_info": { - "mimetype": "image/jpeg", - "size": 46144, - "w": 300, - "h": 300 - }, - "w": 480, - "h": 320, - "duration": 2140786, - "size": 1563685, - "mimetype": "video/mp4" - }, - "msgtype": "m.video" - }, - "type": "m.room.message", - "event_id": "1143273582443PhrSn:example.org", - "room_id": "!1234:example.com", - "sender": "@example:example.org", - "origin_server_ts": 1432735824653, - "unsigned": {"age": 1234} - } - ] - }, + (var req) => messagesResponse, "/client/versions": (var req) => { "versions": [ "r0.0.1", diff --git a/test/RoomList_test.dart b/test/RoomList_test.dart index baa96953..dd30a4b2 100644 --- a/test/RoomList_test.dart +++ b/test/RoomList_test.dart @@ -29,14 +29,17 @@ 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"); - client.homeserver = "https://testserver.abc"; + final Client client = Client("testclient", debug: true); + client.connection.httpClient = FakeMatrixApi(); + await client.checkServer("https://fakeserver.notexisting"); client.prevBatch = "1234"; int updateCount = 0; @@ -84,8 +87,9 @@ void main() { }); test("Restort", () async { - final Client client = Client("testclient"); - client.homeserver = "https://testserver.abc"; + final Client client = Client("testclient", debug: true); + client.connection.httpClient = FakeMatrixApi(); + await client.checkServer("https://fakeserver.notexisting"); client.prevBatch = "1234"; int updateCount = 0; @@ -186,7 +190,7 @@ void main() { await new Future.delayed(new Duration(milliseconds: 50)); expect(updateCount, 5); - expect(roomUpdates, 2); + expect(roomUpdates, 3); expect(insertList, [0, 1]); expect(removeList, []); @@ -224,8 +228,9 @@ void main() { }); test("onlyLeft", () async { - final Client client = Client("testclient"); - client.homeserver = "https://testserver.abc"; + final Client client = Client("testclient", debug: true); + client.connection.httpClient = FakeMatrixApi(); + await client.checkServer("https://fakeserver.notexisting"); client.prevBatch = "1234"; int updateCount = 0; @@ -268,9 +273,9 @@ void main() { expect(roomList.eventSub != null, true); expect(roomList.roomSub != null, true); expect(roomList.rooms[0].id, "2"); - expect(updateCount, 2); expect(insertList, [0]); expect(removeList, []); + expect(updateCount, 2); }); }); } diff --git a/test/Room_test.dart b/test/Room_test.dart index d37dc390..3ceaca84 100644 --- a/test/Room_test.dart +++ b/test/Room_test.dart @@ -145,9 +145,7 @@ void main() { }); test("sendReadReceipt", () async { - final dynamic resp = - await room.sendReadReceipt("§1234:fakeServer.notExisting"); - expect(resp, {}); + await room.sendReadReceipt("§1234:fakeServer.notExisting"); }); test("requestParticipants", () async { @@ -167,28 +165,25 @@ void main() { }); test("setName", () async { - final dynamic resp = await room.setName("Testname"); - expect(resp["event_id"], "42"); + final String eventId = await room.setName("Testname"); + expect(eventId, "42"); }); test("setDescription", () async { - final dynamic resp = await room.setDescription("Testname"); - expect(resp["event_id"], "42"); + final String eventId = await room.setDescription("Testname"); + expect(eventId, "42"); }); test("kick", () async { - final dynamic resp = await room.kick("Testname"); - expect(resp, {}); + await room.kick("Testname"); }); test("ban", () async { - final dynamic resp = await room.ban("Testname"); - expect(resp, {}); + await room.ban("Testname"); }); test("unban", () async { - final dynamic resp = await room.unban("Testname"); - expect(resp, {}); + await room.unban("Testname"); }); test("PowerLevels", () async { @@ -259,18 +254,17 @@ void main() { expect(room.canSendEvent("m.room.power_levels"), false); expect(room.canSendEvent("m.room.member"), false); expect(room.canSendEvent("m.room.message"), true); - final dynamic resp = + final String resp = await room.setPower("@test:fakeServer.notExisting", 90); - expect(resp["event_id"], "42"); + expect(resp, "42"); }); test("invite", () async { - final dynamic resp = await room.invite("Testname"); - expect(resp, {}); + await room.invite("Testname"); }); test("getParticipants", () async { - room.states["@alice:test.abc"] = RoomState( + room.setState(RoomState( senderId: "@alice:test.abc", typeKey: "m.room.member", roomId: room.id, @@ -278,15 +272,14 @@ void main() { eventId: "12345", time: ChatTime.now(), content: {"displayname": "alice"}, - stateKey: "@alice:test.abc"); + stateKey: "@alice:test.abc")); final List userList = room.getParticipants(); - expect(userList.length, 1); - expect(userList[0].displayName, "alice"); + expect(userList.length, 4); + expect(userList[3].displayName, "alice"); }); test("addToDirectChat", () async { - final dynamic resp = await room.addToDirectChat("Testname"); - expect(resp, {}); + await room.addToDirectChat("Testname"); }); test("getTimeline", () async { @@ -295,7 +288,10 @@ void main() { }); test("getUserByMXID", () async { - final User user = await room.getUserByMXID("@getme:example.com"); + User user; + try { + user = await room.getUserByMXID("@getme:example.com"); + } catch (_) {} expect(user.stateKey, "@getme:example.com"); expect(user.calcDisplayname(), "You got me"); });