2019-06-09 11:57:33 +00:00
|
|
|
/*
|
|
|
|
* 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
|
2019-06-21 07:46:53 +00:00
|
|
|
* along with famedlysdk. If not, see <http://www.gnu.org/licenses/>.
|
2019-06-09 11:57:33 +00:00
|
|
|
*/
|
|
|
|
|
2019-06-09 10:16:48 +00:00
|
|
|
import 'dart:async';
|
|
|
|
import 'dart:core';
|
2019-07-12 09:26:07 +00:00
|
|
|
|
2019-12-19 11:26:21 +00:00
|
|
|
import 'package:famedlysdk/famedlysdk.dart';
|
2019-08-07 10:06:28 +00:00
|
|
|
import 'package:famedlysdk/src/AccountData.dart';
|
|
|
|
import 'package:famedlysdk/src/Presence.dart';
|
2019-10-02 11:33:01 +00:00
|
|
|
import 'package:famedlysdk/src/StoreAPI.dart';
|
2019-08-07 10:06:28 +00:00
|
|
|
import 'package:famedlysdk/src/sync/UserUpdate.dart';
|
2019-10-18 11:05:07 +00:00
|
|
|
import 'package:famedlysdk/src/utils/MatrixFile.dart';
|
2020-01-02 14:33:26 +00:00
|
|
|
import 'package:pedantic/pedantic.dart';
|
2019-06-21 11:30:39 +00:00
|
|
|
import 'Room.dart';
|
2020-01-02 14:09:49 +00:00
|
|
|
import 'Event.dart';
|
2019-06-11 11:09:26 +00:00
|
|
|
import 'User.dart';
|
2019-11-30 09:36:30 +00:00
|
|
|
import 'utils/Profile.dart';
|
2020-01-02 14:09:49 +00:00
|
|
|
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';
|
2019-06-09 10:16:48 +00:00
|
|
|
|
2019-09-02 08:33:32 +00:00
|
|
|
typedef AccountDataEventCB = void Function(AccountData accountData);
|
|
|
|
typedef PresenceCB = void Function(Presence presence);
|
2020-01-03 13:21:15 +00:00
|
|
|
typedef RoomSorter = int Function(Room a, Room b);
|
2019-09-02 08:33:32 +00:00
|
|
|
|
2020-01-02 14:09:49 +00:00
|
|
|
enum HTTPType { GET, POST, PUT, DELETE }
|
|
|
|
|
|
|
|
enum LoginState { logged, loggedOut }
|
|
|
|
|
2019-06-09 12:33:25 +00:00
|
|
|
/// Represents a Matrix client to communicate with a
|
2019-06-09 10:16:48 +00:00
|
|
|
/// [Matrix](https://matrix.org) homeserver and is the entry point for this
|
|
|
|
/// SDK.
|
|
|
|
class Client {
|
|
|
|
/// Handles the connection for this client.
|
2020-01-02 14:09:49 +00:00
|
|
|
@deprecated
|
|
|
|
Client get connection => this;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
|
|
|
/// Optional persistent store for all data.
|
2019-10-02 11:33:01 +00:00
|
|
|
StoreAPI store;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
2019-10-02 11:33:01 +00:00
|
|
|
Client(this.clientName, {this.debug = false, this.store}) {
|
|
|
|
if (this.clientName != "testclient") store = null; //Store(this);
|
2020-01-02 14:09:49 +00:00
|
|
|
this.onLoginStateChanged.stream.listen((loginState) {
|
2019-06-09 10:16:48 +00:00
|
|
|
print("LoginState: ${loginState.toString()}");
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-06-12 11:43:14 +00:00
|
|
|
/// Whether debug prints should be displayed.
|
|
|
|
final bool debug;
|
|
|
|
|
2019-06-09 10:16:48 +00:00
|
|
|
/// The required name for this client.
|
|
|
|
final String clientName;
|
|
|
|
|
|
|
|
/// The homeserver this client is communicating with.
|
2020-01-02 14:09:49 +00:00
|
|
|
String get homeserver => _homeserver;
|
|
|
|
String _homeserver;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
|
|
|
/// The Matrix ID of the current logged user.
|
2020-01-02 14:09:49 +00:00
|
|
|
String get userID => _userID;
|
|
|
|
String _userID;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
|
|
|
/// This is the access token for the matrix client. When it is undefined, then
|
|
|
|
/// the user needs to sign in first.
|
2020-01-02 14:09:49 +00:00
|
|
|
String get accessToken => _accessToken;
|
|
|
|
String _accessToken;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
|
|
|
/// This points to the position in the synchronization history.
|
|
|
|
String prevBatch;
|
|
|
|
|
|
|
|
/// The device ID is an unique identifier for this device.
|
2020-01-02 14:09:49 +00:00
|
|
|
String get deviceID => _deviceID;
|
|
|
|
String _deviceID;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
|
|
|
/// The device name is a human readable identifier for this device.
|
2020-01-02 14:09:49 +00:00
|
|
|
String get deviceName => _deviceName;
|
|
|
|
String _deviceName;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
|
|
|
/// Which version of the matrix specification does this server support?
|
2020-01-02 14:09:49 +00:00
|
|
|
List<String> get matrixVersions => _matrixVersions;
|
|
|
|
List<String> _matrixVersions;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
|
|
|
/// Wheither the server supports lazy load members.
|
2020-01-02 14:09:49 +00:00
|
|
|
bool get lazyLoadMembers => _lazyLoadMembers;
|
|
|
|
bool _lazyLoadMembers = false;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
|
|
|
/// Returns the current login state.
|
|
|
|
bool isLogged() => accessToken != null;
|
|
|
|
|
2019-08-07 10:06:28 +00:00
|
|
|
/// A list of all rooms the user is participating or invited.
|
2020-01-02 14:09:49 +00:00
|
|
|
List<Room> get rooms => _rooms;
|
|
|
|
List<Room> _rooms = [];
|
2019-08-07 09:38:51 +00:00
|
|
|
|
2019-08-07 10:06:28 +00:00
|
|
|
/// Key/Value store of account data.
|
|
|
|
Map<String, AccountData> accountData = {};
|
|
|
|
|
|
|
|
/// Presences of users by a given matrix ID
|
|
|
|
Map<String, Presence> presences = {};
|
|
|
|
|
2019-08-08 08:31:39 +00:00
|
|
|
/// Callback will be called on account data updates.
|
|
|
|
AccountDataEventCB onAccountData;
|
|
|
|
|
|
|
|
/// Callback will be called on presences.
|
|
|
|
PresenceCB onPresence;
|
|
|
|
|
2020-01-02 14:09:49 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-08-07 10:06:28 +00:00
|
|
|
void handleUserUpdate(UserUpdate userUpdate) {
|
|
|
|
if (userUpdate.type == "account_data") {
|
|
|
|
AccountData newAccountData = AccountData.fromJson(userUpdate.content);
|
|
|
|
accountData[newAccountData.typeKey] = newAccountData;
|
2019-08-08 08:31:39 +00:00
|
|
|
if (onAccountData != null) onAccountData(newAccountData);
|
2019-08-07 10:06:28 +00:00
|
|
|
}
|
|
|
|
if (userUpdate.type == "presence") {
|
|
|
|
Presence newPresence = Presence.fromJson(userUpdate.content);
|
2019-08-08 08:31:39 +00:00
|
|
|
presences[newPresence.sender] = newPresence;
|
|
|
|
if (onPresence != null) onPresence(newPresence);
|
2019-08-07 10:06:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-08 07:58:37 +00:00
|
|
|
Map<String, dynamic> get directChats =>
|
2019-08-07 10:06:28 +00:00
|
|
|
accountData["m.direct"] != null ? accountData["m.direct"].content : {};
|
|
|
|
|
|
|
|
/// Returns the (first) room ID from the store which is a private chat with the user [userId].
|
|
|
|
/// Returns null if there is none.
|
2019-08-29 09:12:14 +00:00
|
|
|
String getDirectChatFromUserId(String userId) {
|
|
|
|
if (accountData["m.direct"] != null &&
|
|
|
|
accountData["m.direct"].content[userId] is List<dynamic> &&
|
|
|
|
accountData["m.direct"].content[userId].length > 0) {
|
2020-01-02 14:33:26 +00:00
|
|
|
if (getRoomById(accountData["m.direct"].content[userId][0]) != null) {
|
2020-01-02 14:09:49 +00:00
|
|
|
return accountData["m.direct"].content[userId][0];
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
2019-08-29 09:12:14 +00:00
|
|
|
(accountData["m.direct"].content[userId] as List<dynamic>)
|
|
|
|
.remove(accountData["m.direct"].content[userId][0]);
|
2020-01-02 14:09:49 +00:00
|
|
|
this.jsonRequest(
|
2019-08-29 09:12:14 +00:00
|
|
|
type: HTTPType.PUT,
|
|
|
|
action: "/client/r0/user/${userID}/account_data/m.direct",
|
|
|
|
data: directChats);
|
|
|
|
return getDirectChatFromUserId(userId);
|
|
|
|
}
|
2020-01-02 14:33:26 +00:00
|
|
|
for (int i = 0; i < this.rooms.length; i++) {
|
2020-01-02 14:09:49 +00:00
|
|
|
if (this.rooms[i].membership == Membership.invite &&
|
|
|
|
this.rooms[i].states[userID]?.senderId == userId &&
|
2020-01-02 14:33:26 +00:00
|
|
|
this.rooms[i].states[userID].content["is_direct"] == true) {
|
2020-01-02 14:09:49 +00:00
|
|
|
return this.rooms[i].id;
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
|
|
|
}
|
2019-08-29 09:12:14 +00:00
|
|
|
return null;
|
|
|
|
}
|
2019-08-07 10:06:28 +00:00
|
|
|
|
2019-06-09 10:16:48 +00:00
|
|
|
/// Checks the supported versions of the Matrix protocol and the supported
|
|
|
|
/// login types. Returns false if the server is not compatible with the
|
|
|
|
/// client. Automatically sets [matrixVersions] and [lazyLoadMembers].
|
2019-12-29 10:28:33 +00:00
|
|
|
/// Throws FormatException, TimeoutException and MatrixException on error.
|
2019-06-09 10:16:48 +00:00
|
|
|
Future<bool> checkServer(serverUrl) async {
|
2019-12-29 10:28:33 +00:00
|
|
|
try {
|
2020-01-02 14:09:49 +00:00
|
|
|
_homeserver = serverUrl;
|
|
|
|
final versionResp = await this
|
|
|
|
.jsonRequest(type: HTTPType.GET, action: "/client/versions");
|
2019-12-29 10:28:33 +00:00
|
|
|
|
|
|
|
final List<String> versions = List<String>.from(versionResp["versions"]);
|
|
|
|
|
|
|
|
for (int i = 0; i < versions.length; i++) {
|
2020-01-02 14:33:26 +00:00
|
|
|
if (versions[i] == "r0.5.0") {
|
2019-12-29 10:28:33 +00:00
|
|
|
break;
|
2020-01-02 14:33:26 +00:00
|
|
|
} else if (i == versions.length - 1) {
|
2019-12-29 10:28:33 +00:00
|
|
|
return false;
|
|
|
|
}
|
2019-06-09 10:16:48 +00:00
|
|
|
}
|
|
|
|
|
2020-01-02 14:09:49 +00:00
|
|
|
_matrixVersions = versions;
|
2019-06-09 10:16:48 +00:00
|
|
|
|
2019-12-29 10:28:33 +00:00
|
|
|
if (versionResp.containsKey("unstable_features") &&
|
|
|
|
versionResp["unstable_features"].containsKey("m.lazy_load_members")) {
|
2020-01-02 14:09:49 +00:00
|
|
|
_lazyLoadMembers = versionResp["unstable_features"]
|
2019-12-29 10:28:33 +00:00
|
|
|
["m.lazy_load_members"]
|
|
|
|
? true
|
|
|
|
: false;
|
|
|
|
}
|
2019-06-09 10:16:48 +00:00
|
|
|
|
2020-01-02 14:09:49 +00:00
|
|
|
final loginResp = await this
|
|
|
|
.jsonRequest(type: HTTPType.GET, action: "/client/r0/login");
|
2019-06-09 10:16:48 +00:00
|
|
|
|
2019-12-29 10:28:33 +00:00
|
|
|
final List<dynamic> flows = loginResp["flows"];
|
2019-06-09 10:16:48 +00:00
|
|
|
|
2019-12-29 10:28:33 +00:00
|
|
|
for (int i = 0; i < flows.length; i++) {
|
|
|
|
if (flows[i].containsKey("type") &&
|
2020-01-02 14:33:26 +00:00
|
|
|
flows[i]["type"] == "m.login.password") {
|
2019-12-29 10:28:33 +00:00
|
|
|
break;
|
2020-01-02 14:33:26 +00:00
|
|
|
} else if (i == flows.length - 1) {
|
2019-12-29 10:28:33 +00:00
|
|
|
return false;
|
|
|
|
}
|
2019-06-09 10:16:48 +00:00
|
|
|
}
|
2019-12-29 10:28:33 +00:00
|
|
|
return true;
|
|
|
|
} catch (_) {
|
2020-01-02 14:09:49 +00:00
|
|
|
this._homeserver = this._matrixVersions = null;
|
2019-12-29 10:28:33 +00:00
|
|
|
rethrow;
|
2019-06-09 10:16:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Handles the login and allows the client to call all APIs which require
|
2019-12-29 10:28:33 +00:00
|
|
|
/// authentication. Returns false if the login was not successful. Throws
|
|
|
|
/// MatrixException if login was not successful.
|
2019-06-09 10:16:48 +00:00
|
|
|
Future<bool> login(String username, String password) async {
|
2020-01-02 14:09:49 +00:00
|
|
|
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"
|
|
|
|
});
|
2019-06-09 10:16:48 +00:00
|
|
|
|
|
|
|
final userID = loginResp["user_id"];
|
|
|
|
final accessToken = loginResp["access_token"];
|
|
|
|
if (userID == null || accessToken == null) {
|
2019-12-29 10:28:33 +00:00
|
|
|
return false;
|
2019-06-09 10:16:48 +00:00
|
|
|
}
|
|
|
|
|
2020-01-02 14:09:49 +00:00
|
|
|
await this.connect(
|
2019-06-09 10:16:48 +00:00
|
|
|
newToken: accessToken,
|
|
|
|
newUserID: userID,
|
|
|
|
newHomeserver: homeserver,
|
|
|
|
newDeviceName: "",
|
|
|
|
newDeviceID: "",
|
|
|
|
newMatrixVersions: matrixVersions,
|
|
|
|
newLazyLoadMembers: lazyLoadMembers);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Sends a logout command to the homeserver and clears all local data,
|
|
|
|
/// including all persistent data from the store.
|
|
|
|
Future<void> logout() async {
|
2019-12-29 10:28:33 +00:00
|
|
|
try {
|
2020-01-02 14:09:49 +00:00
|
|
|
await this.jsonRequest(type: HTTPType.POST, action: "/client/r0/logout");
|
2019-12-29 10:28:33 +00:00
|
|
|
} catch (exception) {
|
|
|
|
rethrow;
|
|
|
|
} finally {
|
2020-01-02 14:09:49 +00:00
|
|
|
await this.clear();
|
2019-12-29 10:28:33 +00:00
|
|
|
}
|
2019-06-09 10:16:48 +00:00
|
|
|
}
|
|
|
|
|
2019-11-30 09:36:30 +00:00
|
|
|
/// Get the combined profile information for this user. This API may be used to
|
|
|
|
/// fetch the user's own profile information or other users; either locally
|
|
|
|
/// or on remote homeservers.
|
|
|
|
Future<Profile> getProfileFromUserId(String userId) async {
|
2020-01-02 14:09:49 +00:00
|
|
|
final dynamic resp = await this.jsonRequest(
|
2019-11-30 09:36:30 +00:00
|
|
|
type: HTTPType.GET, action: "/client/r0/profile/${userId}");
|
|
|
|
return Profile.fromJson(resp);
|
|
|
|
}
|
|
|
|
|
2019-12-19 11:26:21 +00:00
|
|
|
Future<List<Room>> get archive async {
|
|
|
|
List<Room> archiveList = [];
|
2019-11-29 16:19:32 +00:00
|
|
|
String syncFilters =
|
2019-12-19 11:26:21 +00:00
|
|
|
'{"room":{"include_leave":true,"timeline":{"limit":10}}}';
|
2019-11-29 16:19:32 +00:00
|
|
|
String action = "/client/r0/sync?filter=$syncFilters&timeout=0";
|
2020-01-02 14:09:49 +00:00
|
|
|
final sync = await this.jsonRequest(type: HTTPType.GET, action: action);
|
2019-12-29 10:28:33 +00:00
|
|
|
if (sync["rooms"]["leave"] is Map<String, dynamic>) {
|
2019-12-19 11:26:21 +00:00
|
|
|
for (var entry in sync["rooms"]["leave"].entries) {
|
|
|
|
final String id = entry.key;
|
|
|
|
final dynamic room = entry.value;
|
|
|
|
print(id);
|
|
|
|
print(room.toString());
|
|
|
|
Room leftRoom = Room(
|
|
|
|
id: id,
|
|
|
|
membership: Membership.leave,
|
|
|
|
client: this,
|
|
|
|
roomAccountData: {},
|
|
|
|
mHeroes: []);
|
|
|
|
if (room["account_data"] is Map<String, dynamic> &&
|
|
|
|
room["account_data"]["events"] is List<dynamic>) {
|
|
|
|
for (dynamic event in room["account_data"]["events"]) {
|
|
|
|
leftRoom.roomAccountData[event["type"]] =
|
|
|
|
RoomAccountData.fromJson(event, leftRoom);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (room["timeline"] is Map<String, dynamic> &&
|
|
|
|
room["timeline"]["events"] is List<dynamic>) {
|
|
|
|
for (dynamic event in room["timeline"]["events"]) {
|
2020-01-02 14:09:49 +00:00
|
|
|
leftRoom.setState(Event.fromJson(event, leftRoom));
|
2019-12-19 11:26:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (room["state"] is Map<String, dynamic> &&
|
|
|
|
room["state"]["events"] is List<dynamic>) {
|
|
|
|
for (dynamic event in room["state"]["events"]) {
|
2020-01-02 14:09:49 +00:00
|
|
|
leftRoom.setState(Event.fromJson(event, leftRoom));
|
2019-12-19 11:26:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
archiveList.add(leftRoom);
|
|
|
|
}
|
|
|
|
}
|
2019-11-29 16:19:32 +00:00
|
|
|
return archiveList;
|
2019-09-19 14:00:17 +00:00
|
|
|
}
|
|
|
|
|
2019-07-12 09:26:07 +00:00
|
|
|
Future<dynamic> joinRoomById(String id) async {
|
2020-01-02 14:09:49 +00:00
|
|
|
return await this
|
|
|
|
.jsonRequest(type: HTTPType.POST, action: "/client/r0/join/$id");
|
2019-07-12 09:26:07 +00:00
|
|
|
}
|
|
|
|
|
2019-07-26 12:46:23 +00:00
|
|
|
/// Loads the contact list for this user excluding the user itself.
|
|
|
|
/// Currently the contacts are found by discovering the contacts of
|
|
|
|
/// the famedlyContactDiscovery room, which is
|
2019-07-26 12:00:12 +00:00
|
|
|
/// defined by the autojoin room feature in Synapse.
|
2019-07-26 12:46:23 +00:00
|
|
|
Future<List<User>> loadFamedlyContacts() async {
|
2019-07-26 14:07:29 +00:00
|
|
|
List<User> contacts = [];
|
2020-01-02 14:09:49 +00:00
|
|
|
Room contactDiscoveryRoom =
|
|
|
|
this.getRoomByAlias("#famedlyContactDiscovery:${userID.split(":")[1]}");
|
2020-01-02 14:33:26 +00:00
|
|
|
if (contactDiscoveryRoom != null) {
|
2019-07-26 14:07:29 +00:00
|
|
|
contacts = await contactDiscoveryRoom.requestParticipants();
|
2020-01-02 14:33:26 +00:00
|
|
|
} else {
|
2019-10-14 16:50:10 +00:00
|
|
|
Map<String, bool> userMap = {};
|
2020-01-02 14:09:49 +00:00
|
|
|
for (int i = 0; i < this.rooms.length; i++) {
|
|
|
|
List<User> roomUsers = this.rooms[i].getParticipants();
|
2019-10-14 16:50:10 +00:00
|
|
|
for (int j = 0; j < roomUsers.length; j++) {
|
|
|
|
if (userMap[roomUsers[j].id] != true) contacts.add(roomUsers[j]);
|
|
|
|
userMap[roomUsers[j].id] = true;
|
|
|
|
}
|
2019-10-14 13:20:03 +00:00
|
|
|
}
|
|
|
|
}
|
2019-07-26 12:00:12 +00:00
|
|
|
return contacts;
|
|
|
|
}
|
|
|
|
|
2019-09-26 14:53:08 +00:00
|
|
|
@Deprecated('Please use [createRoom] instead!')
|
|
|
|
Future<String> createGroup(List<User> users) => createRoom(invite: users);
|
|
|
|
|
2019-06-11 11:09:26 +00:00
|
|
|
/// Creates a new group chat and invites the given Users and returns the new
|
2019-09-26 14:53:08 +00:00
|
|
|
/// created room ID. If [params] are provided, invite will be ignored. For the
|
|
|
|
/// moment please look at https://matrix.org/docs/spec/client_server/r0.5.0#post-matrix-client-r0-createroom
|
|
|
|
/// to configure [params].
|
|
|
|
Future<String> createRoom(
|
|
|
|
{List<User> invite, Map<String, dynamic> params}) async {
|
2019-06-11 11:09:26 +00:00
|
|
|
List<String> inviteIDs = [];
|
2020-01-02 14:33:26 +00:00
|
|
|
if (params == null && invite != null) {
|
|
|
|
for (int i = 0; i < invite.length; i++) {
|
|
|
|
inviteIDs.add(invite[i].id);
|
|
|
|
}
|
|
|
|
}
|
2019-06-11 11:09:26 +00:00
|
|
|
|
2019-12-29 10:28:33 +00:00
|
|
|
try {
|
2020-01-02 14:09:49 +00:00
|
|
|
final dynamic resp = await this.jsonRequest(
|
2019-12-29 10:28:33 +00:00
|
|
|
type: HTTPType.POST,
|
|
|
|
action: "/client/r0/createRoom",
|
|
|
|
data: params == null
|
|
|
|
? {
|
|
|
|
"invite": inviteIDs,
|
|
|
|
}
|
|
|
|
: params);
|
|
|
|
return resp["room_id"];
|
|
|
|
} catch (e) {
|
|
|
|
rethrow;
|
2019-06-14 06:09:37 +00:00
|
|
|
}
|
2019-06-11 11:09:26 +00:00
|
|
|
}
|
2019-06-21 07:41:09 +00:00
|
|
|
|
2019-12-29 10:28:33 +00:00
|
|
|
/// Uploads a new user avatar for this user.
|
|
|
|
Future<void> setAvatar(MatrixFile file) async {
|
2020-01-02 14:09:49 +00:00
|
|
|
final uploadResp = await this.upload(file);
|
|
|
|
await this.jsonRequest(
|
2019-09-09 13:22:02 +00:00
|
|
|
type: HTTPType.PUT,
|
|
|
|
action: "/client/r0/profile/$userID/avatar_url",
|
|
|
|
data: {"avatar_url": uploadResp});
|
2019-12-29 10:28:33 +00:00
|
|
|
return;
|
2019-09-09 13:22:02 +00:00
|
|
|
}
|
|
|
|
|
2019-06-21 07:41:09 +00:00
|
|
|
/// Fetches the pushrules for the logged in user.
|
|
|
|
/// These are needed for notifications on Android
|
2020-01-02 14:09:49 +00:00
|
|
|
Future<PushRules> getPushrules() async {
|
|
|
|
final dynamic resp = await this.jsonRequest(
|
2019-07-12 09:26:07 +00:00
|
|
|
type: HTTPType.GET,
|
2019-07-18 16:47:59 +00:00
|
|
|
action: "/client/r0/pushrules/",
|
2019-06-21 07:41:09 +00:00
|
|
|
);
|
|
|
|
|
2020-01-02 14:09:49 +00:00
|
|
|
return PushRules.fromJson(resp);
|
2019-06-21 07:41:09 +00:00
|
|
|
}
|
2019-06-25 13:33:56 +00:00
|
|
|
|
|
|
|
/// This endpoint allows the creation, modification and deletion of pushers for this user ID.
|
2020-01-02 14:09:49 +00:00
|
|
|
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(
|
2019-07-12 09:26:07 +00:00
|
|
|
type: HTTPType.POST,
|
2019-06-25 13:33:56 +00:00
|
|
|
action: "/client/r0/pushers/set",
|
2020-01-02 14:09:49 +00:00
|
|
|
data: data,
|
2019-06-25 13:33:56 +00:00
|
|
|
);
|
2019-12-29 10:28:33 +00:00
|
|
|
return;
|
2019-06-25 13:33:56 +00:00
|
|
|
}
|
2020-01-02 14:09:49 +00:00
|
|
|
|
|
|
|
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"} )
|
2020-01-02 14:33:26 +00:00
|
|
|
final StreamController<EventUpdate> onEvent = StreamController.broadcast();
|
2020-01-02 14:09:49 +00:00
|
|
|
|
|
|
|
/// Outside of the events there are updates for the global chat states which
|
|
|
|
/// are handled by this signal:
|
|
|
|
final StreamController<RoomUpdate> onRoomUpdate =
|
2020-01-02 14:33:26 +00:00
|
|
|
StreamController.broadcast();
|
2020-01-02 14:09:49 +00:00
|
|
|
|
|
|
|
/// Outside of rooms there are account updates like account_data or presences.
|
2020-01-02 14:33:26 +00:00
|
|
|
final StreamController<UserUpdate> onUserEvent = StreamController.broadcast();
|
2020-01-02 14:09:49 +00:00
|
|
|
|
|
|
|
/// Called when the login state e.g. user gets logged out.
|
|
|
|
final StreamController<LoginState> onLoginStateChanged =
|
2020-01-02 14:33:26 +00:00
|
|
|
StreamController.broadcast();
|
2020-01-02 14:09:49 +00:00
|
|
|
|
|
|
|
/// Synchronization erros are coming here.
|
|
|
|
final StreamController<MatrixException> onError =
|
2020-01-02 14:33:26 +00:00
|
|
|
StreamController.broadcast();
|
2020-01-02 14:09:49 +00:00
|
|
|
|
|
|
|
/// This is called once, when the first sync has received.
|
2020-01-02 14:33:26 +00:00
|
|
|
final StreamController<bool> onFirstSync = StreamController.broadcast();
|
2020-01-02 14:09:49 +00:00
|
|
|
|
|
|
|
/// When a new sync response is coming in, this gives the complete payload.
|
2020-01-02 14:33:26 +00:00
|
|
|
final StreamController<dynamic> onSync = StreamController.broadcast();
|
2020-01-02 14:09:49 +00:00
|
|
|
|
|
|
|
/// 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) {
|
2020-01-02 14:33:26 +00:00
|
|
|
await this.store.storeClient();
|
2020-01-02 14:09:49 +00:00
|
|
|
this._rooms = await this.store.getRoomList(onlyLeft: false);
|
2020-01-03 13:21:15 +00:00
|
|
|
this._sortRooms();
|
2020-01-02 14:09:49 +00:00
|
|
|
this.accountData = await this.store.getAccountData();
|
|
|
|
this.presences = await this.store.getPresences();
|
|
|
|
}
|
|
|
|
|
|
|
|
_userEventSub ??= onUserEvent.stream.listen(this.handleUserUpdate);
|
|
|
|
|
|
|
|
onLoginStateChanged.add(LoginState.logged);
|
|
|
|
|
2020-01-02 14:33:26 +00:00
|
|
|
return _sync();
|
2020-01-02 14:09:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2020-01-02 14:33:26 +00:00
|
|
|
if (this.isLogged() == false && this.homeserver == null) {
|
2020-01-02 14:09:49 +00:00
|
|
|
throw ("No homeserver specified.");
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
2020-01-02 14:09:49 +00:00
|
|
|
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 = {};
|
2020-01-02 14:33:26 +00:00
|
|
|
if (type == HTTPType.PUT || type == HTTPType.POST) {
|
2020-01-02 14:09:49 +00:00
|
|
|
headers["Content-Type"] = contentType;
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
|
|
|
if (this.isLogged()) {
|
2020-01-02 14:09:49 +00:00
|
|
|
headers["Authorization"] = "Bearer ${this.accessToken}";
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
2020-01-02 14:09:49 +00:00
|
|
|
|
2020-01-02 14:33:26 +00:00
|
|
|
if (this.debug) {
|
2020-01-02 14:09:49 +00:00
|
|
|
print(
|
|
|
|
"[REQUEST ${type.toString().split('.').last}] Action: $action, Data: $data");
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
2020-01-02 14:09:49 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
2020-01-03 13:27:49 +00:00
|
|
|
jsonResp = jsonDecode(String.fromCharCodes(resp.body.runes))
|
2020-01-02 14:09:49 +00:00
|
|
|
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;
|
2020-01-02 14:33:26 +00:00
|
|
|
if (this.homeserver != "https://fakeServer.notExisting") {
|
2020-01-02 14:09:49 +00:00
|
|
|
fileBytes = file.bytes;
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
2020-01-02 14:09:49 +00:00
|
|
|
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;
|
2020-01-02 14:33:26 +00:00
|
|
|
if (this.store != null) {
|
2020-01-02 14:09:49 +00:00
|
|
|
await this.store.transaction(() {
|
|
|
|
handleSync(syncResp);
|
|
|
|
this.store.storePrevBatch(syncResp);
|
|
|
|
return;
|
|
|
|
});
|
2020-01-02 14:33:26 +00:00
|
|
|
} else {
|
2020-01-02 14:09:49 +00:00
|
|
|
await handleSync(syncResp);
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
2020-01-02 14:09:49 +00:00
|
|
|
if (this.prevBatch == null) this.onFirstSync.add(true);
|
|
|
|
this.prevBatch = syncResp["next_batch"];
|
2020-01-02 14:33:26 +00:00
|
|
|
if (hash == _syncRequest.hashCode) unawaited(_sync());
|
2020-01-02 14:09:49 +00:00
|
|
|
} 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>) {
|
2020-01-02 14:33:26 +00:00
|
|
|
if (sync["rooms"]["join"] is Map<String, dynamic>) {
|
2020-01-02 14:09:49 +00:00
|
|
|
_handleRooms(sync["rooms"]["join"], Membership.join);
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
|
|
|
if (sync["rooms"]["invite"] is Map<String, dynamic>) {
|
2020-01-02 14:09:49 +00:00
|
|
|
_handleRooms(sync["rooms"]["invite"], Membership.invite);
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
|
|
|
if (sync["rooms"]["leave"] is Map<String, dynamic>) {
|
2020-01-02 14:09:49 +00:00
|
|
|
_handleRooms(sync["rooms"]["leave"], Membership.leave);
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
2020-01-02 14:09:49 +00:00
|
|
|
}
|
|
|
|
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>) {
|
2020-01-02 14:33:26 +00:00
|
|
|
if (room["unread_notifications"]["highlight_count"] is num) {
|
2020-01-02 14:09:49 +00:00
|
|
|
highlight_count = room["unread_notifications"]["highlight_count"];
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
|
|
|
if (room["unread_notifications"]["notification_count"] is num) {
|
2020-01-02 14:09:49 +00:00
|
|
|
notification_count =
|
|
|
|
room["unread_notifications"]["notification_count"];
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
2020-01-02 14:09:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (room["timeline"] is Map<String, dynamic>) {
|
2020-01-02 14:33:26 +00:00
|
|
|
if (room["timeline"]["limited"] is bool) {
|
2020-01-02 14:09:49 +00:00
|
|
|
limitedTimeline = room["timeline"]["limited"];
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
|
|
|
if (room["timeline"]["prev_batch"] is String) {
|
2020-01-02 14:09:49 +00:00
|
|
|
prev_batch = room["timeline"]["prev_batch"];
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
2020-01-02 14:09:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2020-01-02 14:33:26 +00:00
|
|
|
unawaited(this.store?.storeRoomUpdate(update));
|
2020-01-02 14:09:49 +00:00
|
|
|
onRoomUpdate.add(update);
|
|
|
|
|
|
|
|
/// Handle now all room events and save them in the database
|
|
|
|
if (room["state"] is Map<String, dynamic> &&
|
2020-01-02 14:33:26 +00:00
|
|
|
room["state"]["events"] is List<dynamic>) {
|
2020-01-02 14:09:49 +00:00
|
|
|
_handleRoomEvents(id, room["state"]["events"], "state");
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
2020-01-02 14:09:49 +00:00
|
|
|
|
|
|
|
if (room["invite_state"] is Map<String, dynamic> &&
|
2020-01-02 14:33:26 +00:00
|
|
|
room["invite_state"]["events"] is List<dynamic>) {
|
2020-01-02 14:09:49 +00:00
|
|
|
_handleRoomEvents(id, room["invite_state"]["events"], "invite_state");
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
2020-01-02 14:09:49 +00:00
|
|
|
|
|
|
|
if (room["timeline"] is Map<String, dynamic> &&
|
2020-01-02 14:33:26 +00:00
|
|
|
room["timeline"]["events"] is List<dynamic>) {
|
2020-01-02 14:09:49 +00:00
|
|
|
_handleRoomEvents(id, room["timeline"]["events"], "timeline");
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
2020-01-02 14:09:49 +00:00
|
|
|
|
|
|
|
if (room["ephemeral"] is Map<String, dynamic> &&
|
2020-01-02 14:33:26 +00:00
|
|
|
room["ephemeral"]["events"] is List<dynamic>) {
|
2020-01-02 14:09:49 +00:00
|
|
|
_handleEphemerals(id, room["ephemeral"]["events"]);
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
2020-01-02 14:09:49 +00:00
|
|
|
|
|
|
|
if (room["account_data"] is Map<String, dynamic> &&
|
2020-01-02 14:33:26 +00:00
|
|
|
room["account_data"]["events"] is List<dynamic>) {
|
2020-01-02 14:09:49 +00:00
|
|
|
_handleRoomEvents(id, room["account_data"]["events"], "account_data");
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
2020-01-02 14:09:49 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2020-01-02 14:33:26 +00:00
|
|
|
for (int i = 0; i < events.length; i++) {
|
2020-01-02 14:09:49 +00:00
|
|
|
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);
|
|
|
|
}
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
2020-01-02 14:09:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2020-01-02 14:33:26 +00:00
|
|
|
if (chatUpdate.prev_batch != null) {
|
2020-01-02 14:09:49 +00:00
|
|
|
rooms[j].prev_batch = chatUpdate.prev_batch;
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
2020-01-02 14:09:49 +00:00
|
|
|
if (chatUpdate.summary != null) {
|
2020-01-02 14:33:26 +00:00
|
|
|
if (chatUpdate.summary.mHeroes != null) {
|
2020-01-02 14:09:49 +00:00
|
|
|
rooms[j].mHeroes = chatUpdate.summary.mHeroes;
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
|
|
|
if (chatUpdate.summary.mJoinedMemberCount != null) {
|
2020-01-02 14:09:49 +00:00
|
|
|
rooms[j].mJoinedMemberCount = chatUpdate.summary.mJoinedMemberCount;
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
|
|
|
if (chatUpdate.summary.mInvitedMemberCount != null) {
|
2020-01-02 14:09:49 +00:00
|
|
|
rooms[j].mInvitedMemberCount = chatUpdate.summary.mInvitedMemberCount;
|
2020-01-02 14:33:26 +00:00
|
|
|
}
|
2020-01-02 14:09:49 +00:00
|
|
|
}
|
|
|
|
if (rooms[j].onUpdate != null) rooms[j].onUpdate();
|
|
|
|
}
|
2020-01-03 13:21:15 +00:00
|
|
|
_sortRooms();
|
2020-01-02 14:09:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
2020-01-03 13:21:15 +00:00
|
|
|
if (eventUpdate.type == "timeline") _sortRooms();
|
2020-01-02 14:09:49 +00:00
|
|
|
}
|
|
|
|
|
2020-01-03 13:21:15 +00:00
|
|
|
bool _sortLock = false;
|
2020-01-02 14:09:49 +00:00
|
|
|
|
2020-01-03 13:21:15 +00:00
|
|
|
/// The compare function how the rooms should be sorted internally. By default
|
|
|
|
/// rooms are sorted by timestamp of the last m.room.message event or the last
|
|
|
|
/// event if there is no known message.
|
|
|
|
RoomSorter sortRoomsBy = (a, b) => b.timeCreated.millisecondsSinceEpoch
|
|
|
|
.compareTo(a.timeCreated.millisecondsSinceEpoch);
|
|
|
|
|
|
|
|
_sortRooms() {
|
2020-01-02 14:09:49 +00:00
|
|
|
if (prevBatch == null) return;
|
2020-01-03 13:21:15 +00:00
|
|
|
if (_sortLock || rooms.length < 2) return;
|
|
|
|
_sortLock = true;
|
|
|
|
rooms?.sort(sortRoomsBy);
|
|
|
|
_sortLock = false;
|
2020-01-02 14:09:49 +00:00
|
|
|
}
|
2019-06-09 10:16:48 +00:00
|
|
|
}
|