[Connection] Throw MatrixErrors

This commit is contained in:
Christian Pauly 2019-12-29 10:28:33 +00:00
parent ccdb33ba69
commit 4c6c3715d9
11 changed files with 458 additions and 461 deletions

View file

@ -24,12 +24,12 @@
library famedlysdk; library famedlysdk;
export 'package:famedlysdk/src/requests/SetPushersRequest.dart'; 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/responses/PushrulesResponse.dart';
export 'package:famedlysdk/src/sync/RoomUpdate.dart'; export 'package:famedlysdk/src/sync/RoomUpdate.dart';
export 'package:famedlysdk/src/sync/EventUpdate.dart'; export 'package:famedlysdk/src/sync/EventUpdate.dart';
export 'package:famedlysdk/src/sync/UserUpdate.dart'; export 'package:famedlysdk/src/sync/UserUpdate.dart';
export 'package:famedlysdk/src/utils/ChatTime.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/MatrixFile.dart';
export 'package:famedlysdk/src/utils/MxContent.dart'; export 'package:famedlysdk/src/utils/MxContent.dart';
export 'package:famedlysdk/src/utils/StatesMap.dart'; export 'package:famedlysdk/src/utils/StatesMap.dart';

View file

@ -38,7 +38,6 @@ import 'RoomList.dart';
import 'RoomState.dart'; import 'RoomState.dart';
import 'User.dart'; import 'User.dart';
import 'requests/SetPushersRequest.dart'; import 'requests/SetPushersRequest.dart';
import 'responses/ErrorResponse.dart';
import 'responses/PushrulesResponse.dart'; import 'responses/PushrulesResponse.dart';
import 'utils/Profile.dart'; import 'utils/Profile.dart';
@ -156,65 +155,56 @@ class Client {
/// Checks the supported versions of the Matrix protocol and the supported /// Checks the supported versions of the Matrix protocol and the supported
/// login types. Returns false if the server is not compatible with the /// login types. Returns false if the server is not compatible with the
/// client. Automatically sets [matrixVersions] and [lazyLoadMembers]. /// client. Automatically sets [matrixVersions] and [lazyLoadMembers].
/// Throws FormatException, TimeoutException and MatrixException on error.
Future<bool> checkServer(serverUrl) async { Future<bool> checkServer(serverUrl) async {
homeserver = serverUrl; try {
homeserver = serverUrl;
final versionResp = await connection.jsonRequest(
type: HTTPType.GET, action: "/client/versions");
final versionResp = await connection.jsonRequest( final List<String> versions = List<String>.from(versionResp["versions"]);
type: HTTPType.GET, action: "/client/versions");
if (versionResp is ErrorResponse) {
connection.onError.add(ErrorResponse(errcode: "NO_RESPONSE", error: ""));
return false;
}
final List<String> versions = List<String>.from(versionResp["versions"]); for (int i = 0; i < versions.length; i++) {
if (versions[i] == "r0.5.0")
if (versions == null) { break;
connection.onError.add(ErrorResponse(errcode: "NO_RESPONSE", error: "")); else if (i == versions.length - 1) {
return false; 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;
} }
}
matrixVersions = versions; matrixVersions = versions;
if (versionResp.containsKey("unstable_features") && if (versionResp.containsKey("unstable_features") &&
versionResp["unstable_features"].containsKey("m.lazy_load_members")) { versionResp["unstable_features"].containsKey("m.lazy_load_members")) {
lazyLoadMembers = versionResp["unstable_features"]["m.lazy_load_members"] lazyLoadMembers = versionResp["unstable_features"]
? true ["m.lazy_load_members"]
: false; ? 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<dynamic> 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;
} }
}
return true; final loginResp = await connection.jsonRequest(
type: HTTPType.GET, action: "/client/r0/login");
final List<dynamic> 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 /// 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<bool> login(String username, String password) async { Future<bool> login(String username, String password) async {
final loginResp = await connection final loginResp = await connection
.jsonRequest(type: HTTPType.POST, action: "/client/r0/login", data: { .jsonRequest(type: HTTPType.POST, action: "/client/r0/login", data: {
@ -228,15 +218,10 @@ class Client {
"initial_device_display_name": "Famedly Talk" "initial_device_display_name": "Famedly Talk"
}); });
if (loginResp is ErrorResponse) {
connection.onError.add(loginResp);
return false;
}
final userID = loginResp["user_id"]; final userID = loginResp["user_id"];
final accessToken = loginResp["access_token"]; final accessToken = loginResp["access_token"];
if (userID == null || accessToken == null) { if (userID == null || accessToken == null) {
connection.onError.add(ErrorResponse(errcode: "NO_SUPPORT", error: "")); return false;
} }
await connection.connect( await connection.connect(
@ -253,11 +238,14 @@ class Client {
/// Sends a logout command to the homeserver and clears all local data, /// Sends a logout command to the homeserver and clears all local data,
/// including all persistent data from the store. /// including all persistent data from the store.
Future<void> logout() async { Future<void> logout() async {
final dynamic resp = await connection.jsonRequest( try {
type: HTTPType.POST, action: "/client/r0/logout"); await connection.jsonRequest(
if (resp is ErrorResponse) connection.onError.add(resp); type: HTTPType.POST, action: "/client/r0/logout");
} catch (exception) {
await connection.clear(); rethrow;
} finally {
await connection.clear();
}
} }
/// Get the combined profile information for this user. This API may be used to /// Get the combined profile information for this user. This API may be used to
@ -266,10 +254,6 @@ class Client {
Future<Profile> getProfileFromUserId(String userId) async { Future<Profile> getProfileFromUserId(String userId) async {
final dynamic resp = await connection.jsonRequest( final dynamic resp = await connection.jsonRequest(
type: HTTPType.GET, action: "/client/r0/profile/${userId}"); type: HTTPType.GET, action: "/client/r0/profile/${userId}");
if (resp is ErrorResponse) {
connection.onError.add(resp);
return null;
}
return Profile.fromJson(resp); return Profile.fromJson(resp);
} }
@ -295,8 +279,7 @@ class Client {
String action = "/client/r0/sync?filter=$syncFilters&timeout=0"; String action = "/client/r0/sync?filter=$syncFilters&timeout=0";
final sync = final sync =
await connection.jsonRequest(type: HTTPType.GET, action: action); await connection.jsonRequest(type: HTTPType.GET, action: action);
if (!(sync is ErrorResponse) && if (sync["rooms"]["leave"] is Map<String, dynamic>) {
sync["rooms"]["leave"] is Map<String, dynamic>) {
for (var entry in sync["rooms"]["leave"].entries) { for (var entry in sync["rooms"]["leave"].entries) {
final String id = entry.key; final String id = entry.key;
final dynamic room = entry.value; final dynamic room = entry.value;
@ -377,33 +360,29 @@ class Client {
if (params == null && invite != null) if (params == null && invite != null)
for (int i = 0; i < invite.length; i++) inviteIDs.add(invite[i].id); for (int i = 0; i < invite.length; i++) inviteIDs.add(invite[i].id);
final dynamic resp = await connection.jsonRequest( try {
type: HTTPType.POST, final dynamic resp = await connection.jsonRequest(
action: "/client/r0/createRoom", type: HTTPType.POST,
data: params == null action: "/client/r0/createRoom",
? { data: params == null
"invite": inviteIDs, ? {
} "invite": inviteIDs,
: params); }
: params);
if (resp is ErrorResponse) { return resp["room_id"];
connection.onError.add(resp); } catch (e) {
return null; rethrow;
} }
return resp["room_id"];
} }
/// Uploads a new user avatar for this user. Returns ErrorResponse if something went wrong. /// Uploads a new user avatar for this user.
Future<dynamic> setAvatar(MatrixFile file) async { Future<void> setAvatar(MatrixFile file) async {
final uploadResp = await connection.upload(file); final uploadResp = await connection.upload(file);
if (uploadResp is ErrorResponse) return uploadResp; await connection.jsonRequest(
final setAvatarResp = await connection.jsonRequest(
type: HTTPType.PUT, type: HTTPType.PUT,
action: "/client/r0/profile/$userID/avatar_url", action: "/client/r0/profile/$userID/avatar_url",
data: {"avatar_url": uploadResp}); data: {"avatar_url": uploadResp});
if (setAvatarResp is ErrorResponse) return setAvatarResp; return;
return null;
} }
/// Fetches the pushrules for the logged in user. /// Fetches the pushrules for the logged in user.
@ -414,26 +393,16 @@ class Client {
action: "/client/r0/pushrules/", action: "/client/r0/pushrules/",
); );
if (resp is ErrorResponse) {
connection.onError.add(resp);
return null;
}
return PushrulesResponse.fromJson(resp); return PushrulesResponse.fromJson(resp);
} }
/// This endpoint allows the creation, modification and deletion of pushers for this user ID. /// This endpoint allows the creation, modification and deletion of pushers for this user ID.
Future<dynamic> setPushers(SetPushersRequest data) async { Future<void> setPushers(SetPushersRequest data) async {
final dynamic resp = await connection.jsonRequest( await connection.jsonRequest(
type: HTTPType.POST, type: HTTPType.POST,
action: "/client/r0/pushers/set", action: "/client/r0/pushers/set",
data: data.toJson(), data: data.toJson(),
); );
return;
if (resp is ErrorResponse) {
connection.onError.add(resp);
}
return resp;
} }
} }

View file

@ -33,10 +33,10 @@ import 'package:mime_type/mime_type.dart';
import 'Client.dart'; import 'Client.dart';
import 'User.dart'; import 'User.dart';
import 'responses/ErrorResponse.dart';
import 'sync/EventUpdate.dart'; import 'sync/EventUpdate.dart';
import 'sync/RoomUpdate.dart'; import 'sync/RoomUpdate.dart';
import 'sync/UserUpdate.dart'; import 'sync/UserUpdate.dart';
import 'utils/MatrixException.dart';
enum HTTPType { GET, POST, PUT, DELETE } enum HTTPType { GET, POST, PUT, DELETE }
@ -74,7 +74,7 @@ class Connection {
new StreamController.broadcast(); new StreamController.broadcast();
/// Synchronization erros are coming here. /// Synchronization erros are coming here.
final StreamController<ErrorResponse> onError = final StreamController<MatrixException> onError =
new StreamController.broadcast(); new StreamController.broadcast();
/// This is called once, when the first sync has received. /// 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). /// 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 /// 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 /// this! For example to send a message to a Matrix room with the id
/// '!fjd823j:example.com' you call: /// '!fjd823j:example.com' you call:
@ -192,7 +194,7 @@ class Connection {
/// ); /// );
/// ``` /// ```
/// ///
Future<dynamic> jsonRequest( Future<Map<String, dynamic>> jsonRequest(
{HTTPType type, {HTTPType type,
String action, String action,
dynamic data = "", dynamic data = "",
@ -219,6 +221,7 @@ class Connection {
"[REQUEST ${type.toString().split('.').last}] Action: $action, Data: $data"); "[REQUEST ${type.toString().split('.').last}] Action: $action, Data: $data");
http.Response resp; http.Response resp;
Map<String, dynamic> jsonResp = {};
try { try {
switch (type.toString().split('.').last) { switch (type.toString().split('.').last) {
case "GET": case "GET":
@ -242,52 +245,47 @@ class Connection {
.timeout(Duration(seconds: timeout)); .timeout(Duration(seconds: timeout));
break; break;
} }
} on TimeoutException catch (_) { jsonResp = jsonDecode(resp.body)
return ErrorResponse( as Map<String, dynamic>; // May throw FormatException
error: "No connection possible...",
errcode: "TIMEOUT",
request: resp?.request);
} catch (e) {
return ErrorResponse(
error: "No connection possible...",
errcode: "NO_CONNECTION",
request: resp?.request);
}
Map<String, dynamic> jsonResp; if (jsonResp.containsKey("errcode") && jsonResp["errcode"] is String) {
try { // The server has responsed with an matrix related error.
jsonResp = jsonDecode(resp.body) as Map<String, dynamic>; MatrixException exception = MatrixException(resp);
} catch (e) { if (exception.error == MatrixError.M_UNKNOWN_TOKEN) {
return ErrorResponse( // The token is no longer valid. Need to sign off....
error: "No connection possible...", onError.add(exception);
errcode: "MALFORMED", clear();
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 (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; return jsonResp;
} }
/// Uploads a file with the name [fileName] as base64 encoded to the server /// Uploads a file with the name [fileName] as base64 encoded to the server
/// and returns the mxc url as a string or an [ErrorResponse]. /// and returns the mxc url as a string.
Future<dynamic> upload(MatrixFile file) async { Future<String> upload(MatrixFile file) async {
dynamic fileBytes; dynamic fileBytes;
if (client.homeserver != "https://fakeServer.notExisting") if (client.homeserver != "https://fakeServer.notExisting")
fileBytes = file.bytes; fileBytes = file.bytes;
String fileName = file.path.split("/").last.toLowerCase(); String fileName = file.path.split("/").last.toLowerCase();
String mimeType = mime(file.path); String mimeType = mime(file.path);
print("[UPLOADING] $fileName, type: $mimeType, size: ${fileBytes?.length}"); print("[UPLOADING] $fileName, type: $mimeType, size: ${fileBytes?.length}");
final dynamic resp = await jsonRequest( final Map<String, dynamic> resp = await jsonRequest(
type: HTTPType.POST, type: HTTPType.POST,
action: "/media/r0/upload?filename=$fileName", action: "/media/r0/upload?filename=$fileName",
data: fileBytes, data: fileBytes,
contentType: mimeType); contentType: mimeType);
if (resp is ErrorResponse) return resp;
return resp["content_uri"]; return resp["content_uri"];
} }
@ -302,32 +300,28 @@ class Connection {
action += "&timeout=30000"; action += "&timeout=30000";
action += "&since=${client.prevBatch}"; action += "&since=${client.prevBatch}";
} }
_syncRequest = jsonRequest(type: HTTPType.GET, action: action); try {
final int hash = _syncRequest.hashCode; _syncRequest = jsonRequest(type: HTTPType.GET, action: action);
final syncResp = await _syncRequest; final int hash = _syncRequest.hashCode;
if (hash != _syncRequest.hashCode) return; final syncResp = await _syncRequest;
if (syncResp is ErrorResponse) { if (hash != _syncRequest.hashCode) return;
//onError.add(syncResp); if (client.store != null)
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), () {}); await client.store.transaction(() {
} else { handleSync(syncResp);
try { client.store.storePrevBatch(syncResp);
if (client.store != null) return;
await client.store.transaction(() { });
handleSync(syncResp); else
client.store.storePrevBatch(syncResp); await handleSync(syncResp);
return; if (client.prevBatch == null) client.connection.onFirstSync.add(true);
}); client.prevBatch = syncResp["next_batch"];
else if (hash == _syncRequest.hashCode) _sync();
await handleSync(syncResp); } on MatrixException catch (exception) {
if (client.prevBatch == null) client.connection.onFirstSync.add(true); onError.add(exception);
client.prevBatch = syncResp["next_batch"]; await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
} catch (e) { } catch (exception) {
onError await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
.add(ErrorResponse(errcode: "CRITICAL_ERROR", error: e.toString()));
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), () {});
}
} }
if (hash == _syncRequest.hashCode) _sync();
} }
void handleSync(dynamic sync) { void handleSync(dynamic sync) {

View file

@ -25,10 +25,10 @@ import 'package:famedlysdk/src/Client.dart';
import 'package:famedlysdk/src/Event.dart'; import 'package:famedlysdk/src/Event.dart';
import 'package:famedlysdk/src/RoomAccountData.dart'; import 'package:famedlysdk/src/RoomAccountData.dart';
import 'package:famedlysdk/src/RoomState.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/EventUpdate.dart';
import 'package:famedlysdk/src/sync/RoomUpdate.dart'; import 'package:famedlysdk/src/sync/RoomUpdate.dart';
import 'package:famedlysdk/src/utils/ChatTime.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/MatrixFile.dart';
import 'package:famedlysdk/src/utils/MxContent.dart'; import 'package:famedlysdk/src/utils/MxContent.dart';
//import 'package:image/image.dart'; //import 'package:image/image.dart';
@ -248,35 +248,33 @@ class Room {
return ChatTime.now(); return ChatTime.now();
} }
/// Call the Matrix API to change the name of this room. /// Call the Matrix API to change the name of this room. Returns the event ID of the
Future<dynamic> setName(String newName) async { /// new m.room.name event.
dynamic res = await client.connection.jsonRequest( Future<String> setName(String newName) async {
final Map<String, dynamic> resp = await client.connection.jsonRequest(
type: HTTPType.PUT, type: HTTPType.PUT,
action: "/client/r0/rooms/${id}/state/m.room.name", action: "/client/r0/rooms/${id}/state/m.room.name",
data: {"name": newName}); data: {"name": newName});
if (res is ErrorResponse) client.connection.onError.add(res); return resp["event_id"];
return res;
} }
/// Call the Matrix API to change the topic of this room. /// Call the Matrix API to change the topic of this room.
Future<dynamic> setDescription(String newName) async { Future<String> setDescription(String newName) async {
dynamic res = await client.connection.jsonRequest( final Map<String, dynamic> resp = await client.connection.jsonRequest(
type: HTTPType.PUT, type: HTTPType.PUT,
action: "/client/r0/rooms/${id}/state/m.room.topic", action: "/client/r0/rooms/${id}/state/m.room.topic",
data: {"topic": newName}); data: {"topic": newName});
if (res is ErrorResponse) client.connection.onError.add(res); return resp["event_id"];
return res;
} }
Future<dynamic> _sendRawEventNow(Map<String, dynamic> content, Future<String> _sendRawEventNow(Map<String, dynamic> content,
{String txid = null}) async { {String txid = null}) async {
if (txid == null) txid = "txid${DateTime.now().millisecondsSinceEpoch}"; if (txid == null) txid = "txid${DateTime.now().millisecondsSinceEpoch}";
final dynamic res = await client.connection.jsonRequest( final Map<String, dynamic> res = await client.connection.jsonRequest(
type: HTTPType.PUT, type: HTTPType.PUT,
action: "/client/r0/rooms/${id}/send/m.room.message/$txid", action: "/client/r0/rooms/${id}/send/m.room.message/$txid",
data: content); data: content);
if (res is ErrorResponse) client.connection.onError.add(res); return res["event_id"];
return res;
} }
Future<String> sendTextEvent(String message, {String txid = null}) => Future<String> sendTextEvent(String message, {String txid = null}) =>
@ -291,8 +289,7 @@ class Room {
if (msgType == "m.video") return sendAudioEvent(file); if (msgType == "m.video") return sendAudioEvent(file);
String fileName = file.path.split("/").last; String fileName = file.path.split("/").last;
final dynamic uploadResp = await client.connection.upload(file); final String uploadResp = await client.connection.upload(file);
if (uploadResp is ErrorResponse) return null;
// Send event // Send event
Map<String, dynamic> content = { Map<String, dynamic> content = {
@ -311,8 +308,7 @@ class Room {
Future<String> sendAudioEvent(MatrixFile file, Future<String> sendAudioEvent(MatrixFile file,
{String txid = null, int width, int height}) async { {String txid = null, int width, int height}) async {
String fileName = file.path.split("/").last; String fileName = file.path.split("/").last;
final dynamic uploadResp = await client.connection.upload(file); final String uploadResp = await client.connection.upload(file);
if (uploadResp is ErrorResponse) return null;
Map<String, dynamic> content = { Map<String, dynamic> content = {
"msgtype": "m.audio", "msgtype": "m.audio",
"body": fileName, "body": fileName,
@ -329,8 +325,7 @@ class Room {
Future<String> sendImageEvent(MatrixFile file, Future<String> sendImageEvent(MatrixFile file,
{String txid = null, int width, int height}) async { {String txid = null, int width, int height}) async {
String fileName = file.path.split("/").last; String fileName = file.path.split("/").last;
final dynamic uploadResp = await client.connection.upload(file); final String uploadResp = await client.connection.upload(file);
if (uploadResp is ErrorResponse) return null;
Map<String, dynamic> content = { Map<String, dynamic> content = {
"msgtype": "m.image", "msgtype": "m.image",
"body": fileName, "body": fileName,
@ -354,8 +349,7 @@ class Room {
int thumbnailWidth, int thumbnailWidth,
int thumbnailHeight}) async { int thumbnailHeight}) async {
String fileName = file.path.split("/").last; String fileName = file.path.split("/").last;
final dynamic uploadResp = await client.connection.upload(file); final String uploadResp = await client.connection.upload(file);
if (uploadResp is ErrorResponse) return null;
Map<String, dynamic> content = { Map<String, dynamic> content = {
"msgtype": "m.video", "msgtype": "m.video",
"body": fileName, "body": fileName,
@ -376,8 +370,7 @@ class Room {
} }
if (thumbnail != null) { if (thumbnail != null) {
String thumbnailName = file.path.split("/").last; String thumbnailName = file.path.split("/").last;
final dynamic thumbnailUploadResp = await client.connection.upload(file); final String thumbnailUploadResp = await client.connection.upload(file);
if (thumbnailUploadResp is ErrorResponse) return null;
content["info"]["thumbnail_url"] = thumbnailUploadResp; content["info"]["thumbnail_url"] = thumbnailUploadResp;
content["info"]["thumbnail_info"] = { content["info"]["thumbnail_info"] = {
"size": thumbnail.size, "size": thumbnail.size,
@ -422,9 +415,18 @@ class Room {
}); });
// Send the text and on success, store and display a *sent* event. // Send the text and on success, store and display a *sent* event.
final dynamic res = await _sendRawEventNow(content, txid: messageID); try {
final String res = await _sendRawEventNow(content, txid: messageID);
if (res is ErrorResponse || !(res["event_id"] is String)) { 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 // On error, set status to -1
eventUpdate.content["status"] = -1; eventUpdate.content["status"] = -1;
eventUpdate.content["unsigned"] = {"transaction_id": messageID}; eventUpdate.content["unsigned"] = {"transaction_id": messageID};
@ -433,16 +435,6 @@ class Room {
client.store.storeEventUpdate(eventUpdate); client.store.storeEventUpdate(eventUpdate);
return; 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; return null;
} }
@ -450,12 +442,16 @@ class Room {
/// Call the Matrix API to join this room if the user is not already a member. /// 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 /// If this room is intended to be a direct chat, the direct chat flag will
/// automatically be set. /// automatically be set.
Future<dynamic> join() async { Future<void> join() async {
dynamic res = await client.connection.jsonRequest( try {
type: HTTPType.POST, action: "/client/r0/rooms/${id}/join"); await client.connection.jsonRequest(
if (res is ErrorResponse) { type: HTTPType.POST, action: "/client/r0/rooms/${id}/join");
client.connection.onError.add(res); if (states.containsKey(client.userID) &&
if (res.error == "No known servers") { 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.store?.forgetRoom(id);
client.connection.onRoomUpdate.add( client.connection.onRoomUpdate.add(
RoomUpdate( RoomUpdate(
@ -465,88 +461,78 @@ class Room {
highlight_count: 0), 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 /// Call the Matrix API to leave this room. If this room is set as a direct
/// chat, this will be removed too. /// chat, this will be removed too.
Future<dynamic> leave() async { Future<void> leave() async {
if (directChatMatrixID != "") await removeFromDirectChat(); if (directChatMatrixID != "") await removeFromDirectChat();
dynamic res = await client.connection.jsonRequest( await client.connection.jsonRequest(
type: HTTPType.POST, action: "/client/r0/rooms/${id}/leave"); type: HTTPType.POST, action: "/client/r0/rooms/${id}/leave");
if (res is ErrorResponse) client.connection.onError.add(res); return;
return res;
} }
/// Call the Matrix API to forget this room if you already left it. /// Call the Matrix API to forget this room if you already left it.
Future<dynamic> forget() async { Future<void> forget() async {
client.store.forgetRoom(id); client.store.forgetRoom(id);
dynamic res = await client.connection.jsonRequest( await client.connection.jsonRequest(
type: HTTPType.POST, action: "/client/r0/rooms/${id}/forget"); type: HTTPType.POST, action: "/client/r0/rooms/${id}/forget");
if (res is ErrorResponse) client.connection.onError.add(res); return;
return res;
} }
/// Call the Matrix API to kick a user from this room. /// Call the Matrix API to kick a user from this room.
Future<dynamic> kick(String userID) async { Future<void> kick(String userID) async {
dynamic res = await client.connection.jsonRequest( await client.connection.jsonRequest(
type: HTTPType.POST, type: HTTPType.POST,
action: "/client/r0/rooms/${id}/kick", action: "/client/r0/rooms/${id}/kick",
data: {"user_id": userID}); data: {"user_id": userID});
if (res is ErrorResponse) client.connection.onError.add(res); return;
return res;
} }
/// Call the Matrix API to ban a user from this room. /// Call the Matrix API to ban a user from this room.
Future<dynamic> ban(String userID) async { Future<void> ban(String userID) async {
dynamic res = await client.connection.jsonRequest( await client.connection.jsonRequest(
type: HTTPType.POST, type: HTTPType.POST,
action: "/client/r0/rooms/${id}/ban", action: "/client/r0/rooms/${id}/ban",
data: {"user_id": userID}); data: {"user_id": userID});
if (res is ErrorResponse) client.connection.onError.add(res); return;
return res;
} }
/// Call the Matrix API to unban a banned user from this room. /// Call the Matrix API to unban a banned user from this room.
Future<dynamic> unban(String userID) async { Future<void> unban(String userID) async {
dynamic res = await client.connection.jsonRequest( await client.connection.jsonRequest(
type: HTTPType.POST, type: HTTPType.POST,
action: "/client/r0/rooms/${id}/unban", action: "/client/r0/rooms/${id}/unban",
data: {"user_id": userID}); data: {"user_id": userID});
if (res is ErrorResponse) client.connection.onError.add(res); return;
return res;
} }
/// Set the power level of the user with the [userID] to the value [power]. /// Set the power level of the user with the [userID] to the value [power].
Future<dynamic> 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<String> setPower(String userID, int power) async {
if (states["m.room.power_levels"] == null) return null; if (states["m.room.power_levels"] == null) return null;
Map<String, dynamic> powerMap = {} Map<String, dynamic> powerMap = {}
..addAll(states["m.room.power_levels"].content); ..addAll(states["m.room.power_levels"].content);
if (powerMap["users"] == null) powerMap["users"] = {}; if (powerMap["users"] == null) powerMap["users"] = {};
powerMap["users"][userID] = power; powerMap["users"][userID] = power;
dynamic res = await client.connection.jsonRequest( final Map<String, dynamic> resp = await client.connection.jsonRequest(
type: HTTPType.PUT, type: HTTPType.PUT,
action: "/client/r0/rooms/$id/state/m.room.power_levels", action: "/client/r0/rooms/$id/state/m.room.power_levels",
data: powerMap); data: powerMap);
if (res is ErrorResponse) client.connection.onError.add(res); return resp["event_id"];
return res;
} }
/// Call the Matrix API to invite a user to this room. /// Call the Matrix API to invite a user to this room.
Future<dynamic> invite(String userID) async { Future<void> invite(String userID) async {
dynamic res = await client.connection.jsonRequest( await client.connection.jsonRequest(
type: HTTPType.POST, type: HTTPType.POST,
action: "/client/r0/rooms/${id}/invite", action: "/client/r0/rooms/${id}/invite",
data: {"user_id": userID}); data: {"user_id": userID});
if (res is ErrorResponse) client.connection.onError.add(res); return;
return res;
} }
/// Request more previous events from the server. [historyCount] defines how much events should /// Request more previous events from the server. [historyCount] defines how much events should
@ -559,8 +545,6 @@ class Room {
action: 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=${Connection.syncFilters}");
if (resp is ErrorResponse) return;
if (onHistoryReceived != null) onHistoryReceived(); if (onHistoryReceived != null) onHistoryReceived();
prev_batch = resp["end"]; prev_batch = resp["end"];
client.store?.storeRoomPrevBatch(this); client.store?.storeRoomPrevBatch(this);
@ -634,51 +618,51 @@ class Room {
); );
} }
/// Sets this room as a direct chat for this user. /// Sets this room as a direct chat for this user if not already.
Future<dynamic> addToDirectChat(String userID) async { Future<void> addToDirectChat(String userID) async {
Map<String, dynamic> directChats = client.directChats; Map<String, dynamic> directChats = client.directChats;
if (directChats.containsKey(userID)) if (!directChats[userID].contains(id)) if (directChats.containsKey(userID)) if (!directChats[userID].contains(id))
directChats[userID].add(id); directChats[userID].add(id);
else else
return null; // Is already in direct chats return; // Is already in direct chats
else else
directChats[userID] = [id]; directChats[userID] = [id];
final resp = await client.connection.jsonRequest( await client.connection.jsonRequest(
type: HTTPType.PUT, type: HTTPType.PUT,
action: "/client/r0/user/${client.userID}/account_data/m.direct", action: "/client/r0/user/${client.userID}/account_data/m.direct",
data: directChats); data: directChats);
return resp; return;
} }
/// Sets this room as a direct chat for this user. /// Removes this room from all direct chat tags.
Future<dynamic> removeFromDirectChat() async { Future<void> removeFromDirectChat() async {
Map<String, dynamic> directChats = client.directChats; Map<String, dynamic> directChats = client.directChats;
if (directChats.containsKey(directChatMatrixID) && if (directChats.containsKey(directChatMatrixID) &&
directChats[directChatMatrixID].contains(id)) directChats[directChatMatrixID].contains(id))
directChats[directChatMatrixID].remove(id); directChats[directChatMatrixID].remove(id);
else 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, type: HTTPType.PUT,
action: "/client/r0/user/${client.userID}/account_data/m.direct", action: "/client/r0/user/${client.userID}/account_data/m.direct",
data: directChats); data: directChats);
return resp; return;
} }
/// Sends *m.fully_read* and *m.read* for the given event ID. /// Sends *m.fully_read* and *m.read* for the given event ID.
Future<dynamic> sendReadReceipt(String eventID) async { Future<void> sendReadReceipt(String eventID) async {
this.notificationCount = 0; this.notificationCount = 0;
client?.store?.resetNotificationCount(this.id); client?.store?.resetNotificationCount(this.id);
final dynamic resp = client.connection.jsonRequest( client.connection.jsonRequest(
type: HTTPType.POST, type: HTTPType.POST,
action: "/client/r0/rooms/$id/read_markers", action: "/client/r0/rooms/$id/read_markers",
data: { data: {
"m.fully_read": eventID, "m.fully_read": eventID,
"m.read": eventID, "m.read": eventID,
}); });
return resp; return;
} }
/// Returns a Room from a json String which comes normally from the store. If the /// 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( dynamic res = await client.connection.jsonRequest(
type: HTTPType.GET, action: "/client/r0/rooms/${id}/members"); type: HTTPType.GET, action: "/client/r0/rooms/${id}/members");
if (res is ErrorResponse || !(res["chunk"] is List<dynamic>))
return participants;
for (num i = 0; i < res["chunk"].length; i++) { for (num i = 0; i < res["chunk"].length; i++) {
User newUser = RoomState.fromJson(res["chunk"][i], this).asUser; User newUser = RoomState.fromJson(res["chunk"][i], this).asUser;
@ -794,7 +776,9 @@ class Room {
if (states[mxID] != null) if (states[mxID] != null)
return states[mxID].asUser; return states[mxID].asUser;
else { else {
requestUser(mxID); try {
requestUser(mxID);
} catch (_) {}
return User(mxID, room: this); return User(mxID, room: this);
} }
} }
@ -805,12 +789,14 @@ class Room {
/// lazy loading. /// lazy loading.
Future<User> requestUser(String mxID) async { Future<User> requestUser(String mxID) async {
if (mxID == null || !_requestingMatrixIds.add(mxID)) return null; if (mxID == null || !_requestingMatrixIds.add(mxID)) return null;
final dynamic resp = await client.connection.jsonRequest( Map<String, dynamic> resp;
type: HTTPType.GET, try {
action: "/client/r0/rooms/$id/state/m.room.member/$mxID"); resp = await client.connection.jsonRequest(
if (resp is ErrorResponse) { type: HTTPType.GET,
action: "/client/r0/rooms/$id/state/m.room.member/$mxID");
} catch (exception) {
_requestingMatrixIds.remove(mxID); _requestingMatrixIds.remove(mxID);
return null; rethrow;
} }
final User user = User(mxID, final User user = User(mxID,
displayName: resp["displayname"], displayName: resp["displayname"],
@ -837,7 +823,6 @@ class Room {
Future<Event> getEventById(String eventID) async { Future<Event> getEventById(String eventID) async {
final dynamic resp = await client.connection.jsonRequest( final dynamic resp = await client.connection.jsonRequest(
type: HTTPType.GET, action: "/client/r0/rooms/$id/event/$eventID"); type: HTTPType.GET, action: "/client/r0/rooms/$id/event/$eventID");
if (resp is ErrorResponse) return null;
return Event.fromJson(resp, this); return Event.fromJson(resp, this);
} }
@ -865,16 +850,15 @@ class Room {
return null; return null;
} }
/// Uploads a new user avatar for this room. Returns ErrorResponse if something went wrong /// Uploads a new user avatar for this room. Returns the event ID of the new
/// and the event ID otherwise. /// m.room.avatar event.
Future<dynamic> setAvatar(MatrixFile file) async { Future<String> setAvatar(MatrixFile file) async {
final uploadResp = await client.connection.upload(file); final String uploadResp = await client.connection.upload(file);
if (uploadResp is ErrorResponse) return uploadResp; final Map<String, dynamic> setAvatarResp = await client.connection
final setAvatarResp = await client.connection.jsonRequest( .jsonRequest(
type: HTTPType.PUT, type: HTTPType.PUT,
action: "/client/r0/rooms/$id/state/m.room.avatar/", action: "/client/r0/rooms/$id/state/m.room.avatar/",
data: {"url": uploadResp}); data: {"url": uploadResp});
if (setAvatarResp is ErrorResponse) return setAvatarResp;
return setAvatarResp["event_id"]; return setAvatarResp["event_id"];
} }
@ -979,7 +963,6 @@ class Room {
type: HTTPType.DELETE, type: HTTPType.DELETE,
action: "/client/r0/pushrules/global/override/$id", action: "/client/r0/pushrules/global/override/$id",
data: {}); data: {});
if (resp == ErrorResponse) return resp;
resp = await client.connection.jsonRequest( resp = await client.connection.jsonRequest(
type: HTTPType.PUT, type: HTTPType.PUT,
action: "/client/r0/pushrules/global/room/$id", action: "/client/r0/pushrules/global/room/$id",
@ -1001,7 +984,6 @@ class Room {
type: HTTPType.DELETE, type: HTTPType.DELETE,
action: "/client/r0/pushrules/global/room/$id", action: "/client/r0/pushrules/global/room/$id",
data: {}); data: {});
if (resp == ErrorResponse) return resp;
} }
resp = await client.connection.jsonRequest( resp = await client.connection.jsonRequest(
type: HTTPType.PUT, type: HTTPType.PUT,

View file

@ -24,7 +24,6 @@
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/src/Room.dart'; import 'package:famedlysdk/src/Room.dart';
import 'package:famedlysdk/src/RoomState.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/ChatTime.dart';
import 'package:famedlysdk/src/utils/MxContent.dart'; import 'package:famedlysdk/src/utils/MxContent.dart';
@ -113,28 +112,16 @@ class User extends RoomState {
: displayName; : displayName;
/// Call the Matrix API to kick this user from this room. /// Call the Matrix API to kick this user from this room.
Future<dynamic> kick() async { Future<void> kick() => room.kick(id);
dynamic res = await room.kick(id);
return res;
}
/// Call the Matrix API to ban this user from this room. /// Call the Matrix API to ban this user from this room.
Future<dynamic> ban() async { Future<void> ban() => room.ban(id);
dynamic res = await room.ban(id);
return res;
}
/// Call the Matrix API to unban this banned user from this room. /// Call the Matrix API to unban this banned user from this room.
Future<dynamic> unban() async { Future<void> unban() => room.unban(id);
dynamic res = await room.unban(id);
return res;
}
/// Call the Matrix API to change the power level of this user. /// Call the Matrix API to change the power level of this user.
Future<dynamic> setPower(int power) async { Future<void> setPower(int power) => room.setPower(id, power);
dynamic res = await room.setPower(id, power);
return res;
}
/// Returns an existing direct chat ID with this user or creates a new one. /// Returns an existing direct chat ID with this user or creates a new one.
/// Returns null on error. /// Returns null on error.
@ -153,11 +140,6 @@ class User extends RoomState {
"preset": "trusted_private_chat" "preset": "trusted_private_chat"
}); });
if (resp is ErrorResponse) {
room.client.connection.onError.add(resp);
return null;
}
final String newRoomID = resp["room_id"]; final String newRoomID = resp["room_id"];
if (newRoomID == null) return newRoomID; if (newRoomID == null) return newRoomID;

View file

@ -1,51 +0,0 @@
/*
* Copyright (c) 2019 Zender & Kurtz GbR.
*
* Authors:
* Christian Pauly <krille@famedly.com>
* Marcel Radzio <mtrnord@famedly.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<String, dynamic> json, http.Request newRequest) {
errcode = json['errcode'];
error = json['error'] ?? "";
request = newRequest;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['errcode'] = this.errcode;
data['error'] = this.error;
return data;
}
}

View file

@ -0,0 +1,105 @@
/*
* Copyright (c) 2019 Zender & Kurtz GbR.
*
* Authors:
* Christian Pauly <krille@famedly.com>
* Marcel Radzio <mtrnord@famedly.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<String, dynamic> 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<AuthenticationFlow> get authenticationFlows {
if (!raw.containsKey("flows") || !(raw["flows"] is List)) return null;
List<AuthenticationFlow> flows = [];
for (Map<String, dynamic> flow in raw["flows"]) {
if (flow["stages"] is List<String>) {
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<String, dynamic> get authenticationParams => raw["params"];
/// Returns the list of already completed authentication flows from previous requests.
List<String> 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<String> stages;
const AuthenticationFlow(this.stages);
}

View file

@ -30,11 +30,11 @@ import 'package:famedlysdk/src/Presence.dart';
import 'package:famedlysdk/src/Room.dart'; import 'package:famedlysdk/src/Room.dart';
import 'package:famedlysdk/src/User.dart'; import 'package:famedlysdk/src/User.dart';
import 'package:famedlysdk/src/requests/SetPushersRequest.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/responses/PushrulesResponse.dart';
import 'package:famedlysdk/src/sync/EventUpdate.dart'; import 'package:famedlysdk/src/sync/EventUpdate.dart';
import 'package:famedlysdk/src/sync/RoomUpdate.dart'; import 'package:famedlysdk/src/sync/RoomUpdate.dart';
import 'package:famedlysdk/src/sync/UserUpdate.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/MatrixFile.dart';
import 'package:famedlysdk/src/utils/Profile.dart'; import 'package:famedlysdk/src/utils/Profile.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
@ -60,9 +60,6 @@ void main() {
userUpdateListFuture = matrix.connection.onUserEvent.stream.toList(); userUpdateListFuture = matrix.connection.onUserEvent.stream.toList();
test('Login', () async { test('Login', () async {
Future<ErrorResponse> errorFuture =
matrix.connection.onError.stream.first;
int presenceCounter = 0; int presenceCounter = 0;
int accountDataCounter = 0; int accountDataCounter = 0;
matrix.onPresence = (Presence data) { matrix.onPresence = (Presence data) {
@ -72,25 +69,26 @@ void main() {
accountDataCounter++; accountDataCounter++;
}; };
final bool checkResp1 = expect(matrix.homeserver, null);
await matrix.checkServer("https://fakeserver.wrongaddress"); expect(matrix.matrixVersions, null);
final bool checkResp2 =
await matrix.checkServer("https://fakeserver.notexisting");
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); final Map<String, dynamic> resp = await matrix.connection
expect(checkResp2, true);
expect(checkError.errcode, "NO_RESPONSE");
final resp = await matrix.connection
.jsonRequest(type: HTTPType.POST, action: "/client/r0/login", data: { .jsonRequest(type: HTTPType.POST, action: "/client/r0/login", data: {
"type": "m.login.password", "type": "m.login.password",
"user": "test", "user": "test",
"password": "1234", "password": "1234",
"initial_device_display_name": "Fluffy Matrix Client" "initial_device_display_name": "Fluffy Matrix Client"
}); });
expect(resp is ErrorResponse, false);
Future<LoginState> loginStateFuture = Future<LoginState> loginStateFuture =
matrix.connection.onLoginStateChanged.stream.first; matrix.connection.onLoginStateChanged.stream.first;
@ -176,15 +174,19 @@ void main() {
}); });
test('Try to get ErrorResponse', () async { test('Try to get ErrorResponse', () async {
final resp = await matrix.connection MatrixException expectedException;
.jsonRequest(type: HTTPType.PUT, action: "/non/existing/path"); try {
expect(resp is ErrorResponse, true); 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 { test('Logout', () async {
final dynamic resp = await matrix.connection await matrix.connection
.jsonRequest(type: HTTPType.POST, action: "/client/r0/logout"); .jsonRequest(type: HTTPType.POST, action: "/client/r0/logout");
expect(resp is ErrorResponse, false);
Future<LoginState> loginStateFuture = Future<LoginState> loginStateFuture =
matrix.connection.onLoginStateChanged.stream.first; matrix.connection.onLoginStateChanged.stream.first;
@ -331,8 +333,7 @@ void main() {
test('setAvatar', () async { test('setAvatar', () async {
final MatrixFile testFile = final MatrixFile testFile =
MatrixFile(bytes: [], path: "fake/path/file.jpeg"); MatrixFile(bytes: [], path: "fake/path/file.jpeg");
final dynamic resp = await matrix.setAvatar(testFile); await matrix.setAvatar(testFile);
expect(resp, null);
}); });
test('getPushrules', () async { test('getPushrules', () async {
@ -352,8 +353,7 @@ void main() {
lang: "en", lang: "en",
data: PusherData( data: PusherData(
format: "event_id_only", url: "https://examplepushserver.com")); format: "event_id_only", url: "https://examplepushserver.com"));
final dynamic resp = await matrix.setPushers(data); await matrix.setPushers(data);
expect(resp is ErrorResponse, false);
}); });
test('joinRoomById', () async { test('joinRoomById', () async {
@ -389,8 +389,13 @@ void main() {
test('Logout when token is unknown', () async { test('Logout when token is unknown', () async {
Future<LoginState> loginStateFuture = Future<LoginState> loginStateFuture =
matrix.connection.onLoginStateChanged.stream.first; 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; LoginState state = await loginStateFuture;
expect(state, LoginState.loggedOut); expect(state, LoginState.loggedOut);

View file

@ -48,12 +48,17 @@ class FakeMatrixApi extends MockClient {
if (request.url.origin != "https://fakeserver.notexisting") if (request.url.origin != "https://fakeserver.notexisting")
return Response( return Response(
"<html><head></head><body>Not found...</body></html>", 50); "<html><head></head><body>Not found...</body></html>", 404);
// Call API // Call API
if (api.containsKey(method) && api[method].containsKey(action)) if (api.containsKey(method) && api[method].containsKey(action))
res = api[method][action](data); 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 = { res = {
"errcode": "M_UNRECOGNIZED", "errcode": "M_UNRECOGNIZED",
"error": "Unrecognized request" "error": "Unrecognized request"
@ -62,6 +67,64 @@ class FakeMatrixApi extends MockClient {
return Response(json.encode(res), 100); return Response(json.encode(res), 100);
}); });
static Map<String, dynamic> 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": "<b>This is an example text message</b>"
},
"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<String, dynamic> syncResponse = { static Map<String, dynamic> syncResponse = {
"next_batch": Random().nextDouble().toString(), "next_batch": Random().nextDouble().toString(),
"presence": { "presence": {
@ -457,6 +520,8 @@ class FakeMatrixApi extends MockClient {
static final Map<String, Map<String, dynamic>> api = { static final Map<String, Map<String, dynamic>> api = {
"GET": { "GET": {
"/client/r0/rooms/1/state/m.room.member/@alice:example.com": (var req) =>
{"displayname": "Alice"},
"/client/r0/profile/@getme:example.com": (var req) => { "/client/r0/profile/@getme:example.com": (var req) => {
"avatar_url": "mxc://test", "avatar_url": "mxc://test",
"displayname": "You got me", "displayname": "You got me",
@ -480,65 +545,10 @@ class FakeMatrixApi extends MockClient {
"origin_server_ts": 1432735824653, "origin_server_ts": 1432735824653,
"unsigned": {"age": 1234} "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": "/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) => { (var req) => 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": "<b>This is an example text message</b>"
},
"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}
}
]
},
"/client/versions": (var req) => { "/client/versions": (var req) => {
"versions": [ "versions": [
"r0.0.1", "r0.0.1",

View file

@ -29,14 +29,17 @@ import 'package:famedlysdk/src/sync/RoomUpdate.dart';
import 'package:famedlysdk/src/utils/ChatTime.dart'; import 'package:famedlysdk/src/utils/ChatTime.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'FakeMatrixApi.dart';
void main() { void main() {
/// All Tests related to the MxContent /// All Tests related to the MxContent
group("RoomList", () { group("RoomList", () {
final roomID = "!1:example.com"; final roomID = "!1:example.com";
test("Create and insert one room", () async { test("Create and insert one room", () async {
final Client client = Client("testclient"); final Client client = Client("testclient", debug: true);
client.homeserver = "https://testserver.abc"; client.connection.httpClient = FakeMatrixApi();
await client.checkServer("https://fakeserver.notexisting");
client.prevBatch = "1234"; client.prevBatch = "1234";
int updateCount = 0; int updateCount = 0;
@ -84,8 +87,9 @@ void main() {
}); });
test("Restort", () async { test("Restort", () async {
final Client client = Client("testclient"); final Client client = Client("testclient", debug: true);
client.homeserver = "https://testserver.abc"; client.connection.httpClient = FakeMatrixApi();
await client.checkServer("https://fakeserver.notexisting");
client.prevBatch = "1234"; client.prevBatch = "1234";
int updateCount = 0; int updateCount = 0;
@ -186,7 +190,7 @@ void main() {
await new Future.delayed(new Duration(milliseconds: 50)); await new Future.delayed(new Duration(milliseconds: 50));
expect(updateCount, 5); expect(updateCount, 5);
expect(roomUpdates, 2); expect(roomUpdates, 3);
expect(insertList, [0, 1]); expect(insertList, [0, 1]);
expect(removeList, []); expect(removeList, []);
@ -224,8 +228,9 @@ void main() {
}); });
test("onlyLeft", () async { test("onlyLeft", () async {
final Client client = Client("testclient"); final Client client = Client("testclient", debug: true);
client.homeserver = "https://testserver.abc"; client.connection.httpClient = FakeMatrixApi();
await client.checkServer("https://fakeserver.notexisting");
client.prevBatch = "1234"; client.prevBatch = "1234";
int updateCount = 0; int updateCount = 0;
@ -268,9 +273,9 @@ void main() {
expect(roomList.eventSub != null, true); expect(roomList.eventSub != null, true);
expect(roomList.roomSub != null, true); expect(roomList.roomSub != null, true);
expect(roomList.rooms[0].id, "2"); expect(roomList.rooms[0].id, "2");
expect(updateCount, 2);
expect(insertList, [0]); expect(insertList, [0]);
expect(removeList, []); expect(removeList, []);
expect(updateCount, 2);
}); });
}); });
} }

View file

@ -145,9 +145,7 @@ void main() {
}); });
test("sendReadReceipt", () async { test("sendReadReceipt", () async {
final dynamic resp = await room.sendReadReceipt("§1234:fakeServer.notExisting");
await room.sendReadReceipt("§1234:fakeServer.notExisting");
expect(resp, {});
}); });
test("requestParticipants", () async { test("requestParticipants", () async {
@ -167,28 +165,25 @@ void main() {
}); });
test("setName", () async { test("setName", () async {
final dynamic resp = await room.setName("Testname"); final String eventId = await room.setName("Testname");
expect(resp["event_id"], "42"); expect(eventId, "42");
}); });
test("setDescription", () async { test("setDescription", () async {
final dynamic resp = await room.setDescription("Testname"); final String eventId = await room.setDescription("Testname");
expect(resp["event_id"], "42"); expect(eventId, "42");
}); });
test("kick", () async { test("kick", () async {
final dynamic resp = await room.kick("Testname"); await room.kick("Testname");
expect(resp, {});
}); });
test("ban", () async { test("ban", () async {
final dynamic resp = await room.ban("Testname"); await room.ban("Testname");
expect(resp, {});
}); });
test("unban", () async { test("unban", () async {
final dynamic resp = await room.unban("Testname"); await room.unban("Testname");
expect(resp, {});
}); });
test("PowerLevels", () async { test("PowerLevels", () async {
@ -259,18 +254,17 @@ void main() {
expect(room.canSendEvent("m.room.power_levels"), false); expect(room.canSendEvent("m.room.power_levels"), false);
expect(room.canSendEvent("m.room.member"), false); expect(room.canSendEvent("m.room.member"), false);
expect(room.canSendEvent("m.room.message"), true); expect(room.canSendEvent("m.room.message"), true);
final dynamic resp = final String resp =
await room.setPower("@test:fakeServer.notExisting", 90); await room.setPower("@test:fakeServer.notExisting", 90);
expect(resp["event_id"], "42"); expect(resp, "42");
}); });
test("invite", () async { test("invite", () async {
final dynamic resp = await room.invite("Testname"); await room.invite("Testname");
expect(resp, {});
}); });
test("getParticipants", () async { test("getParticipants", () async {
room.states["@alice:test.abc"] = RoomState( room.setState(RoomState(
senderId: "@alice:test.abc", senderId: "@alice:test.abc",
typeKey: "m.room.member", typeKey: "m.room.member",
roomId: room.id, roomId: room.id,
@ -278,15 +272,14 @@ void main() {
eventId: "12345", eventId: "12345",
time: ChatTime.now(), time: ChatTime.now(),
content: {"displayname": "alice"}, content: {"displayname": "alice"},
stateKey: "@alice:test.abc"); stateKey: "@alice:test.abc"));
final List<User> userList = room.getParticipants(); final List<User> userList = room.getParticipants();
expect(userList.length, 1); expect(userList.length, 4);
expect(userList[0].displayName, "alice"); expect(userList[3].displayName, "alice");
}); });
test("addToDirectChat", () async { test("addToDirectChat", () async {
final dynamic resp = await room.addToDirectChat("Testname"); await room.addToDirectChat("Testname");
expect(resp, {});
}); });
test("getTimeline", () async { test("getTimeline", () async {
@ -295,7 +288,10 @@ void main() {
}); });
test("getUserByMXID", () async { 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.stateKey, "@getme:example.com");
expect(user.calcDisplayname(), "You got me"); expect(user.calcDisplayname(), "You got me");
}); });