Merge branch 'sdk-refactor-simplify' into 'master'

[SDK| Big refactoring

See merge request famedly/famedlysdk!142
This commit is contained in:
Christian Pauly 2020-01-02 14:09:49 +00:00
commit 920d7144ec
33 changed files with 1387 additions and 2211 deletions

View file

@ -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: {

View file

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

View file

@ -21,8 +21,9 @@
* along with famedlysdk. If not, see <http://www.gnu.org/licenses/>.
*/
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<String, dynamic> content;
@ -35,7 +36,7 @@ class AccountData {
/// Get a State event from a table row or from the event stream.
factory AccountData.fromJson(Map<String, dynamic> jsonPayload) {
final Map<String, dynamic> content =
RoomState.getMapFromPayload(jsonPayload['content']);
Event.getMapFromPayload(jsonPayload['content']);
return AccountData(content: content, typeKey: jsonPayload['type']);
}
}

View file

@ -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<String> matrixVersions;
List<String> get matrixVersions => _matrixVersions;
List<String> _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<Room> get rooms => _rooms;
List<Room> _rooms = [];
/// Key/Value store of account data.
Map<String, AccountData> 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<dynamic> &&
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<dynamic>)
.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<bool> 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<String> versions = List<String>.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<dynamic> 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<bool> 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<void> 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<Profile> 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<Room> rooms = roomList.rooms;
return RoomList(
client: this,
onlyLeft: false,
onUpdate: onUpdate,
onInsert: onInsert,
onRemove: onRemove,
rooms: rooms);
}
Future<List<Room>> get archive async {
List<Room> 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<String, dynamic>) {
for (var entry in sync["rooms"]["leave"].entries) {
final String id = entry.key;
@ -301,13 +313,13 @@ class Client {
if (room["timeline"] is Map<String, dynamic> &&
room["timeline"]["events"] is List<dynamic>) {
for (dynamic event in room["timeline"]["events"]) {
leftRoom.setState(RoomState.fromJson(event, leftRoom));
leftRoom.setState(Event.fromJson(event, leftRoom));
}
}
if (room["state"] is Map<String, dynamic> &&
room["state"]["events"] is List<dynamic>) {
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<dynamic> 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<List<User>> loadFamedlyContacts() async {
List<User> 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<String, bool> userMap = {};
for (int i = 0; i < roomList.rooms.length; i++) {
List<User> roomUsers = roomList.rooms[i].getParticipants();
for (int i = 0; i < this.rooms.length; i++) {
List<User> 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<void> 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<PushrulesResponse> getPushrules() async {
final dynamic resp = await connection.jsonRequest(
Future<PushRules> 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<void> setPushers(SetPushersRequest data) async {
await connection.jsonRequest(
Future<void> setPushers(String pushKey, String kind, String appId,
String appDisplayName, String deviceDisplayName, String lang, String url,
{bool append, String profileTag, String format}) async {
Map<String, dynamic> 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<EventUpdate> onEvent =
new StreamController.broadcast();
/// Outside of the events there are updates for the global chat states which
/// are handled by this signal:
final StreamController<RoomUpdate> onRoomUpdate =
new StreamController.broadcast();
/// Outside of rooms there are account updates like account_data or presences.
final StreamController<UserUpdate> onUserEvent =
new StreamController.broadcast();
/// Called when the login state e.g. user gets logged out.
final StreamController<LoginState> onLoginStateChanged =
new StreamController.broadcast();
/// Synchronization erros are coming here.
final StreamController<MatrixException> onError =
new StreamController.broadcast();
/// This is called once, when the first sync has received.
final StreamController<bool> onFirstSync = new StreamController.broadcast();
/// When a new sync response is coming in, this gives the complete payload.
final StreamController<dynamic> 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<String> 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<Map<String, dynamic>> jsonRequest(
{HTTPType type,
String action,
dynamic data = "",
int timeout,
String contentType = "application/json"}) async {
if (this.isLogged() == false && this.homeserver == null)
throw ("No homeserver specified.");
if (timeout == null) timeout = syncTimeoutSec + 5;
dynamic json;
if (data is Map) data.removeWhere((k, v) => v == null);
(!(data is String)) ? json = jsonEncode(data) : json = data;
if (data is List<int> || action.startsWith("/media/r0/upload")) json = data;
final url = "${this.homeserver}/_matrix${action}";
Map<String, String> headers = {};
if (type == HTTPType.PUT || type == HTTPType.POST)
headers["Content-Type"] = contentType;
if (this.isLogged())
headers["Authorization"] = "Bearer ${this.accessToken}";
if (this.debug)
print(
"[REQUEST ${type.toString().split('.').last}] Action: $action, Data: $data");
http.Response resp;
Map<String, dynamic> jsonResp = {};
try {
switch (type.toString().split('.').last) {
case "GET":
resp = await httpClient
.get(url, headers: headers)
.timeout(Duration(seconds: timeout));
break;
case "POST":
resp = await httpClient
.post(url, body: json, headers: headers)
.timeout(Duration(seconds: timeout));
break;
case "PUT":
resp = await httpClient
.put(url, body: json, headers: headers)
.timeout(Duration(seconds: timeout));
break;
case "DELETE":
resp = await httpClient
.delete(url, headers: headers)
.timeout(Duration(seconds: timeout));
break;
}
jsonResp = jsonDecode(resp.body)
as Map<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();
}
throw exception;
}
if (this.debug) print("[RESPONSE] ${jsonResp.toString()}");
} on ArgumentError catch (exception) {
print(exception);
// Ignore this error
} catch (_) {
print(_);
rethrow;
}
return jsonResp;
}
/// Uploads a file with the name [fileName] as base64 encoded to the server
/// and returns the mxc url as a string.
Future<String> upload(MatrixFile file) async {
dynamic fileBytes;
if (this.homeserver != "https://fakeServer.notExisting")
fileBytes = file.bytes;
String fileName = file.path.split("/").last.toLowerCase();
String mimeType = mime(file.path);
print("[UPLOADING] $fileName, type: $mimeType, size: ${fileBytes?.length}");
final Map<String, dynamic> resp = await jsonRequest(
type: HTTPType.POST,
action: "/media/r0/upload?filename=$fileName",
data: fileBytes,
contentType: mimeType);
return resp["content_uri"];
}
Future<dynamic> _syncRequest;
Future<void> _sync() async {
if (this.isLogged() == false) return;
String action = "/client/r0/sync?filter=$syncFilters";
if (this.prevBatch != null) {
action += "&timeout=30000";
action += "&since=${this.prevBatch}";
}
try {
_syncRequest = jsonRequest(type: HTTPType.GET, action: action);
final int hash = _syncRequest.hashCode;
final syncResp = await _syncRequest;
if (hash != _syncRequest.hashCode) return;
if (this.store != null)
await this.store.transaction(() {
handleSync(syncResp);
this.store.storePrevBatch(syncResp);
return;
});
else
await handleSync(syncResp);
if (this.prevBatch == null) this.onFirstSync.add(true);
this.prevBatch = syncResp["next_batch"];
if (hash == _syncRequest.hashCode) _sync();
} on MatrixException catch (exception) {
onError.add(exception);
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
} catch (exception) {
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
}
}
void handleSync(dynamic sync) {
if (sync["rooms"] is Map<String, dynamic>) {
if (sync["rooms"]["join"] is Map<String, dynamic>)
_handleRooms(sync["rooms"]["join"], Membership.join);
if (sync["rooms"]["invite"] is Map<String, dynamic>)
_handleRooms(sync["rooms"]["invite"], Membership.invite);
if (sync["rooms"]["leave"] is Map<String, dynamic>)
_handleRooms(sync["rooms"]["leave"], Membership.leave);
}
if (sync["presence"] is Map<String, dynamic> &&
sync["presence"]["events"] is List<dynamic>) {
_handleGlobalEvents(sync["presence"]["events"], "presence");
}
if (sync["account_data"] is Map<String, dynamic> &&
sync["account_data"]["events"] is List<dynamic>) {
_handleGlobalEvents(sync["account_data"]["events"], "account_data");
}
if (sync["to_device"] is Map<String, dynamic> &&
sync["to_device"]["events"] is List<dynamic>) {
_handleGlobalEvents(sync["to_device"]["events"], "to_device");
}
onSync.add(sync);
}
void _handleRooms(Map<String, dynamic> rooms, Membership membership) {
rooms.forEach((String id, dynamic room) async {
// calculate the notification counts, the limitedTimeline and prevbatch
num highlight_count = 0;
num notification_count = 0;
String prev_batch = "";
bool limitedTimeline = false;
if (room["unread_notifications"] is Map<String, dynamic>) {
if (room["unread_notifications"]["highlight_count"] is num)
highlight_count = room["unread_notifications"]["highlight_count"];
if (room["unread_notifications"]["notification_count"] is num)
notification_count =
room["unread_notifications"]["notification_count"];
}
if (room["timeline"] is Map<String, dynamic>) {
if (room["timeline"]["limited"] is bool)
limitedTimeline = room["timeline"]["limited"];
if (room["timeline"]["prev_batch"] is String)
prev_batch = room["timeline"]["prev_batch"];
}
RoomSummary summary;
if (room["summary"] is Map<String, dynamic>) {
summary = RoomSummary.fromJson(room["summary"]);
}
RoomUpdate update = RoomUpdate(
id: id,
membership: membership,
notification_count: notification_count,
highlight_count: highlight_count,
limitedTimeline: limitedTimeline,
prev_batch: prev_batch,
summary: summary,
);
_updateRoomsByRoomUpdate(update);
this.store?.storeRoomUpdate(update);
onRoomUpdate.add(update);
/// Handle now all room events and save them in the database
if (room["state"] is Map<String, dynamic> &&
room["state"]["events"] is List<dynamic>)
_handleRoomEvents(id, room["state"]["events"], "state");
if (room["invite_state"] is Map<String, dynamic> &&
room["invite_state"]["events"] is List<dynamic>)
_handleRoomEvents(id, room["invite_state"]["events"], "invite_state");
if (room["timeline"] is Map<String, dynamic> &&
room["timeline"]["events"] is List<dynamic>)
_handleRoomEvents(id, room["timeline"]["events"], "timeline");
if (room["ephemeral"] is Map<String, dynamic> &&
room["ephemeral"]["events"] is List<dynamic>)
_handleEphemerals(id, room["ephemeral"]["events"]);
if (room["account_data"] is Map<String, dynamic> &&
room["account_data"]["events"] is List<dynamic>)
_handleRoomEvents(id, room["account_data"]["events"], "account_data");
});
}
void _handleEphemerals(String id, List<dynamic> events) {
for (num i = 0; i < events.length; i++) {
_handleEvent(events[i], id, "ephemeral");
// Receipt events are deltas between two states. We will create a
// fake room account data event for this and store the difference
// there.
if (events[i]["type"] == "m.receipt") {
Room room = this.getRoomById(id);
if (room == null) room = Room(id: id);
Map<String, dynamic> receiptStateContent =
room.roomAccountData["m.receipt"]?.content ?? {};
for (var eventEntry in events[i]["content"].entries) {
final String eventID = eventEntry.key;
if (events[i]["content"][eventID]["m.read"] != null) {
final Map<String, dynamic> userTimestampMap =
events[i]["content"][eventID]["m.read"];
for (var userTimestampMapEntry in userTimestampMap.entries) {
final String mxid = userTimestampMapEntry.key;
// Remove previous receipt event from this user
for (var entry in receiptStateContent.entries) {
if (entry.value["m.read"] is Map<String, dynamic> &&
entry.value["m.read"].containsKey(mxid)) {
entry.value["m.read"].remove(mxid);
break;
}
}
if (userTimestampMap[mxid] is Map<String, dynamic> &&
userTimestampMap[mxid].containsKey("ts")) {
receiptStateContent[mxid] = {
"event_id": eventID,
"ts": userTimestampMap[mxid]["ts"],
};
}
}
}
}
events[i]["content"] = receiptStateContent;
_handleEvent(events[i], id, "account_data");
}
}
}
void _handleRoomEvents(String chat_id, List<dynamic> events, String type) {
for (num i = 0; i < events.length; i++) {
_handleEvent(events[i], chat_id, type);
}
}
void _handleGlobalEvents(List<dynamic> events, String type) {
for (int i = 0; i < events.length; i++)
if (events[i]["type"] is String &&
events[i]["content"] is Map<String, dynamic>) {
UserUpdate update = UserUpdate(
eventType: events[i]["type"],
type: type,
content: events[i],
);
this.store?.storeUserEventUpdate(update);
onUserEvent.add(update);
}
}
void _handleEvent(Map<String, dynamic> event, String roomID, String type) {
if (event["type"] is String && event["content"] is Map<String, dynamic>) {
EventUpdate update = EventUpdate(
eventType: event["type"],
roomID: roomID,
type: type,
content: event,
);
_updateRoomsByEventUpdate(update);
this.store?.storeEventUpdate(update);
onEvent.add(update);
}
}
void _updateRoomsByRoomUpdate(RoomUpdate chatUpdate) {
// Update the chat list item.
// Search the room in the rooms
num j = 0;
for (j = 0; j < rooms.length; j++) {
if (rooms[j].id == chatUpdate.id) break;
}
final bool found = (j < rooms.length && rooms[j].id == chatUpdate.id);
final bool isLeftRoom = chatUpdate.membership == Membership.leave;
// Does the chat already exist in the list rooms?
if (!found && !isLeftRoom) {
num position = chatUpdate.membership == Membership.invite ? 0 : j;
// Add the new chat to the list
Room newRoom = Room(
id: chatUpdate.id,
membership: chatUpdate.membership,
prev_batch: chatUpdate.prev_batch,
highlightCount: chatUpdate.highlight_count,
notificationCount: chatUpdate.notification_count,
mHeroes: chatUpdate.summary?.mHeroes,
mJoinedMemberCount: chatUpdate.summary?.mJoinedMemberCount,
mInvitedMemberCount: chatUpdate.summary?.mInvitedMemberCount,
roomAccountData: {},
client: this,
);
rooms.insert(position, newRoom);
}
// If the membership is "leave" then remove the item and stop here
else if (found && isLeftRoom) {
rooms.removeAt(j);
}
// Update notification, highlight count and/or additional informations
else if (found &&
chatUpdate.membership != Membership.leave &&
(rooms[j].membership != chatUpdate.membership ||
rooms[j].notificationCount != chatUpdate.notification_count ||
rooms[j].highlightCount != chatUpdate.highlight_count ||
chatUpdate.summary != null)) {
rooms[j].membership = chatUpdate.membership;
rooms[j].notificationCount = chatUpdate.notification_count;
rooms[j].highlightCount = chatUpdate.highlight_count;
if (chatUpdate.prev_batch != null)
rooms[j].prev_batch = chatUpdate.prev_batch;
if (chatUpdate.summary != null) {
if (chatUpdate.summary.mHeroes != null)
rooms[j].mHeroes = chatUpdate.summary.mHeroes;
if (chatUpdate.summary.mJoinedMemberCount != null)
rooms[j].mJoinedMemberCount = chatUpdate.summary.mJoinedMemberCount;
if (chatUpdate.summary.mInvitedMemberCount != null)
rooms[j].mInvitedMemberCount = chatUpdate.summary.mInvitedMemberCount;
}
if (rooms[j].onUpdate != null) rooms[j].onUpdate();
}
sortAndUpdate();
}
void _updateRoomsByEventUpdate(EventUpdate eventUpdate) {
if (eventUpdate.type == "history") return;
// Search the room in the rooms
num j = 0;
for (j = 0; j < rooms.length; j++) {
if (rooms[j].id == eventUpdate.roomID) break;
}
final bool found = (j < rooms.length && rooms[j].id == eventUpdate.roomID);
if (!found) return;
if (eventUpdate.type == "timeline" ||
eventUpdate.type == "state" ||
eventUpdate.type == "invite_state") {
Event stateEvent = Event.fromJson(eventUpdate.content, rooms[j]);
if (stateEvent.type == EventTypes.Redaction) {
final String redacts = eventUpdate.content["redacts"];
rooms[j].states.states.forEach(
(String key, Map<String, Event> states) => states.forEach(
(String key, Event state) {
if (state.eventId == redacts) {
state.setRedactionEvent(stateEvent);
}
},
),
);
} else {
Event prevState =
rooms[j].getState(stateEvent.typeKey, stateEvent.stateKey);
if (prevState != null &&
prevState.time.millisecondsSinceEpoch >
stateEvent.time.millisecondsSinceEpoch) return;
rooms[j].setState(stateEvent);
}
} else if (eventUpdate.type == "account_data") {
rooms[j].roomAccountData[eventUpdate.eventType] =
RoomAccountData.fromJson(eventUpdate.content, rooms[j]);
} else if (eventUpdate.type == "ephemeral") {
rooms[j].ephemerals[eventUpdate.eventType] =
RoomAccountData.fromJson(eventUpdate.content, rooms[j]);
}
if (rooms[j].onUpdate != null) rooms[j].onUpdate();
if (eventUpdate.type == "timeline") sortAndUpdate();
}
bool sortLock = false;
sortAndUpdate() {
if (prevBatch == null) return;
if (sortLock || rooms.length < 2) return;
sortLock = true;
rooms?.sort((a, b) => b.timeCreated.millisecondsSinceEpoch
.compareTo(a.timeCreated.millisecondsSinceEpoch));
sortLock = false;
}
}

View file

@ -1,494 +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 'dart:async';
import 'dart:convert';
import 'dart:core';
import 'package:famedlysdk/src/Room.dart';
import 'package:famedlysdk/src/RoomList.dart';
import 'package:famedlysdk/src/utils/MatrixFile.dart';
import 'package:http/http.dart' as http;
import 'package:mime_type/mime_type.dart';
import 'Client.dart';
import 'User.dart';
import 'sync/EventUpdate.dart';
import 'sync/RoomUpdate.dart';
import 'sync/UserUpdate.dart';
import 'utils/MatrixException.dart';
enum HTTPType { GET, POST, PUT, DELETE }
/// Represents a Matrix connection to communicate with a
/// [Matrix](https://matrix.org) homeserver.
class Connection {
final Client client;
Connection(this.client);
static String syncFilters = '{"room":{"state":{"lazy_load_members":true}}}';
/// Handles the connection to the Matrix Homeserver. You can change this to a
/// MockClient for testing.
http.Client httpClient = http.Client();
/// The newEvent signal is the most important signal in this concept. Every time
/// the app receives a new synchronization, this event is called for every signal
/// to update the GUI. For example, for a new message, it is called:
/// onRoomEvent( "m.room.message", "!chat_id:server.com", "timeline", {sender: "@bob:server.com", body: "Hello world"} )
final StreamController<EventUpdate> onEvent =
new StreamController.broadcast();
/// Outside of the events there are updates for the global chat states which
/// are handled by this signal:
final StreamController<RoomUpdate> onRoomUpdate =
new StreamController.broadcast();
/// Outside of rooms there are account updates like account_data or presences.
final StreamController<UserUpdate> onUserEvent =
new StreamController.broadcast();
/// Called when the login state e.g. user gets logged out.
final StreamController<LoginState> onLoginStateChanged =
new StreamController.broadcast();
/// Synchronization erros are coming here.
final StreamController<MatrixException> onError =
new StreamController.broadcast();
/// This is called once, when the first sync has received.
final StreamController<bool> onFirstSync = new StreamController.broadcast();
/// When a new sync response is coming in, this gives the complete payload.
final StreamController<dynamic> 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<String> newMatrixVersions,
bool newLazyLoadMembers,
String newPrevBatch}) async {
client.accessToken = newToken;
client.homeserver = newHomeserver;
client.userID = newUserID;
client.deviceID = newDeviceID;
client.deviceName = newDeviceName;
client.matrixVersions = newMatrixVersions;
client.lazyLoadMembers = newLazyLoadMembers;
client.prevBatch = newPrevBatch;
List<Room> rooms = [];
if (client.store != null) {
client.store.storeClient();
rooms = await client.store.getRoomList(onlyLeft: false);
client.accountData = await client.store.getAccountData();
client.presences = await client.store.getPresences();
}
client.roomList = RoomList(
client: client,
onlyLeft: false,
onUpdate: null,
onInsert: null,
onRemove: null,
rooms: rooms);
_userEventSub ??= onUserEvent.stream.listen(client.handleUserUpdate);
onLoginStateChanged.add(LoginState.logged);
_sync();
}
StreamSubscription _userEventSub;
/// Resets all settings and stops the synchronisation.
void clear() {
client.store?.clear();
client.accessToken = client.homeserver = client.userID = client.deviceID =
client.deviceName = client.matrixVersions =
client.lazyLoadMembers = client.prevBatch = null;
onLoginStateChanged.add(LoginState.loggedOut);
}
/// Used for all Matrix json requests using the [c2s API](https://matrix.org/docs/spec/client_server/r0.4.0.html).
///
/// Throws: TimeoutException, FormatException, MatrixException
///
/// You must first call [this.connect()] or set [this.homeserver] before you can use
/// this! For example to send a message to a Matrix room with the id
/// '!fjd823j:example.com' you call:
///
/// ```
/// final resp = await jsonRequest(
/// type: HTTPType.PUT,
/// action: "/r0/rooms/!fjd823j:example.com/send/m.room.message/$txnId",
/// data: {
/// "msgtype": "m.text",
/// "body": "hello"
/// }
/// );
/// ```
///
Future<Map<String, dynamic>> jsonRequest(
{HTTPType type,
String action,
dynamic data = "",
int timeout,
String contentType = "application/json"}) async {
if (client.isLogged() == false && client.homeserver == null)
throw ("No homeserver specified.");
if (timeout == null) timeout = syncTimeoutSec + 5;
dynamic json;
if (data is Map) data.removeWhere((k, v) => v == null);
(!(data is String)) ? json = jsonEncode(data) : json = data;
if (data is List<int> || action.startsWith("/media/r0/upload")) json = data;
final url = "${client.homeserver}/_matrix${action}";
Map<String, String> headers = {};
if (type == HTTPType.PUT || type == HTTPType.POST)
headers["Content-Type"] = contentType;
if (client.isLogged())
headers["Authorization"] = "Bearer ${client.accessToken}";
if (client.debug)
print(
"[REQUEST ${type.toString().split('.').last}] Action: $action, Data: $data");
http.Response resp;
Map<String, dynamic> jsonResp = {};
try {
switch (type.toString().split('.').last) {
case "GET":
resp = await httpClient
.get(url, headers: headers)
.timeout(Duration(seconds: timeout));
break;
case "POST":
resp = await httpClient
.post(url, body: json, headers: headers)
.timeout(Duration(seconds: timeout));
break;
case "PUT":
resp = await httpClient
.put(url, body: json, headers: headers)
.timeout(Duration(seconds: timeout));
break;
case "DELETE":
resp = await httpClient
.delete(url, headers: headers)
.timeout(Duration(seconds: timeout));
break;
}
jsonResp = jsonDecode(resp.body)
as Map<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();
}
throw exception;
}
if (client.debug) print("[RESPONSE] ${jsonResp.toString()}");
} on ArgumentError catch (exception) {
print(exception);
// Ignore this error
} catch (_) {
print(_);
rethrow;
}
return jsonResp;
}
/// Uploads a file with the name [fileName] as base64 encoded to the server
/// and returns the mxc url as a string.
Future<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 Map<String, dynamic> resp = await jsonRequest(
type: HTTPType.POST,
action: "/media/r0/upload?filename=$fileName",
data: fileBytes,
contentType: mimeType);
return resp["content_uri"];
}
Future<dynamic> _syncRequest;
Future<void> _sync() async {
if (client.isLogged() == false) return;
String action = "/client/r0/sync?filter=$syncFilters";
if (client.prevBatch != null) {
action += "&timeout=30000";
action += "&since=${client.prevBatch}";
}
try {
_syncRequest = jsonRequest(type: HTTPType.GET, action: action);
final int hash = _syncRequest.hashCode;
final syncResp = await _syncRequest;
if (hash != _syncRequest.hashCode) return;
if (client.store != null)
await client.store.transaction(() {
handleSync(syncResp);
client.store.storePrevBatch(syncResp);
return;
});
else
await handleSync(syncResp);
if (client.prevBatch == null) client.connection.onFirstSync.add(true);
client.prevBatch = syncResp["next_batch"];
if (hash == _syncRequest.hashCode) _sync();
} on MatrixException catch (exception) {
onError.add(exception);
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
} catch (exception) {
await Future.delayed(Duration(seconds: syncErrorTimeoutSec), _sync);
}
}
void handleSync(dynamic sync) {
if (sync["rooms"] is Map<String, dynamic>) {
if (sync["rooms"]["join"] is Map<String, dynamic>)
_handleRooms(sync["rooms"]["join"], Membership.join);
if (sync["rooms"]["invite"] is Map<String, dynamic>)
_handleRooms(sync["rooms"]["invite"], Membership.invite);
if (sync["rooms"]["leave"] is Map<String, dynamic>)
_handleRooms(sync["rooms"]["leave"], Membership.leave);
}
if (sync["presence"] is Map<String, dynamic> &&
sync["presence"]["events"] is List<dynamic>) {
_handleGlobalEvents(sync["presence"]["events"], "presence");
}
if (sync["account_data"] is Map<String, dynamic> &&
sync["account_data"]["events"] is List<dynamic>) {
_handleGlobalEvents(sync["account_data"]["events"], "account_data");
}
if (sync["to_device"] is Map<String, dynamic> &&
sync["to_device"]["events"] is List<dynamic>) {
_handleGlobalEvents(sync["to_device"]["events"], "to_device");
}
onSync.add(sync);
}
void _handleRooms(Map<String, dynamic> rooms, Membership membership) {
rooms.forEach((String id, dynamic room) async {
// calculate the notification counts, the limitedTimeline and prevbatch
num highlight_count = 0;
num notification_count = 0;
String prev_batch = "";
bool limitedTimeline = false;
if (room["unread_notifications"] is Map<String, dynamic>) {
if (room["unread_notifications"]["highlight_count"] is num)
highlight_count = room["unread_notifications"]["highlight_count"];
if (room["unread_notifications"]["notification_count"] is num)
notification_count =
room["unread_notifications"]["notification_count"];
}
if (room["timeline"] is Map<String, dynamic>) {
if (room["timeline"]["limited"] is bool)
limitedTimeline = room["timeline"]["limited"];
if (room["timeline"]["prev_batch"] is String)
prev_batch = room["timeline"]["prev_batch"];
}
RoomSummary summary;
if (room["summary"] is Map<String, dynamic>) {
summary = RoomSummary.fromJson(room["summary"]);
}
RoomUpdate update = RoomUpdate(
id: id,
membership: membership,
notification_count: notification_count,
highlight_count: highlight_count,
limitedTimeline: limitedTimeline,
prev_batch: prev_batch,
summary: summary,
);
client.store?.storeRoomUpdate(update);
onRoomUpdate.add(update);
/// Handle now all room events and save them in the database
if (room["state"] is Map<String, dynamic> &&
room["state"]["events"] is List<dynamic>)
_handleRoomEvents(id, room["state"]["events"], "state");
if (room["invite_state"] is Map<String, dynamic> &&
room["invite_state"]["events"] is List<dynamic>)
_handleRoomEvents(id, room["invite_state"]["events"], "invite_state");
if (room["timeline"] is Map<String, dynamic> &&
room["timeline"]["events"] is List<dynamic>)
_handleRoomEvents(id, room["timeline"]["events"], "timeline");
if (room["ephemeral"] is Map<String, dynamic> &&
room["ephemeral"]["events"] is List<dynamic>)
_handleEphemerals(id, room["ephemeral"]["events"]);
if (room["account_data"] is Map<String, dynamic> &&
room["account_data"]["events"] is List<dynamic>)
_handleRoomEvents(id, room["account_data"]["events"], "account_data");
});
}
void _handleEphemerals(String id, List<dynamic> events) {
for (num i = 0; i < events.length; i++) {
_handleEvent(events[i], id, "ephemeral");
// Receipt events are deltas between two states. We will create a
// fake room account data event for this and store the difference
// there.
if (events[i]["type"] == "m.receipt") {
Room room = client.roomList.getRoomById(id);
if (room == null) room = Room(id: id);
Map<String, dynamic> receiptStateContent =
room.roomAccountData["m.receipt"]?.content ?? {};
for (var eventEntry in events[i]["content"].entries) {
final String eventID = eventEntry.key;
if (events[i]["content"][eventID]["m.read"] != null) {
final Map<String, dynamic> userTimestampMap =
events[i]["content"][eventID]["m.read"];
for (var userTimestampMapEntry in userTimestampMap.entries) {
final String mxid = userTimestampMapEntry.key;
// Remove previous receipt event from this user
for (var entry in receiptStateContent.entries) {
if (entry.value["m.read"] is Map<String, dynamic> &&
entry.value["m.read"].containsKey(mxid)) {
entry.value["m.read"].remove(mxid);
break;
}
}
if (userTimestampMap[mxid] is Map<String, dynamic> &&
userTimestampMap[mxid].containsKey("ts")) {
receiptStateContent[mxid] = {
"event_id": eventID,
"ts": userTimestampMap[mxid]["ts"],
};
}
}
}
}
events[i]["content"] = receiptStateContent;
_handleEvent(events[i], id, "account_data");
}
}
}
void _handleRoomEvents(String chat_id, List<dynamic> events, String type) {
for (num i = 0; i < events.length; i++) {
_handleEvent(events[i], chat_id, type);
}
}
void _handleGlobalEvents(List<dynamic> events, String type) {
for (int i = 0; i < events.length; i++)
if (events[i]["type"] is String &&
events[i]["content"] is Map<String, dynamic>) {
UserUpdate update = UserUpdate(
eventType: events[i]["type"],
type: type,
content: events[i],
);
client.store?.storeUserEventUpdate(update);
onUserEvent.add(update);
}
}
void _handleEvent(Map<String, dynamic> event, String roomID, String type) {
if (event["type"] is String && event["content"] is Map<String, dynamic>) {
EventUpdate update = EventUpdate(
eventType: event["type"],
roomID: roomID,
type: type,
content: event,
);
client.store?.storeEventUpdate(update);
onEvent.add(update);
}
}
}
enum LoginState { logged, loggedOut }

View file

@ -21,73 +21,268 @@
* along with famedlysdk. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:famedlysdk/src/RoomState.dart';
import 'package:famedlysdk/src/sync/EventUpdate.dart';
import 'package:famedlysdk/src/utils/ChatTime.dart';
import 'dart:convert';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/src/utils/Receipt.dart';
import './Room.dart';
/// Defines a timeline event for a room.
class Event extends RoomState {
class Event {
/// The Matrix ID for this event in the format '$localpart:server.abc'. Please not
/// that account data, presence and other events may not have an eventId.
final String eventId;
/// The json payload of the content. The content highly depends on the type.
Map<String, dynamic> content;
/// The type String of this event. For example 'm.room.message'.
final String typeKey;
/// The ID of the room this event belongs to.
final String roomId;
/// The user who has sent this event if it is not a global account data event.
final String senderId;
User get sender => room.getUserByMXIDSync(senderId);
/// The time this event has received at the server. May be null for events like
/// account data.
final DateTime time;
/// Optional additional content for this event.
Map<String, dynamic> unsigned;
/// The room this event belongs to. May be null.
final Room room;
/// Optional. The previous content for this state.
/// This will be present only for state events appearing in the timeline.
/// If this is not a state event, or there is no previous content, this key will be null.
Map<String, dynamic> prevContent;
/// Optional. This key will only be present for state events. A unique key which defines
/// the overwriting semantics for this piece of room state.
final String stateKey;
/// The status of this event.
/// -1=ERROR
/// 0=SENDING
/// 1=SENT
/// 2=RECEIVED
/// 2=TIMELINE
/// 3=ROOM_STATE
int status;
static const int defaultStatus = 2;
static const Map<String, int> STATUS_TYPE = {
"ERROR": -1,
"SENDING": 0,
"SENT": 1,
"TIMELINE": 2,
"ROOM_STATE": 3,
};
/// Optional. The event that redacted this event, if any. Otherwise null.
Event get redactedBecause =>
unsigned != null && unsigned.containsKey("redacted_because")
? Event.fromJson(unsigned["redacted_because"], room)
: null;
bool get redacted => redactedBecause != null;
User get stateKeyUser => room.getUserByMXIDSync(stateKey);
Event(
{this.status = defaultStatus,
dynamic content,
String typeKey,
String eventId,
String roomId,
String senderId,
ChatTime time,
dynamic unsigned,
dynamic prevContent,
String stateKey,
Room room,
Event redactedBecause})
: super(
content: content,
typeKey: typeKey,
eventId: eventId,
roomId: roomId,
senderId: senderId,
time: time,
unsigned: unsigned,
prevContent: prevContent,
stateKey: stateKey,
room: room);
this.content,
this.typeKey,
this.eventId,
this.roomId,
this.senderId,
this.time,
this.unsigned,
this.prevContent,
this.stateKey,
this.room});
static Map<String, dynamic> getMapFromPayload(dynamic payload) {
if (payload is String)
try {
return json.decode(payload);
} catch (e) {
return {};
}
if (payload is Map<String, dynamic>) return payload;
return {};
}
/// Get a State event from a table row or from the event stream.
factory Event.fromJson(Map<String, dynamic> jsonPayload, Room room) {
final Map<String, dynamic> content =
RoomState.getMapFromPayload(jsonPayload['content']);
Event.getMapFromPayload(jsonPayload['content']);
final Map<String, dynamic> unsigned =
RoomState.getMapFromPayload(jsonPayload['unsigned']);
Event.getMapFromPayload(jsonPayload['unsigned']);
final Map<String, dynamic> prevContent =
RoomState.getMapFromPayload(jsonPayload['prev_content']);
Event redactedBecause = null;
if (unsigned.containsKey("redacted_because"))
redactedBecause = Event.fromJson(unsigned["redacted_because"], room);
Event.getMapFromPayload(jsonPayload['prev_content']);
return Event(
status: jsonPayload['status'] ?? defaultStatus,
status: jsonPayload['status'] ?? defaultStatus,
stateKey: jsonPayload['state_key'],
prevContent: prevContent,
content: content,
typeKey: jsonPayload['type'],
eventId: jsonPayload['event_id'],
roomId: jsonPayload['room_id'],
senderId: jsonPayload['sender'],
time: jsonPayload.containsKey('origin_server_ts')
? DateTime.fromMillisecondsSinceEpoch(jsonPayload['origin_server_ts'])
: DateTime.now(),
unsigned: unsigned,
room: room,
);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
if (this.stateKey != null) data['state_key'] = this.stateKey;
if (this.prevContent != null && this.prevContent.isNotEmpty)
data['prev_content'] = this.prevContent;
data['content'] = this.content;
data['type'] = this.typeKey;
data['event_id'] = this.eventId;
data['room_id'] = this.roomId;
data['sender'] = this.senderId;
data['origin_server_ts'] = this.time.millisecondsSinceEpoch;
if (this.unsigned != null && this.unsigned.isNotEmpty)
data['unsigned'] = this.unsigned;
return data;
}
Event get timelineEvent => Event(
content: content,
typeKey: jsonPayload['type'],
eventId: jsonPayload['event_id'],
roomId: jsonPayload['room_id'],
senderId: jsonPayload['sender'],
time: ChatTime(jsonPayload['origin_server_ts']),
unsigned: unsigned,
prevContent: prevContent,
stateKey: jsonPayload['state_key'],
typeKey: typeKey,
eventId: eventId,
room: room,
redactedBecause: redactedBecause);
roomId: roomId,
senderId: senderId,
time: time,
unsigned: unsigned,
status: 1,
);
/// The unique key of this event. For events with a [stateKey], it will be the
/// stateKey. Otherwise it will be the [type] as a string.
@deprecated
String get key => stateKey == null || stateKey.isEmpty ? typeKey : stateKey;
User get asUser => User.fromState(
stateKey: stateKey,
prevContent: prevContent,
content: content,
typeKey: typeKey,
eventId: eventId,
roomId: roomId,
senderId: senderId,
time: time,
unsigned: unsigned,
room: room);
/// Get the real type.
EventTypes get type {
switch (typeKey) {
case "m.room.avatar":
return EventTypes.RoomAvatar;
case "m.room.name":
return EventTypes.RoomName;
case "m.room.topic":
return EventTypes.RoomTopic;
case "m.room.Aliases":
return EventTypes.RoomAliases;
case "m.room.canonical_alias":
return EventTypes.RoomCanonicalAlias;
case "m.room.create":
return EventTypes.RoomCreate;
case "m.room.redaction":
return EventTypes.Redaction;
case "m.room.join_rules":
return EventTypes.RoomJoinRules;
case "m.room.member":
return EventTypes.RoomMember;
case "m.room.power_levels":
return EventTypes.RoomPowerLevels;
case "m.room.guest_access":
return EventTypes.GuestAccess;
case "m.room.history_visibility":
return EventTypes.HistoryVisibility;
case "m.room.message":
switch (content["msgtype"] ?? "m.text") {
case "m.text":
if (content.containsKey("m.relates_to")) {
return EventTypes.Reply;
}
return EventTypes.Text;
case "m.notice":
return EventTypes.Notice;
case "m.emote":
return EventTypes.Emote;
case "m.image":
return EventTypes.Image;
case "m.video":
return EventTypes.Video;
case "m.audio":
return EventTypes.Audio;
case "m.file":
return EventTypes.File;
case "m.location":
return EventTypes.Location;
}
}
return EventTypes.Unknown;
}
void setRedactionEvent(Event redactedBecause) {
unsigned = {
"redacted_because": redactedBecause.toJson(),
};
prevContent = null;
List<String> contentKeyWhiteList = [];
switch (type) {
case EventTypes.RoomMember:
contentKeyWhiteList.add("membership");
break;
case EventTypes.RoomMember:
contentKeyWhiteList.add("membership");
break;
case EventTypes.RoomCreate:
contentKeyWhiteList.add("creator");
break;
case EventTypes.RoomJoinRules:
contentKeyWhiteList.add("join_rule");
break;
case EventTypes.RoomPowerLevels:
contentKeyWhiteList.add("ban");
contentKeyWhiteList.add("events");
contentKeyWhiteList.add("events_default");
contentKeyWhiteList.add("kick");
contentKeyWhiteList.add("redact");
contentKeyWhiteList.add("state_default");
contentKeyWhiteList.add("users");
contentKeyWhiteList.add("users_default");
break;
case EventTypes.RoomAliases:
contentKeyWhiteList.add("aliases");
break;
case EventTypes.HistoryVisibility:
contentKeyWhiteList.add("history_visibility");
break;
default:
break;
}
List<String> toRemoveList = [];
for (var entry in content.entries) {
if (contentKeyWhiteList.indexOf(entry.key) == -1) {
toRemoveList.add(entry.key);
}
}
toRemoveList.forEach((s) => content.remove(s));
}
/// Returns the body of this event if it has a body.
@ -110,8 +305,8 @@ class Event extends RoomState {
List<Receipt> receiptsList = [];
for (var entry in room.roomAccountData["m.receipt"].content.entries) {
if (entry.value["event_id"] == eventId)
receiptsList.add(Receipt(
room.getUserByMXIDSync(entry.key), ChatTime(entry.value["ts"])));
receiptsList.add(Receipt(room.getUserByMXIDSync(entry.key),
DateTime.fromMillisecondsSinceEpoch(entry.value["ts"])));
}
return receiptsList;
}
@ -123,7 +318,7 @@ class Event extends RoomState {
if (room.client.store != null)
await room.client.store.removeEvent(eventId);
room.client.connection.onEvent.add(EventUpdate(
room.client.onEvent.add(EventUpdate(
roomID: room.id,
type: "timeline",
eventType: typeKey,
@ -152,3 +347,28 @@ class Event extends RoomState {
Future<dynamic> redact({String reason, String txid}) =>
room.redactEvent(eventId, reason: reason, txid: txid);
}
enum EventTypes {
Text,
Emote,
Notice,
Image,
Video,
Audio,
Redaction,
File,
Location,
Reply,
RoomAliases,
RoomCanonicalAlias,
RoomCreate,
RoomJoinRules,
RoomMember,
RoomPowerLevels,
RoomName,
RoomTopic,
RoomAvatar,
GuestAccess,
HistoryVisibility,
Unknown,
}

View file

@ -42,8 +42,8 @@ class Presence {
Presence.fromJson(Map<String, dynamic> json)
: sender = json['sender'],
displayname = json['content']['avatar_url'],
avatarUrl = MxContent(json['content']['avatar_url']),
displayname = json['content']['displayname'],
avatarUrl = MxContent(json['content']['avatar_url'] ?? ""),
currentlyActive = json['content']['currently_active'],
lastActiveAgo = json['content']['last_active_ago'],
presence = PresenceType.values.firstWhere(

View file

@ -24,10 +24,8 @@
import 'package:famedlysdk/src/Client.dart';
import 'package:famedlysdk/src/Event.dart';
import 'package:famedlysdk/src/RoomAccountData.dart';
import 'package:famedlysdk/src/RoomState.dart';
import 'package:famedlysdk/src/sync/EventUpdate.dart';
import 'package:famedlysdk/src/sync/RoomUpdate.dart';
import 'package:famedlysdk/src/utils/ChatTime.dart';
import 'package:famedlysdk/src/utils/MatrixException.dart';
import 'package:famedlysdk/src/utils/MatrixFile.dart';
import 'package:famedlysdk/src/utils/MxContent.dart';
@ -35,7 +33,6 @@ import 'package:famedlysdk/src/utils/MxContent.dart';
import 'package:mime_type/mime_type.dart';
import './User.dart';
import 'Connection.dart';
import 'Timeline.dart';
import 'utils/StatesMap.dart';
@ -76,14 +73,14 @@ class Room {
/// Key-Value store for private account data only visible for this user.
Map<String, RoomAccountData> roomAccountData = {};
/// Returns the [RoomState] for the given [typeKey] and optional [stateKey].
/// Returns the [Event] for the given [typeKey] and optional [stateKey].
/// If no [stateKey] is provided, it defaults to an empty string.
RoomState getState(String typeKey, [String stateKey = ""]) =>
Event getState(String typeKey, [String stateKey = ""]) =>
states.states[typeKey] != null ? states.states[typeKey][stateKey] : null;
/// Adds the [state] to this room and overwrites a state with the same
/// typeKey/stateKey key pair if there is one.
void setState(RoomState state) {
void setState(Event state) {
if (!states.states.containsKey(state.typeKey))
states.states[state.typeKey] = {};
states.states[state.typeKey][state.stateKey ?? ""] = state;
@ -150,13 +147,15 @@ class Room {
String notificationSettings;
Event get lastEvent {
ChatTime lastTime = ChatTime(0);
DateTime lastTime = DateTime.fromMillisecondsSinceEpoch(0);
Event lastEvent = getState("m.room.message")?.timelineEvent;
if (lastEvent == null)
states.forEach((final String key, final entry) {
if (!entry.containsKey("")) return;
final RoomState state = entry[""];
if (state.time != null && state.time > lastTime) {
final Event state = entry[""];
if (state.time != null &&
state.time.millisecondsSinceEpoch >
lastTime.millisecondsSinceEpoch) {
lastTime = state.time;
lastEvent = state.timelineEvent;
}
@ -211,7 +210,7 @@ class Room {
} else {
if (states["m.room.member"] is Map<String, dynamic>) {
for (var entry in states["m.room.member"].entries) {
RoomState state = entry.value;
Event state = entry.value;
if (state.type == EventTypes.RoomMember &&
state.stateKey != client?.userID) heroes.add(state.stateKey);
}
@ -241,17 +240,17 @@ class Room {
}
/// When the last message received.
ChatTime get timeCreated {
DateTime get timeCreated {
if (lastEvent != null)
return lastEvent.time;
else
return ChatTime.now();
return DateTime.now();
}
/// Call the Matrix API to change the name of this room. Returns the event ID of the
/// new m.room.name event.
Future<String> setName(String newName) async {
final Map<String, dynamic> resp = await client.connection.jsonRequest(
final Map<String, dynamic> resp = await client.jsonRequest(
type: HTTPType.PUT,
action: "/client/r0/rooms/${id}/state/m.room.name",
data: {"name": newName});
@ -260,7 +259,7 @@ class Room {
/// Call the Matrix API to change the topic of this room.
Future<String> setDescription(String newName) async {
final Map<String, dynamic> resp = await client.connection.jsonRequest(
final Map<String, dynamic> resp = await client.jsonRequest(
type: HTTPType.PUT,
action: "/client/r0/rooms/${id}/state/m.room.topic",
data: {"topic": newName});
@ -270,7 +269,7 @@ class Room {
Future<String> _sendRawEventNow(Map<String, dynamic> content,
{String txid = null}) async {
if (txid == null) txid = "txid${DateTime.now().millisecondsSinceEpoch}";
final Map<String, dynamic> res = await client.connection.jsonRequest(
final Map<String, dynamic> res = await client.jsonRequest(
type: HTTPType.PUT,
action: "/client/r0/rooms/${id}/send/m.room.message/$txid",
data: content);
@ -289,7 +288,7 @@ class Room {
if (msgType == "m.video") return sendAudioEvent(file);
String fileName = file.path.split("/").last;
final String uploadResp = await client.connection.upload(file);
final String uploadResp = await client.upload(file);
// Send event
Map<String, dynamic> content = {
@ -308,7 +307,7 @@ class Room {
Future<String> sendAudioEvent(MatrixFile file,
{String txid = null, int width, int height}) async {
String fileName = file.path.split("/").last;
final String uploadResp = await client.connection.upload(file);
final String uploadResp = await client.upload(file);
Map<String, dynamic> content = {
"msgtype": "m.audio",
"body": fileName,
@ -325,7 +324,7 @@ class Room {
Future<String> sendImageEvent(MatrixFile file,
{String txid = null, int width, int height}) async {
String fileName = file.path.split("/").last;
final String uploadResp = await client.connection.upload(file);
final String uploadResp = await client.upload(file);
Map<String, dynamic> content = {
"msgtype": "m.image",
"body": fileName,
@ -349,7 +348,7 @@ class Room {
int thumbnailWidth,
int thumbnailHeight}) async {
String fileName = file.path.split("/").last;
final String uploadResp = await client.connection.upload(file);
final String uploadResp = await client.upload(file);
Map<String, dynamic> content = {
"msgtype": "m.video",
"body": fileName,
@ -370,7 +369,7 @@ class Room {
}
if (thumbnail != null) {
String thumbnailName = file.path.split("/").last;
final String thumbnailUploadResp = await client.connection.upload(file);
final String thumbnailUploadResp = await client.upload(file);
content["info"]["thumbnail_url"] = thumbnailUploadResp;
content["info"]["thumbnail_info"] = {
"size": thumbnail.size,
@ -408,7 +407,7 @@ class Room {
"origin_server_ts": now,
"content": content
});
client.connection.onEvent.add(eventUpdate);
client.onEvent.add(eventUpdate);
await client.store?.transaction(() {
client.store.storeEventUpdate(eventUpdate);
return;
@ -420,7 +419,7 @@ class Room {
eventUpdate.content["status"] = 1;
eventUpdate.content["unsigned"] = {"transaction_id": messageID};
eventUpdate.content["event_id"] = res;
client.connection.onEvent.add(eventUpdate);
client.onEvent.add(eventUpdate);
await client.store?.transaction(() {
client.store.storeEventUpdate(eventUpdate);
return;
@ -430,7 +429,7 @@ class Room {
// On error, set status to -1
eventUpdate.content["status"] = -1;
eventUpdate.content["unsigned"] = {"transaction_id": messageID};
client.connection.onEvent.add(eventUpdate);
client.onEvent.add(eventUpdate);
await client.store?.transaction(() {
client.store.storeEventUpdate(eventUpdate);
return;
@ -444,7 +443,7 @@ class Room {
/// automatically be set.
Future<void> join() async {
try {
await client.connection.jsonRequest(
await client.jsonRequest(
type: HTTPType.POST, action: "/client/r0/rooms/${id}/join");
if (states.containsKey(client.userID) &&
states[client.userID].content["is_direct"] is bool &&
@ -453,7 +452,7 @@ class Room {
} on MatrixException catch (exception) {
if (exception.errorMessage == "No known servers") {
client.store?.forgetRoom(id);
client.connection.onRoomUpdate.add(
client.onRoomUpdate.add(
RoomUpdate(
id: id,
membership: Membership.leave,
@ -469,7 +468,7 @@ class Room {
/// chat, this will be removed too.
Future<void> leave() async {
if (directChatMatrixID != "") await removeFromDirectChat();
await client.connection.jsonRequest(
await client.jsonRequest(
type: HTTPType.POST, action: "/client/r0/rooms/${id}/leave");
return;
}
@ -477,14 +476,14 @@ class Room {
/// Call the Matrix API to forget this room if you already left it.
Future<void> forget() async {
client.store.forgetRoom(id);
await client.connection.jsonRequest(
await client.jsonRequest(
type: HTTPType.POST, action: "/client/r0/rooms/${id}/forget");
return;
}
/// Call the Matrix API to kick a user from this room.
Future<void> kick(String userID) async {
await client.connection.jsonRequest(
await client.jsonRequest(
type: HTTPType.POST,
action: "/client/r0/rooms/${id}/kick",
data: {"user_id": userID});
@ -493,7 +492,7 @@ class Room {
/// Call the Matrix API to ban a user from this room.
Future<void> ban(String userID) async {
await client.connection.jsonRequest(
await client.jsonRequest(
type: HTTPType.POST,
action: "/client/r0/rooms/${id}/ban",
data: {"user_id": userID});
@ -502,7 +501,7 @@ class Room {
/// Call the Matrix API to unban a banned user from this room.
Future<void> unban(String userID) async {
await client.connection.jsonRequest(
await client.jsonRequest(
type: HTTPType.POST,
action: "/client/r0/rooms/${id}/unban",
data: {"user_id": userID});
@ -519,7 +518,7 @@ class Room {
if (powerMap["users"] == null) powerMap["users"] = {};
powerMap["users"][userID] = power;
final Map<String, dynamic> resp = await client.connection.jsonRequest(
final Map<String, dynamic> resp = await client.jsonRequest(
type: HTTPType.PUT,
action: "/client/r0/rooms/$id/state/m.room.power_levels",
data: powerMap);
@ -528,7 +527,7 @@ class Room {
/// Call the Matrix API to invite a user to this room.
Future<void> invite(String userID) async {
await client.connection.jsonRequest(
await client.jsonRequest(
type: HTTPType.POST,
action: "/client/r0/rooms/${id}/invite",
data: {"user_id": userID});
@ -540,10 +539,10 @@ class Room {
/// the historical events will be published in the onEvent stream.
Future<void> requestHistory(
{int historyCount = DefaultHistoryCount, onHistoryReceived}) async {
final dynamic resp = await client.connection.jsonRequest(
final dynamic resp = await client.jsonRequest(
type: HTTPType.GET,
action:
"/client/r0/rooms/$id/messages?from=${prev_batch}&dir=b&limit=$historyCount&filter=${Connection.syncFilters}");
"/client/r0/rooms/$id/messages?from=${prev_batch}&dir=b&limit=$historyCount&filter=${Client.syncFilters}");
if (onHistoryReceived != null) onHistoryReceived();
prev_batch = resp["end"];
@ -562,7 +561,7 @@ class Room {
eventType: resp["state"][i]["type"],
content: resp["state"][i],
);
client.connection.onEvent.add(eventUpdate);
client.onEvent.add(eventUpdate);
client.store.storeEventUpdate(eventUpdate);
}
return;
@ -575,7 +574,7 @@ class Room {
eventType: resp["state"][i]["type"],
content: resp["state"][i],
);
client.connection.onEvent.add(eventUpdate);
client.onEvent.add(eventUpdate);
}
}
}
@ -589,7 +588,7 @@ class Room {
eventType: history[i]["type"],
content: history[i],
);
client.connection.onEvent.add(eventUpdate);
client.onEvent.add(eventUpdate);
client.store.storeEventUpdate(eventUpdate);
client.store.txn.rawUpdate(
"UPDATE Rooms SET prev_batch=? WHERE room_id=?", [resp["end"], id]);
@ -604,10 +603,10 @@ class Room {
eventType: history[i]["type"],
content: history[i],
);
client.connection.onEvent.add(eventUpdate);
client.onEvent.add(eventUpdate);
}
}
client.connection.onRoomUpdate.add(
client.onRoomUpdate.add(
RoomUpdate(
id: id,
membership: membership,
@ -628,7 +627,7 @@ class Room {
else
directChats[userID] = [id];
await client.connection.jsonRequest(
await client.jsonRequest(
type: HTTPType.PUT,
action: "/client/r0/user/${client.userID}/account_data/m.direct",
data: directChats);
@ -644,7 +643,7 @@ class Room {
else
return; // Nothing to do here
await client.connection.jsonRequest(
await client.jsonRequest(
type: HTTPType.PUT,
action: "/client/r0/user/${client.userID}/account_data/m.direct",
data: directChats);
@ -655,7 +654,7 @@ class Room {
Future<void> sendReadReceipt(String eventID) async {
this.notificationCount = 0;
client?.store?.resetNotificationCount(this.id);
client.connection.jsonRequest(
client.jsonRequest(
type: HTTPType.POST,
action: "/client/r0/rooms/$id/read_markers",
data: {
@ -689,7 +688,7 @@ class Room {
if (states != null) {
List<Map<String, dynamic>> rawStates = await states;
for (int i = 0; i < rawStates.length; i++) {
RoomState newState = RoomState.fromJson(rawStates[i], newRoom);
Event newState = Event.fromJson(rawStates[i], newRoom);
newRoom.setState(newState);
}
}
@ -740,7 +739,7 @@ class Room {
List<User> userList = [];
if (states["m.room.member"] is Map<String, dynamic>) {
for (var entry in states["m.room.member"].entries) {
RoomState state = entry.value;
Event state = entry.value;
if (state.type == EventTypes.RoomMember) userList.add(state.asUser);
}
}
@ -752,11 +751,11 @@ class Room {
Future<List<User>> requestParticipants() async {
List<User> participants = [];
dynamic res = await client.connection.jsonRequest(
dynamic res = await client.jsonRequest(
type: HTTPType.GET, action: "/client/r0/rooms/${id}/members");
for (num i = 0; i < res["chunk"].length; i++) {
User newUser = RoomState.fromJson(res["chunk"][i], this).asUser;
User newUser = Event.fromJson(res["chunk"][i], this).asUser;
if (newUser.membership != Membership.leave) participants.add(newUser);
}
@ -791,7 +790,7 @@ class Room {
if (mxID == null || !_requestingMatrixIds.add(mxID)) return null;
Map<String, dynamic> resp;
try {
resp = await client.connection.jsonRequest(
resp = await client.jsonRequest(
type: HTTPType.GET,
action: "/client/r0/rooms/$id/state/m.room.member/$mxID");
} catch (exception) {
@ -821,7 +820,7 @@ class Room {
/// Searches for the event on the server. Returns null if not found.
Future<Event> getEventById(String eventID) async {
final dynamic resp = await client.connection.jsonRequest(
final dynamic resp = await client.jsonRequest(
type: HTTPType.GET, action: "/client/r0/rooms/$id/event/$eventID");
return Event.fromJson(resp, this);
}
@ -829,7 +828,7 @@ class Room {
/// Returns the power level of the given user ID.
int getPowerLevelByUserId(String userId) {
int powerLevel = 0;
RoomState powerLevelState = states["m.room.power_levels"];
Event powerLevelState = states["m.room.power_levels"];
if (powerLevelState == null) return powerLevel;
if (powerLevelState.content["users_default"] is int)
powerLevel = powerLevelState.content["users_default"];
@ -844,7 +843,7 @@ class Room {
/// Returns the power levels from all users for this room or null if not given.
Map<String, int> get powerLevels {
RoomState powerLevelState = states["m.room.power_levels"];
Event powerLevelState = states["m.room.power_levels"];
if (powerLevelState.content["users"] is Map<String, int>)
return powerLevelState.content["users"];
return null;
@ -853,12 +852,11 @@ class Room {
/// Uploads a new user avatar for this room. Returns the event ID of the new
/// m.room.avatar event.
Future<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});
final String uploadResp = await client.upload(file);
final Map<String, dynamic> setAvatarResp = await client.jsonRequest(
type: HTTPType.PUT,
action: "/client/r0/rooms/$id/state/m.room.avatar/",
data: {"url": uploadResp});
return setAvatarResp["event_id"];
}
@ -946,12 +944,12 @@ class Room {
// All push notifications should be sent to the user
case PushRuleState.notify:
if (pushRuleState == PushRuleState.dont_notify)
resp = await client.connection.jsonRequest(
resp = await client.jsonRequest(
type: HTTPType.DELETE,
action: "/client/r0/pushrules/global/override/$id",
data: {});
else if (pushRuleState == PushRuleState.mentions_only)
resp = await client.connection.jsonRequest(
resp = await client.jsonRequest(
type: HTTPType.DELETE,
action: "/client/r0/pushrules/global/room/$id",
data: {});
@ -959,18 +957,18 @@ class Room {
// Only when someone mentions the user, a push notification should be sent
case PushRuleState.mentions_only:
if (pushRuleState == PushRuleState.dont_notify) {
resp = await client.connection.jsonRequest(
resp = await client.jsonRequest(
type: HTTPType.DELETE,
action: "/client/r0/pushrules/global/override/$id",
data: {});
resp = await client.connection.jsonRequest(
resp = await client.jsonRequest(
type: HTTPType.PUT,
action: "/client/r0/pushrules/global/room/$id",
data: {
"actions": ["dont_notify"]
});
} else if (pushRuleState == PushRuleState.notify)
resp = await client.connection.jsonRequest(
resp = await client.jsonRequest(
type: HTTPType.PUT,
action: "/client/r0/pushrules/global/room/$id",
data: {
@ -980,12 +978,12 @@ class Room {
// No push notification should be ever sent for this room.
case PushRuleState.dont_notify:
if (pushRuleState == PushRuleState.mentions_only) {
resp = await client.connection.jsonRequest(
resp = await client.jsonRequest(
type: HTTPType.DELETE,
action: "/client/r0/pushrules/global/room/$id",
data: {});
}
resp = await client.connection.jsonRequest(
resp = await client.jsonRequest(
type: HTTPType.PUT,
action: "/client/r0/pushrules/global/override/$id",
data: {
@ -1010,7 +1008,7 @@ class Room {
messageID = txid;
Map<String, dynamic> data = {};
if (reason != null) data["reason"] = reason;
final dynamic resp = await client.connection.jsonRequest(
final dynamic resp = await client.jsonRequest(
type: HTTPType.PUT,
action: "/client/r0/rooms/$id/redact/$eventId/$messageID",
data: data);
@ -1022,7 +1020,7 @@ class Room {
"typing": isTyping,
};
if (timeout != null) data["timeout"] = timeout;
return client.connection.jsonRequest(
return client.jsonRequest(
type: HTTPType.PUT,
action: "/client/r0/rooms/${this.id}/typing/${client.userID}",
data: data,

View file

@ -23,7 +23,7 @@
import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/src/AccountData.dart';
import 'package:famedlysdk/src/RoomState.dart';
import 'package:famedlysdk/src/Event.dart';
/// Stripped down events for account data and ephemrals of a room.
class RoomAccountData extends AccountData {
@ -40,7 +40,7 @@ class RoomAccountData extends AccountData {
factory RoomAccountData.fromJson(
Map<String, dynamic> jsonPayload, Room room) {
final Map<String, dynamic> content =
RoomState.getMapFromPayload(jsonPayload['content']);
Event.getMapFromPayload(jsonPayload['content']);
return RoomAccountData(
content: content,
typeKey: jsonPayload['type'],

View file

@ -1,226 +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 'dart:async';
import 'dart:core';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/src/RoomState.dart';
import 'Client.dart';
import 'Room.dart';
import 'User.dart';
import 'sync/EventUpdate.dart';
import 'sync/RoomUpdate.dart';
typedef onRoomListUpdateCallback = void Function();
typedef onRoomListInsertCallback = void Function(int insertID);
typedef onRoomListRemoveCallback = void Function(int insertID);
/// Represents a list of rooms for this client, which will automatically update
/// itself and call the [onUpdate], [onInsert] and [onDelete] callbacks. To get
/// the initial room list, use the store or create a RoomList instance by using
/// [client.getRoomList].
class RoomList {
final Client client;
List<Room> rooms = [];
final bool onlyLeft;
/// Will be called, when the room list has changed. Can be used e.g. to update
/// the state of a StatefulWidget.
final onRoomListUpdateCallback onUpdate;
/// Will be called, when a new room is added to the list.
final onRoomListInsertCallback onInsert;
/// Will be called, when a room has been removed from the list.
final onRoomListRemoveCallback onRemove;
StreamSubscription<EventUpdate> eventSub;
StreamSubscription<RoomUpdate> roomSub;
StreamSubscription<bool> firstSyncSub;
RoomList(
{this.client,
this.rooms,
this.onUpdate,
this.onInsert,
this.onRemove,
this.onlyLeft = false}) {
eventSub ??= client.connection.onEvent.stream.listen(_handleEventUpdate);
roomSub ??= client.connection.onRoomUpdate.stream.listen(_handleRoomUpdate);
firstSyncSub ??=
client.connection.onFirstSync.stream.listen((b) => sortAndUpdate());
sort();
}
RoomList copyWith({
Client client,
List<Room> rooms,
onRoomListUpdateCallback onUpdate,
onRoomListInsertCallback onInsert,
onRoomListRemoveCallback onRemove,
bool onlyLeft,
}) {
return RoomList(
client: client ?? this.client,
rooms: rooms ?? this.rooms,
onUpdate: onUpdate ?? this.onUpdate,
onInsert: onInsert ?? this.onInsert,
onRemove: onRemove ?? this.onRemove,
onlyLeft: onlyLeft ?? this.onlyLeft,
);
}
Room getRoomByAlias(String alias) {
for (int i = 0; i < rooms.length; i++) {
if (rooms[i].canonicalAlias == alias) return rooms[i];
}
return null;
}
Room getRoomById(String id) {
for (int j = 0; j < rooms.length; j++) {
if (rooms[j].id == id) return rooms[j];
}
return null;
}
void _handleRoomUpdate(RoomUpdate chatUpdate) {
// Update the chat list item.
// Search the room in the rooms
num j = 0;
for (j = 0; j < rooms.length; j++) {
if (rooms[j].id == chatUpdate.id) break;
}
final bool found = (j < rooms.length && rooms[j].id == chatUpdate.id);
final bool isLeftRoom = chatUpdate.membership == Membership.leave;
// Does the chat already exist in the list rooms?
if (!found && ((!onlyLeft && !isLeftRoom) || (onlyLeft && isLeftRoom))) {
num position = chatUpdate.membership == Membership.invite ? 0 : j;
// Add the new chat to the list
Room newRoom = Room(
id: chatUpdate.id,
membership: chatUpdate.membership,
prev_batch: chatUpdate.prev_batch,
highlightCount: chatUpdate.highlight_count,
notificationCount: chatUpdate.notification_count,
mHeroes: chatUpdate.summary?.mHeroes,
mJoinedMemberCount: chatUpdate.summary?.mJoinedMemberCount,
mInvitedMemberCount: chatUpdate.summary?.mInvitedMemberCount,
roomAccountData: {},
client: client,
);
rooms.insert(position, newRoom);
if (onInsert != null) onInsert(position);
}
// If the membership is "leave" or not "leave" but onlyLeft=true then remove the item and stop here
else if (found &&
((!onlyLeft && isLeftRoom) || (onlyLeft && !isLeftRoom))) {
rooms.removeAt(j);
if (onRemove != null) onRemove(j);
}
// Update notification, highlight count and/or additional informations
else if (found &&
chatUpdate.membership != Membership.leave &&
(rooms[j].membership != chatUpdate.membership ||
rooms[j].notificationCount != chatUpdate.notification_count ||
rooms[j].highlightCount != chatUpdate.highlight_count ||
chatUpdate.summary != null)) {
rooms[j].membership = chatUpdate.membership;
rooms[j].notificationCount = chatUpdate.notification_count;
rooms[j].highlightCount = chatUpdate.highlight_count;
if (chatUpdate.prev_batch != null)
rooms[j].prev_batch = chatUpdate.prev_batch;
if (chatUpdate.summary != null) {
if (chatUpdate.summary.mHeroes != null)
rooms[j].mHeroes = chatUpdate.summary.mHeroes;
if (chatUpdate.summary.mJoinedMemberCount != null)
rooms[j].mJoinedMemberCount = chatUpdate.summary.mJoinedMemberCount;
if (chatUpdate.summary.mInvitedMemberCount != null)
rooms[j].mInvitedMemberCount = chatUpdate.summary.mInvitedMemberCount;
}
if (rooms[j].onUpdate != null) rooms[j].onUpdate();
}
sortAndUpdate();
}
void _handleEventUpdate(EventUpdate eventUpdate) {
if (eventUpdate.type == "history") return;
// Search the room in the rooms
num j = 0;
for (j = 0; j < rooms.length; j++) {
if (rooms[j].id == eventUpdate.roomID) break;
}
final bool found = (j < rooms.length && rooms[j].id == eventUpdate.roomID);
if (!found) return;
if (eventUpdate.type == "timeline" ||
eventUpdate.type == "state" ||
eventUpdate.type == "invite_state") {
RoomState stateEvent = RoomState.fromJson(eventUpdate.content, rooms[j]);
if (stateEvent.type == EventTypes.Redaction) {
final String redacts = eventUpdate.content["redacts"];
rooms[j].states.states.forEach(
(String key, Map<String, RoomState> states) => states.forEach(
(String key, RoomState state) {
if (state.eventId == redacts) {
state.setRedactionEvent(stateEvent);
}
},
),
);
} else {
RoomState prevState =
rooms[j].getState(stateEvent.typeKey, stateEvent.stateKey);
if (prevState != null && prevState.time > stateEvent.time) return;
rooms[j].setState(stateEvent);
}
} else if (eventUpdate.type == "account_data") {
rooms[j].roomAccountData[eventUpdate.eventType] =
RoomAccountData.fromJson(eventUpdate.content, rooms[j]);
} else if (eventUpdate.type == "ephemeral") {
rooms[j].ephemerals[eventUpdate.eventType] =
RoomAccountData.fromJson(eventUpdate.content, rooms[j]);
}
if (rooms[j].onUpdate != null) rooms[j].onUpdate();
if (eventUpdate.type == "timeline") sortAndUpdate();
}
bool sortLock = false;
sort() {
if (sortLock || rooms.length < 2) return;
sortLock = true;
rooms?.sort((a, b) =>
b.timeCreated.toTimeStamp().compareTo(a.timeCreated.toTimeStamp()));
sortLock = false;
}
sortAndUpdate() {
if (client.prevBatch == null) return;
sort();
if (onUpdate != null) onUpdate();
}
}

View file

@ -1,291 +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 'dart:convert';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/src/utils/ChatTime.dart';
import './Room.dart';
class RoomState {
/// The Matrix ID for this event in the format '$localpart:server.abc'. Please not
/// that account data, presence and other events may not have an eventId.
final String eventId;
/// The json payload of the content. The content highly depends on the type.
Map<String, dynamic> content;
/// The type String of this event. For example 'm.room.message'.
final String typeKey;
/// The ID of the room this event belongs to.
final String roomId;
/// The user who has sent this event if it is not a global account data event.
final String senderId;
User get sender => room.getUserByMXIDSync(senderId);
/// The time this event has received at the server. May be null for events like
/// account data.
final ChatTime time;
/// Optional additional content for this event.
Map<String, dynamic> unsigned;
/// The room this event belongs to. May be null.
final Room room;
/// Optional. The previous content for this state.
/// This will be present only for state events appearing in the timeline.
/// If this is not a state event, or there is no previous content, this key will be null.
Map<String, dynamic> prevContent;
/// Optional. This key will only be present for state events. A unique key which defines
/// the overwriting semantics for this piece of room state.
final String stateKey;
/// Optional. The event that redacted this event, if any. Otherwise null.
RoomState get redactedBecause =>
unsigned != null && unsigned.containsKey("redacted_because")
? RoomState.fromJson(unsigned["redacted_because"], room)
: null;
bool get redacted => redactedBecause != null;
User get stateKeyUser => room.getUserByMXIDSync(stateKey);
RoomState(
{this.content,
this.typeKey,
this.eventId,
this.roomId,
this.senderId,
this.time,
this.unsigned,
this.prevContent,
this.stateKey,
this.room});
static Map<String, dynamic> getMapFromPayload(dynamic payload) {
if (payload is String)
try {
return json.decode(payload);
} catch (e) {
return {};
}
if (payload is Map<String, dynamic>) return payload;
return {};
}
/// Get a State event from a table row or from the event stream.
factory RoomState.fromJson(Map<String, dynamic> jsonPayload, Room room) {
final Map<String, dynamic> content =
RoomState.getMapFromPayload(jsonPayload['content']);
final Map<String, dynamic> unsigned =
RoomState.getMapFromPayload(jsonPayload['unsigned']);
final Map<String, dynamic> prevContent =
RoomState.getMapFromPayload(jsonPayload['prev_content']);
return RoomState(
stateKey: jsonPayload['state_key'],
prevContent: prevContent,
content: content,
typeKey: jsonPayload['type'],
eventId: jsonPayload['event_id'],
roomId: jsonPayload['room_id'],
senderId: jsonPayload['sender'],
time: ChatTime(jsonPayload['origin_server_ts']),
unsigned: unsigned,
room: room,
);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
if (this.stateKey != null) data['state_key'] = this.stateKey;
if (this.prevContent != null && this.prevContent.isNotEmpty)
data['prev_content'] = this.prevContent;
data['content'] = this.content;
data['type'] = this.typeKey;
data['event_id'] = this.eventId;
data['room_id'] = this.roomId;
data['sender'] = this.senderId;
data['origin_server_ts'] = this.time.toTimeStamp();
if (this.unsigned != null && this.unsigned.isNotEmpty)
data['unsigned'] = this.unsigned;
return data;
}
Event get timelineEvent => Event(
content: content,
typeKey: typeKey,
eventId: eventId,
room: room,
roomId: roomId,
senderId: senderId,
time: time,
unsigned: unsigned,
status: 1,
);
/// The unique key of this event. For events with a [stateKey], it will be the
/// stateKey. Otherwise it will be the [type] as a string.
@deprecated
String get key => stateKey == null || stateKey.isEmpty ? typeKey : stateKey;
User get asUser => User.fromState(
stateKey: stateKey,
prevContent: prevContent,
content: content,
typeKey: typeKey,
eventId: eventId,
roomId: roomId,
senderId: senderId,
time: time,
unsigned: unsigned,
room: room);
/// Get the real type.
EventTypes get type {
switch (typeKey) {
case "m.room.avatar":
return EventTypes.RoomAvatar;
case "m.room.name":
return EventTypes.RoomName;
case "m.room.topic":
return EventTypes.RoomTopic;
case "m.room.Aliases":
return EventTypes.RoomAliases;
case "m.room.canonical_alias":
return EventTypes.RoomCanonicalAlias;
case "m.room.create":
return EventTypes.RoomCreate;
case "m.room.redaction":
return EventTypes.Redaction;
case "m.room.join_rules":
return EventTypes.RoomJoinRules;
case "m.room.member":
return EventTypes.RoomMember;
case "m.room.power_levels":
return EventTypes.RoomPowerLevels;
case "m.room.guest_access":
return EventTypes.GuestAccess;
case "m.room.history_visibility":
return EventTypes.HistoryVisibility;
case "m.room.message":
switch (content["msgtype"] ?? "m.text") {
case "m.text":
if (content.containsKey("m.relates_to")) {
return EventTypes.Reply;
}
return EventTypes.Text;
case "m.notice":
return EventTypes.Notice;
case "m.emote":
return EventTypes.Emote;
case "m.image":
return EventTypes.Image;
case "m.video":
return EventTypes.Video;
case "m.audio":
return EventTypes.Audio;
case "m.file":
return EventTypes.File;
case "m.location":
return EventTypes.Location;
}
}
return EventTypes.Unknown;
}
void setRedactionEvent(RoomState redactedBecause) {
unsigned = {
"redacted_because": redactedBecause.toJson(),
};
prevContent = null;
List<String> contentKeyWhiteList = [];
switch (type) {
case EventTypes.RoomMember:
contentKeyWhiteList.add("membership");
break;
case EventTypes.RoomMember:
contentKeyWhiteList.add("membership");
break;
case EventTypes.RoomCreate:
contentKeyWhiteList.add("creator");
break;
case EventTypes.RoomJoinRules:
contentKeyWhiteList.add("join_rule");
break;
case EventTypes.RoomPowerLevels:
contentKeyWhiteList.add("ban");
contentKeyWhiteList.add("events");
contentKeyWhiteList.add("events_default");
contentKeyWhiteList.add("kick");
contentKeyWhiteList.add("redact");
contentKeyWhiteList.add("state_default");
contentKeyWhiteList.add("users");
contentKeyWhiteList.add("users_default");
break;
case EventTypes.RoomAliases:
contentKeyWhiteList.add("aliases");
break;
case EventTypes.HistoryVisibility:
contentKeyWhiteList.add("history_visibility");
break;
default:
break;
}
List<String> toRemoveList = [];
for (var entry in content.entries) {
if (contentKeyWhiteList.indexOf(entry.key) == -1) {
toRemoveList.add(entry.key);
}
}
toRemoveList.forEach((s) => content.remove(s));
}
}
enum EventTypes {
Text,
Emote,
Notice,
Image,
Video,
Audio,
Redaction,
File,
Location,
Reply,
RoomAliases,
RoomCanonicalAlias,
RoomCreate,
RoomJoinRules,
RoomMember,
RoomPowerLevels,
RoomName,
RoomTopic,
RoomAvatar,
GuestAccess,
HistoryVisibility,
Unknown,
}

View file

@ -75,7 +75,7 @@ class Timeline {
}
Timeline({this.room, this.events, this.onUpdate, this.onInsert}) {
sub ??= room.client.connection.onEvent.stream.listen(_handleEventUpdate);
sub ??= room.client.onEvent.stream.listen(_handleEventUpdate);
}
int _findEvent({String event_id, String unsigned_txid}) {
@ -148,8 +148,8 @@ class Timeline {
sort() {
if (sortLock || events.length < 2) return;
sortLock = true;
events
?.sort((a, b) => b.time.toTimeStamp().compareTo(a.time.toTimeStamp()));
events?.sort((a, b) =>
b.time.millisecondsSinceEpoch.compareTo(a.time.millisecondsSinceEpoch));
sortLock = false;
}

View file

@ -23,16 +23,13 @@
import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/src/Room.dart';
import 'package:famedlysdk/src/RoomState.dart';
import 'package:famedlysdk/src/utils/ChatTime.dart';
import 'package:famedlysdk/src/Event.dart';
import 'package:famedlysdk/src/utils/MxContent.dart';
import 'Connection.dart';
enum Membership { join, invite, leave, ban }
/// Represents a Matrix User which may be a participant in a Matrix Room.
class User extends RoomState {
class User extends Event {
factory User(
String id, {
String membership,
@ -50,7 +47,7 @@ class User extends RoomState {
typeKey: "m.room.member",
roomId: room?.id,
room: room,
time: ChatTime.now(),
time: DateTime.now(),
);
}
@ -62,7 +59,7 @@ class User extends RoomState {
String eventId,
String roomId,
String senderId,
ChatTime time,
DateTime time,
dynamic unsigned,
Room room})
: super(
@ -131,7 +128,7 @@ class User extends RoomState {
if (roomID != null) return roomID;
// Start a new direct chat
final dynamic resp = await room.client.connection.jsonRequest(
final dynamic resp = await room.client.jsonRequest(
type: HTTPType.POST,
action: "/client/r0/createRoom",
data: {

View file

@ -1,81 +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 famedly. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:json_annotation/json_annotation.dart';
part 'SetPushersRequest.g.dart';
@JsonSerializable(explicitToJson: true, nullable: false, includeIfNull: false)
class SetPushersRequest {
// Required Keys
@JsonKey(nullable: false)
String lang;
@JsonKey(nullable: false)
String device_display_name;
@JsonKey(nullable: false)
String app_display_name;
@JsonKey(nullable: false)
String app_id;
@JsonKey(nullable: false)
String kind;
@JsonKey(nullable: false)
String pushkey;
@JsonKey(nullable: false)
PusherData data;
// Optional keys
String profile_tag;
bool append;
SetPushersRequest({
this.lang,
this.device_display_name,
this.app_display_name,
this.app_id,
this.kind,
this.pushkey,
this.data,
this.profile_tag,
this.append,
});
factory SetPushersRequest.fromJson(Map<String, dynamic> json) =>
_$SetPushersRequestFromJson(json);
Map<String, dynamic> toJson() => _$SetPushersRequestToJson(this);
}
@JsonSerializable(explicitToJson: true, nullable: false, includeIfNull: false)
class PusherData {
String url;
String format;
PusherData({
this.url,
this.format,
});
factory PusherData.fromJson(Map<String, dynamic> json) =>
_$PusherDataFromJson(json);
Map<String, dynamic> toJson() => _$PusherDataToJson(this);
}

View file

@ -1,41 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'SetPushersRequest.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SetPushersRequest _$SetPushersRequestFromJson(Map<String, dynamic> json) {
return SetPushersRequest(
lang: json['lang'] as String,
device_display_name: json['device_display_name'] as String,
app_display_name: json['app_display_name'] as String,
app_id: json['app_id'] as String,
kind: json['kind'] as String,
pushkey: json['pushkey'] as String,
data: PusherData.fromJson(json['data'] as Map<String, dynamic>),
profile_tag: json['profile_tag'] as String,
append: json['append'] as bool);
}
Map<String, dynamic> _$SetPushersRequestToJson(SetPushersRequest instance) =>
<String, dynamic>{
'lang': instance.lang,
'device_display_name': instance.device_display_name,
'app_display_name': instance.app_display_name,
'app_id': instance.app_id,
'kind': instance.kind,
'pushkey': instance.pushkey,
'data': instance.data.toJson(),
'profile_tag': instance.profile_tag,
'append': instance.append
};
PusherData _$PusherDataFromJson(Map<String, dynamic> json) {
return PusherData(
url: json['url'] as String, format: json['format'] as String);
}
Map<String, dynamic> _$PusherDataToJson(PusherData instance) =>
<String, dynamic>{'url': instance.url, 'format': instance.format};

View file

@ -1,111 +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 famedly. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:json_annotation/json_annotation.dart';
part 'PushrulesResponse.g.dart';
@JsonSerializable(explicitToJson: true, nullable: false)
class PushrulesResponse {
@JsonKey(nullable: false)
Global global;
PushrulesResponse(
this.global,
);
factory PushrulesResponse.fromJson(Map<String, dynamic> json) =>
_$PushrulesResponseFromJson(json);
Map<String, dynamic> toJson() => _$PushrulesResponseToJson(this);
}
@JsonSerializable(explicitToJson: true)
class Global {
List<PushRule> content;
List<PushRule> room;
List<PushRule> sender;
List<PushRule> override;
List<PushRule> underride;
Global(
this.content,
this.room,
this.sender,
this.override,
this.underride,
);
factory Global.fromJson(Map<String, dynamic> json) => _$GlobalFromJson(json);
Map<String, dynamic> toJson() => _$GlobalToJson(this);
}
@JsonSerializable(explicitToJson: true)
class PushRule {
@JsonKey(nullable: false)
List<dynamic> actions;
List<Condition> conditions;
@JsonKey(nullable: false, name: "default")
bool contentDefault;
@JsonKey(nullable: false)
bool enabled;
@JsonKey(nullable: false)
String ruleId;
String pattern;
PushRule(
this.actions,
this.conditions,
this.contentDefault,
this.enabled,
this.ruleId,
this.pattern,
);
factory PushRule.fromJson(Map<String, dynamic> json) =>
_$PushRuleFromJson(json);
Map<String, dynamic> toJson() => _$PushRuleToJson(this);
}
@JsonSerializable(explicitToJson: true)
class Condition {
String key;
@JsonKey(name: "is")
String conditionIs;
@JsonKey(nullable: false)
String kind;
String pattern;
Condition(
this.key,
this.conditionIs,
this.kind,
this.pattern,
);
factory Condition.fromJson(Map<String, dynamic> json) =>
_$ConditionFromJson(json);
Map<String, dynamic> toJson() => _$ConditionToJson(this);
}

View file

@ -1,81 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'PushrulesResponse.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PushrulesResponse _$PushrulesResponseFromJson(Map<String, dynamic> json) {
return PushrulesResponse(
Global.fromJson(json['global'] as Map<String, dynamic>));
}
Map<String, dynamic> _$PushrulesResponseToJson(PushrulesResponse instance) =>
<String, dynamic>{'global': instance.global.toJson()};
Global _$GlobalFromJson(Map<String, dynamic> json) {
return Global(
(json['content'] as List)
?.map((e) =>
e == null ? null : PushRule.fromJson(e as Map<String, dynamic>))
?.toList(),
(json['room'] as List)
?.map((e) =>
e == null ? null : PushRule.fromJson(e as Map<String, dynamic>))
?.toList(),
(json['sender'] as List)
?.map((e) =>
e == null ? null : PushRule.fromJson(e as Map<String, dynamic>))
?.toList(),
(json['override'] as List)
?.map((e) =>
e == null ? null : PushRule.fromJson(e as Map<String, dynamic>))
?.toList(),
(json['underride'] as List)
?.map((e) =>
e == null ? null : PushRule.fromJson(e as Map<String, dynamic>))
?.toList());
}
Map<String, dynamic> _$GlobalToJson(Global instance) => <String, dynamic>{
'content': instance.content?.map((e) => e?.toJson())?.toList(),
'room': instance.room?.map((e) => e?.toJson())?.toList(),
'sender': instance.sender?.map((e) => e?.toJson())?.toList(),
'override': instance.override?.map((e) => e?.toJson())?.toList(),
'underride': instance.underride?.map((e) => e?.toJson())?.toList()
};
PushRule _$PushRuleFromJson(Map<String, dynamic> json) {
return PushRule(
json['actions'] as List,
(json['conditions'] as List)
?.map((e) =>
e == null ? null : Condition.fromJson(e as Map<String, dynamic>))
?.toList(),
json['default'] as bool,
json['enabled'] as bool,
json['ruleId'] as String,
json['pattern'] as String);
}
Map<String, dynamic> _$PushRuleToJson(PushRule instance) => <String, dynamic>{
'actions': instance.actions,
'conditions': instance.conditions?.map((e) => e?.toJson())?.toList(),
'default': instance.contentDefault,
'enabled': instance.enabled,
'ruleId': instance.ruleId,
'pattern': instance.pattern
};
Condition _$ConditionFromJson(Map<String, dynamic> json) {
return Condition(json['key'] as String, json['is'] as String,
json['kind'] as String, json['pattern'] as String);
}
Map<String, dynamic> _$ConditionToJson(Condition instance) => <String, dynamic>{
'key': instance.key,
'is': instance.conditionIs,
'kind': instance.kind,
'pattern': instance.pattern
};

View file

@ -1,139 +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/>.
*/
/// Used to localize and present time in a chat application manner.
class ChatTime {
DateTime dateTime = DateTime.now();
/// Insert with a timestamp [ts] which represents the milliseconds since
/// the Unix epoch.
ChatTime(num ts) {
if (ts != null) dateTime = DateTime.fromMillisecondsSinceEpoch(ts);
}
/// Returns a ChatTime object which represents the current time.
ChatTime.now() {
dateTime = DateTime.now();
}
/// Returns [toTimeString()] if the ChatTime is today, the name of the week
/// day if the ChatTime is this week and a date string else.
String toString() {
DateTime now = DateTime.now();
bool sameYear = now.year == dateTime.year;
bool sameDay =
sameYear && now.month == dateTime.month && now.day == dateTime.day;
bool sameWeek = sameYear &&
!sameDay &&
now.millisecondsSinceEpoch - dateTime.millisecondsSinceEpoch <
1000 * 60 * 60 * 24 * 7;
if (sameDay) {
return toTimeString();
} else if (sameWeek) {
switch (dateTime.weekday) {
case 1:
return "Montag";
case 2:
return "Dienstag";
case 3:
return "Mittwoch";
case 4:
return "Donnerstag";
case 5:
return "Freitag";
case 6:
return "Samstag";
case 7:
return "Sonntag";
}
} else if (sameYear) {
return "${_z(dateTime.day)}.${_z(dateTime.month)}";
}
return "${_z(dateTime.day)}.${_z(dateTime.month)}.${_z(dateTime.year)}";
}
/// Returns the milliseconds since the Unix epoch.
num toTimeStamp() {
return dateTime.millisecondsSinceEpoch;
}
operator <(ChatTime other) {
return this.toTimeStamp() < other.toTimeStamp();
}
operator >(ChatTime other) {
return this.toTimeStamp() > other.toTimeStamp();
}
operator >=(ChatTime other) {
return this.toTimeStamp() >= other.toTimeStamp();
}
operator <=(ChatTime other) {
return this.toTimeStamp() <= other.toTimeStamp();
}
operator ==(dynamic other) {
if (other is ChatTime)
return this.toTimeStamp() == other.toTimeStamp();
else
return false;
}
/// Two message events can belong to the same environment. That means that they
/// don't need to display the time they were sent because they are close
/// enaugh.
static final minutesBetweenEnvironments = 5;
/// Checks if two ChatTimes are close enough to belong to the same
/// environment.
bool sameEnvironment(ChatTime prevTime) {
return toTimeStamp() - prevTime.toTimeStamp() <
1000 * 60 * minutesBetweenEnvironments;
}
/// Returns a simple time String.
String toTimeString() {
return "${_z(dateTime.hour)}:${_z(dateTime.minute)}";
}
/// If the ChatTime is today, this returns [toTimeString()], if not it also
/// shows the date.
String toEventTimeString() {
DateTime now = DateTime.now();
bool sameYear = now.year == dateTime.year;
bool sameDay =
sameYear && now.month == dateTime.month && now.day == dateTime.day;
if (sameDay) return toTimeString();
return "${toString()}, ${toTimeString()}";
}
static String _z(int i) => i < 10 ? "0${i.toString()}" : i.toString();
}

View file

@ -1,3 +1,4 @@
/// Workaround until [File] in dart:io and dart:html is unified
class MatrixFile {
List<int> bytes;
String path;

View file

@ -1,64 +0,0 @@
class PushRule {
final String ruleId;
final bool isDefault;
final bool enabled;
final List<Conditions> conditions;
final List<dynamic> actions;
PushRule(
{this.ruleId,
this.isDefault,
this.enabled,
this.conditions,
this.actions});
PushRule.fromJson(Map<String, dynamic> json)
: ruleId = json['rule_id'],
isDefault = json['is_default'],
enabled = json['enabled'],
conditions = _getConditionsFromJson(json['conditions']),
actions = json['actions'];
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['rule_id'] = this.ruleId;
data['is_default'] = this.isDefault;
data['enabled'] = this.enabled;
if (this.conditions != null) {
data['conditions'] = this.conditions.map((v) => v.toJson()).toList();
}
data['actions'] = this.actions;
return data;
}
static List<Conditions> _getConditionsFromJson(List<dynamic> json) {
List<Conditions> conditions = [];
if (json == null) return conditions;
for (int i = 0; i < json.length; i++) {
conditions.add(Conditions.fromJson(json[i]));
}
return conditions;
}
}
class Conditions {
String key;
String kind;
String pattern;
Conditions({this.key, this.kind, this.pattern});
Conditions.fromJson(Map<String, dynamic> json) {
key = json['key'];
kind = json['kind'];
pattern = json['pattern'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['key'] = this.key;
data['kind'] = this.kind;
data['pattern'] = this.pattern;
return data;
}
}

View file

@ -0,0 +1,83 @@
/// The global ruleset.
class PushRules {
final GlobalPushRules global;
PushRules.fromJson(Map<String, dynamic> json)
: this.global = GlobalPushRules.fromJson(json["global"]);
}
/// The global ruleset.
class GlobalPushRules {
final List<PushRule> content;
final List<PushRule> override;
final List<PushRule> room;
final List<PushRule> sender;
final List<PushRule> underride;
GlobalPushRules.fromJson(Map<String, dynamic> json)
: this.content = json.containsKey("content")
? PushRule.fromJsonList(json["content"])
: null,
this.override = json.containsKey("override")
? PushRule.fromJsonList(json["content"])
: null,
this.room = json.containsKey("room")
? PushRule.fromJsonList(json["room"])
: null,
this.sender = json.containsKey("sender")
? PushRule.fromJsonList(json["sender"])
: null,
this.underride = json.containsKey("underride")
? PushRule.fromJsonList(json["underride"])
: null;
}
/// A single pushrule.
class PushRule {
final List actions;
final bool isDefault;
final bool enabled;
final String ruleId;
final List<PushRuleConditions> conditions;
final String pattern;
static List<PushRule> fromJsonList(List<dynamic> list) {
List<PushRule> objList = [];
list.forEach((json) {
objList.add(PushRule.fromJson(json));
});
return objList;
}
PushRule.fromJson(Map<String, dynamic> json)
: this.actions = json["actions"],
this.isDefault = json["default"],
this.enabled = json["enabled"],
this.ruleId = json["rule_id"],
this.conditions = json.containsKey("conditions")
? PushRuleConditions.fromJsonList(json["conditions"])
: null,
this.pattern = json["pattern"];
}
/// Conditions when this pushrule should be active.
class PushRuleConditions {
final String kind;
final String key;
final String pattern;
final String is_;
static List<PushRuleConditions> fromJsonList(List<dynamic> list) {
List<PushRuleConditions> objList = [];
list.forEach((json) {
objList.add(PushRuleConditions.fromJson(json));
});
return objList;
}
PushRuleConditions.fromJson(Map<String, dynamic> json)
: this.kind = json["kind"],
this.key = json["key"],
this.pattern = json["pattern"],
this.is_ = json["is"];
}

View file

@ -1,11 +1,10 @@
import 'package:famedlysdk/src/utils/ChatTime.dart';
import '../User.dart';
/// Represents a receipt.
/// This [user] has read an event at the given [time].
class Receipt {
final User user;
final ChatTime time;
final DateTime time;
const Receipt(this.user, this.time);
}

View file

@ -3,9 +3,9 @@ import 'package:famedlysdk/famedlysdk.dart';
/// Matrix room states are addressed by a tuple of the [type] and an
/// optional [stateKey].
class StatesMap {
Map<String, Map<String, RoomState>> states = {};
Map<String, Map<String, Event>> states = {};
/// Returns either the [RoomState] or a map of state_keys to [RoomState] objects.
/// Returns either the [Event] or a map of state_keys to [Event] objects.
/// If you just enter a MatrixID, it will try to return the corresponding m.room.member event.
dynamic operator [](String key) {
//print("[Warning] This method will be depracated in the future!");
@ -14,7 +14,7 @@ class StatesMap {
return states["m.room.member"][key];
}
if (!states.containsKey(key)) states[key] = {};
if (states[key][""] is RoomState)
if (states[key][""] is Event)
return states[key][""];
else if (states[key].length == 0)
return null;
@ -22,7 +22,7 @@ class StatesMap {
return states[key];
}
void operator []=(String key, RoomState val) {
void operator []=(String key, Event val) {
//print("[Warning] This method will be depracated in the future!");
if (key.startsWith("@") && key.contains(":")) {
if (!states.containsKey("m.room.member")) states["m.room.member"] = {};

View file

@ -1,63 +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:test/test.dart';
import 'package:famedlysdk/src/utils/ChatTime.dart';
void main() {
/// All Tests related to the ChatTime
group("ChatTime", () {
test("Comparing", () async {
final int originServerTs = DateTime.now().millisecondsSinceEpoch -
(ChatTime.minutesBetweenEnvironments - 1) * 1000 * 60;
final int oldOriginServerTs = DateTime.now().millisecondsSinceEpoch -
(ChatTime.minutesBetweenEnvironments + 1) * 1000 * 60;
final ChatTime chatTime = ChatTime(originServerTs);
final ChatTime oldChatTime = ChatTime(oldOriginServerTs);
final ChatTime nowTime = ChatTime.now();
expect(chatTime.toTimeStamp(), originServerTs);
expect(nowTime.toTimeStamp() > chatTime.toTimeStamp(), true);
expect(nowTime.sameEnvironment(chatTime), true);
expect(nowTime.sameEnvironment(oldChatTime), false);
expect(chatTime > oldChatTime, true);
expect(chatTime < oldChatTime, false);
expect(chatTime >= oldChatTime, true);
expect(chatTime <= oldChatTime, false);
expect(chatTime == chatTime, true);
expect(chatTime == oldChatTime, false);
});
test("Formatting", () async {
final int timestamp = DateTime.now().millisecondsSinceEpoch;
final ChatTime chatTime = ChatTime(timestamp);
//expect(chatTime.toTimeString(),"05:36"); // This depends on the time and your timezone ;)
expect(chatTime.toTimeString(), chatTime.toEventTimeString());
final ChatTime oldChatTime = ChatTime(156014498475);
expect(oldChatTime.toString(), "11.12.1974");
});
});
}

View file

@ -25,12 +25,9 @@ import 'dart:async';
import 'package:famedlysdk/src/AccountData.dart';
import 'package:famedlysdk/src/Client.dart';
import 'package:famedlysdk/src/Connection.dart';
import 'package:famedlysdk/src/Presence.dart';
import 'package:famedlysdk/src/Room.dart';
import 'package:famedlysdk/src/User.dart';
import 'package:famedlysdk/src/requests/SetPushersRequest.dart';
import 'package:famedlysdk/src/responses/PushrulesResponse.dart';
import 'package:famedlysdk/src/sync/EventUpdate.dart';
import 'package:famedlysdk/src/sync/RoomUpdate.dart';
import 'package:famedlysdk/src/sync/UserUpdate.dart';
@ -53,11 +50,11 @@ void main() {
/// Check if all Elements get created
matrix = Client("testclient", debug: true);
matrix.connection.httpClient = FakeMatrixApi();
matrix.httpClient = FakeMatrixApi();
roomUpdateListFuture = matrix.connection.onRoomUpdate.stream.toList();
eventUpdateListFuture = matrix.connection.onEvent.stream.toList();
userUpdateListFuture = matrix.connection.onUserEvent.stream.toList();
roomUpdateListFuture = matrix.onRoomUpdate.stream.toList();
eventUpdateListFuture = matrix.onEvent.stream.toList();
userUpdateListFuture = matrix.onUserEvent.stream.toList();
test('Login', () async {
int presenceCounter = 0;
@ -82,7 +79,7 @@ void main() {
expect(matrix.matrixVersions,
["r0.0.1", "r0.1.0", "r0.2.0", "r0.3.0", "r0.4.0", "r0.5.0"]);
final Map<String, dynamic> resp = await matrix.connection
final Map<String, dynamic> resp = await matrix
.jsonRequest(type: HTTPType.POST, action: "/client/r0/login", data: {
"type": "m.login.password",
"user": "test",
@ -91,11 +88,11 @@ void main() {
});
Future<LoginState> loginStateFuture =
matrix.connection.onLoginStateChanged.stream.first;
Future<bool> firstSyncFuture = matrix.connection.onFirstSync.stream.first;
Future<dynamic> syncFuture = matrix.connection.onSync.stream.first;
matrix.onLoginStateChanged.stream.first;
Future<bool> firstSyncFuture = matrix.onFirstSync.stream.first;
Future<dynamic> syncFuture = matrix.onSync.stream.first;
matrix.connection.connect(
matrix.connect(
newToken: resp["access_token"],
newUserID: resp["user_id"],
newHomeserver: matrix.homeserver,
@ -121,23 +118,23 @@ void main() {
expect(matrix.accountData.length, 3);
expect(matrix.getDirectChatFromUserId("@bob:example.com"),
"!726s6s6q:example.com");
expect(matrix.roomList.rooms[1].directChatMatrixID, "@bob:example.com");
expect(matrix.rooms[1].directChatMatrixID, "@bob:example.com");
expect(matrix.directChats, matrix.accountData["m.direct"].content);
expect(matrix.presences.length, 1);
expect(matrix.roomList.rooms[1].ephemerals.length, 2);
expect(matrix.roomList.rooms[1].typingUsers.length, 1);
expect(matrix.roomList.rooms[1].typingUsers[0].id, "@alice:example.com");
expect(matrix.roomList.rooms[1].roomAccountData.length, 3);
expect(matrix.rooms[1].ephemerals.length, 2);
expect(matrix.rooms[1].typingUsers.length, 1);
expect(matrix.rooms[1].typingUsers[0].id, "@alice:example.com");
expect(matrix.rooms[1].roomAccountData.length, 3);
expect(
matrix.roomList.rooms[1].roomAccountData["m.receipt"]
matrix.rooms[1].roomAccountData["m.receipt"]
.content["@alice:example.com"]["ts"],
1436451550453);
expect(
matrix.roomList.rooms[1].roomAccountData["m.receipt"]
matrix.rooms[1].roomAccountData["m.receipt"]
.content["@alice:example.com"]["event_id"],
"7365636s6r6432:example.com");
expect(matrix.roomList.rooms.length, 2);
expect(matrix.roomList.rooms[1].canonicalAlias,
expect(matrix.rooms.length, 2);
expect(matrix.rooms[1].canonicalAlias,
"#famedlyContactDiscovery:${matrix.userID.split(":")[1]}");
final List<User> contacts = await matrix.loadFamedlyContacts();
expect(contacts.length, 1);
@ -147,25 +144,30 @@ void main() {
expect(presenceCounter, 1);
expect(accountDataCounter, 3);
matrix.connection.onEvent.add(
EventUpdate(
roomID: "!726s6s6q:example.com",
type: "state",
eventType: "m.room.canonical_alias",
content: {
"sender": "@alice:example.com",
"type": "m.room.canonical_alias",
"content": {"alias": ""},
"state_key": "",
"origin_server_ts": 1417731086799,
"event_id": "66697273743033:example.com"
},
),
);
matrix.handleSync({
"rooms": {
"join": {
"!726s6s6q:example.com": {
"state": {
"events": [
{
"sender": "@alice:example.com",
"type": "m.room.canonical_alias",
"content": {"alias": ""},
"state_key": "",
"origin_server_ts": 1417731086799,
"event_id": "66697273743033:example.com"
}
]
}
}
}
}
});
await new Future.delayed(new Duration(milliseconds: 50));
expect(
matrix.roomList.getRoomByAlias(
matrix.getRoomByAlias(
"#famedlyContactDiscovery:${matrix.userID.split(":")[1]}"),
null);
final List<User> altContacts = await matrix.loadFamedlyContacts();
@ -176,8 +178,8 @@ void main() {
test('Try to get ErrorResponse', () async {
MatrixException expectedException;
try {
await matrix.connection
.jsonRequest(type: HTTPType.PUT, action: "/non/existing/path");
await matrix.jsonRequest(
type: HTTPType.PUT, action: "/non/existing/path");
} on MatrixException catch (exception) {
expectedException = exception;
}
@ -185,13 +187,13 @@ void main() {
});
test('Logout', () async {
await matrix.connection
.jsonRequest(type: HTTPType.POST, action: "/client/r0/logout");
await matrix.jsonRequest(
type: HTTPType.POST, action: "/client/r0/logout");
Future<LoginState> loginStateFuture =
matrix.connection.onLoginStateChanged.stream.first;
matrix.onLoginStateChanged.stream.first;
matrix.connection.clear();
matrix.clear();
expect(matrix.accessToken == null, true);
expect(matrix.homeserver == null, true);
@ -207,11 +209,11 @@ void main() {
});
test('Room Update Test', () async {
matrix.connection.onRoomUpdate.close();
matrix.onRoomUpdate.close();
List<RoomUpdate> roomUpdateList = await roomUpdateListFuture;
expect(roomUpdateList.length, 2);
expect(roomUpdateList.length, 3);
expect(roomUpdateList[0].id == "!726s6s6q:example.com", true);
expect(roomUpdateList[0].membership == Membership.join, true);
@ -229,7 +231,7 @@ void main() {
});
test('Event Update Test', () async {
matrix.connection.onEvent.close();
matrix.onEvent.close();
List<EventUpdate> eventUpdateList = await eventUpdateListFuture;
@ -281,7 +283,7 @@ void main() {
});
test('User Update Test', () async {
matrix.connection.onUserEvent.close();
matrix.onUserEvent.close();
List<UserUpdate> eventUpdateList = await userUpdateListFuture;
@ -299,11 +301,11 @@ void main() {
test('Login', () async {
matrix = Client("testclient", debug: true);
matrix.connection.httpClient = FakeMatrixApi();
matrix.httpClient = FakeMatrixApi();
roomUpdateListFuture = matrix.connection.onRoomUpdate.stream.toList();
eventUpdateListFuture = matrix.connection.onEvent.stream.toList();
userUpdateListFuture = matrix.connection.onUserEvent.stream.toList();
roomUpdateListFuture = matrix.onRoomUpdate.stream.toList();
eventUpdateListFuture = matrix.onEvent.stream.toList();
userUpdateListFuture = matrix.onUserEvent.stream.toList();
final bool checkResp =
await matrix.checkServer("https://fakeServer.notExisting");
@ -326,7 +328,7 @@ void main() {
final MatrixFile testFile =
MatrixFile(bytes: [], path: "fake/path/file.jpeg");
final dynamic resp = await matrix.connection.upload(testFile);
final dynamic resp = await matrix.upload(testFile);
expect(resp, "mxc://example.com/AQwafuaFswefuhsfAFAgsw");
});
@ -337,23 +339,14 @@ void main() {
});
test('getPushrules', () async {
final PushrulesResponse pushrules = await matrix.getPushrules();
final PushrulesResponse awaited_resp = PushrulesResponse.fromJson(
FakeMatrixApi.api["GET"]["/client/r0/pushrules/"](""));
expect(pushrules.toJson(), awaited_resp.toJson());
final pushrules = await matrix.getPushrules();
expect(pushrules != null, true);
});
test('setPushers', () async {
final SetPushersRequest data = SetPushersRequest(
app_id: "com.famedly.famedlysdk",
device_display_name: "GitLabCi",
app_display_name: "famedlySDK",
pushkey: "abcdefg",
kind: "http",
lang: "en",
data: PusherData(
format: "event_id_only", url: "https://examplepushserver.com"));
await matrix.setPushers(data);
await matrix.setPushers("abcdefg", "http", "com.famedly.famedlysdk",
"famedlySDK", "GitLabCi", "en", "https://examplepushserver.com",
format: "event_id_only");
});
test('joinRoomById', () async {
@ -388,11 +381,11 @@ void main() {
test('Logout when token is unknown', () async {
Future<LoginState> loginStateFuture =
matrix.connection.onLoginStateChanged.stream.first;
matrix.onLoginStateChanged.stream.first;
try {
await matrix.connection
.jsonRequest(type: HTTPType.DELETE, action: "/unknown/token");
await matrix.jsonRequest(
type: HTTPType.DELETE, action: "/unknown/token");
} on MatrixException catch (exception) {
expect(exception.error, MatrixError.M_UNKNOWN_TOKEN);
}

View file

@ -24,7 +24,7 @@
import 'dart:convert';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/src/RoomState.dart';
import 'package:famedlysdk/src/Event.dart';
import 'package:test/test.dart';
import 'FakeMatrixApi.dart';
@ -68,7 +68,7 @@ void main() {
expect(event.getBody(), body);
expect(event.type, EventTypes.Text);
jsonObj["state_key"] = "";
RoomState state = RoomState.fromJson(jsonObj, null);
Event state = Event.fromJson(jsonObj, null);
expect(state.eventId, id);
expect(state.stateKey, "");
expect(state.timelineEvent.status, 1);
@ -174,7 +174,7 @@ void main() {
"type": "m.room.redaction",
"unsigned": {"age": 1234}
};
RoomState redactedBecause = RoomState.fromJson(redactionEventJson, room);
Event redactedBecause = Event.fromJson(redactionEventJson, room);
Event event = Event.fromJson(jsonObj, room);
event.setRedactionEvent(redactedBecause);
expect(event.redacted, true);
@ -196,7 +196,7 @@ void main() {
test("sendAgain", () async {
Client matrix = Client("testclient", debug: true);
matrix.connection.httpClient = FakeMatrixApi();
matrix.httpClient = FakeMatrixApi();
await matrix.checkServer("https://fakeServer.notExisting");
await matrix.login("test", "1234");

View file

@ -25,12 +25,15 @@ import 'package:test/test.dart';
import 'package:famedlysdk/src/Client.dart';
import 'package:famedlysdk/src/utils/MxContent.dart';
import 'FakeMatrixApi.dart';
void main() {
/// All Tests related to the MxContent
group("MxContent", () {
test("Formatting", () async {
Client client = Client("testclient");
client.homeserver = "https://testserver.abc";
client.httpClient = FakeMatrixApi();
await client.checkServer("https://fakeserver.notexisting");
final String mxc = "mxc://exampleserver.abc/abcdefghijklmn";
final MxContent content = MxContent(mxc);
@ -45,7 +48,8 @@ void main() {
});
test("Not crashing if null", () async {
Client client = Client("testclient");
client.homeserver = "https://testserver.abc";
client.httpClient = FakeMatrixApi();
await client.checkServer("https://fakeserver.notexisting");
final MxContent content = MxContent(null);
expect(content.getDownloadLink(client),
"${client.homeserver}/_matrix/media/r0/download/");

185
test/PushRules_test.dart Normal file
View file

@ -0,0 +1,185 @@
/*
* 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:famedlysdk/src/utils/PushRules.dart';
import 'package:test/test.dart';
void main() {
/// All Tests related to the MxContent
group("PushRules", () {
test("Create", () async {
final Map<String, dynamic> json = {
"global": {
"content": [
{
"actions": [
"notify",
{"set_tweak": "sound", "value": "default"},
{"set_tweak": "highlight"}
],
"default": true,
"enabled": true,
"pattern": "alice",
"rule_id": ".m.rule.contains_user_name"
}
],
"override": [
{
"actions": ["dont_notify"],
"conditions": [],
"default": true,
"enabled": false,
"rule_id": ".m.rule.master"
},
{
"actions": ["dont_notify"],
"conditions": [
{
"key": "content.msgtype",
"kind": "event_match",
"pattern": "m.notice"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.suppress_notices"
}
],
"room": [],
"sender": [],
"underride": [
{
"actions": [
"notify",
{"set_tweak": "sound", "value": "ring"},
{"set_tweak": "highlight", "value": false}
],
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.call.invite"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.call"
},
{
"actions": [
"notify",
{"set_tweak": "sound", "value": "default"},
{"set_tweak": "highlight"}
],
"conditions": [
{"kind": "contains_display_name"}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.contains_display_name"
},
{
"actions": [
"notify",
{"set_tweak": "sound", "value": "default"},
{"set_tweak": "highlight", "value": false}
],
"conditions": [
{"kind": "room_member_count", "is": "2"},
{
"kind": "event_match",
"key": "type",
"pattern": "m.room.message"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.room_one_to_one"
},
{
"actions": [
"notify",
{"set_tweak": "sound", "value": "default"},
{"set_tweak": "highlight", "value": false}
],
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.room.member"
},
{
"key": "content.membership",
"kind": "event_match",
"pattern": "invite"
},
{
"key": "state_key",
"kind": "event_match",
"pattern": "@alice:example.com"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.invite_for_me"
},
{
"actions": [
"notify",
{"set_tweak": "highlight", "value": false}
],
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.room.member"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.member_event"
},
{
"actions": [
"notify",
{"set_tweak": "highlight", "value": false}
],
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.room.message"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.message"
}
]
}
};
expect(PushRules.fromJson(json) != null, true);
});
});
}

View file

@ -1,281 +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:famedlysdk/src/Client.dart';
import 'package:famedlysdk/src/RoomList.dart';
import 'package:famedlysdk/src/User.dart';
import 'package:famedlysdk/src/sync/EventUpdate.dart';
import 'package:famedlysdk/src/sync/RoomUpdate.dart';
import 'package:famedlysdk/src/utils/ChatTime.dart';
import 'package:test/test.dart';
import 'FakeMatrixApi.dart';
void main() {
/// All Tests related to the MxContent
group("RoomList", () {
final roomID = "!1:example.com";
test("Create and insert one room", () async {
final Client client = Client("testclient", debug: true);
client.connection.httpClient = FakeMatrixApi();
await client.checkServer("https://fakeserver.notexisting");
client.prevBatch = "1234";
int updateCount = 0;
List<int> insertList = [];
List<int> removeList = [];
RoomList roomList = RoomList(
client: client,
rooms: [],
onUpdate: () {
updateCount++;
},
onInsert: (int insertID) {
insertList.add(insertID);
},
onRemove: (int removeID) {
insertList.add(removeID);
});
expect(roomList.eventSub != null, true);
expect(roomList.roomSub != null, true);
client.connection.onRoomUpdate.add(RoomUpdate(
id: roomID,
membership: Membership.join,
notification_count: 2,
highlight_count: 1,
limitedTimeline: false,
prev_batch: "1234",
));
await new Future.delayed(new Duration(milliseconds: 50));
expect(updateCount, 1);
expect(insertList, [0]);
expect(removeList, []);
expect(roomList.rooms.length, 1);
expect(roomList.rooms[0].id, roomID);
expect(roomList.rooms[0].membership, Membership.join);
expect(roomList.rooms[0].notificationCount, 2);
expect(roomList.rooms[0].highlightCount, 1);
expect(roomList.rooms[0].prev_batch, "1234");
expect(roomList.rooms[0].timeCreated, ChatTime.now());
});
test("Restort", () async {
final Client client = Client("testclient", debug: true);
client.connection.httpClient = FakeMatrixApi();
await client.checkServer("https://fakeserver.notexisting");
client.prevBatch = "1234";
int updateCount = 0;
List<int> insertList = [];
List<int> removeList = [];
RoomList roomList = RoomList(
client: client,
rooms: [],
onUpdate: () {
updateCount++;
},
onInsert: (int insertID) {
insertList.add(insertID);
},
onRemove: (int removeID) {
insertList.add(removeID);
});
client.connection.onRoomUpdate.add(RoomUpdate(
id: "1",
membership: Membership.join,
notification_count: 2,
highlight_count: 1,
limitedTimeline: false,
prev_batch: "1234",
));
client.connection.onRoomUpdate.add(RoomUpdate(
id: "2",
membership: Membership.join,
notification_count: 2,
highlight_count: 1,
limitedTimeline: false,
prev_batch: "1234",
));
client.connection.onRoomUpdate.add(RoomUpdate(
id: "1",
membership: Membership.join,
notification_count: 2,
highlight_count: 1,
limitedTimeline: false,
prev_batch: "12345",
summary: RoomSummary(
mHeroes: ["@alice:example.com"],
mJoinedMemberCount: 1,
mInvitedMemberCount: 1)));
await new Future.delayed(new Duration(milliseconds: 50));
expect(roomList.eventSub != null, true);
expect(roomList.roomSub != null, true);
expect(roomList.rooms[0].id, "1");
expect(roomList.rooms[1].id, "2");
expect(roomList.rooms[0].prev_batch, "12345");
expect(roomList.rooms[0].displayname, "alice");
expect(roomList.rooms[0].mJoinedMemberCount, 1);
expect(roomList.rooms[0].mInvitedMemberCount, 1);
ChatTime now = ChatTime.now();
int roomUpdates = 0;
roomList.rooms[0].onUpdate = () {
roomUpdates++;
};
roomList.rooms[1].onUpdate = () {
roomUpdates++;
};
client.connection.onEvent.add(EventUpdate(
type: "timeline",
roomID: "1",
eventType: "m.room.message",
content: {
"type": "m.room.message",
"content": {"msgtype": "m.text", "body": "Testcase"},
"sender": "@alice:example.com",
"room_id": "1",
"status": 2,
"event_id": "1",
"origin_server_ts": now.toTimeStamp() - 1000
}));
client.connection.onEvent.add(EventUpdate(
type: "timeline",
roomID: "2",
eventType: "m.room.message",
content: {
"type": "m.room.message",
"content": {"msgtype": "m.text", "body": "Testcase 2"},
"sender": "@alice:example.com",
"room_id": "1",
"status": 2,
"event_id": "2",
"origin_server_ts": now.toTimeStamp()
}));
await new Future.delayed(new Duration(milliseconds: 50));
expect(updateCount, 5);
expect(roomUpdates, 3);
expect(insertList, [0, 1]);
expect(removeList, []);
expect(roomList.rooms.length, 2);
expect(
roomList.rooms[0].timeCreated > roomList.rooms[1].timeCreated, true);
expect(roomList.rooms[0].id, "2");
expect(roomList.rooms[1].id, "1");
expect(roomList.rooms[0].lastMessage, "Testcase 2");
expect(roomList.rooms[0].timeCreated, now);
client.connection.onEvent.add(EventUpdate(
type: "timeline",
roomID: "1",
eventType: "m.room.redaction",
content: {
"content": {"reason": "Spamming"},
"event_id": "143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"redacts": "1",
"room_id": "1",
"sender": "@example:example.org",
"type": "m.room.redaction",
"unsigned": {"age": 1234}
}));
await new Future.delayed(new Duration(milliseconds: 50));
expect(updateCount, 6);
expect(insertList, [0, 1]);
expect(removeList, []);
expect(roomList.rooms.length, 2);
expect(roomList.rooms[1].getState("m.room.message").eventId, "1");
expect(roomList.rooms[1].getState("m.room.message").redacted, true);
});
test("onlyLeft", () async {
final Client client = Client("testclient", debug: true);
client.connection.httpClient = FakeMatrixApi();
await client.checkServer("https://fakeserver.notexisting");
client.prevBatch = "1234";
int updateCount = 0;
List<int> insertList = [];
List<int> removeList = [];
RoomList roomList = RoomList(
client: client,
onlyLeft: true,
rooms: [],
onUpdate: () {
updateCount++;
},
onInsert: (int insertID) {
insertList.add(insertID);
},
onRemove: (int removeID) {
insertList.add(removeID);
});
client.connection.onRoomUpdate.add(RoomUpdate(
id: "1",
membership: Membership.join,
notification_count: 2,
highlight_count: 1,
limitedTimeline: false,
prev_batch: "1234",
));
client.connection.onRoomUpdate.add(RoomUpdate(
id: "2",
membership: Membership.leave,
notification_count: 2,
highlight_count: 1,
limitedTimeline: false,
prev_batch: "1234",
));
await new Future.delayed(new Duration(milliseconds: 50));
expect(roomList.eventSub != null, true);
expect(roomList.roomSub != null, true);
expect(roomList.rooms[0].id, "2");
expect(insertList, [0]);
expect(removeList, []);
expect(updateCount, 2);
});
});
}

View file

@ -24,10 +24,8 @@
import 'package:famedlysdk/src/Client.dart';
import 'package:famedlysdk/src/Event.dart';
import 'package:famedlysdk/src/Room.dart';
import 'package:famedlysdk/src/RoomState.dart';
import 'package:famedlysdk/src/Timeline.dart';
import 'package:famedlysdk/src/User.dart';
import 'package:famedlysdk/src/utils/ChatTime.dart';
import 'package:famedlysdk/src/utils/MatrixFile.dart';
import 'package:test/test.dart';
@ -41,7 +39,7 @@ void main() {
group("Room", () {
test('Login', () async {
matrix = Client("testclient", debug: true);
matrix.connection.httpClient = FakeMatrixApi();
matrix.httpClient = FakeMatrixApi();
final bool checkResp =
await matrix.checkServer("https://fakeServer.notExisting");
@ -86,7 +84,7 @@ void main() {
expect(room.mHeroes, heroes);
expect(room.displayname, "alice, bob, charley");
room.states["m.room.canonical_alias"] = RoomState(
room.states["m.room.canonical_alias"] = Event(
senderId: "@test:example.com",
typeKey: "m.room.canonical_alias",
roomId: room.id,
@ -97,7 +95,7 @@ void main() {
expect(room.displayname, "testalias");
expect(room.canonicalAlias, "#testalias:example.com");
room.states["m.room.name"] = RoomState(
room.states["m.room.name"] = Event(
senderId: "@test:example.com",
typeKey: "m.room.name",
roomId: room.id,
@ -108,7 +106,7 @@ void main() {
expect(room.displayname, "testname");
expect(room.topic, "");
room.states["m.room.topic"] = RoomState(
room.states["m.room.topic"] = Event(
senderId: "@test:example.com",
typeKey: "m.room.topic",
roomId: room.id,
@ -119,7 +117,7 @@ void main() {
expect(room.topic, "testtopic");
expect(room.avatar.mxc, "");
room.states["m.room.avatar"] = RoomState(
room.states["m.room.avatar"] = Event(
senderId: "@test:example.com",
typeKey: "m.room.avatar",
roomId: room.id,
@ -130,13 +128,13 @@ void main() {
expect(room.avatar.mxc, "mxc://testurl");
expect(room.lastEvent, null);
room.states["m.room.message"] = RoomState(
room.states["m.room.message"] = Event(
senderId: "@test:example.com",
typeKey: "m.room.message",
roomId: room.id,
room: room,
eventId: "12345",
time: ChatTime.now(),
time: DateTime.now(),
content: {"msgtype": "m.text", "body": "test"},
stateKey: "");
expect(room.lastEvent.eventId, "12345");
@ -187,7 +185,7 @@ void main() {
});
test("PowerLevels", () async {
room.states["m.room.power_levels"] = RoomState(
room.states["m.room.power_levels"] = Event(
senderId: "@test:example.com",
typeKey: "m.room.power_levels",
roomId: room.id,
@ -223,7 +221,7 @@ void main() {
expect(room.powerLevels,
room.states["m.room.power_levels"].content["users"]);
room.states["m.room.power_levels"] = RoomState(
room.states["m.room.power_levels"] = Event(
senderId: "@test:example.com",
typeKey: "m.room.power_levels",
roomId: room.id,
@ -264,13 +262,13 @@ void main() {
});
test("getParticipants", () async {
room.setState(RoomState(
room.setState(Event(
senderId: "@alice:test.abc",
typeKey: "m.room.member",
roomId: room.id,
room: room,
eventId: "12345",
time: ChatTime.now(),
time: DateTime.now(),
content: {"displayname": "alice"},
stateKey: "@alice:test.abc"));
final List<User> userList = room.getParticipants();

View file

@ -30,7 +30,7 @@ void main() {
group("StateKeys", () {
test("Operator overload", () async {
StatesMap states = StatesMap();
states["m.room.name"] = RoomState(
states["m.room.name"] = Event(
eventId: "1",
content: {"name": "test"},
typeKey: "m.room.name",
@ -38,7 +38,7 @@ void main() {
roomId: "!test:test.test",
senderId: "@alice:test.test");
states["@alice:test.test"] = RoomState(
states["@alice:test.test"] = Event(
eventId: "2",
content: {"membership": "join"},
typeKey: "m.room.name",
@ -46,7 +46,7 @@ void main() {
roomId: "!test:test.test",
senderId: "@alice:test.test");
states["m.room.member"]["@bob:test.test"] = RoomState(
states["m.room.member"]["@bob:test.test"] = Event(
eventId: "3",
content: {"membership": "join"},
typeKey: "m.room.name",
@ -54,7 +54,7 @@ void main() {
roomId: "!test:test.test",
senderId: "@bob:test.test");
states["com.test.custom"] = RoomState(
states["com.test.custom"] = Event(
eventId: "4",
content: {"custom": "stuff"},
typeKey: "com.test.custom",

View file

@ -27,20 +27,18 @@ import 'package:famedlysdk/src/Client.dart';
import 'package:famedlysdk/src/Room.dart';
import 'package:famedlysdk/src/Timeline.dart';
import 'package:famedlysdk/src/sync/EventUpdate.dart';
import 'package:famedlysdk/src/utils/ChatTime.dart';
import 'FakeMatrixApi.dart';
void main() {
/// All Tests related to the MxContent
group("Timeline", () {
final String roomID = "!1234:example.com";
final testTimeStamp = ChatTime.now().toTimeStamp();
final testTimeStamp = DateTime.now().millisecondsSinceEpoch;
int updateCount = 0;
List<int> insertList = [];
Client client = Client("testclient", debug: true);
client.connection.httpClient = FakeMatrixApi();
client.homeserver = "https://fakeServer.notExisting";
client.httpClient = FakeMatrixApi();
Room room = Room(
id: roomID, client: client, prev_batch: "1234", roomAccountData: {});
@ -55,7 +53,8 @@ void main() {
});
test("Create", () async {
client.connection.onEvent.add(EventUpdate(
await client.checkServer("https://fakeServer.notExisting");
client.onEvent.add(EventUpdate(
type: "timeline",
roomID: roomID,
eventType: "m.room.message",
@ -68,7 +67,7 @@ void main() {
"origin_server_ts": testTimeStamp
}));
client.connection.onEvent.add(EventUpdate(
client.onEvent.add(EventUpdate(
type: "timeline",
roomID: roomID,
eventType: "m.room.message",
@ -91,9 +90,12 @@ void main() {
expect(timeline.events.length, 2);
expect(timeline.events[0].eventId, "1");
expect(timeline.events[0].sender.id, "@alice:example.com");
expect(timeline.events[0].time.toTimeStamp(), testTimeStamp);
expect(timeline.events[0].time.millisecondsSinceEpoch, testTimeStamp);
expect(timeline.events[0].getBody(), "Testcase");
expect(timeline.events[0].time > timeline.events[1].time, true);
expect(
timeline.events[0].time.millisecondsSinceEpoch >
timeline.events[1].time.millisecondsSinceEpoch,
true);
expect(timeline.events[0].receipts, []);
room.roomAccountData["m.receipt"] = RoomAccountData.fromJson({
@ -112,7 +114,7 @@ void main() {
expect(timeline.events[0].receipts.length, 1);
expect(timeline.events[0].receipts[0].user.id, "@alice:example.com");
client.connection.onEvent.add(EventUpdate(
client.onEvent.add(EventUpdate(
type: "timeline",
roomID: roomID,
eventType: "m.room.redaction",
@ -145,7 +147,7 @@ void main() {
expect(timeline.events[0].eventId, "42");
expect(timeline.events[0].status, 1);
client.connection.onEvent.add(EventUpdate(
client.onEvent.add(EventUpdate(
type: "timeline",
roomID: roomID,
eventType: "m.room.message",
@ -169,7 +171,7 @@ void main() {
});
test("Send message with error", () async {
client.connection.onEvent.add(EventUpdate(
client.onEvent.add(EventUpdate(
type: "timeline",
roomID: roomID,
eventType: "m.room.message",

View file

@ -21,7 +21,7 @@
* along with famedlysdk. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:famedlysdk/src/RoomState.dart';
import 'package:famedlysdk/src/Event.dart';
import 'package:famedlysdk/src/User.dart';
import 'package:test/test.dart';
@ -49,7 +49,7 @@ void main() {
"state_key": id
};
User user = RoomState.fromJson(jsonObj, null).asUser;
User user = Event.fromJson(jsonObj, null).asUser;
expect(user.id, id);
expect(user.membership, membership);