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

View file

@ -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,28 +155,19 @@ 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<bool> checkServer(serverUrl) async {
try {
homeserver = serverUrl;
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<String> versions = List<String>.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;
}
}
@ -186,17 +176,14 @@ class Client {
if (versionResp.containsKey("unstable_features") &&
versionResp["unstable_features"].containsKey("m.lazy_load_members")) {
lazyLoadMembers = versionResp["unstable_features"]["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<dynamic> flows = loginResp["flows"];
@ -205,16 +192,19 @@ class Client {
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;
} 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<bool> 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,12 +238,15 @@ class Client {
/// Sends a logout command to the homeserver and clears all local data,
/// including all persistent data from the store.
Future<void> logout() async {
final dynamic resp = await connection.jsonRequest(
try {
await connection.jsonRequest(
type: HTTPType.POST, action: "/client/r0/logout");
if (resp is ErrorResponse) connection.onError.add(resp);
} catch (exception) {
rethrow;
} finally {
await connection.clear();
}
}
/// Get the combined profile information for this user. This API may be used to
/// fetch the user's own profile information or other users; either locally
@ -266,10 +254,6 @@ class Client {
Future<Profile> 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<String, dynamic>) {
if (sync["rooms"]["leave"] is Map<String, dynamic>) {
for (var entry in sync["rooms"]["leave"].entries) {
final String id = entry.key;
final dynamic room = entry.value;
@ -377,6 +360,7 @@ class Client {
if (params == null && invite != null)
for (int i = 0; i < invite.length; i++) inviteIDs.add(invite[i].id);
try {
final dynamic resp = await connection.jsonRequest(
type: HTTPType.POST,
action: "/client/r0/createRoom",
@ -385,25 +369,20 @@ class Client {
"invite": inviteIDs,
}
: params);
if (resp is ErrorResponse) {
connection.onError.add(resp);
return null;
}
return resp["room_id"];
} catch (e) {
rethrow;
}
}
/// Uploads a new user avatar for this user. Returns ErrorResponse if something went wrong.
Future<dynamic> setAvatar(MatrixFile file) async {
/// Uploads a new user avatar for this user.
Future<void> 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<dynamic> setPushers(SetPushersRequest data) async {
final dynamic resp = await connection.jsonRequest(
Future<void> 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;
}
}

View file

@ -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<ErrorResponse> onError =
final StreamController<MatrixException> 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<dynamic> jsonRequest(
Future<Map<String, dynamic>> 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<String, dynamic> 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<String, dynamic>; // May throw FormatException
if (jsonResp.containsKey("errcode") && jsonResp["errcode"] is String) {
// The server has responsed with an matrix related error.
MatrixException exception = MatrixException(resp);
if (exception.error == MatrixError.M_UNKNOWN_TOKEN) {
// The token is no longer valid. Need to sign off....
onError.add(exception);
clear();
}
Map<String, dynamic> jsonResp;
try {
jsonResp = jsonDecode(resp.body) as Map<String, dynamic>;
} 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);
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<dynamic> upload(MatrixFile file) async {
/// and returns the mxc url as a string.
Future<String> 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<String, dynamic> 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,15 +300,11 @@ class Connection {
action += "&timeout=30000";
action += "&since=${client.prevBatch}";
}
try {
_syncRequest = jsonRequest(type: HTTPType.GET, action: action);
final int hash = _syncRequest.hashCode;
final syncResp = await _syncRequest;
if (hash != _syncRequest.hashCode) return;
if (syncResp is ErrorResponse) {
//onError.add(syncResp);
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), () {});
} else {
try {
if (client.store != null)
await client.store.transaction(() {
handleSync(syncResp);
@ -321,13 +315,13 @@ class Connection {
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), () {});
}
}
if (hash == _syncRequest.hashCode) _sync();
} on MatrixException catch (exception) {
onError.add(exception);
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
} catch (exception) {
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
}
}
void handleSync(dynamic sync) {

View file

@ -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<dynamic> 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<String> setName(String newName) async {
final Map<String, dynamic> 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<dynamic> setDescription(String newName) async {
dynamic res = await client.connection.jsonRequest(
Future<String> setDescription(String newName) async {
final Map<String, dynamic> 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<dynamic> _sendRawEventNow(Map<String, dynamic> content,
Future<String> _sendRawEventNow(Map<String, dynamic> content,
{String txid = null}) async {
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,
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<String> 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<String, dynamic> content = {
@ -311,8 +308,7 @@ class Room {
Future<String> 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<String, dynamic> content = {
"msgtype": "m.audio",
"body": fileName,
@ -329,8 +325,7 @@ class Room {
Future<String> 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<String, dynamic> 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<String, dynamic> 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<dynamic> join() async {
dynamic res = await client.connection.jsonRequest(
Future<void> join() async {
try {
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") {
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<dynamic> leave() async {
Future<void> 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<dynamic> forget() async {
Future<void> 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<dynamic> kick(String userID) async {
dynamic res = await client.connection.jsonRequest(
Future<void> 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<dynamic> ban(String userID) async {
dynamic res = await client.connection.jsonRequest(
Future<void> 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<dynamic> unban(String userID) async {
dynamic res = await client.connection.jsonRequest(
Future<void> 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<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;
Map<String, dynamic> 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<String, dynamic> 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<dynamic> invite(String userID) async {
dynamic res = await client.connection.jsonRequest(
Future<void> 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<dynamic> addToDirectChat(String userID) async {
/// Sets this room as a direct chat for this user if not already.
Future<void> addToDirectChat(String userID) async {
Map<String, dynamic> 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<dynamic> removeFromDirectChat() async {
/// Removes this room from all direct chat tags.
Future<void> removeFromDirectChat() async {
Map<String, dynamic> 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<dynamic> sendReadReceipt(String eventID) async {
Future<void> 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<dynamic>))
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 {
try {
requestUser(mxID);
} catch (_) {}
return User(mxID, room: this);
}
}
@ -805,12 +789,14 @@ class Room {
/// lazy loading.
Future<User> requestUser(String mxID) async {
if (mxID == null || !_requestingMatrixIds.add(mxID)) return null;
final dynamic resp = await client.connection.jsonRequest(
Map<String, dynamic> resp;
try {
resp = await client.connection.jsonRequest(
type: HTTPType.GET,
action: "/client/r0/rooms/$id/state/m.room.member/$mxID");
if (resp is ErrorResponse) {
} catch (exception) {
_requestingMatrixIds.remove(mxID);
return null;
rethrow;
}
final User user = User(mxID,
displayName: resp["displayname"],
@ -837,7 +823,6 @@ class Room {
Future<Event> 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<dynamic> setAvatar(MatrixFile file) async {
final uploadResp = await client.connection.upload(file);
if (uploadResp is ErrorResponse) return uploadResp;
final setAvatarResp = await client.connection.jsonRequest(
/// Uploads a new user avatar for this room. Returns the event ID of the new
/// m.room.avatar event.
Future<String> setAvatar(MatrixFile file) async {
final String uploadResp = await client.connection.upload(file);
final Map<String, dynamic> 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;
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,

View file

@ -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<dynamic> kick() async {
dynamic res = await room.kick(id);
return res;
}
Future<void> kick() => room.kick(id);
/// Call the Matrix API to ban this user from this room.
Future<dynamic> ban() async {
dynamic res = await room.ban(id);
return res;
}
Future<void> ban() => room.ban(id);
/// Call the Matrix API to unban this banned user from this room.
Future<dynamic> unban() async {
dynamic res = await room.unban(id);
return res;
}
Future<void> unban() => room.unban(id);
/// Call the Matrix API to change the power level of this user.
Future<dynamic> setPower(int power) async {
dynamic res = await room.setPower(id, power);
return res;
}
Future<void> 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;

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/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<ErrorResponse> 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 =
expect(matrix.homeserver, null);
expect(matrix.matrixVersions, null);
try {
await matrix.checkServer("https://fakeserver.wrongaddress");
final bool checkResp2 =
} 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"]);
ErrorResponse checkError = await errorFuture;
expect(checkResp1, false);
expect(checkResp2, true);
expect(checkError.errcode, "NO_RESPONSE");
final resp = await matrix.connection
final Map<String, dynamic> 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<LoginState> loginStateFuture =
matrix.connection.onLoginStateChanged.stream.first;
@ -176,15 +174,19 @@ void main() {
});
test('Try to get ErrorResponse', () async {
final resp = await matrix.connection
MatrixException expectedException;
try {
await matrix.connection
.jsonRequest(type: HTTPType.PUT, action: "/non/existing/path");
expect(resp is ErrorResponse, true);
} 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<LoginState> 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<LoginState> loginStateFuture =
matrix.connection.onLoginStateChanged.stream.first;
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);

View file

@ -48,12 +48,17 @@ class FakeMatrixApi extends MockClient {
if (request.url.origin != "https://fakeserver.notexisting")
return Response(
"<html><head></head><body>Not found...</body></html>", 50);
"<html><head></head><body>Not found...</body></html>", 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<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 = {
"next_batch": Random().nextDouble().toString(),
"presence": {
@ -457,6 +520,8 @@ class FakeMatrixApi extends MockClient {
static final Map<String, Map<String, dynamic>> 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": "<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}
}
]
},
(var req) => messagesResponse,
"/client/versions": (var req) => {
"versions": [
"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: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);
});
});
}

View file

@ -145,9 +145,7 @@ void main() {
});
test("sendReadReceipt", () async {
final dynamic resp =
await room.sendReadReceipt("§1234:fakeServer.notExisting");
expect(resp, {});
});
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<User> 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");
});