diff --git a/README.md b/README.md
index c4409f0..c0ac0c2 100644
--- a/README.md
+++ b/README.md
@@ -39,15 +39,15 @@ Client matrix = Client("HappyChat", store: Store(this));
3. Connect to a Matrix Homeserver and listen to the streams:
```dart
-matrix.connection.onLoginStateChanged.stream.listen((bool loginState){
+matrix.onLoginStateChanged.stream.listen((bool loginState){
print("LoginState: ${loginState.toString()}");
});
-matrix.connection.onEvent.stream.listen((EventUpdate eventUpdate){
+matrix.onEvent.stream.listen((EventUpdate eventUpdate){
print("New event update!");
});
-matrix.connection.onRoomUpdate.stream.listen((RoomUpdate eventUpdate){
+matrix.onRoomUpdate.stream.listen((RoomUpdate eventUpdate){
print("New room update!");
});
@@ -59,7 +59,7 @@ final bool loginValid = await matrix.login("username", "password");
4. Send a message to a Room:
```dart
-final resp = await matrix.connection.jsonRequest(
+final resp = await matrix.jsonRequest(
type: "PUT",
action: "/r0/rooms/!fjd823j:example.com/send/m.room.message/$txnId",
data: {
diff --git a/lib/famedlysdk.dart b/lib/famedlysdk.dart
index cfdf3f6..223bb01 100644
--- a/lib/famedlysdk.dart
+++ b/lib/famedlysdk.dart
@@ -23,25 +23,21 @@
library famedlysdk;
-export 'package:famedlysdk/src/requests/SetPushersRequest.dart';
-export 'package:famedlysdk/src/responses/PushrulesResponse.dart';
export 'package:famedlysdk/src/sync/RoomUpdate.dart';
export 'package:famedlysdk/src/sync/EventUpdate.dart';
export 'package:famedlysdk/src/sync/UserUpdate.dart';
-export 'package:famedlysdk/src/utils/ChatTime.dart';
export 'package:famedlysdk/src/utils/MatrixException.dart';
export 'package:famedlysdk/src/utils/MatrixFile.dart';
export 'package:famedlysdk/src/utils/MxContent.dart';
+export 'package:famedlysdk/src/utils/Profile.dart';
+export 'package:famedlysdk/src/utils/PushRules.dart';
export 'package:famedlysdk/src/utils/StatesMap.dart';
export 'package:famedlysdk/src/AccountData.dart';
export 'package:famedlysdk/src/Client.dart';
-export 'package:famedlysdk/src/Connection.dart';
export 'package:famedlysdk/src/Event.dart';
export 'package:famedlysdk/src/Presence.dart';
export 'package:famedlysdk/src/Room.dart';
export 'package:famedlysdk/src/RoomAccountData.dart';
-export 'package:famedlysdk/src/RoomList.dart';
-export 'package:famedlysdk/src/RoomState.dart';
export 'package:famedlysdk/src/StoreAPI.dart';
export 'package:famedlysdk/src/Timeline.dart';
export 'package:famedlysdk/src/User.dart';
diff --git a/lib/src/AccountData.dart b/lib/src/AccountData.dart
index b3a0a96..7d41ebc 100644
--- a/lib/src/AccountData.dart
+++ b/lib/src/AccountData.dart
@@ -21,8 +21,9 @@
* along with famedlysdk. If not, see .
*/
-import 'package:famedlysdk/src/RoomState.dart';
+import 'package:famedlysdk/famedlysdk.dart';
+/// The global private data created by this user.
class AccountData {
/// The json payload of the content. The content highly depends on the type.
final Map content;
@@ -35,7 +36,7 @@ class AccountData {
/// Get a State event from a table row or from the event stream.
factory AccountData.fromJson(Map jsonPayload) {
final Map content =
- RoomState.getMapFromPayload(jsonPayload['content']);
+ Event.getMapFromPayload(jsonPayload['content']);
return AccountData(content: content, typeKey: jsonPayload['type']);
}
}
diff --git a/lib/src/Client.dart b/lib/src/Client.dart
index d6e8e79..da6f757 100644
--- a/lib/src/Client.dart
+++ b/lib/src/Client.dart
@@ -30,35 +30,40 @@ import 'package:famedlysdk/src/Presence.dart';
import 'package:famedlysdk/src/StoreAPI.dart';
import 'package:famedlysdk/src/sync/UserUpdate.dart';
import 'package:famedlysdk/src/utils/MatrixFile.dart';
-
-import 'Connection.dart';
import 'Room.dart';
-import 'RoomList.dart';
-//import 'Store.dart';
-import 'RoomState.dart';
+import 'Event.dart';
import 'User.dart';
-import 'requests/SetPushersRequest.dart';
-import 'responses/PushrulesResponse.dart';
import 'utils/Profile.dart';
+import 'dart:convert';
+import 'package:famedlysdk/src/Room.dart';
+import 'package:http/http.dart' as http;
+import 'package:mime_type/mime_type.dart';
+import 'sync/EventUpdate.dart';
+import 'sync/RoomUpdate.dart';
+import 'sync/UserUpdate.dart';
+import 'utils/MatrixException.dart';
typedef AccountDataEventCB = void Function(AccountData accountData);
typedef PresenceCB = void Function(Presence presence);
+enum HTTPType { GET, POST, PUT, DELETE }
+
+enum LoginState { logged, loggedOut }
+
/// Represents a Matrix client to communicate with a
/// [Matrix](https://matrix.org) homeserver and is the entry point for this
/// SDK.
class Client {
/// Handles the connection for this client.
- Connection connection;
+ @deprecated
+ Client get connection => this;
/// Optional persistent store for all data.
StoreAPI store;
Client(this.clientName, {this.debug = false, this.store}) {
- connection = Connection(this);
-
if (this.clientName != "testclient") store = null; //Store(this);
- connection.onLoginStateChanged.stream.listen((loginState) {
+ this.onLoginStateChanged.stream.listen((loginState) {
print("LoginState: ${loginState.toString()}");
});
}
@@ -70,35 +75,43 @@ class Client {
final String clientName;
/// The homeserver this client is communicating with.
- String homeserver;
+ String get homeserver => _homeserver;
+ String _homeserver;
/// The Matrix ID of the current logged user.
- String userID;
+ String get userID => _userID;
+ String _userID;
/// This is the access token for the matrix client. When it is undefined, then
/// the user needs to sign in first.
- String accessToken;
+ String get accessToken => _accessToken;
+ String _accessToken;
/// This points to the position in the synchronization history.
String prevBatch;
/// The device ID is an unique identifier for this device.
- String deviceID;
+ String get deviceID => _deviceID;
+ String _deviceID;
/// The device name is a human readable identifier for this device.
- String deviceName;
+ String get deviceName => _deviceName;
+ String _deviceName;
/// Which version of the matrix specification does this server support?
- List matrixVersions;
+ List get matrixVersions => _matrixVersions;
+ List _matrixVersions;
/// Wheither the server supports lazy load members.
- bool lazyLoadMembers = false;
+ bool get lazyLoadMembers => _lazyLoadMembers;
+ bool _lazyLoadMembers = false;
/// Returns the current login state.
bool isLogged() => accessToken != null;
/// A list of all rooms the user is participating or invited.
- RoomList roomList;
+ List get rooms => _rooms;
+ List _rooms = [];
/// Key/Value store of account data.
Map accountData = {};
@@ -112,6 +125,20 @@ class Client {
/// Callback will be called on presences.
PresenceCB onPresence;
+ Room getRoomByAlias(String alias) {
+ for (int i = 0; i < rooms.length; i++) {
+ if (rooms[i].canonicalAlias == alias) return rooms[i];
+ }
+ return null;
+ }
+
+ Room getRoomById(String id) {
+ for (int j = 0; j < rooms.length; j++) {
+ if (rooms[j].id == id) return rooms[j];
+ }
+ return null;
+ }
+
void handleUserUpdate(UserUpdate userUpdate) {
if (userUpdate.type == "account_data") {
AccountData newAccountData = AccountData.fromJson(userUpdate.content);
@@ -134,21 +161,21 @@ class Client {
if (accountData["m.direct"] != null &&
accountData["m.direct"].content[userId] is List &&
accountData["m.direct"].content[userId].length > 0) {
- if (roomList.getRoomById(accountData["m.direct"].content[userId][0]) !=
- null) return accountData["m.direct"].content[userId][0];
+ if (getRoomById(accountData["m.direct"].content[userId][0]) != null)
+ return accountData["m.direct"].content[userId][0];
(accountData["m.direct"].content[userId] as List)
.remove(accountData["m.direct"].content[userId][0]);
- connection.jsonRequest(
+ this.jsonRequest(
type: HTTPType.PUT,
action: "/client/r0/user/${userID}/account_data/m.direct",
data: directChats);
return getDirectChatFromUserId(userId);
}
- for (int i = 0; i < roomList.rooms.length; i++)
- if (roomList.rooms[i].membership == Membership.invite &&
- roomList.rooms[i].states[userID]?.senderId == userId &&
- roomList.rooms[i].states[userID].content["is_direct"] == true)
- return roomList.rooms[i].id;
+ for (int i = 0; i < this.rooms.length; i++)
+ if (this.rooms[i].membership == Membership.invite &&
+ this.rooms[i].states[userID]?.senderId == userId &&
+ this.rooms[i].states[userID].content["is_direct"] == true)
+ return this.rooms[i].id;
return null;
}
@@ -158,9 +185,9 @@ class Client {
/// Throws FormatException, TimeoutException and MatrixException on error.
Future checkServer(serverUrl) async {
try {
- homeserver = serverUrl;
- final versionResp = await connection.jsonRequest(
- type: HTTPType.GET, action: "/client/versions");
+ _homeserver = serverUrl;
+ final versionResp = await this
+ .jsonRequest(type: HTTPType.GET, action: "/client/versions");
final List versions = List.from(versionResp["versions"]);
@@ -172,18 +199,18 @@ class Client {
}
}
- matrixVersions = versions;
+ _matrixVersions = versions;
if (versionResp.containsKey("unstable_features") &&
versionResp["unstable_features"].containsKey("m.lazy_load_members")) {
- lazyLoadMembers = versionResp["unstable_features"]
+ _lazyLoadMembers = versionResp["unstable_features"]
["m.lazy_load_members"]
? true
: false;
}
- final loginResp = await connection.jsonRequest(
- type: HTTPType.GET, action: "/client/r0/login");
+ final loginResp = await this
+ .jsonRequest(type: HTTPType.GET, action: "/client/r0/login");
final List flows = loginResp["flows"];
@@ -197,7 +224,7 @@ class Client {
}
return true;
} catch (_) {
- this.homeserver = this.matrixVersions = null;
+ this._homeserver = this._matrixVersions = null;
rethrow;
}
}
@@ -206,17 +233,19 @@ class Client {
/// authentication. Returns false if the login was not successful. Throws
/// MatrixException if login was not successful.
Future login(String username, String password) async {
- final loginResp = await connection
- .jsonRequest(type: HTTPType.POST, action: "/client/r0/login", data: {
- "type": "m.login.password",
- "user": username,
- "identifier": {
- "type": "m.id.user",
- "user": username,
- },
- "password": password,
- "initial_device_display_name": "Famedly Talk"
- });
+ final loginResp = await jsonRequest(
+ type: HTTPType.POST,
+ action: "/client/r0/login",
+ data: {
+ "type": "m.login.password",
+ "user": username,
+ "identifier": {
+ "type": "m.id.user",
+ "user": username,
+ },
+ "password": password,
+ "initial_device_display_name": "Famedly Talk"
+ });
final userID = loginResp["user_id"];
final accessToken = loginResp["access_token"];
@@ -224,7 +253,7 @@ class Client {
return false;
}
- await connection.connect(
+ await this.connect(
newToken: accessToken,
newUserID: userID,
newHomeserver: homeserver,
@@ -239,12 +268,11 @@ class Client {
/// including all persistent data from the store.
Future logout() async {
try {
- await connection.jsonRequest(
- type: HTTPType.POST, action: "/client/r0/logout");
+ await this.jsonRequest(type: HTTPType.POST, action: "/client/r0/logout");
} catch (exception) {
rethrow;
} finally {
- await connection.clear();
+ await this.clear();
}
}
@@ -252,33 +280,17 @@ class Client {
/// fetch the user's own profile information or other users; either locally
/// or on remote homeservers.
Future getProfileFromUserId(String userId) async {
- final dynamic resp = await connection.jsonRequest(
+ final dynamic resp = await this.jsonRequest(
type: HTTPType.GET, action: "/client/r0/profile/${userId}");
return Profile.fromJson(resp);
}
- /// Creates a new [RoomList] object.
- RoomList getRoomList(
- {onRoomListUpdateCallback onUpdate,
- onRoomListInsertCallback onInsert,
- onRoomListRemoveCallback onRemove}) {
- List rooms = roomList.rooms;
- return RoomList(
- client: this,
- onlyLeft: false,
- onUpdate: onUpdate,
- onInsert: onInsert,
- onRemove: onRemove,
- rooms: rooms);
- }
-
Future> get archive async {
List archiveList = [];
String syncFilters =
'{"room":{"include_leave":true,"timeline":{"limit":10}}}';
String action = "/client/r0/sync?filter=$syncFilters&timeout=0";
- final sync =
- await connection.jsonRequest(type: HTTPType.GET, action: action);
+ final sync = await this.jsonRequest(type: HTTPType.GET, action: action);
if (sync["rooms"]["leave"] is Map) {
for (var entry in sync["rooms"]["leave"].entries) {
final String id = entry.key;
@@ -301,13 +313,13 @@ class Client {
if (room["timeline"] is Map &&
room["timeline"]["events"] is List) {
for (dynamic event in room["timeline"]["events"]) {
- leftRoom.setState(RoomState.fromJson(event, leftRoom));
+ leftRoom.setState(Event.fromJson(event, leftRoom));
}
}
if (room["state"] is Map &&
room["state"]["events"] is List) {
for (dynamic event in room["state"]["events"]) {
- leftRoom.setState(RoomState.fromJson(event, leftRoom));
+ leftRoom.setState(Event.fromJson(event, leftRoom));
}
}
archiveList.add(leftRoom);
@@ -316,12 +328,9 @@ class Client {
return archiveList;
}
- /// Searches in the roomList and in the archive for a room with the given [id].
- Room getRoomById(String id) => roomList.getRoomById(id);
-
Future joinRoomById(String id) async {
- return await connection.jsonRequest(
- type: HTTPType.POST, action: "/client/r0/join/$id");
+ return await this
+ .jsonRequest(type: HTTPType.POST, action: "/client/r0/join/$id");
}
/// Loads the contact list for this user excluding the user itself.
@@ -330,14 +339,14 @@ class Client {
/// defined by the autojoin room feature in Synapse.
Future> loadFamedlyContacts() async {
List contacts = [];
- Room contactDiscoveryRoom = roomList
- .getRoomByAlias("#famedlyContactDiscovery:${userID.split(":")[1]}");
+ Room contactDiscoveryRoom =
+ this.getRoomByAlias("#famedlyContactDiscovery:${userID.split(":")[1]}");
if (contactDiscoveryRoom != null)
contacts = await contactDiscoveryRoom.requestParticipants();
else {
Map userMap = {};
- for (int i = 0; i < roomList.rooms.length; i++) {
- List roomUsers = roomList.rooms[i].getParticipants();
+ for (int i = 0; i < this.rooms.length; i++) {
+ List roomUsers = this.rooms[i].getParticipants();
for (int j = 0; j < roomUsers.length; j++) {
if (userMap[roomUsers[j].id] != true) contacts.add(roomUsers[j]);
userMap[roomUsers[j].id] = true;
@@ -361,7 +370,7 @@ class Client {
for (int i = 0; i < invite.length; i++) inviteIDs.add(invite[i].id);
try {
- final dynamic resp = await connection.jsonRequest(
+ final dynamic resp = await this.jsonRequest(
type: HTTPType.POST,
action: "/client/r0/createRoom",
data: params == null
@@ -377,8 +386,8 @@ class Client {
/// Uploads a new user avatar for this user.
Future setAvatar(MatrixFile file) async {
- final uploadResp = await connection.upload(file);
- await connection.jsonRequest(
+ final uploadResp = await this.upload(file);
+ await this.jsonRequest(
type: HTTPType.PUT,
action: "/client/r0/profile/$userID/avatar_url",
data: {"avatar_url": uploadResp});
@@ -387,22 +396,584 @@ class Client {
/// Fetches the pushrules for the logged in user.
/// These are needed for notifications on Android
- Future getPushrules() async {
- final dynamic resp = await connection.jsonRequest(
+ Future getPushrules() async {
+ final dynamic resp = await this.jsonRequest(
type: HTTPType.GET,
action: "/client/r0/pushrules/",
);
- return PushrulesResponse.fromJson(resp);
+ return PushRules.fromJson(resp);
}
/// This endpoint allows the creation, modification and deletion of pushers for this user ID.
- Future setPushers(SetPushersRequest data) async {
- await connection.jsonRequest(
+ Future setPushers(String pushKey, String kind, String appId,
+ String appDisplayName, String deviceDisplayName, String lang, String url,
+ {bool append, String profileTag, String format}) async {
+ Map data = {
+ "lang": lang,
+ "kind": kind,
+ "app_display_name": appDisplayName,
+ "device_display_name": deviceDisplayName,
+ "profile_tag": profileTag,
+ "app_id": appId,
+ "pushkey": pushKey,
+ "data": {"url": url}
+ };
+
+ if (format != null) data["data"]["format"] = format;
+ if (profileTag != null) data["profile_tag"] = profileTag;
+ if (append != null) data["append"] = append;
+
+ await this.jsonRequest(
type: HTTPType.POST,
action: "/client/r0/pushers/set",
- data: data.toJson(),
+ data: data,
);
return;
}
+
+ static String syncFilters = '{"room":{"state":{"lazy_load_members":true}}}';
+
+ http.Client httpClient = http.Client();
+
+ /// The newEvent signal is the most important signal in this concept. Every time
+ /// the app receives a new synchronization, this event is called for every signal
+ /// to update the GUI. For example, for a new message, it is called:
+ /// onRoomEvent( "m.room.message", "!chat_id:server.com", "timeline", {sender: "@bob:server.com", body: "Hello world"} )
+ final StreamController onEvent =
+ new StreamController.broadcast();
+
+ /// Outside of the events there are updates for the global chat states which
+ /// are handled by this signal:
+ final StreamController onRoomUpdate =
+ new StreamController.broadcast();
+
+ /// Outside of rooms there are account updates like account_data or presences.
+ final StreamController onUserEvent =
+ new StreamController.broadcast();
+
+ /// Called when the login state e.g. user gets logged out.
+ final StreamController onLoginStateChanged =
+ new StreamController.broadcast();
+
+ /// Synchronization erros are coming here.
+ final StreamController onError =
+ new StreamController.broadcast();
+
+ /// This is called once, when the first sync has received.
+ final StreamController onFirstSync = new StreamController.broadcast();
+
+ /// When a new sync response is coming in, this gives the complete payload.
+ final StreamController onSync = new StreamController.broadcast();
+
+ /// Matrix synchronisation is done with https long polling. This needs a
+ /// timeout which is usually 30 seconds.
+ int syncTimeoutSec = 30;
+
+ /// How long should the app wait until it retrys the synchronisation after
+ /// an error?
+ int syncErrorTimeoutSec = 3;
+
+ /// Sets the user credentials and starts the synchronisation.
+ ///
+ /// Before you can connect you need at least an [accessToken], a [homeserver],
+ /// a [userID], a [deviceID], and a [deviceName].
+ ///
+ /// You get this informations
+ /// by logging in to your Matrix account, using the [login API](https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-login).
+ ///
+ /// To log in you can use [jsonRequest()] after you have set the [homeserver]
+ /// to a valid url. For example:
+ ///
+ /// ```
+ /// final resp = await matrix
+ /// .jsonRequest(type: HTTPType.POST, action: "/client/r0/login", data: {
+ /// "type": "m.login.password",
+ /// "user": "test",
+ /// "password": "1234",
+ /// "initial_device_display_name": "Fluffy Matrix Client"
+ /// });
+ /// ```
+ ///
+ /// Returns:
+ ///
+ /// ```
+ /// {
+ /// "user_id": "@cheeky_monkey:matrix.org",
+ /// "access_token": "abc123",
+ /// "device_id": "GHTYAJCE"
+ /// }
+ /// ```
+ ///
+ /// Sends [LoginState.logged] to [onLoginStateChanged].
+ void connect(
+ {String newToken,
+ String newHomeserver,
+ String newUserID,
+ String newDeviceName,
+ String newDeviceID,
+ List newMatrixVersions,
+ bool newLazyLoadMembers,
+ String newPrevBatch}) async {
+ this._accessToken = newToken;
+ this._homeserver = newHomeserver;
+ this._userID = newUserID;
+ this._deviceID = newDeviceID;
+ this._deviceName = newDeviceName;
+ this._matrixVersions = newMatrixVersions;
+ this._lazyLoadMembers = newLazyLoadMembers;
+ this.prevBatch = newPrevBatch;
+
+ if (this.store != null) {
+ this.store.storeClient();
+ this._rooms = await this.store.getRoomList(onlyLeft: false);
+ this.accountData = await this.store.getAccountData();
+ this.presences = await this.store.getPresences();
+ }
+
+ _userEventSub ??= onUserEvent.stream.listen(this.handleUserUpdate);
+
+ onLoginStateChanged.add(LoginState.logged);
+
+ _sync();
+ }
+
+ StreamSubscription _userEventSub;
+
+ /// Resets all settings and stops the synchronisation.
+ void clear() {
+ this.store?.clear();
+ this._accessToken = this._homeserver = this._userID = this._deviceID = this
+ ._deviceName =
+ this._matrixVersions = this._lazyLoadMembers = this.prevBatch = null;
+ onLoginStateChanged.add(LoginState.loggedOut);
+ }
+
+ /// Used for all Matrix json requests using the [c2s API](https://matrix.org/docs/spec/client_server/r0.4.0.html).
+ ///
+ /// Throws: TimeoutException, FormatException, MatrixException
+ ///
+ /// You must first call [this.connect()] or set [this.homeserver] before you can use
+ /// this! For example to send a message to a Matrix room with the id
+ /// '!fjd823j:example.com' you call:
+ ///
+ /// ```
+ /// final resp = await jsonRequest(
+ /// type: HTTPType.PUT,
+ /// action: "/r0/rooms/!fjd823j:example.com/send/m.room.message/$txnId",
+ /// data: {
+ /// "msgtype": "m.text",
+ /// "body": "hello"
+ /// }
+ /// );
+ /// ```
+ ///
+ Future